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.
- package/CHANGELOG.md +147 -0
- package/README.md +8 -1
- package/README.zh-CN.md +8 -1
- package/dist/cli.js +265 -30
- package/dist/cli.js.map +1 -1
- package/dist/core/a2a.d.ts +55 -0
- package/dist/core/a2a.d.ts.map +1 -0
- package/dist/core/a2a.js +254 -0
- package/dist/core/a2a.js.map +1 -0
- package/dist/core/approval-bus.d.ts +40 -0
- package/dist/core/approval-bus.d.ts.map +1 -1
- package/dist/core/approval-bus.js +125 -0
- package/dist/core/approval-bus.js.map +1 -1
- package/dist/core/approval-router.d.ts.map +1 -1
- package/dist/core/approval-router.js +31 -2
- package/dist/core/approval-router.js.map +1 -1
- package/dist/core/artifacts.d.ts +86 -0
- package/dist/core/artifacts.d.ts.map +1 -0
- package/dist/core/artifacts.js +306 -0
- package/dist/core/artifacts.js.map +1 -0
- package/dist/core/commands/a2a.d.ts +3 -0
- package/dist/core/commands/a2a.d.ts.map +1 -0
- package/dist/core/commands/a2a.js +162 -0
- package/dist/core/commands/a2a.js.map +1 -0
- package/dist/core/commands/job.d.ts.map +1 -1
- package/dist/core/commands/job.js +11 -2
- package/dist/core/commands/job.js.map +1 -1
- package/dist/core/commands/outbox.d.ts +3 -0
- package/dist/core/commands/outbox.d.ts.map +1 -0
- package/dist/core/commands/outbox.js +92 -0
- package/dist/core/commands/outbox.js.map +1 -0
- package/dist/core/job-board.d.ts +122 -1
- package/dist/core/job-board.d.ts.map +1 -1
- package/dist/core/job-board.js +432 -21
- package/dist/core/job-board.js.map +1 -1
- package/dist/core/job-recovery.d.ts +48 -0
- package/dist/core/job-recovery.d.ts.map +1 -0
- package/dist/core/job-recovery.js +185 -0
- package/dist/core/job-recovery.js.map +1 -0
- package/dist/core/message-sink.d.ts +63 -0
- package/dist/core/message-sink.d.ts.map +1 -0
- package/dist/core/message-sink.js +296 -0
- package/dist/core/message-sink.js.map +1 -0
- package/dist/core/outbox.d.ts +71 -0
- package/dist/core/outbox.d.ts.map +1 -0
- package/dist/core/outbox.js +301 -0
- package/dist/core/outbox.js.map +1 -0
- package/dist/core/reminders.d.ts.map +1 -1
- package/dist/core/reminders.js +12 -1
- package/dist/core/reminders.js.map +1 -1
- package/dist/core/restart-completion.d.ts.map +1 -1
- package/dist/core/restart-completion.js +18 -1
- package/dist/core/restart-completion.js.map +1 -1
- package/dist/core/router.d.ts +8 -0
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +16 -0
- package/dist/core/router.js.map +1 -1
- package/dist/core/types.d.ts +22 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/plugins/agents/claude-code/index.d.ts.map +1 -1
- package/dist/plugins/agents/claude-code/index.js +5 -0
- package/dist/plugins/agents/claude-code/index.js.map +1 -1
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts +46 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.d.ts.map +1 -1
- package/dist/plugins/agents/claude-code/mcp-approval-server.js +158 -0
- package/dist/plugins/agents/claude-code/mcp-approval-server.js.map +1 -1
- package/dist/plugins/agents/codex/index.d.ts.map +1 -1
- package/dist/plugins/agents/codex/index.js +5 -0
- package/dist/plugins/agents/codex/index.js.map +1 -1
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.d.ts.map +1 -1
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.js +5 -0
- package/dist/plugins/agents/opencode/opencode-stdio-adapter.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +28 -16
- package/dist/web/server.js.map +1 -1
- 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"}
|