agim-cli 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +147 -0
  2. package/README.md +8 -1
  3. package/README.zh-CN.md +8 -1
  4. package/dist/cli.js +265 -30
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/a2a.d.ts +55 -0
  7. package/dist/core/a2a.d.ts.map +1 -0
  8. package/dist/core/a2a.js +254 -0
  9. package/dist/core/a2a.js.map +1 -0
  10. package/dist/core/approval-bus.d.ts +40 -0
  11. package/dist/core/approval-bus.d.ts.map +1 -1
  12. package/dist/core/approval-bus.js +125 -0
  13. package/dist/core/approval-bus.js.map +1 -1
  14. package/dist/core/approval-router.d.ts.map +1 -1
  15. package/dist/core/approval-router.js +31 -2
  16. package/dist/core/approval-router.js.map +1 -1
  17. package/dist/core/artifacts.d.ts +86 -0
  18. package/dist/core/artifacts.d.ts.map +1 -0
  19. package/dist/core/artifacts.js +306 -0
  20. package/dist/core/artifacts.js.map +1 -0
  21. package/dist/core/commands/a2a.d.ts +3 -0
  22. package/dist/core/commands/a2a.d.ts.map +1 -0
  23. package/dist/core/commands/a2a.js +162 -0
  24. package/dist/core/commands/a2a.js.map +1 -0
  25. package/dist/core/commands/job.d.ts.map +1 -1
  26. package/dist/core/commands/job.js +11 -2
  27. package/dist/core/commands/job.js.map +1 -1
  28. package/dist/core/commands/outbox.d.ts +3 -0
  29. package/dist/core/commands/outbox.d.ts.map +1 -0
  30. package/dist/core/commands/outbox.js +92 -0
  31. package/dist/core/commands/outbox.js.map +1 -0
  32. package/dist/core/job-board.d.ts +122 -1
  33. package/dist/core/job-board.d.ts.map +1 -1
  34. package/dist/core/job-board.js +432 -21
  35. package/dist/core/job-board.js.map +1 -1
  36. package/dist/core/job-recovery.d.ts +48 -0
  37. package/dist/core/job-recovery.d.ts.map +1 -0
  38. package/dist/core/job-recovery.js +185 -0
  39. package/dist/core/job-recovery.js.map +1 -0
  40. package/dist/core/message-sink.d.ts +63 -0
  41. package/dist/core/message-sink.d.ts.map +1 -0
  42. package/dist/core/message-sink.js +296 -0
  43. package/dist/core/message-sink.js.map +1 -0
  44. package/dist/core/outbox.d.ts +71 -0
  45. package/dist/core/outbox.d.ts.map +1 -0
  46. package/dist/core/outbox.js +301 -0
  47. package/dist/core/outbox.js.map +1 -0
  48. package/dist/core/reminders.d.ts.map +1 -1
  49. package/dist/core/reminders.js +12 -1
  50. package/dist/core/reminders.js.map +1 -1
  51. package/dist/core/restart-completion.d.ts.map +1 -1
  52. package/dist/core/restart-completion.js +18 -1
  53. package/dist/core/restart-completion.js.map +1 -1
  54. package/dist/core/router.d.ts +8 -0
  55. package/dist/core/router.d.ts.map +1 -1
  56. package/dist/core/router.js +16 -0
  57. package/dist/core/router.js.map +1 -1
  58. package/dist/core/types.d.ts +22 -0
  59. package/dist/core/types.d.ts.map +1 -1
  60. package/dist/plugins/agents/claude-code/index.d.ts.map +1 -1
  61. package/dist/plugins/agents/claude-code/index.js +5 -0
  62. package/dist/plugins/agents/claude-code/index.js.map +1 -1
  63. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +46 -0
  64. package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -1
  65. package/dist/plugins/agents/claude-code/mcp-approval-server.js +158 -0
  66. package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -1
  67. package/dist/plugins/agents/codex/index.d.ts.map +1 -1
  68. package/dist/plugins/agents/codex/index.js +5 -0
  69. package/dist/plugins/agents/codex/index.js.map +1 -1
  70. package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -1
  71. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +5 -0
  72. package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -1
  73. package/dist/web/server.d.ts.map +1 -1
  74. package/dist/web/server.js +28 -16
  75. package/dist/web/server.js.map +1 -1
  76. package/package.json +1 -1
@@ -0,0 +1,185 @@
1
+ // Job recovery — Phase 3 lifecycle hook.
2
+ //
3
+ // On startup, after messengers + sink worker are wired, this module walks
4
+ // the jobs table looking for `kind='inline' status='interrupted'` rows
5
+ // whose interruption happened inside the recovery window (default 10 min).
6
+ // For each such row it:
7
+ //
8
+ // 1. Sends a one-shot retry prompt to the originating thread via sink.
9
+ // Falls back to outbox queue if the IM platform is still warming up.
10
+ // 2. Registers the row in an in-memory per-thread map keyed by
11
+ // `${platform}:${channelId}:${threadId}`. The map entry expires after
12
+ // the window so a forgotten interruption doesn't hijack the user's
13
+ // next message forever.
14
+ // 3. The cli message handler calls `tryHandleRecoveryReply()` at the
15
+ // top of every inbound message; if the thread has a pending
16
+ // interrupted-job and the message looks like "1" / "重发" / "2" /
17
+ // "取消", we resolve the choice + clear the entry.
18
+ //
19
+ // Older interrupted rows (past the window) get swept to 'abandoned' so the
20
+ // table stays bounded.
21
+ //
22
+ // Module-level state:
23
+ // - `pending` is in-memory only. Process restarts wipe it. That's
24
+ // intentional: each restart re-runs scanInterruptedAndNotify() and
25
+ // re-derives the state from SQLite. We don't ever want a recovery
26
+ // entry that outlives the row it points at.
27
+ import { logger as rootLogger } from './logger.js';
28
+ import { createInlineJob, findRecoverableInterrupted, getRecoveryWindowMs, markInterruptedCancelled, markJobReplacedBy, sweepAbandonedInterrupted, } from './job-board.js';
29
+ import { sink } from './message-sink.js';
30
+ const log = rootLogger.child({ component: 'job-recovery' });
31
+ /** Thread-keyed pending entries. One per thread — if a thread happens to
32
+ * have multiple interrupted rows (rare), we surface the most recent only
33
+ * and the rest get swept to 'abandoned' next sweep. */
34
+ const pending = new Map();
35
+ /** Test hook — wipe the in-memory state. */
36
+ export function _resetPendingForTests() {
37
+ pending.clear();
38
+ }
39
+ /** Test hook — inspect current state. */
40
+ export function _peekPendingForTests() {
41
+ return new Map(pending);
42
+ }
43
+ function buildThreadKey(j) {
44
+ // Inline jobs always carry thread_key; if some legacy row doesn't, we skip
45
+ // (caller filters that out, but defense-in-depth).
46
+ return j.thread_key;
47
+ }
48
+ function parseThreadKey(key) {
49
+ // Composite is `${platform}:${channelId}:${threadId}` but threadId itself
50
+ // may contain ':' (Discord, some web clients). Split off the first two
51
+ // segments and treat the rest as threadId.
52
+ const parts = key.split(':');
53
+ if (parts.length < 3)
54
+ return null;
55
+ return { platform: parts[0], channelId: parts[1], threadId: parts.slice(2).join(':') };
56
+ }
57
+ function previewPrompt(p, max = 40) {
58
+ const flat = p.replace(/\s+/g, ' ').trim();
59
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat;
60
+ }
61
+ function formatNotice(j, windowMin) {
62
+ return [
63
+ '⚠️ 上次的消息被服务重启中断了:',
64
+ `「${previewPrompt(j.prompt)}」`,
65
+ '',
66
+ `回复 \`1\` 重发 / \`2\` 取消(${windowMin} 分钟内有效)`,
67
+ ].join('\n');
68
+ }
69
+ /** Run once on agim startup, after messengers + sink are up. */
70
+ export async function scanInterruptedAndNotify() {
71
+ const windowMs = getRecoveryWindowMs();
72
+ const windowMin = Math.round(windowMs / 60_000);
73
+ // First sweep stale rows so the notify pass doesn't waste cycles on them.
74
+ const abandoned = sweepAbandonedInterrupted(windowMs);
75
+ const rows = findRecoverableInterrupted(windowMs);
76
+ let notified = 0;
77
+ for (const j of rows) {
78
+ const key = buildThreadKey(j);
79
+ const parsed = parseThreadKey(key);
80
+ if (!parsed) {
81
+ log.warn({ event: 'recovery.bad_thread_key', jobId: j.id, threadKey: key }, 'skipping row with malformed thread_key');
82
+ continue;
83
+ }
84
+ // Only the most recent per-thread row goes into the pending map. Older
85
+ // ones get swept to 'cancelled' so the user isn't asked to retry two
86
+ // overlapping interruptions on the same thread.
87
+ if (pending.has(key)) {
88
+ markInterruptedCancelled(j.id);
89
+ continue;
90
+ }
91
+ const interruptedAtMs = j.completed_at
92
+ ? Date.parse(`${j.completed_at}Z`)
93
+ : Date.now();
94
+ pending.set(key, {
95
+ jobId: j.id,
96
+ agent: j.agent,
97
+ prompt: j.prompt,
98
+ threadKey: key,
99
+ platform: parsed.platform,
100
+ channelId: parsed.channelId,
101
+ threadId: parsed.threadId,
102
+ creatorId: j.creator_id,
103
+ expiresAt: interruptedAtMs + windowMs,
104
+ });
105
+ try {
106
+ await sink.deliver({
107
+ platform: parsed.platform,
108
+ channelId: parsed.channelId,
109
+ threadId: parsed.threadId,
110
+ payload: formatNotice(j, windowMin),
111
+ kind: 'text',
112
+ priority: 'normal',
113
+ });
114
+ notified++;
115
+ }
116
+ catch (err) {
117
+ log.warn({
118
+ event: 'recovery.notify_failed', jobId: j.id, threadKey: key,
119
+ err: err instanceof Error ? err.message : String(err),
120
+ }, 'recovery notice send threw — sink already enqueues, this is best-effort');
121
+ }
122
+ }
123
+ if (notified > 0 || abandoned > 0) {
124
+ log.info({ event: 'recovery.scan_done', notified, abandoned, windowMs }, `Recovery scan: notified ${notified}, abandoned ${abandoned}`);
125
+ }
126
+ return { notified, abandoned };
127
+ }
128
+ /** Periodic in-memory expiry sweep. Removes entries whose `expiresAt` has
129
+ * passed without a user reply. Called from cli.ts on a setInterval. */
130
+ export function sweepExpiredPending(now = Date.now()) {
131
+ let n = 0;
132
+ for (const [key, entry] of pending.entries()) {
133
+ if (entry.expiresAt <= now) {
134
+ pending.delete(key);
135
+ // Row in DB stays 'interrupted' until the next startup scan re-sweeps
136
+ // it to 'abandoned'. That's fine — abandoned-at-DB and expired-in-memory
137
+ // converge eventually.
138
+ n++;
139
+ }
140
+ }
141
+ return n;
142
+ }
143
+ /** Try to interpret a user's reply as a recovery decision. Returns an
144
+ * outcome the caller can render. Side-effect: on a recognized decision,
145
+ * the pending entry is removed and the DB row transitions. */
146
+ export function tryHandleRecoveryReply(threadKey, text) {
147
+ const entry = pending.get(threadKey);
148
+ if (!entry)
149
+ return { kind: 'not-pending' };
150
+ const trimmed = text.trim();
151
+ const isRetry = /^(1|重发|重试|retry|y|yes)$/i.test(trimmed);
152
+ const isCancel = /^(2|取消|算了|cancel|n|no)$/i.test(trimmed);
153
+ if (!isRetry && !isCancel)
154
+ return { kind: 'not-recovery-reply' };
155
+ pending.delete(threadKey);
156
+ if (isCancel) {
157
+ markInterruptedCancelled(entry.jobId);
158
+ return { kind: 'cancelled', oldJobId: entry.jobId };
159
+ }
160
+ // Retry: spawn a new inline job with the same prompt + thread coordinates
161
+ // so the cli main loop's onAgentResolved hook (which won't run here —
162
+ // recovery is outside the message handler) won't update the agent. We
163
+ // use the agent that was recorded on the original row. The new row goes
164
+ // straight to 'pending' for whatever picks it up next; recovery itself
165
+ // doesn't run the agent — it relies on the cli main loop seeing this
166
+ // row's prompt re-injected. The caller (router intercept) decides how
167
+ // to actually run it (typically: synthesize an inbound message with
168
+ // entry.prompt and let handleMessage do its normal thing).
169
+ const newJobId = createInlineJob({
170
+ agent: entry.agent,
171
+ prompt: entry.prompt,
172
+ threadKey: entry.threadKey,
173
+ creatorId: entry.creatorId,
174
+ });
175
+ if (newJobId > 0)
176
+ markJobReplacedBy(entry.jobId, newJobId);
177
+ return { kind: 'retried', newJobId, oldJobId: entry.jobId, entry };
178
+ }
179
+ /** Convenience for cli.ts: was the thread expecting a recovery reply just
180
+ * before we routed this message? Used to skip approval / reminder
181
+ * interceptors when the user is replying to a recovery prompt. */
182
+ export function hasPendingRecovery(threadKey) {
183
+ return pending.has(threadKey);
184
+ }
185
+ //# sourceMappingURL=job-recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"job-recovery.js","sourceRoot":"","sources":["../../src/core/job-recovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ import type { MessengerAdapter } from './types.js';
2
+ import { type OutboxKind, type OutboxPriority } from './outbox.js';
3
+ export type SinkKind = OutboxKind;
4
+ export type SinkPriority = OutboxPriority;
5
+ export interface SinkPayload {
6
+ threadId: string;
7
+ platform: string;
8
+ channelId: string;
9
+ /** For 'text': the message body (string).
10
+ * For 'card': the card object (will be JSON-encoded for storage).
11
+ * For 'typing': 'start' or 'stop'. */
12
+ payload: string | object;
13
+ kind: SinkKind;
14
+ /** Default 'normal'. 'high' tries a synchronous send first and falls
15
+ * back to the queue only on failure — use for approval / restart
16
+ * notices where 1s+ worker latency matters. */
17
+ priority?: SinkPriority;
18
+ /** Link this delivery back to a jobs row (inline job / scheduled job).
19
+ * Phase 2 will use this to mark jobs.status='delivered' when the row
20
+ * reaches the IM. */
21
+ jobId?: number | null;
22
+ /** Optional explicit thread key. Default is
23
+ * `${platform}:${channelId}:${threadId}` (matches session.ts). */
24
+ threadKey?: string;
25
+ /** Optional already-resolved adapter. When provided, sink uses it
26
+ * directly for the sync attempt instead of consulting the global
27
+ * registry. Required by callers that maintain their own resolution
28
+ * indirection (e.g. approval-router.install accepts a custom
29
+ * resolveMessenger hook used by tests + multi-tenant setups). The
30
+ * outbox row is still recorded with platform=<name>; if the sync
31
+ * attempt fails, the worker re-resolves via registry the normal way. */
32
+ adapter?: MessengerAdapter;
33
+ }
34
+ export interface SinkResult {
35
+ /** Row id in the outbox table. -1 if persistence is unavailable AND the
36
+ * sync fallback also failed (the rare double-failure case — caller has
37
+ * no recourse beyond logging). */
38
+ outboxId: number;
39
+ /** True = the message was sent inline before this call returned (only
40
+ * possible when priority='high' and the sync attempt succeeded).
41
+ * False = queued for the worker. */
42
+ immediate: boolean;
43
+ }
44
+ /** Resolve a platform name (e.g. 'wechat' / 'wechat-ilink' / 'feishu') to
45
+ * the actual MessengerAdapter. Exact match first, then a fuzzy `${name}-*`
46
+ * / `${name}_*` prefix walk so reminders.ts / scheduler / web callers can
47
+ * pass the generic platform ('wechat') and still hit 'wechat-ilink'. Mirrors
48
+ * the helper that already exists in reminders.ts:873 — kept in sink so the
49
+ * rest of the codebase can stop caring about adapter naming.
50
+ *
51
+ * Exported for tests; production callers go through deliver(). */
52
+ export declare function resolveMessenger(platform: string): MessengerAdapter | undefined;
53
+ export declare function deliver(p: SinkPayload): Promise<SinkResult>;
54
+ export declare function startWorker(): void;
55
+ export declare function stopWorker(): void;
56
+ /** Test hook — run one tick synchronously. */
57
+ export declare function _tickWorkerForTests(): Promise<void>;
58
+ export declare const sink: {
59
+ deliver: typeof deliver;
60
+ startWorker: typeof startWorker;
61
+ stopWorker: typeof stopWorker;
62
+ };
63
+ //# sourceMappingURL=message-sink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-sink.d.ts","sourceRoot":"","sources":["../../src/core/message-sink.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAGlD,OAAO,EAEL,KAAK,UAAU,EAAE,KAAK,cAAc,EACrC,MAAM,aAAa,CAAA;AAKpB,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAA;AACjC,MAAM,MAAM,YAAY,GAAG,cAAc,CAAA;AAEzC,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB;;2CAEuC;IACvC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB,IAAI,EAAE,QAAQ,CAAA;IACd;;oDAEgD;IAChD,QAAQ,CAAC,EAAE,YAAY,CAAA;IACvB;;0BAEsB;IACtB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB;uEACmE;IACnE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;6EAMyE;IACzE,OAAO,CAAC,EAAE,gBAAgB,CAAA;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB;;uCAEmC;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB;;yCAEqC;IACrC,SAAS,EAAE,OAAO,CAAA;CACnB;AAUD;;;;;;;mEAOmE;AACnE,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAS/E;AAoDD,wBAAsB,OAAO,CAAC,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAqFjE;AAwED,wBAAgB,WAAW,IAAI,IAAI,CAgBlC;AAED,wBAAgB,UAAU,IAAI,IAAI,CAOjC;AAED,8CAA8C;AAC9C,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAEzD;AAGD,eAAO,MAAM,IAAI;;;;CAIhB,CAAA"}
@@ -0,0 +1,296 @@
1
+ // Message Sink — single chokepoint for all outbound IM messages.
2
+ //
3
+ // Why exist:
4
+ // - Previously, sendMessage was called directly from 5 sites (cli main
5
+ // loop, reminders, approval-router x2, restart-completion). Any of
6
+ // these losing a reply due to messenger error / brief disconnect /
7
+ // adapter crash dropped the message irrecoverably.
8
+ // - Sink writes every outbound to the persistent outbox table first
9
+ // (or tries a sync send for priority=high then falls back to the
10
+ // queue on failure). A background worker drains the queue with
11
+ // exponential backoff. Replies survive crashes and reconnects.
12
+ //
13
+ // Design constraints (per docs/task-recovery-plan.md):
14
+ // - Reminder polish, memo polish, and any other content-shaping logic
15
+ // happens *upstream* of sink — sink only receives plain payload + IM
16
+ // coordinates. Don't add transformations here.
17
+ // - Worker is single-threaded to preserve per-thread ordering. If
18
+ // throughput becomes a bottleneck, partition by thread_key.
19
+ // - Approval cards with messageId return paths (sendApprovalCard /
20
+ // editApprovalCard) do NOT go through sink — they need the synchronous
21
+ // messageId for the card-edit flow. Only plain text fallbacks from
22
+ // approval-router migrate to sink.
23
+ // - Don't bring up the worker until messengers have finished start()
24
+ // (cli.ts calls sink.startWorker() after registry.start()). Otherwise
25
+ // the very first tick after restart hits "messenger not registered"
26
+ // for every queued row.
27
+ import { registry } from './registry.js';
28
+ import { logger as rootLogger } from './logger.js';
29
+ import { enqueue, pickPending, markDelivered, markFailed, } from './outbox.js';
30
+ import { markJobDelivered, markJobFailed } from './job-board.js';
31
+ const log = rootLogger.child({ component: 'message-sink' });
32
+ function defaultThreadKey(p) {
33
+ return p.threadKey ?? `${p.platform}:${p.channelId}:${p.threadId}`;
34
+ }
35
+ function payloadToString(payload) {
36
+ return typeof payload === 'string' ? payload : JSON.stringify(payload);
37
+ }
38
+ /** Resolve a platform name (e.g. 'wechat' / 'wechat-ilink' / 'feishu') to
39
+ * the actual MessengerAdapter. Exact match first, then a fuzzy `${name}-*`
40
+ * / `${name}_*` prefix walk so reminders.ts / scheduler / web callers can
41
+ * pass the generic platform ('wechat') and still hit 'wechat-ilink'. Mirrors
42
+ * the helper that already exists in reminders.ts:873 — kept in sink so the
43
+ * rest of the codebase can stop caring about adapter naming.
44
+ *
45
+ * Exported for tests; production callers go through deliver(). */
46
+ export function resolveMessenger(platform) {
47
+ const exact = registry.getMessenger(platform);
48
+ if (exact)
49
+ return exact;
50
+ for (const name of registry.listMessengers()) {
51
+ if (name === platform || name.startsWith(`${platform}-`) || name.startsWith(`${platform}_`)) {
52
+ return registry.getMessenger(name);
53
+ }
54
+ }
55
+ return undefined;
56
+ }
57
+ /** Attempt the actual messenger send. Throws on failure so the worker
58
+ * loop can record it via markFailed(). For 'card' on adapters without
59
+ * sendCard, we fall back to a JSON-stringified text body so the message
60
+ * is still surfaced (better than dropping silently).
61
+ *
62
+ * If `overrideAdapter` is provided the sync path uses it directly (the
63
+ * caller has already done resolution); the worker tick never passes
64
+ * one and always re-resolves via registry. */
65
+ async function doSend(row, overrideAdapter) {
66
+ const adapter = overrideAdapter ?? resolveMessenger(row.platform);
67
+ if (!adapter) {
68
+ throw new Error(`messenger not registered: ${row.platform}`);
69
+ }
70
+ if (row.kind === 'text') {
71
+ await adapter.sendMessage(row.thread_id, row.payload);
72
+ return;
73
+ }
74
+ if (row.kind === 'card') {
75
+ if (adapter.sendCard) {
76
+ let cardObj;
77
+ try {
78
+ cardObj = JSON.parse(row.payload);
79
+ }
80
+ catch {
81
+ throw new Error('card payload is not valid JSON');
82
+ }
83
+ await adapter.sendCard(row.thread_id, cardObj);
84
+ return;
85
+ }
86
+ // No native card support → degrade to text. The adapter's sendMessage
87
+ // path is required by the type contract so this is always safe.
88
+ await adapter.sendMessage(row.thread_id, row.payload);
89
+ return;
90
+ }
91
+ if (row.kind === 'typing') {
92
+ if (adapter.sendTyping) {
93
+ const isTyping = row.payload === 'start';
94
+ await adapter.sendTyping(row.thread_id, isTyping);
95
+ }
96
+ // Adapter lacks typing → silently no-op. Typing indicators are
97
+ // best-effort; never re-queue.
98
+ return;
99
+ }
100
+ throw new Error(`unknown outbox kind: ${row.kind}`);
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // Public API: deliver()
104
+ // ---------------------------------------------------------------------------
105
+ export async function deliver(p) {
106
+ const priority = p.priority ?? 'normal';
107
+ const threadKey = defaultThreadKey(p);
108
+ const payloadStr = payloadToString(p.payload);
109
+ // Typing indicators are pure UX hints — never persist them. If the worker
110
+ // delivered a "start typing" row two minutes later it would be ridiculous.
111
+ // We fire-and-forget directly; failure is ignored.
112
+ if (p.kind === 'typing') {
113
+ const adapter = p.adapter ?? resolveMessenger(p.platform);
114
+ if (adapter?.sendTyping) {
115
+ try {
116
+ await adapter.sendTyping(p.threadId, payloadStr === 'start');
117
+ }
118
+ catch (err) {
119
+ log.debug({ event: 'sink.typing_failed', err: err instanceof Error ? err.message : String(err) }, 'typing send failed (ignored)');
120
+ }
121
+ }
122
+ return { outboxId: -1, immediate: true };
123
+ }
124
+ if (priority === 'high') {
125
+ // Try sync first. Build a synthetic row so doSend() works against the
126
+ // same code path the worker uses. attempts=0 — we haven't recorded
127
+ // anything yet.
128
+ const synthetic = {
129
+ id: -1, job_id: p.jobId ?? null, thread_key: threadKey,
130
+ platform: p.platform, channel_id: p.channelId, thread_id: p.threadId,
131
+ payload: payloadStr, kind: p.kind, priority,
132
+ status: 'pending', attempts: 0,
133
+ last_attempt_at: null, last_error: null, delivered_at: null,
134
+ next_attempt_at: null, created_at: new Date().toISOString(),
135
+ };
136
+ try {
137
+ await doSend(synthetic, p.adapter);
138
+ // Sync win — record a delivered row for audit + Phase 2 jobs.delivered_at hook.
139
+ const id = enqueue({
140
+ threadKey, platform: p.platform, channelId: p.channelId, threadId: p.threadId,
141
+ payload: payloadStr, kind: p.kind, priority, jobId: p.jobId ?? null,
142
+ });
143
+ if (id > 0)
144
+ markDelivered(id);
145
+ // Phase 2: cascade delivery confirmation to the linked inline job
146
+ // so its row transitions completed → delivered alongside the outbox row.
147
+ if (p.jobId && p.jobId > 0)
148
+ markJobDelivered(p.jobId);
149
+ return { outboxId: id, immediate: true };
150
+ }
151
+ catch (err) {
152
+ log.warn({
153
+ event: 'sink.high_priority_sync_failed',
154
+ platform: p.platform,
155
+ threadId: p.threadId,
156
+ err: err instanceof Error ? err.message : String(err),
157
+ }, 'high-priority sync send failed; falling back to outbox queue');
158
+ // Fall through to enqueue + worker tries again later.
159
+ }
160
+ }
161
+ // priority='normal' (or 'high' fallback): enqueue and let the worker drain.
162
+ const id = enqueue({
163
+ threadKey, platform: p.platform, channelId: p.channelId, threadId: p.threadId,
164
+ payload: payloadStr, kind: p.kind, priority, jobId: p.jobId ?? null,
165
+ });
166
+ if (id < 0) {
167
+ // Outbox itself is broken (sqlite disabled, e.g. on bun without the
168
+ // native module). Last-ditch: send directly so the user still gets
169
+ // a reply on this happy path. Failure here is logged and dropped —
170
+ // there is no remaining mechanism to recover.
171
+ log.error({ event: 'sink.outbox_unavailable', platform: p.platform, threadId: p.threadId }, 'outbox enqueue returned -1; attempting last-ditch direct send');
172
+ try {
173
+ await doSend({
174
+ id: -1, job_id: p.jobId ?? null, thread_key: threadKey,
175
+ platform: p.platform, channel_id: p.channelId, thread_id: p.threadId,
176
+ payload: payloadStr, kind: p.kind, priority,
177
+ status: 'pending', attempts: 0,
178
+ last_attempt_at: null, last_error: null, delivered_at: null,
179
+ next_attempt_at: null, created_at: new Date().toISOString(),
180
+ }, p.adapter);
181
+ return { outboxId: -1, immediate: true };
182
+ }
183
+ catch (err) {
184
+ log.error({ event: 'sink.last_ditch_failed', err: err instanceof Error ? err.message : String(err) }, 'last-ditch send failed; message is lost');
185
+ return { outboxId: -1, immediate: false };
186
+ }
187
+ }
188
+ return { outboxId: id, immediate: false };
189
+ }
190
+ // ---------------------------------------------------------------------------
191
+ // Worker
192
+ // ---------------------------------------------------------------------------
193
+ /** How often the worker pulls pending rows. Override with IMHUB_OUTBOX_TICK_MS. */
194
+ function resolveTickMs() {
195
+ const raw = process.env.IMHUB_OUTBOX_TICK_MS;
196
+ if (raw) {
197
+ const n = parseInt(raw, 10);
198
+ if (Number.isFinite(n) && n > 0)
199
+ return n;
200
+ }
201
+ return 1000;
202
+ }
203
+ /** Max rows pulled per tick. Worker is single-threaded so this caps how
204
+ * long one tick can take before it self-yields. */
205
+ function resolveBatchSize() {
206
+ const raw = process.env.IMHUB_OUTBOX_BATCH;
207
+ if (raw) {
208
+ const n = parseInt(raw, 10);
209
+ if (Number.isFinite(n) && n > 0)
210
+ return n;
211
+ }
212
+ return 10;
213
+ }
214
+ let workerTimer = null;
215
+ let workerBusy = false;
216
+ let workerStopRequested = false;
217
+ async function workerTick() {
218
+ if (workerBusy)
219
+ return; // previous tick still running
220
+ workerBusy = true;
221
+ try {
222
+ const batch = pickPending(resolveBatchSize());
223
+ for (const row of batch) {
224
+ if (workerStopRequested)
225
+ break;
226
+ try {
227
+ await doSend(row);
228
+ markDelivered(row.id);
229
+ // Phase 2: cascade delivery → linked inline job. row.job_id is
230
+ // populated when sink.deliver was called with a SinkPayload.jobId.
231
+ // markJobDelivered no-ops on id<=0 / non-'completed' rows so this
232
+ // is safe even when the job was cancelled mid-flight.
233
+ if (row.job_id && row.job_id > 0)
234
+ markJobDelivered(row.job_id);
235
+ log.debug({ event: 'sink.delivered', id: row.id, platform: row.platform, kind: row.kind, jobId: row.job_id }, 'outbox row delivered');
236
+ }
237
+ catch (err) {
238
+ const errMsg = err instanceof Error ? err.message : String(err);
239
+ const { gaveUp, attempts } = markFailed(row.id, errMsg);
240
+ log.warn({
241
+ event: gaveUp ? 'sink.giving_up' : 'sink.retry_scheduled',
242
+ id: row.id, platform: row.platform, kind: row.kind,
243
+ attempts, err: errMsg,
244
+ }, gaveUp
245
+ ? `outbox row ${row.id} gave up after ${attempts} attempts: ${errMsg}`
246
+ : `outbox row ${row.id} failed (attempt ${attempts}), will retry`);
247
+ // Phase 2: when outbox gives up, the linked job's reply will never
248
+ // reach the user. Mark the job 'failed' so /jobs accurately reflects
249
+ // the terminal state. Non-giving-up failures stay 'completed' — the
250
+ // worker will retry soon and we don't want to flicker the status.
251
+ if (gaveUp && row.job_id && row.job_id > 0) {
252
+ markJobFailed(row.job_id, `outbox gave up: ${errMsg}`);
253
+ }
254
+ }
255
+ }
256
+ }
257
+ finally {
258
+ workerBusy = false;
259
+ }
260
+ }
261
+ export function startWorker() {
262
+ if (workerTimer !== null)
263
+ return;
264
+ workerStopRequested = false;
265
+ const ms = resolveTickMs();
266
+ workerTimer = setInterval(() => {
267
+ void workerTick().catch((err) => {
268
+ log.error({ event: 'sink.worker_unhandled', err: err instanceof Error ? err.message : String(err) }, 'worker tick threw unhandled');
269
+ });
270
+ }, ms);
271
+ // Don't keep the process alive just for the worker — let SIGTERM / SIGINT
272
+ // shut things down naturally.
273
+ if (typeof workerTimer === 'object' && workerTimer && 'unref' in workerTimer) {
274
+ workerTimer.unref();
275
+ }
276
+ log.info({ event: 'sink.worker_started', tickMs: ms }, 'outbox worker started');
277
+ }
278
+ export function stopWorker() {
279
+ workerStopRequested = true;
280
+ if (workerTimer) {
281
+ clearInterval(workerTimer);
282
+ workerTimer = null;
283
+ log.info({ event: 'sink.worker_stopped' }, 'outbox worker stopped');
284
+ }
285
+ }
286
+ /** Test hook — run one tick synchronously. */
287
+ export async function _tickWorkerForTests() {
288
+ await workerTick();
289
+ }
290
+ // Convenience namespace export so call sites read `sink.deliver(...)`.
291
+ export const sink = {
292
+ deliver,
293
+ startWorker,
294
+ stopWorker,
295
+ };
296
+ //# sourceMappingURL=message-sink.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-sink.js","sourceRoot":"","sources":["../../src/core/message-sink.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,EAAE;AACF,aAAa;AACb,yEAAyE;AACzE,uEAAuE;AACvE,uEAAuE;AACvE,uDAAuD;AACvD,sEAAsE;AACtE,qEAAqE;AACrE,mEAAmE;AACnE,mEAAmE;AACnE,EAAE;AACF,uDAAuD;AACvD,wEAAwE;AACxE,yEAAyE;AACzE,mDAAmD;AACnD,oEAAoE;AACpE,gEAAgE;AAChE,qEAAqE;AACrE,2EAA2E;AAC3E,uEAAuE;AACvE,uCAAuC;AACvC,uEAAuE;AACvE,0EAA0E;AAC1E,wEAAwE;AACxE,4BAA4B;AAG5B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EACL,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,GAEhD,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEhE,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAA;AA8C3D,SAAS,gBAAgB,CAAC,CAAc;IACtC,OAAO,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAA;AACpE,CAAC;AAED,SAAS,eAAe,CAAC,OAA+B;IACtD,OAAO,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;AACxE,CAAC;AAED;;;;;;;mEAOmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,KAAK,GAAG,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAA;IAC7C,IAAI,KAAK;QAAE,OAAO,KAAK,CAAA;IACvB,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,cAAc,EAAE,EAAE,CAAC;QAC7C,IAAI,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,CAAC,EAAE,CAAC;YAC5F,OAAO,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,CAAA;QACpC,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;;;+CAO+C;AAC/C,KAAK,UAAU,MAAM,CAAC,GAAc,EAAE,eAAkC;IACtE,MAAM,OAAO,GAAiC,eAAe,IAAI,gBAAgB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IAC/F,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,6BAA6B,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAA;IAC9D,CAAC;IAED,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACxB,MAAM,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;QACrD,OAAM;IACR,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QACxB,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;YACrB,IAAI,OAAgB,CAAA;YACpB,IAAI,CAAC;gBACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACnC,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAA;YACnD,CAAC;YACD,MAAM,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;YAC9C,OAAM;QACR,CAAC;QACD,sEAAsE;QACtE,gEAAgE;QAChE,MAAM,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;QACrD,OAAM;IACR,CAAC;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC1B,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC;YACvB,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,KAAK,OAAO,CAAA;YACxC,MAAM,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;QACnD,CAAC;QACD,+DAA+D;QAC/D,+BAA+B;QAC/B,OAAM;IACR,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,CAAC,IAAI,EAAE,CAAC,CAAA;AACrD,CAAC;AAED,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,CAAc;IAC1C,MAAM,QAAQ,GAAiB,CAAC,CAAC,QAAQ,IAAI,QAAQ,CAAA;IACrD,MAAM,SAAS,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAA;IACrC,MAAM,UAAU,GAAG,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;IAE7C,0EAA0E;IAC1E,2EAA2E;IAC3E,mDAAmD;IACnD,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,IAAI,gBAAgB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAA;QACzD,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC;YACxB,IAAI,CAAC;gBACH,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE,UAAU,KAAK,OAAO,CAAC,CAAA;YAC9D,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAC9F,8BAA8B,CAAC,CAAA;YACnC,CAAC;QACH,CAAC;QACD,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAA;IAC1C,CAAC;IAED,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;QACxB,sEAAsE;QACtE,mEAAmE;QACnE,gBAAgB;QAChB,MAAM,SAAS,GAAc;YAC3B,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,EAAE,UAAU,EAAE,SAAS;YACtD,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,QAAQ;YACpE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ;YAC3C,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;YAC9B,eAAe,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI;YAC3D,eAAe,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SAC5D,CAAA;QACD,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;YAClC,gFAAgF;YAChF,MAAM,EAAE,GAAG,OAAO,CAAC;gBACjB,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBAC7E,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI;aACpE,CAAC,CAAA;YACF,IAAI,EAAE,GAAG,CAAC;gBAAE,aAAa,CAAC,EAAE,CAAC,CAAA;YAC7B,kEAAkE;YAClE,yEAAyE;YACzE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC;gBAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;YACrD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAA;QAC1C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,IAAI,CAAC;gBACP,KAAK,EAAE,gCAAgC;gBACvC,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;aACtD,EAAE,8DAA8D,CAAC,CAAA;YAClE,sDAAsD;QACxD,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,MAAM,EAAE,GAAG,OAAO,CAAC;QACjB,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ;QAC7E,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI;KACpE,CAAC,CAAA;IACF,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACX,oEAAoE;QACpE,mEAAmE;QACnE,mEAAmE;QACnE,8CAA8C;QAC9C,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,EACxF,+DAA+D,CAAC,CAAA;QAClE,IAAI,CAAC;YACH,MAAM,MAAM,CAAC;gBACX,EAAE,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,EAAE,UAAU,EAAE,SAAS;gBACtD,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,QAAQ;gBACpE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,QAAQ;gBAC3C,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;gBAC9B,eAAe,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI;gBAC3D,eAAe,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;aAC5D,EAAE,CAAC,CAAC,OAAO,CAAC,CAAA;YACb,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAA;QAC1C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,wBAAwB,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAClG,yCAAyC,CAAC,CAAA;YAC5C,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAA;QAC3C,CAAC;IACH,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAA;AAC3C,CAAC;AAED,8EAA8E;AAC9E,SAAS;AACT,8EAA8E;AAE9E,mFAAmF;AACnF,SAAS,aAAa;IACpB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAA;IAC5C,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QAC3B,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,CAAC,CAAA;IAC3C,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;oDACoD;AACpD,SAAS,gBAAgB;IACvB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAA;IAC1C,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QAC3B,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,CAAC,CAAA;IAC3C,CAAC;IACD,OAAO,EAAE,CAAA;AACX,CAAC;AAED,IAAI,WAAW,GAA0C,IAAI,CAAA;AAC7D,IAAI,UAAU,GAAG,KAAK,CAAA;AACtB,IAAI,mBAAmB,GAAG,KAAK,CAAA;AAE/B,KAAK,UAAU,UAAU;IACvB,IAAI,UAAU;QAAE,OAAM,CAAE,8BAA8B;IACtD,UAAU,GAAG,IAAI,CAAA;IACjB,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,WAAW,CAAC,gBAAgB,EAAE,CAAC,CAAA;QAC7C,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;YACxB,IAAI,mBAAmB;gBAAE,MAAK;YAC9B,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,GAAG,CAAC,CAAA;gBACjB,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBACrB,+DAA+D;gBAC/D,mEAAmE;gBACnE,kEAAkE;gBAClE,sDAAsD;gBACtD,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;oBAAE,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;gBAC9D,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,EAC1G,sBAAsB,CAAC,CAAA;YAC3B,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,MAAM,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;gBAC/D,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;gBACvD,GAAG,CAAC,IAAI,CAAC;oBACP,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,sBAAsB;oBACzD,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI;oBAClD,QAAQ,EAAE,GAAG,EAAE,MAAM;iBACtB,EAAE,MAAM;oBACP,CAAC,CAAC,cAAc,GAAG,CAAC,EAAE,kBAAkB,QAAQ,cAAc,MAAM,EAAE;oBACtE,CAAC,CAAC,cAAc,GAAG,CAAC,EAAE,oBAAoB,QAAQ,eAAe,CAAC,CAAA;gBACpE,mEAAmE;gBACnE,qEAAqE;gBACrE,oEAAoE;gBACpE,kEAAkE;gBAClE,IAAI,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC3C,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,mBAAmB,MAAM,EAAE,CAAC,CAAA;gBACxD,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;YAAS,CAAC;QACT,UAAU,GAAG,KAAK,CAAA;IACpB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,IAAI,WAAW,KAAK,IAAI;QAAE,OAAM;IAChC,mBAAmB,GAAG,KAAK,CAAA;IAC3B,MAAM,EAAE,GAAG,aAAa,EAAE,CAAA;IAC1B,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,KAAK,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YAC9B,GAAG,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,GAAG,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EACjG,6BAA6B,CAAC,CAAA;QAClC,CAAC,CAAC,CAAA;IACJ,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,0EAA0E;IAC1E,8BAA8B;IAC9B,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,IAAI,OAAO,IAAI,WAAW,EAAE,CAAC;QAC5E,WAAqC,CAAC,KAAK,EAAE,CAAA;IAChD,CAAC;IACD,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,uBAAuB,CAAC,CAAA;AACjF,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,mBAAmB,GAAG,IAAI,CAAA;IAC1B,IAAI,WAAW,EAAE,CAAC;QAChB,aAAa,CAAC,WAAW,CAAC,CAAA;QAC1B,WAAW,GAAG,IAAI,CAAA;QAClB,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,uBAAuB,CAAC,CAAA;IACrE,CAAC;AACH,CAAC;AAED,8CAA8C;AAC9C,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,MAAM,UAAU,EAAE,CAAA;AACpB,CAAC;AAED,uEAAuE;AACvE,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,OAAO;IACP,WAAW;IACX,UAAU;CACX,CAAA"}
@@ -0,0 +1,71 @@
1
+ import type Database from 'better-sqlite3';
2
+ export type OutboxStatus = 'pending' | 'delivered' | 'giving_up';
3
+ export type OutboxKind = 'text' | 'card' | 'typing';
4
+ export type OutboxPriority = 'high' | 'normal';
5
+ export interface OutboxRow {
6
+ id: number;
7
+ job_id: number | null;
8
+ thread_key: string;
9
+ platform: string;
10
+ channel_id: string;
11
+ thread_id: string;
12
+ payload: string;
13
+ kind: OutboxKind;
14
+ priority: OutboxPriority;
15
+ status: OutboxStatus;
16
+ attempts: number;
17
+ last_attempt_at: string | null;
18
+ last_error: string | null;
19
+ delivered_at: string | null;
20
+ next_attempt_at: string | null;
21
+ created_at: string;
22
+ }
23
+ export interface EnqueueOpts {
24
+ threadKey: string;
25
+ platform: string;
26
+ channelId: string;
27
+ threadId: string;
28
+ /** For text/card: serialized payload (text body or JSON of card). For typing: unused, pass ''. */
29
+ payload: string;
30
+ kind: OutboxKind;
31
+ priority?: OutboxPriority;
32
+ jobId?: number | null;
33
+ }
34
+ export declare const MAX_ATTEMPTS: 6;
35
+ /** Stop the retention sweep (graceful shutdown / tests). */
36
+ export declare function stopOutboxRetentionSweep(): void;
37
+ /** Close the underlying SQLite handle. Called by cli.ts on graceful shutdown. */
38
+ export declare function closeOutboxDb(): void;
39
+ /** Insert a new pending row. Returns the row id, or -1 if the DB is broken
40
+ * (caller should fall back to direct send). */
41
+ export declare function enqueue(opts: EnqueueOpts): number;
42
+ /** Pick up to `limit` pending rows whose next_attempt_at is now-or-past,
43
+ * highest priority first. Worker calls this on each tick. */
44
+ export declare function pickPending(limit?: number): OutboxRow[];
45
+ /** Mark a row delivered. */
46
+ export declare function markDelivered(id: number): void;
47
+ /** Record a failed attempt. If attempts have exhausted the backoff schedule
48
+ * the row transitions to 'giving_up' (and no longer drains). Otherwise it
49
+ * stays 'pending' with next_attempt_at = now + backoff. */
50
+ export declare function markFailed(id: number, error: string): {
51
+ gaveUp: boolean;
52
+ attempts: number;
53
+ };
54
+ /** Get one row by id. */
55
+ export declare function getOutbox(id: number): OutboxRow | null;
56
+ /** List rows in 'giving_up' or 'pending' state for /outbox command. */
57
+ export declare function listOutbox(opts?: {
58
+ status?: OutboxStatus;
59
+ limit?: number;
60
+ threadKey?: string;
61
+ }): OutboxRow[];
62
+ /** Aggregate counts for /outbox status. */
63
+ export declare function getOutboxStats(): Record<OutboxStatus, number>;
64
+ /** Resurrect a 'giving_up' row back to 'pending' so the worker tries again.
65
+ * Resets attempts to 0 so the full backoff ladder applies again. */
66
+ export declare function retryGivingUp(id: number): boolean;
67
+ /** Prune delivered + giving_up rows older than the retention window. */
68
+ export declare function pruneOldOutbox(d?: Database.Database): number;
69
+ /** Test hook: wipe everything. Only safe in unit tests. */
70
+ export declare function _resetOutboxForTests(): void;
71
+ //# sourceMappingURL=outbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.d.ts","sourceRoot":"","sources":["../../src/core/outbox.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAA;AAQ1C,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAA;AAChE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;AACnD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,QAAQ,CAAA;AAE9C,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,UAAU,CAAA;IAChB,QAAQ,EAAE,cAAc,CAAA;IACxB,MAAM,EAAE,YAAY,CAAA;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,kGAAkG;IAClG,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,UAAU,CAAA;IAChB,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAMD,eAAO,MAAM,YAAY,GAAyB,CAAA;AA6DlD,4DAA4D;AAC5D,wBAAgB,wBAAwB,IAAI,IAAI,CAK/C;AAED,iFAAiF;AACjF,wBAAgB,aAAa,IAAI,IAAI,CAEpC;AAMD;gDACgD;AAChD,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CA2BjD;AAED;8DAC8D;AAC9D,wBAAgB,WAAW,CAAC,KAAK,GAAE,MAAW,GAAG,SAAS,EAAE,CAqB3D;AAED,4BAA4B;AAC5B,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAoB9C;AAED;;4DAE4D;AAC5D,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAwC3F;AAED,yBAAyB;AACzB,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAStD;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,YAAY,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,SAAS,EAAE,CA0BhH;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,IAAI,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAY7D;AAED;qEACqE;AACrE,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAgBjD;AAED,wEAAwE;AACxE,wBAAgB,cAAc,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,QAAQ,GAAG,MAAM,CAyB5D;AAED,2DAA2D;AAC3D,wBAAgB,oBAAoB,IAAI,IAAI,CAI3C"}