/** * OrcDeliveryEnforcerService * * **Why this exists** (2026-05-23 incident, the "affiliate research" case): * * Steve asked the team to research affiliate programs at 12:59 PM. ORC * acknowledged at 1:00 PM with "Atlas + Sage/Kai 派出去了, ETA 30-40min". * Sage finished Phase 1 at 1:15, Kai finished Phase 2 at 1:28 — but * Steve never saw the result. ORC's terminal logs claimed the pipeline * was "closed" and the result "already routed to Steve at Slack thread * ...", but there were zero `/api/slack/send` calls all day. ORC's * internal mental model believed delivery happened; the actual skill * call didn't. * * Root cause: ORC's prompt has a HARD pre-yield gate ("any `[CHAT:slack-…]` * MUST have a `reply-slack` before yielding"). But for a long-running * delegation, the user-visible deliverable comes from the *agent* via * `[DONE]`-style status reports — long AFTER ORC's initial ack. ORC * reads the agent's `[DONE]` as internal team chatter, narrates "pipeline * closed", and yields without delivering to Steve. The prompt rule * doesn't catch this case because the prompt looks for `[CHAT:slack-…]` * (user-originating) not `[DONE]` (agent-originating). * * **What this service does:** * * 1. When a worker agent posts `[DONE]` / `[COMPLETED]` to a * slack-prefixed conversation (chat.controller's "Agent status * routed to orchestrator" path), call {@link markPendingDelivery}. * The service records a pending delivery with `dueBy = now + 3 min`. * * 2. When ORC successfully posts via `/api/slack/send` to the same * thread, call {@link markDelivered}. Pending delivery cleared. * * 3. A timer scans every 30 s. For any pending delivery whose `dueBy` * has passed AND ORC hasn't replied, enqueue a `[DELIVER_REQUIRED]` * system message to ORC via MessageQueueService. ORC's prompt * treats `[DELIVER_REQUIRED]` exactly like `[CHAT:slack-…]` for * the pre-yield gate. * * 4. Exponential backoff between reminders for the same thread, to * avoid spamming ORC if it's genuinely stuck or rate-limited. * First reminder at 3 min, then 10 min, then 30 min, then stop. * * 5. (issue #731) Before tracking, and again before every reminder, the * thread's recorded activity is consulted via an injected * {@link ThreadActivityProvider}. A `[DONE]` naming a thread whose * last OWNER message is older than {@link STALE_THREAD_MAX_AGE_MS} * is not tracked (nobody is waiting there — the agent's environment * just still carries an old delegation's conversationId), and a * pending entry is dropped silently once ANY reply landed on the * thread after the `[DONE]`, whether or not it came through * `/api/slack/send`. * * Fire-and-forget on all writes — a fault in the enforcer MUST NOT * block chat or slack flow. * * @module services/orc/orc-delivery-enforcer.service */ /** * A `[DONE]` naming a Slack thread whose last OWNER message is older than * this is not tracked (issue #731). A user who asked for something and has * been silent on that thread for a day is not "waiting on a deliverable" * there — the deliverable, if any, goes out as a fresh top-level message. */ export declare const STALE_THREAD_MAX_AGE_MS: number; /** * Identifier for a slack thread — channelId + threadTs (or empty if * not a slack conversation, in which case we skip tracking entirely). */ export interface SlackThreadKey { channelId: string; threadTs: string; } export interface PendingDelivery { key: SlackThreadKey; /** The full conversation id, e.g. `slack-D0AC…-1779…`. Stored for the reminder text. */ conversationId: string; /** Session name of the agent that posted [DONE]. */ agentSender: string; /** First ~200 chars of the agent's [DONE] message — surfaces in the reminder. */ deliverableSummary: string; /** When the agent posted (ms). */ doneAt: number; /** Number of reminders fired so far (drives REMINDER_CADENCE_MS index). */ remindersFired: number; /** Next eligible reminder time (ms). */ nextDueAt: number; } /** * Function the enforcer uses to send a `[DELIVER_REQUIRED]` message to * ORC. Injected at construction time so tests can stub it and so the * service has no hard dep on MessageQueueService. */ export type DeliveryReminderSink = (params: { conversationId: string; text: string; }) => void; /** * What the chat store knows about a Slack thread (issue #731). * Timestamps are epoch ms; `null` when no such row exists. */ export interface ThreadActivity { /** Last message authored by the owner (`sender_type='user'`) on the thread. */ lastOwnerMessageAt: number | null; /** Last non-owner message (agent/orc/system reply) on the thread. */ lastReplyAt: number | null; } /** * Looks up a thread's recorded activity. Returns `null` when the thread is * unknown to the store, in which case the enforcer keeps its pre-#731 * behaviour (track and remind). Injected so the service has no hard dep on * ChatV2Service and tests can stub it. */ export type ThreadActivityProvider = (key: SlackThreadKey) => ThreadActivity | null; export interface OrcDeliveryEnforcerOptions { /** Wired from index.ts to enqueue the reminder via MessageQueueService. */ reminderSink: DeliveryReminderSink; /** Scan interval. Default 30 s. */ tickIntervalMs?: number; /** Optional thread-activity lookup (issue #731). Absent → legacy behaviour. */ threadActivityProvider?: ThreadActivityProvider; } /** * Singleton scope — there's one ORC, one queue of pending deliveries. */ export declare class OrcDeliveryEnforcerService { private static instance; private readonly logger; private readonly reminderSink; private readonly tickIntervalMs; private readonly threadActivityProvider; private timer; /** key = `${channelId}::${threadTs}` */ private readonly pending; constructor(opts: OrcDeliveryEnforcerOptions); static getInstance(): OrcDeliveryEnforcerService | null; static setInstance(next: OrcDeliveryEnforcerService | null): void; /** Start the periodic scan. Idempotent. */ start(): void; stop(): void; /** * Call this from `chat.controller` when an agent posts a `[DONE]`-style * status to a slack-prefixed conversation. Safe to call multiple times * for the same thread — the most recent agent message wins (we * surface only the latest deliverable summary in the reminder). * * @returns True when a pending delivery is now tracked (the orchestrator owes the owner this) */ markPendingDelivery(input: { conversationId: string; agentSender: string; text: string; /** Injectable clock (tests). */ now?: number; }): boolean; /** * Call this from `slack.controller` after a successful `/api/slack/send`. * Cleared regardless of which agent originated the underlying deliverable. */ markDelivered(input: { channelId: string; threadTs?: string | null; }): void; /** Test affordance — peek at the in-memory ledger. */ _peekPending(): PendingDelivery[]; /** * Public for tests + the manual-trigger endpoint. Otherwise driven by * the internal timer. */ tick(now?: number): { firedFor: string[]; }; /** * Consult the thread-activity provider, failing open: a store hiccup must * never break delivery enforcement, so any throw reads as "unknown". * * @param key - Thread to look up * @returns Activity, or null when unavailable/unknown */ private lookupActivity; private formatReminder; } export declare function isAgentDeliveryMarker(text: string): boolean; /** * Parse a chat-v2 conversation id of the form `slack--` * into { channelId, threadTs }. Returns null for non-slack ids. Mirrors * the regex in `request-sla.subscriber` so the enforcer agrees with the * SLA layer on what "the same thread" means. */ export declare function parseSlackConversationId(conversationId: string): SlackThreadKey | null; /** * Is this sender the orchestrator itself (by session name or the display * names the prompt/skills use for it)? * * @param sender - `senderName` as posted to `/chat/agent-response` * @returns True for the orchestrator */ export declare function isOrchestratorSender(sender: string): boolean; //# sourceMappingURL=orc-delivery-enforcer.service.d.ts.map