@agenticmail/claudecode 0.1.8 → 0.1.9
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-M5HV2M67.js → chunk-3D5VXS5Y.js} +1 -1
- package/dist/{chunk-V5LH4BNZ.js → chunk-52LXPWO7.js} +1 -1
- package/dist/{chunk-JVDPUO35.js → chunk-CQLUFM7N.js} +1 -1
- package/dist/{chunk-5HUEJNT5.js → chunk-FBO6F4IC.js} +86 -1
- package/dist/{chunk-VYBBCFP5.js → chunk-V3QMDNTR.js} +1 -1
- package/dist/{chunk-XHDRGX46.js → chunk-WAUWKOHA.js} +4 -4
- package/dist/cli.js +4 -4
- package/dist/dispatcher-bin.js +2 -2
- package/dist/dispatcher.d.ts +44 -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
|
@@ -187,7 +187,7 @@ function renderPersonaBody(input) {
|
|
|
187
187
|
` 1. Read the new message with \`${tool("read_email")}\`.`,
|
|
188
188
|
` 2. Load the rest of the thread with \`${tool("search_emails")}({ subject: "<core subject>", _account: "${agent.name}" })\` and read each prior message. You MUST have full thread context before acting.`,
|
|
189
189
|
` 3. Look at To + CC across the thread \u2014 those are your teammates. They will each be woken on every reply-all just like you were.`,
|
|
190
|
-
` 4. Decide if it's YOUR turn: are you addressed by name? Is the previous-stage handoff to your role? Is a question pending for you? When in doubt, stay silent \u2014 over-replying creates noise.`,
|
|
190
|
+
` 4. Decide if it's YOUR turn: are you addressed by name? Is the previous-stage handoff to your role? Is a question pending for you? **If a teammate replied within the last 60 seconds, assume they are handling this turn and stay silent** \u2014 simultaneous replies are noise. When in doubt, stay silent \u2014 over-replying creates noise.`,
|
|
191
191
|
` 5. If yes: \`${tool("reply_email")}({ uid, replyAll: true, text: "...", _account: "${agent.name}" })\`. Sign with your name. If you're handing off, name the next teammate explicitly ("Orion \u2014 over to you, please \u2026"). To bring a new teammate in, just add them to CC.`,
|
|
192
192
|
` 6. If no: \`mark_read\` and return. Silence IS a valid contribution.`,
|
|
193
193
|
"",
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
listAccounts,
|
|
3
3
|
renderPersonaBody,
|
|
4
4
|
resolveConfig
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-3D5VXS5Y.js";
|
|
6
6
|
|
|
7
7
|
// src/persona-loader.ts
|
|
8
8
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -72,6 +72,18 @@ function isTaskNotificationSubject(subject) {
|
|
|
72
72
|
}
|
|
73
73
|
return false;
|
|
74
74
|
}
|
|
75
|
+
function threadIdFromSubject(subject) {
|
|
76
|
+
if (!subject) return "";
|
|
77
|
+
let s = subject.trim();
|
|
78
|
+
while (true) {
|
|
79
|
+
const next = s.replace(/^(re|fwd?|fw)(\[\d+\])?:\s*/i, "");
|
|
80
|
+
if (next === s) break;
|
|
81
|
+
s = next;
|
|
82
|
+
}
|
|
83
|
+
return s.toLowerCase().trim();
|
|
84
|
+
}
|
|
85
|
+
var DEFAULT_MAX_WAKES_PER_THREAD = 10;
|
|
86
|
+
var DEFAULT_WAKE_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
75
87
|
async function runWorker(query, persona, userPrompt, agent, mcpServerName, mcpCommand, mcpArgs, mcpEnv, log, abortSignal) {
|
|
76
88
|
const opts = {
|
|
77
89
|
systemPrompt: persona,
|
|
@@ -168,6 +180,10 @@ function newMailPrompt(agent, event) {
|
|
|
168
180
|
` - You were directly asked a question and nobody has answered yet.`,
|
|
169
181
|
` No if:`,
|
|
170
182
|
` - The current ask is targeted at a teammate (their turn, not yours).`,
|
|
183
|
+
` - **A teammate replied within the last 60 seconds.** They are likely`,
|
|
184
|
+
` already handling this turn; jumping in creates simultaneous replies`,
|
|
185
|
+
` and confusion. Assume good faith and stay silent unless their reply`,
|
|
186
|
+
` was clearly off-target.`,
|
|
171
187
|
` - You have nothing substantive to add right now.`,
|
|
172
188
|
` When in doubt, stay silent \u2014 over-replying creates noise. Better to let`,
|
|
173
189
|
` the right teammate take the turn than to step on theirs.`,
|
|
@@ -239,6 +255,15 @@ var Dispatcher = class {
|
|
|
239
255
|
running = 0;
|
|
240
256
|
waiters = [];
|
|
241
257
|
stopped = false;
|
|
258
|
+
/**
|
|
259
|
+
* Wake-budget store, keyed by `${accountId}::${threadId}`. See the
|
|
260
|
+
* comment block on WakeBudgetEntry for the failure modes this guards.
|
|
261
|
+
* Pruned opportunistically on each lookup — no separate timer.
|
|
262
|
+
*/
|
|
263
|
+
wakeBudget = /* @__PURE__ */ new Map();
|
|
264
|
+
maxWakesPerThread;
|
|
265
|
+
wakeWindowMs;
|
|
266
|
+
now;
|
|
242
267
|
constructor(opts = {}) {
|
|
243
268
|
this.cfg = resolveConfig(opts);
|
|
244
269
|
this.maxConcurrent = opts.maxConcurrentWorkers ?? DEFAULT_MAX_CONCURRENT;
|
|
@@ -248,10 +273,63 @@ var Dispatcher = class {
|
|
|
248
273
|
this.query = opts.querySdk ?? defaultQuery();
|
|
249
274
|
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
250
275
|
this.log = opts.log ?? defaultLog;
|
|
276
|
+
this.maxWakesPerThread = opts.maxWakesPerThread ?? DEFAULT_MAX_WAKES_PER_THREAD;
|
|
277
|
+
this.wakeWindowMs = opts.wakeWindowMs ?? DEFAULT_WAKE_WINDOW_MS;
|
|
278
|
+
this.now = opts.nowMs ?? Date.now;
|
|
251
279
|
if (!this.cfg.masterKey) {
|
|
252
280
|
throw new Error("Dispatcher requires AgenticMail master key. Run `agenticmail setup` first.");
|
|
253
281
|
}
|
|
254
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Charge one wake against the (agent, thread) budget. Returns true
|
|
285
|
+
* if the wake should proceed, false if the circuit breaker is open.
|
|
286
|
+
*
|
|
287
|
+
* Empty threadId means "no thread context" (a fresh standalone email
|
|
288
|
+
* with no Subject — rare); we always allow those since there is no
|
|
289
|
+
* thread to runaway on.
|
|
290
|
+
*/
|
|
291
|
+
chargeWake(accountId, threadId) {
|
|
292
|
+
if (!threadId) return { ok: true };
|
|
293
|
+
const key = `${accountId}::${threadId}`;
|
|
294
|
+
const now = this.now();
|
|
295
|
+
let entry = this.wakeBudget.get(key);
|
|
296
|
+
if (entry && now - entry.firstWakeAtMs >= this.wakeWindowMs) {
|
|
297
|
+
entry = void 0;
|
|
298
|
+
this.wakeBudget.delete(key);
|
|
299
|
+
}
|
|
300
|
+
if (!entry) {
|
|
301
|
+
entry = { count: 1, firstWakeAtMs: now };
|
|
302
|
+
this.wakeBudget.set(key, entry);
|
|
303
|
+
this.maybePruneWakeBudget(now);
|
|
304
|
+
return { ok: true, count: 1 };
|
|
305
|
+
}
|
|
306
|
+
if (entry.count >= this.maxWakesPerThread) {
|
|
307
|
+
return {
|
|
308
|
+
ok: false,
|
|
309
|
+
count: entry.count,
|
|
310
|
+
mutedUntilMs: entry.firstWakeAtMs + this.wakeWindowMs
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
entry.count++;
|
|
314
|
+
return { ok: true, count: entry.count };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Drop wake-budget entries that have aged out of their window.
|
|
318
|
+
*
|
|
319
|
+
* Called inline from chargeWake, but at most once per ~1024 inserts so
|
|
320
|
+
* the cost stays bounded. We don't need a separate timer because the
|
|
321
|
+
* Map only grows on real wakes (capped by maxWakesPerThread per pair),
|
|
322
|
+
* and the prune is O(n) over the current entries — cheap enough.
|
|
323
|
+
*/
|
|
324
|
+
wakeBudgetInsertsSinceLastPrune = 0;
|
|
325
|
+
maybePruneWakeBudget(now) {
|
|
326
|
+
this.wakeBudgetInsertsSinceLastPrune++;
|
|
327
|
+
if (this.wakeBudgetInsertsSinceLastPrune < 1024) return;
|
|
328
|
+
this.wakeBudgetInsertsSinceLastPrune = 0;
|
|
329
|
+
for (const [k, v] of this.wakeBudget) {
|
|
330
|
+
if (now - v.firstWakeAtMs >= this.wakeWindowMs) this.wakeBudget.delete(k);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
255
333
|
async start() {
|
|
256
334
|
this.log("info", `[dispatcher] starting (maxConcurrent=${this.maxConcurrent}, syncEvery=${this.syncIntervalMs}ms)`);
|
|
257
335
|
await this.syncAccounts();
|
|
@@ -291,6 +369,13 @@ var Dispatcher = class {
|
|
|
291
369
|
return;
|
|
292
370
|
}
|
|
293
371
|
if (ch) rememberBounded(ch.seenUids, event.uid);
|
|
372
|
+
const threadId = threadIdFromSubject(subject);
|
|
373
|
+
const verdict = this.chargeWake(account.id, threadId);
|
|
374
|
+
if (!verdict.ok) {
|
|
375
|
+
const minutesUntil = verdict.mutedUntilMs ? Math.max(0, Math.round((verdict.mutedUntilMs - this.now()) / 6e4)) : 0;
|
|
376
|
+
this.log("warn", `[dispatcher] wake-budget exhausted for "${account.name}" on thread "${threadId}" (count=${verdict.count}, cap=${this.maxWakesPerThread}); muted for ~${minutesUntil}min. uid=${event.uid}, subject="${subject ?? ""}"`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
294
379
|
await this.spawnWorker(account, newMailPrompt(account, event), { kind: "new-mail", uid: event.uid });
|
|
295
380
|
return;
|
|
296
381
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
install
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-52LXPWO7.js";
|
|
4
4
|
import {
|
|
5
5
|
status
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-V3QMDNTR.js";
|
|
7
7
|
import {
|
|
8
8
|
uninstall
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-CQLUFM7N.js";
|
|
10
10
|
import {
|
|
11
11
|
AgenticMailApiError
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-3D5VXS5Y.js";
|
|
13
13
|
|
|
14
14
|
// src/http-routes.ts
|
|
15
15
|
import { Router } from "express";
|
package/dist/cli.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
install
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-52LXPWO7.js";
|
|
5
5
|
import {
|
|
6
6
|
status
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-V3QMDNTR.js";
|
|
8
8
|
import {
|
|
9
9
|
uninstall
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-CQLUFM7N.js";
|
|
11
11
|
import "./chunk-US5FT2UB.js";
|
|
12
12
|
import {
|
|
13
13
|
AgenticMailApiError
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-3D5VXS5Y.js";
|
|
15
15
|
|
|
16
16
|
// src/cli.ts
|
|
17
17
|
var GREEN = (s) => `\x1B[32m${s}\x1B[0m`;
|
package/dist/dispatcher-bin.js
CHANGED
package/dist/dispatcher.d.ts
CHANGED
|
@@ -73,6 +73,20 @@ interface DispatcherOptions extends ResolveConfigOptions {
|
|
|
73
73
|
sseReconnectBaseMs?: number;
|
|
74
74
|
/** Max backoff between SSE reconnect attempts. Default 60s. */
|
|
75
75
|
sseReconnectMaxMs?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Max times a single agent is woken on the same thread before the
|
|
78
|
+
* circuit breaker trips. Default 10. Protects against reply loops,
|
|
79
|
+
* storms when many agents share a thread, and stuck agents that
|
|
80
|
+
* keep replying without making progress. Per-(agent, thread).
|
|
81
|
+
*/
|
|
82
|
+
maxWakesPerThread?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Window (ms) for the per-thread wake counter. Default 24h. The
|
|
85
|
+
* counter resets after this period elapses since the FIRST wake in
|
|
86
|
+
* the window — wall-clock-relative, not sliding, so a runaway
|
|
87
|
+
* thread stays muted for the full period (which is what we want).
|
|
88
|
+
*/
|
|
89
|
+
wakeWindowMs?: number;
|
|
76
90
|
/** Override the Claude Agent SDK `query` function. Used by tests. */
|
|
77
91
|
querySdk?: QueryFn;
|
|
78
92
|
/** Override the global `fetch`. Used by tests. */
|
|
@@ -80,6 +94,8 @@ interface DispatcherOptions extends ResolveConfigOptions {
|
|
|
80
94
|
/** Override the global EventSource. Optional — we don't use EventSource
|
|
81
95
|
* by default (fetch + reader is simpler). */
|
|
82
96
|
log?: (level: 'info' | 'warn' | 'error', msg: string) => void;
|
|
97
|
+
/** Override Date.now() — tests use this to advance the budget clock. */
|
|
98
|
+
nowMs?: () => number;
|
|
83
99
|
}
|
|
84
100
|
/** Minimal Claude Agent SDK query signature we use. */
|
|
85
101
|
interface QueryFn {
|
|
@@ -107,7 +123,35 @@ declare class Dispatcher {
|
|
|
107
123
|
private running;
|
|
108
124
|
private waiters;
|
|
109
125
|
private stopped;
|
|
126
|
+
/**
|
|
127
|
+
* Wake-budget store, keyed by `${accountId}::${threadId}`. See the
|
|
128
|
+
* comment block on WakeBudgetEntry for the failure modes this guards.
|
|
129
|
+
* Pruned opportunistically on each lookup — no separate timer.
|
|
130
|
+
*/
|
|
131
|
+
private wakeBudget;
|
|
132
|
+
private maxWakesPerThread;
|
|
133
|
+
private wakeWindowMs;
|
|
134
|
+
private now;
|
|
110
135
|
constructor(opts?: DispatcherOptions);
|
|
136
|
+
/**
|
|
137
|
+
* Charge one wake against the (agent, thread) budget. Returns true
|
|
138
|
+
* if the wake should proceed, false if the circuit breaker is open.
|
|
139
|
+
*
|
|
140
|
+
* Empty threadId means "no thread context" (a fresh standalone email
|
|
141
|
+
* with no Subject — rare); we always allow those since there is no
|
|
142
|
+
* thread to runaway on.
|
|
143
|
+
*/
|
|
144
|
+
private chargeWake;
|
|
145
|
+
/**
|
|
146
|
+
* Drop wake-budget entries that have aged out of their window.
|
|
147
|
+
*
|
|
148
|
+
* Called inline from chargeWake, but at most once per ~1024 inserts so
|
|
149
|
+
* the cost stays bounded. We don't need a separate timer because the
|
|
150
|
+
* Map only grows on real wakes (capped by maxWakesPerThread per pair),
|
|
151
|
+
* and the prune is O(n) over the current entries — cheap enough.
|
|
152
|
+
*/
|
|
153
|
+
private wakeBudgetInsertsSinceLastPrune;
|
|
154
|
+
private maybePruneWakeBudget;
|
|
111
155
|
start(): Promise<void>;
|
|
112
156
|
stop(): Promise<void>;
|
|
113
157
|
/** Public for tests — directly hand an event to the routing path. */
|
package/dist/dispatcher.js
CHANGED
package/dist/http-routes.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createIntegrationRoutes
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-WAUWKOHA.js";
|
|
4
|
+
import "./chunk-52LXPWO7.js";
|
|
5
|
+
import "./chunk-V3QMDNTR.js";
|
|
6
|
+
import "./chunk-CQLUFM7N.js";
|
|
7
7
|
import "./chunk-US5FT2UB.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-3D5VXS5Y.js";
|
|
9
9
|
export {
|
|
10
10
|
createIntegrationRoutes
|
|
11
11
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Dispatcher,
|
|
3
3
|
loadPersonaForAgent
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FBO6F4IC.js";
|
|
5
5
|
import {
|
|
6
6
|
createIntegrationRoutes
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-WAUWKOHA.js";
|
|
8
8
|
import {
|
|
9
9
|
install
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-52LXPWO7.js";
|
|
11
11
|
import {
|
|
12
12
|
status
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-V3QMDNTR.js";
|
|
14
14
|
import {
|
|
15
15
|
uninstall
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-CQLUFM7N.js";
|
|
17
17
|
import "./chunk-US5FT2UB.js";
|
|
18
18
|
import {
|
|
19
19
|
AgenticMailApiError,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
renderPersonaBody,
|
|
27
27
|
renderSubagentMarkdown,
|
|
28
28
|
resolveConfig
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-3D5VXS5Y.js";
|
|
30
30
|
export {
|
|
31
31
|
AgenticMailApiError,
|
|
32
32
|
Dispatcher,
|
package/dist/install.js
CHANGED
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.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|