@agenticmail/claudecode 0.2.5 → 0.2.7
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/dist/{chunk-ZTSWLNUP.js → chunk-4EHV3I6W.js} +1 -1
- package/dist/{chunk-SBP7MJP2.js → chunk-4VQP57SO.js} +29 -0
- package/dist/{chunk-D5SE7UVB.js → chunk-JOK76WRC.js} +257 -12
- package/dist/{chunk-2GPBHK2M.js → chunk-JUEHPFKY.js} +1 -1
- package/dist/{chunk-O4H76K3B.js → chunk-Q5BA2J2C.js} +1 -1
- package/dist/{chunk-PTLX7SQV.js → chunk-ZFLOFC6P.js} +4 -4
- package/dist/cli.js +4 -4
- package/dist/dispatcher-bin.js +2 -2
- package/dist/dispatcher.d.ts +45 -0
- package/dist/dispatcher.js +2 -2
- package/dist/http-routes.js +5 -5
- package/dist/index.js +6 -6
- package/dist/install.js +2 -2
- package/dist/status.js +2 -2
- package/dist/uninstall.js +2 -2
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
listAccounts,
|
|
13
13
|
renderSubagentMarkdown,
|
|
14
14
|
resolveConfig
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-4VQP57SO.js";
|
|
16
16
|
|
|
17
17
|
// src/install.ts
|
|
18
18
|
import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, unlinkSync } from "fs";
|
|
@@ -82,6 +82,33 @@ async function ensureAccount(apiUrl, masterKey, name, role = "assistant") {
|
|
|
82
82
|
async function deleteAccount(apiUrl, masterKey, id) {
|
|
83
83
|
await request(apiUrl, masterKey, `/accounts/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
84
84
|
}
|
|
85
|
+
async function listInboxForAgent(apiUrl, agentApiKey, opts = {}) {
|
|
86
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 50, 100));
|
|
87
|
+
const url = `${apiUrl.replace(/\/$/, "")}/api/agenticmail/mail/inbox?limit=${limit}`;
|
|
88
|
+
const res = await fetch(url, {
|
|
89
|
+
headers: { "Authorization": `Bearer ${agentApiKey}` },
|
|
90
|
+
signal: AbortSignal.timeout(15e3)
|
|
91
|
+
});
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw new AgenticMailApiError(res.status, `mail/inbox HTTP ${res.status}`);
|
|
94
|
+
}
|
|
95
|
+
const data = await res.json();
|
|
96
|
+
return data?.messages ?? [];
|
|
97
|
+
}
|
|
98
|
+
async function listPendingTasksForAgent(apiUrl, agentApiKey) {
|
|
99
|
+
const url = `${apiUrl.replace(/\/$/, "")}/api/agenticmail/tasks/pending`;
|
|
100
|
+
const res = await fetch(url, {
|
|
101
|
+
headers: { "Authorization": `Bearer ${agentApiKey}` },
|
|
102
|
+
signal: AbortSignal.timeout(15e3)
|
|
103
|
+
});
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
if (res.status === 404) return [];
|
|
106
|
+
throw new AgenticMailApiError(res.status, `tasks/pending HTTP ${res.status}`);
|
|
107
|
+
}
|
|
108
|
+
const data = await res.json();
|
|
109
|
+
const rows = Array.isArray(data) ? data : data?.tasks ?? [];
|
|
110
|
+
return rows.filter((t) => (t.status ?? "").toLowerCase() === "pending");
|
|
111
|
+
}
|
|
85
112
|
|
|
86
113
|
// src/config.ts
|
|
87
114
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -255,6 +282,8 @@ export {
|
|
|
255
282
|
getAccountByName,
|
|
256
283
|
ensureAccount,
|
|
257
284
|
deleteAccount,
|
|
285
|
+
listInboxForAgent,
|
|
286
|
+
listPendingTasksForAgent,
|
|
258
287
|
resolveConfig,
|
|
259
288
|
MANAGED_BY_MARKER,
|
|
260
289
|
renderPersonaBody,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
listAccounts,
|
|
3
|
+
listInboxForAgent,
|
|
4
|
+
listPendingTasksForAgent,
|
|
3
5
|
renderPersonaBody,
|
|
4
6
|
resolveConfig
|
|
5
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-4VQP57SO.js";
|
|
6
8
|
|
|
7
9
|
// src/persona-loader.ts
|
|
8
10
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -35,10 +37,129 @@ function loadPersonaForAgent(opts) {
|
|
|
35
37
|
return { body, source: "generated" };
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
// src/dispatcher.ts
|
|
39
|
-
import {
|
|
40
|
-
import { join as join2 } from "path";
|
|
40
|
+
// src/dispatcher-state.ts
|
|
41
|
+
import { readFileSync as readFileSync2, writeFileSync, renameSync, mkdirSync, existsSync as existsSync2 } from "fs";
|
|
42
|
+
import { dirname, join as join2 } from "path";
|
|
41
43
|
import { homedir } from "os";
|
|
44
|
+
var STATE_VERSION = 1;
|
|
45
|
+
var SEEN_UIDS_CAP = 256;
|
|
46
|
+
var FLUSH_INTERVAL_MS = 2e3;
|
|
47
|
+
function emptyState() {
|
|
48
|
+
return { version: STATE_VERSION, savedAtMs: 0, accounts: {} };
|
|
49
|
+
}
|
|
50
|
+
function defaultStatePath() {
|
|
51
|
+
return join2(homedir(), ".agenticmail", "dispatcher-state.json");
|
|
52
|
+
}
|
|
53
|
+
var DispatcherState = class {
|
|
54
|
+
path;
|
|
55
|
+
state = emptyState();
|
|
56
|
+
dirty = false;
|
|
57
|
+
flushTimer = null;
|
|
58
|
+
constructor(opts) {
|
|
59
|
+
this.path = opts?.path ?? defaultStatePath();
|
|
60
|
+
this.load();
|
|
61
|
+
}
|
|
62
|
+
/** Read the state file from disk. Missing / corrupt → empty state. */
|
|
63
|
+
load() {
|
|
64
|
+
try {
|
|
65
|
+
if (!existsSync2(this.path)) return;
|
|
66
|
+
const raw = readFileSync2(this.path, "utf8");
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
if (parsed && typeof parsed === "object" && parsed.version === STATE_VERSION) {
|
|
69
|
+
const accounts = {};
|
|
70
|
+
for (const [id, cursor] of Object.entries(parsed.accounts ?? {})) {
|
|
71
|
+
if (cursor && typeof cursor === "object" && typeof cursor.lastSeenUid === "number" && Array.isArray(cursor.seenUids)) {
|
|
72
|
+
const c = cursor;
|
|
73
|
+
accounts[id] = {
|
|
74
|
+
lastSeenUid: c.lastSeenUid,
|
|
75
|
+
seenUids: c.seenUids.filter((u) => Number.isFinite(u) && u > 0).slice(-SEEN_UIDS_CAP)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
this.state = { version: STATE_VERSION, savedAtMs: parsed.savedAtMs ?? 0, accounts };
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
this.state = emptyState();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Read the cursor for one account. Missing → undefined. */
|
|
86
|
+
getCursor(accountId) {
|
|
87
|
+
return this.state.accounts[accountId];
|
|
88
|
+
}
|
|
89
|
+
/** All known account ids in the persisted state. */
|
|
90
|
+
knownAccounts() {
|
|
91
|
+
return Object.keys(this.state.accounts);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Record that the dispatcher routed UID `uid` for `accountId`.
|
|
95
|
+
* Updates lastSeenUid (monotonic max) and appends to seenUids with
|
|
96
|
+
* bounding. Marks state dirty + schedules a debounced flush.
|
|
97
|
+
*/
|
|
98
|
+
markSeen(accountId, uid) {
|
|
99
|
+
if (!Number.isFinite(uid) || uid <= 0) return;
|
|
100
|
+
let cur = this.state.accounts[accountId];
|
|
101
|
+
if (!cur) {
|
|
102
|
+
cur = { lastSeenUid: 0, seenUids: [] };
|
|
103
|
+
this.state.accounts[accountId] = cur;
|
|
104
|
+
}
|
|
105
|
+
if (uid > cur.lastSeenUid) cur.lastSeenUid = uid;
|
|
106
|
+
if (!cur.seenUids.includes(uid)) {
|
|
107
|
+
cur.seenUids.push(uid);
|
|
108
|
+
if (cur.seenUids.length > SEEN_UIDS_CAP) {
|
|
109
|
+
cur.seenUids = cur.seenUids.slice(-SEEN_UIDS_CAP);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
this.scheduleFlush();
|
|
113
|
+
}
|
|
114
|
+
/** Drop the cursor for an account that vanished. */
|
|
115
|
+
forget(accountId) {
|
|
116
|
+
if (this.state.accounts[accountId]) {
|
|
117
|
+
delete this.state.accounts[accountId];
|
|
118
|
+
this.scheduleFlush();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
scheduleFlush() {
|
|
122
|
+
this.dirty = true;
|
|
123
|
+
if (this.flushTimer) return;
|
|
124
|
+
this.flushTimer = setTimeout(() => {
|
|
125
|
+
this.flushTimer = null;
|
|
126
|
+
try {
|
|
127
|
+
this.flushNow();
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}, FLUSH_INTERVAL_MS);
|
|
131
|
+
this.flushTimer.unref?.();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Synchronously write state to disk via atomic rename. Safe to call
|
|
135
|
+
* from a shutdown handler — finishes before the process exits.
|
|
136
|
+
*/
|
|
137
|
+
flushNow() {
|
|
138
|
+
if (!this.dirty) return;
|
|
139
|
+
const dir = dirname(this.path);
|
|
140
|
+
try {
|
|
141
|
+
mkdirSync(dir, { recursive: true });
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
const out = { ...this.state, savedAtMs: Date.now(), version: STATE_VERSION };
|
|
145
|
+
const tmp = `${this.path}.tmp`;
|
|
146
|
+
writeFileSync(tmp, JSON.stringify(out));
|
|
147
|
+
renameSync(tmp, this.path);
|
|
148
|
+
this.dirty = false;
|
|
149
|
+
}
|
|
150
|
+
/** Cancel the debounce timer (used during shutdown). */
|
|
151
|
+
stop() {
|
|
152
|
+
if (this.flushTimer) {
|
|
153
|
+
clearTimeout(this.flushTimer);
|
|
154
|
+
this.flushTimer = null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/dispatcher.ts
|
|
160
|
+
import { mkdirSync as mkdirSync2, createWriteStream, rmSync } from "fs";
|
|
161
|
+
import { join as join3 } from "path";
|
|
162
|
+
import { homedir as homedir2 } from "os";
|
|
42
163
|
import { ThreadCache, AgentMemoryStore, threadIdFor, normalizeSubject } from "@agenticmail/core";
|
|
43
164
|
function extractSubject(event) {
|
|
44
165
|
if (typeof event.subject === "string") return event.subject;
|
|
@@ -450,6 +571,21 @@ var Dispatcher = class {
|
|
|
450
571
|
*/
|
|
451
572
|
threadCache;
|
|
452
573
|
agentMemory;
|
|
574
|
+
/**
|
|
575
|
+
* Persistent dispatcher state — per-account `{ lastSeenUid, seenUids[] }`
|
|
576
|
+
* that survives a restart. On `start()` we use it to seed each
|
|
577
|
+
* channel's `seenUids` (so IMAP IDLE replays of old UIDs stay
|
|
578
|
+
* deduped) and to drive the catch-up scan (anything strictly
|
|
579
|
+
* newer than `lastSeenUid` got missed during downtime — route it
|
|
580
|
+
* through handleEvent like a synthetic SSE 'new' event).
|
|
581
|
+
*
|
|
582
|
+
* Writes are debounced inside the state module; we just call
|
|
583
|
+
* `markSeen(accountId, uid)` everywhere we decide on a UID.
|
|
584
|
+
*/
|
|
585
|
+
state;
|
|
586
|
+
/** Tracks which accounts have already gone through catch-up + pending-task scan
|
|
587
|
+
* so reconnects don't replay the same backlog. */
|
|
588
|
+
caughtUp = /* @__PURE__ */ new Set();
|
|
453
589
|
/**
|
|
454
590
|
* Coalesced wake queue. Keyed by `${accountId}::${threadId}`,
|
|
455
591
|
* each entry holds the pending events + the timer that will
|
|
@@ -488,10 +624,13 @@ var Dispatcher = class {
|
|
|
488
624
|
this.threadCache = new ThreadCache({ cacheDir: opts.threadCacheDir });
|
|
489
625
|
this.agentMemory = new AgentMemoryStore({ memoryDir: opts.agentMemoryDir });
|
|
490
626
|
this.wakeCoalesceMs = opts.wakeCoalesceMs ?? DEFAULT_WAKE_COALESCE_MS;
|
|
627
|
+
this.state = new DispatcherState({ path: opts.stateFilePath });
|
|
628
|
+
this.disableCatchupScan = !!opts.disableCatchupScan;
|
|
491
629
|
if (!this.cfg.masterKey) {
|
|
492
630
|
throw new Error("Dispatcher requires AgenticMail master key. Run `agenticmail setup` first.");
|
|
493
631
|
}
|
|
494
632
|
}
|
|
633
|
+
disableCatchupScan = false;
|
|
495
634
|
/**
|
|
496
635
|
* Charge one wake against the (agent, thread) budget. Returns true
|
|
497
636
|
* if the wake should proceed, false if the circuit breaker is open.
|
|
@@ -590,6 +729,12 @@ var Dispatcher = class {
|
|
|
590
729
|
this.channels.clear();
|
|
591
730
|
for (const entry of this.wakeCoalesce.values()) clearTimeout(entry.timer);
|
|
592
731
|
this.wakeCoalesce.clear();
|
|
732
|
+
try {
|
|
733
|
+
this.state.stop();
|
|
734
|
+
this.state.flushNow();
|
|
735
|
+
} catch (err) {
|
|
736
|
+
this.log("warn", `[dispatcher] could not flush state on stop: ${err.message}`);
|
|
737
|
+
}
|
|
593
738
|
this.log("info", "[dispatcher] stopped");
|
|
594
739
|
}
|
|
595
740
|
/** Public for tests — directly hand an event to the routing path. */
|
|
@@ -620,9 +765,11 @@ var Dispatcher = class {
|
|
|
620
765
|
if (ch && Date.now() < ch.suppressTaskMailUntilMs && isTaskNotificationSubject(subject)) {
|
|
621
766
|
this.log("info", `[dispatcher] suppressed task-notification mail wake for "${account.name}" (uid=${event.uid}, subject="${subject}") \u2014 task event already dispatched`);
|
|
622
767
|
rememberBounded(ch.seenUids, event.uid);
|
|
768
|
+
this.state.markSeen(account.id, event.uid);
|
|
623
769
|
return;
|
|
624
770
|
}
|
|
625
771
|
if (ch) rememberBounded(ch.seenUids, event.uid);
|
|
772
|
+
this.state.markSeen(account.id, event.uid);
|
|
626
773
|
if (isThreadClosedSubject(subject)) {
|
|
627
774
|
this.log("info", `[dispatcher] thread closed (subject="${subject ?? ""}") \u2014 skipping wake for "${account.name}" uid=${event.uid}`);
|
|
628
775
|
this.postSkipped(account, event, "thread-closed", `subject contains a thread-close marker: "${subject ?? ""}"`);
|
|
@@ -711,6 +858,8 @@ var Dispatcher = class {
|
|
|
711
858
|
ch.stopping = true;
|
|
712
859
|
ch.controller?.abort();
|
|
713
860
|
this.channels.delete(id);
|
|
861
|
+
this.state.forget(id);
|
|
862
|
+
this.caughtUp.delete(id);
|
|
714
863
|
this.log("info", `[dispatcher] account "${ch.account.name}" removed \u2014 closed SSE channel`);
|
|
715
864
|
}
|
|
716
865
|
}
|
|
@@ -719,17 +868,19 @@ var Dispatcher = class {
|
|
|
719
868
|
this.channels.get(account.id).account = account;
|
|
720
869
|
continue;
|
|
721
870
|
}
|
|
871
|
+
const persistedCursor = this.state.getCursor(account.id);
|
|
872
|
+
const seenUids = new Set(persistedCursor?.seenUids ?? []);
|
|
722
873
|
const ch = {
|
|
723
874
|
account,
|
|
724
875
|
controller: null,
|
|
725
876
|
stopping: false,
|
|
726
877
|
backoffMs: this.reconnectBaseMs,
|
|
727
|
-
seenUids
|
|
878
|
+
seenUids,
|
|
728
879
|
seenTaskIds: /* @__PURE__ */ new Set(),
|
|
729
880
|
suppressTaskMailUntilMs: 0
|
|
730
881
|
};
|
|
731
882
|
this.channels.set(account.id, ch);
|
|
732
|
-
this.log("info", `[dispatcher] opening SSE for "${account.name}" (${account.email})`);
|
|
883
|
+
this.log("info", `[dispatcher] opening SSE for "${account.name}" (${account.email})` + (persistedCursor ? ` (restored ${seenUids.size} seen UIDs, lastSeenUid=${persistedCursor.lastSeenUid})` : ""));
|
|
733
884
|
void this.runChannel(ch);
|
|
734
885
|
}
|
|
735
886
|
}
|
|
@@ -863,6 +1014,88 @@ var Dispatcher = class {
|
|
|
863
1014
|
ch.backoffMs = Math.min(ch.backoffMs * 2, this.reconnectMaxMs);
|
|
864
1015
|
}
|
|
865
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* One-shot backlog scan after a (re)connect: route unprocessed mail
|
|
1019
|
+
* + pending tasks that arrived while the dispatcher was unreachable.
|
|
1020
|
+
*
|
|
1021
|
+
* Mail path: pull the newest 50 envelopes from `/mail/inbox`. For
|
|
1022
|
+
* each UID strictly greater than the persisted `lastSeenUid` (and
|
|
1023
|
+
* not already in the channel's `seenUids`), synthesise an SSE
|
|
1024
|
+
* `new` event and hand it to `handleEvent`. The wake-budget
|
|
1025
|
+
* circuit breaker still applies, so a runaway thread that hit
|
|
1026
|
+
* the cap pre-restart STAYS muted — restart isn't a free reset.
|
|
1027
|
+
*
|
|
1028
|
+
* Tasks path: fetch `/tasks/pending`. Anything not in the
|
|
1029
|
+
* channel's `seenTaskIds` becomes a synthetic task SSE event.
|
|
1030
|
+
*
|
|
1031
|
+
* Failures here are NEVER fatal — they're "best effort". The
|
|
1032
|
+
* dispatcher continues processing live SSE traffic regardless.
|
|
1033
|
+
*/
|
|
1034
|
+
async runCatchUp(ch) {
|
|
1035
|
+
const account = ch.account;
|
|
1036
|
+
try {
|
|
1037
|
+
const envelopes = await listInboxForAgent(this.cfg.apiUrl, account.apiKey, { limit: 50 });
|
|
1038
|
+
const cursor = this.state.getCursor(account.id);
|
|
1039
|
+
if (!cursor) {
|
|
1040
|
+
if (envelopes.length > 0) {
|
|
1041
|
+
let maxUid = 0;
|
|
1042
|
+
for (const e of envelopes) {
|
|
1043
|
+
if (Number.isFinite(e.uid) && e.uid > 0) {
|
|
1044
|
+
ch.seenUids.add(e.uid);
|
|
1045
|
+
if (e.uid > maxUid) maxUid = e.uid;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (maxUid > 0) this.state.markSeen(account.id, maxUid);
|
|
1049
|
+
this.log("info", `[dispatcher] catch-up for "${account.name}": first run, seeded cursor at uid=${maxUid} (skipping ${envelopes.length} pre-existing messages)`);
|
|
1050
|
+
}
|
|
1051
|
+
} else {
|
|
1052
|
+
const lastSeenUid = cursor.lastSeenUid;
|
|
1053
|
+
const sorted = envelopes.filter((e) => Number.isFinite(e.uid) && e.uid > lastSeenUid && !ch.seenUids.has(e.uid)).sort((a, b) => a.uid - b.uid);
|
|
1054
|
+
if (sorted.length > 0) {
|
|
1055
|
+
this.log("info", `[dispatcher] catch-up for "${account.name}": replaying ${sorted.length} unprocessed UIDs (lastSeenUid=${lastSeenUid})`);
|
|
1056
|
+
for (const env of sorted) {
|
|
1057
|
+
const event = {
|
|
1058
|
+
type: "new",
|
|
1059
|
+
uid: env.uid,
|
|
1060
|
+
subject: env.subject,
|
|
1061
|
+
// Format `from` to match what extractFrom expects
|
|
1062
|
+
// (top-level string OR nested under message). Use the
|
|
1063
|
+
// first sender's address — that's what the live SSE
|
|
1064
|
+
// path delivers.
|
|
1065
|
+
from: env.from?.[0]?.address,
|
|
1066
|
+
message: {
|
|
1067
|
+
subject: env.subject,
|
|
1068
|
+
from: env.from,
|
|
1069
|
+
to: env.to
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
await this.handleEvent(account, event);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
this.log("warn", `[dispatcher] catch-up mail scan failed for "${account.name}": ${err.message}`);
|
|
1078
|
+
}
|
|
1079
|
+
try {
|
|
1080
|
+
const tasks = await listPendingTasksForAgent(this.cfg.apiUrl, account.apiKey);
|
|
1081
|
+
const fresh = tasks.filter((t) => t.id && !ch.seenTaskIds.has(t.id));
|
|
1082
|
+
if (fresh.length > 0) {
|
|
1083
|
+
this.log("info", `[dispatcher] catch-up for "${account.name}": replaying ${fresh.length} pending tasks`);
|
|
1084
|
+
for (const t of fresh) {
|
|
1085
|
+
const event = {
|
|
1086
|
+
type: "task",
|
|
1087
|
+
taskId: t.id,
|
|
1088
|
+
taskType: t.task_type ?? t.type,
|
|
1089
|
+
task: t.description ?? t.task ?? "",
|
|
1090
|
+
assignee: account.name
|
|
1091
|
+
};
|
|
1092
|
+
await this.handleEvent(account, event);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
} catch (err) {
|
|
1096
|
+
this.log("warn", `[dispatcher] catch-up task scan failed for "${account.name}": ${err.message}`);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
866
1099
|
/** Single SSE attach. Returns when the stream closes for any reason. */
|
|
867
1100
|
async streamOne(ch) {
|
|
868
1101
|
const url = `${this.cfg.apiUrl.replace(/\/$/, "")}/api/agenticmail/events`;
|
|
@@ -877,6 +1110,12 @@ var Dispatcher = class {
|
|
|
877
1110
|
throw new Error(`SSE handshake HTTP ${res.status}`);
|
|
878
1111
|
}
|
|
879
1112
|
ch.backoffMs = this.reconnectBaseMs;
|
|
1113
|
+
if (!this.caughtUp.has(ch.account.id) && !this.disableCatchupScan) {
|
|
1114
|
+
this.caughtUp.add(ch.account.id);
|
|
1115
|
+
void this.runCatchUp(ch).catch(
|
|
1116
|
+
(err) => this.log("warn", `[dispatcher] catch-up scan failed for "${ch.account.name}": ${err.message}`)
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
880
1119
|
const reader = res.body.getReader();
|
|
881
1120
|
const decoder = new TextDecoder();
|
|
882
1121
|
let buffer = "";
|
|
@@ -987,6 +1226,7 @@ var Dispatcher = class {
|
|
|
987
1226
|
if (!entry) return;
|
|
988
1227
|
this.wakeCoalesce.delete(key);
|
|
989
1228
|
if (this.stopped) return;
|
|
1229
|
+
if (entry.events.length === 0) return;
|
|
990
1230
|
const verdict = this.chargeWake(entry.account.id, entry.threadId);
|
|
991
1231
|
if (!verdict.ok) {
|
|
992
1232
|
this.log("warn", `[dispatcher] wake-budget exhausted for "${entry.account.name}" on thread "${entry.threadId}" \u2014 dropped batch of ${entry.events.length}`);
|
|
@@ -1077,12 +1317,12 @@ var Dispatcher = class {
|
|
|
1077
1317
|
kind: ctx.kind,
|
|
1078
1318
|
trigger: { uid: ctx.uid, taskId: ctx.taskId, subject: ctx.subject, from: ctx.from }
|
|
1079
1319
|
});
|
|
1080
|
-
const logsDir =
|
|
1320
|
+
const logsDir = join3(homedir2(), ".agenticmail", "worker-logs");
|
|
1081
1321
|
try {
|
|
1082
|
-
|
|
1322
|
+
mkdirSync2(logsDir, { recursive: true });
|
|
1083
1323
|
} catch {
|
|
1084
1324
|
}
|
|
1085
|
-
const logPath =
|
|
1325
|
+
const logPath = join3(logsDir, `${sanitizeId(workerId)}.log`);
|
|
1086
1326
|
let logStream = null;
|
|
1087
1327
|
try {
|
|
1088
1328
|
logStream = createWriteStream(logPath, { flags: "a" });
|
|
@@ -1096,9 +1336,9 @@ var Dispatcher = class {
|
|
|
1096
1336
|
}
|
|
1097
1337
|
};
|
|
1098
1338
|
writeLog(`worker_started agent=${account.name} kind=${ctx.kind}${ctx.uid ? " uid=" + ctx.uid : ""}${ctx.taskId ? " task=" + ctx.taskId : ""}`);
|
|
1099
|
-
const cwdDir =
|
|
1339
|
+
const cwdDir = join3(homedir2(), ".agenticmail", "worker-cwds", sanitizeId(workerId));
|
|
1100
1340
|
try {
|
|
1101
|
-
|
|
1341
|
+
mkdirSync2(cwdDir, { recursive: true });
|
|
1102
1342
|
} catch {
|
|
1103
1343
|
}
|
|
1104
1344
|
let turnCount = 0;
|
|
@@ -1173,7 +1413,12 @@ var Dispatcher = class {
|
|
|
1173
1413
|
}
|
|
1174
1414
|
}
|
|
1175
1415
|
const ch = this.channels.get(account.id);
|
|
1176
|
-
if (ch)
|
|
1416
|
+
if (ch) {
|
|
1417
|
+
for (const uid of digestedUids) {
|
|
1418
|
+
rememberBounded(ch.seenUids, uid);
|
|
1419
|
+
this.state.markSeen(account.id, uid);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1177
1422
|
}
|
|
1178
1423
|
try {
|
|
1179
1424
|
releaseAgentLock();
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
uninstall
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-JUEHPFKY.js";
|
|
4
4
|
import {
|
|
5
5
|
install
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-4EHV3I6W.js";
|
|
7
7
|
import {
|
|
8
8
|
status
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-Q5BA2J2C.js";
|
|
10
10
|
import {
|
|
11
11
|
AgenticMailApiError
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-4VQP57SO.js";
|
|
13
13
|
|
|
14
14
|
// src/http-routes.ts
|
|
15
15
|
import { Router } from "express";
|
package/dist/cli.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
uninstall
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JUEHPFKY.js";
|
|
5
5
|
import {
|
|
6
6
|
install
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-4EHV3I6W.js";
|
|
8
8
|
import "./chunk-DKTAW2N5.js";
|
|
9
9
|
import {
|
|
10
10
|
status
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-Q5BA2J2C.js";
|
|
12
12
|
import "./chunk-US5FT2UB.js";
|
|
13
13
|
import {
|
|
14
14
|
AgenticMailApiError
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-4VQP57SO.js";
|
|
16
16
|
|
|
17
17
|
// src/cli.ts
|
|
18
18
|
var GREEN = (s) => `\x1B[32m${s}\x1B[0m`;
|
package/dist/dispatcher-bin.js
CHANGED
package/dist/dispatcher.d.ts
CHANGED
|
@@ -119,6 +119,17 @@ interface DispatcherOptions extends ResolveConfigOptions {
|
|
|
119
119
|
/** Override the AgentMemoryStore disk root. Same rationale as
|
|
120
120
|
* threadCacheDir — only tests should set this. */
|
|
121
121
|
agentMemoryDir?: string;
|
|
122
|
+
/** Override the dispatcher state file (per-account cursors used
|
|
123
|
+
* for restart recovery). Tests use a tmpdir; production runs
|
|
124
|
+
* against ~/.agenticmail/dispatcher-state.json. */
|
|
125
|
+
stateFilePath?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Disable catch-up scan + pending-task scan on channel open.
|
|
128
|
+
* Default false. Tests that don't want the dispatcher hitting the
|
|
129
|
+
* inbox/tasks endpoints on first connect set this true. Has no
|
|
130
|
+
* effect on the persisted seenUids restore — that's always on.
|
|
131
|
+
*/
|
|
132
|
+
disableCatchupScan?: boolean;
|
|
122
133
|
}
|
|
123
134
|
/** Minimal Claude Agent SDK query signature we use. */
|
|
124
135
|
interface QueryFn {
|
|
@@ -180,6 +191,21 @@ declare class Dispatcher {
|
|
|
180
191
|
*/
|
|
181
192
|
private threadCache;
|
|
182
193
|
private agentMemory;
|
|
194
|
+
/**
|
|
195
|
+
* Persistent dispatcher state — per-account `{ lastSeenUid, seenUids[] }`
|
|
196
|
+
* that survives a restart. On `start()` we use it to seed each
|
|
197
|
+
* channel's `seenUids` (so IMAP IDLE replays of old UIDs stay
|
|
198
|
+
* deduped) and to drive the catch-up scan (anything strictly
|
|
199
|
+
* newer than `lastSeenUid` got missed during downtime — route it
|
|
200
|
+
* through handleEvent like a synthetic SSE 'new' event).
|
|
201
|
+
*
|
|
202
|
+
* Writes are debounced inside the state module; we just call
|
|
203
|
+
* `markSeen(accountId, uid)` everywhere we decide on a UID.
|
|
204
|
+
*/
|
|
205
|
+
private state;
|
|
206
|
+
/** Tracks which accounts have already gone through catch-up + pending-task scan
|
|
207
|
+
* so reconnects don't replay the same backlog. */
|
|
208
|
+
private caughtUp;
|
|
183
209
|
/**
|
|
184
210
|
* Coalesced wake queue. Keyed by `${accountId}::${threadId}`,
|
|
185
211
|
* each entry holds the pending events + the timer that will
|
|
@@ -204,6 +230,7 @@ declare class Dispatcher {
|
|
|
204
230
|
* events to wake on" — the host has no liveness signal. */
|
|
205
231
|
private processHeartbeatTimer;
|
|
206
232
|
constructor(opts?: DispatcherOptions);
|
|
233
|
+
private disableCatchupScan;
|
|
207
234
|
/**
|
|
208
235
|
* Charge one wake against the (agent, thread) budget. Returns true
|
|
209
236
|
* if the wake should proceed, false if the circuit breaker is open.
|
|
@@ -270,6 +297,24 @@ declare class Dispatcher {
|
|
|
270
297
|
private handleSystemEvent;
|
|
271
298
|
/** Watch one account's SSE stream forever; reconnect with backoff on drop. */
|
|
272
299
|
private runChannel;
|
|
300
|
+
/**
|
|
301
|
+
* One-shot backlog scan after a (re)connect: route unprocessed mail
|
|
302
|
+
* + pending tasks that arrived while the dispatcher was unreachable.
|
|
303
|
+
*
|
|
304
|
+
* Mail path: pull the newest 50 envelopes from `/mail/inbox`. For
|
|
305
|
+
* each UID strictly greater than the persisted `lastSeenUid` (and
|
|
306
|
+
* not already in the channel's `seenUids`), synthesise an SSE
|
|
307
|
+
* `new` event and hand it to `handleEvent`. The wake-budget
|
|
308
|
+
* circuit breaker still applies, so a runaway thread that hit
|
|
309
|
+
* the cap pre-restart STAYS muted — restart isn't a free reset.
|
|
310
|
+
*
|
|
311
|
+
* Tasks path: fetch `/tasks/pending`. Anything not in the
|
|
312
|
+
* channel's `seenTaskIds` becomes a synthetic task SSE event.
|
|
313
|
+
*
|
|
314
|
+
* Failures here are NEVER fatal — they're "best effort". The
|
|
315
|
+
* dispatcher continues processing live SSE traffic regardless.
|
|
316
|
+
*/
|
|
317
|
+
private runCatchUp;
|
|
273
318
|
/** Single SSE attach. Returns when the stream closes for any reason. */
|
|
274
319
|
private streamOne;
|
|
275
320
|
/**
|
package/dist/dispatcher.js
CHANGED
package/dist/http-routes.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createIntegrationRoutes
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-ZFLOFC6P.js";
|
|
4
|
+
import "./chunk-JUEHPFKY.js";
|
|
5
|
+
import "./chunk-4EHV3I6W.js";
|
|
6
6
|
import "./chunk-DKTAW2N5.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-Q5BA2J2C.js";
|
|
8
8
|
import "./chunk-US5FT2UB.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-4VQP57SO.js";
|
|
10
10
|
export {
|
|
11
11
|
createIntegrationRoutes
|
|
12
12
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Dispatcher,
|
|
3
3
|
loadPersonaForAgent
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JOK76WRC.js";
|
|
5
5
|
import {
|
|
6
6
|
createIntegrationRoutes
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ZFLOFC6P.js";
|
|
8
8
|
import {
|
|
9
9
|
uninstall
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-JUEHPFKY.js";
|
|
11
11
|
import {
|
|
12
12
|
install
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-4EHV3I6W.js";
|
|
14
14
|
import "./chunk-DKTAW2N5.js";
|
|
15
15
|
import {
|
|
16
16
|
status
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-Q5BA2J2C.js";
|
|
18
18
|
import "./chunk-US5FT2UB.js";
|
|
19
19
|
import {
|
|
20
20
|
AgenticMailApiError,
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
renderPersonaBody,
|
|
28
28
|
renderSubagentMarkdown,
|
|
29
29
|
resolveConfig
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-4VQP57SO.js";
|
|
31
31
|
export {
|
|
32
32
|
AgenticMailApiError,
|
|
33
33
|
Dispatcher,
|
package/dist/install.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
install,
|
|
3
3
|
selectExposableAgents
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-4EHV3I6W.js";
|
|
5
5
|
import "./chunk-DKTAW2N5.js";
|
|
6
6
|
import "./chunk-US5FT2UB.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-4VQP57SO.js";
|
|
8
8
|
export {
|
|
9
9
|
install,
|
|
10
10
|
selectExposableAgents
|
package/dist/status.js
CHANGED
package/dist/uninstall.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticmail/claudecode",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Claude Code integration for AgenticMail — surfaces every AgenticMail agent as a native Claude Code subagent so any Claude Code session can delegate to them with the Agent tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|