@agenticmail/claudecode 0.2.19 → 0.2.20

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.
@@ -160,7 +160,95 @@ var DispatcherState = class {
160
160
  import { mkdirSync as mkdirSync2, createWriteStream, rmSync } from "fs";
161
161
  import { join as join3 } from "path";
162
162
  import { homedir as homedir2 } from "os";
163
- import { ThreadCache, AgentMemoryStore, threadIdFor, normalizeSubject } from "@agenticmail/core";
163
+ import { ThreadCache, AgentMemoryStore, threadIdFor, normalizeSubject, loadHostSession, forgetHostSession } from "@agenticmail/core";
164
+
165
+ // src/bridge-wake.ts
166
+ async function resumeBridgeSession(query, input, log) {
167
+ const startMs = Date.now();
168
+ const timeoutMs = input.timeoutMs ?? 5 * 60 * 1e3;
169
+ let result = { ok: false };
170
+ try {
171
+ log("info", `[bridge-wake] resuming Claude Code session ${input.sessionId.slice(0, 8)}\u2026 for ${input.bridge.name}`);
172
+ const opts = {
173
+ resume: input.sessionId,
174
+ cwd: input.cwd,
175
+ mcpServers: {
176
+ agenticmail: {
177
+ command: "agenticmail-mcp",
178
+ args: [],
179
+ env: input.mcpEnv
180
+ }
181
+ },
182
+ permissionMode: "bypassPermissions",
183
+ // Headless mode — no stdin tty.
184
+ includePartialMessages: false
185
+ };
186
+ let assistantText = "";
187
+ let timedOut = false;
188
+ const timeoutHandle = setTimeout(() => {
189
+ timedOut = true;
190
+ }, timeoutMs);
191
+ timeoutHandle.unref?.();
192
+ try {
193
+ const stream = query({ prompt: input.prompt, options: opts });
194
+ for await (const event of stream) {
195
+ if (timedOut) break;
196
+ const e = event;
197
+ if (e.type === "assistant" && Array.isArray(e.message?.content)) {
198
+ for (const block of e.message.content) {
199
+ const b = block;
200
+ if (b.type === "text" && typeof b.text === "string") {
201
+ assistantText = b.text;
202
+ }
203
+ }
204
+ }
205
+ }
206
+ } finally {
207
+ clearTimeout(timeoutHandle);
208
+ }
209
+ if (timedOut) {
210
+ log("warn", `[bridge-wake] timeout after ${timeoutMs}ms \u2014 bridge wake gave up`);
211
+ return { ok: false, error: "timeout", durationMs: Date.now() - startMs };
212
+ }
213
+ result = { ok: true, text: assistantText, durationMs: Date.now() - startMs };
214
+ log("info", `[bridge-wake] resumed session ok (${result.durationMs}ms, ${assistantText.length} chars)`);
215
+ return result;
216
+ } catch (err) {
217
+ const msg = err?.message ?? String(err);
218
+ const m = msg.toLowerCase();
219
+ const expired = m.includes("session not found") || m.includes("invalid session") || m.includes("session expired") || m.includes("no such session") || m.includes("unknown session");
220
+ const sdkMissing = m.includes("cannot find module") || m.includes("could not be found") || m.includes("command not found");
221
+ const error = expired ? "session-expired" : sdkMissing ? "sdk-missing" : "other";
222
+ log("warn", `[bridge-wake] resume failed (${error}): ${msg.slice(0, 200)}`);
223
+ return { ok: false, error, errorMessage: msg, durationMs: Date.now() - startMs };
224
+ }
225
+ }
226
+ function composeBridgeWakePrompt(args) {
227
+ const subject = args.subject ?? "(no subject)";
228
+ const from = args.from ?? "unknown";
229
+ const preview = (args.preview ?? "").slice(0, 600);
230
+ return [
231
+ `\u{1F380} Bridge mail arrived \u2014 headless wake.`,
232
+ "",
233
+ `You are being resumed against your last session because new mail landed in your bridge inbox (${args.bridgeName}@localhost) and you weren't actively at the keyboard.`,
234
+ "",
235
+ `Trigger:`,
236
+ ` UID: ${args.uid}`,
237
+ ` From: ${from}`,
238
+ ` Subject: ${subject}`,
239
+ ` Preview: ${preview}`,
240
+ "",
241
+ `Read it with mcp__agenticmail__read_email({ uid: ${args.uid} }) and decide:`,
242
+ ` \xB7 Does it need a reply from YOU (the operator's session)? Reply via mcp__agenticmail__reply_email.`,
243
+ ` \xB7 Does it need a teammate to act? Forward / re-route by replying with wake: ["<teammate>"].`,
244
+ ` \xB7 Is it [NEEDS OPERATOR] / [BLOCKED]? Then it's actually for the human \u2014 mark it unread, and the operator will see it on their next keystroke.`,
245
+ ` \xB7 Is it FYI noise? mark_read and exit.`,
246
+ "",
247
+ `Keep this turn SHORT. You're being resumed to handle ONE piece of mail, not to continue the prior conversation.`
248
+ ].join("\n");
249
+ }
250
+
251
+ // src/dispatcher.ts
164
252
  function extractSubject(event) {
165
253
  if (typeof event.subject === "string") return event.subject;
166
254
  if (event.message && typeof event.message.subject === "string") return event.message.subject;
@@ -761,6 +849,14 @@ var Dispatcher = class _Dispatcher {
761
849
  /** Public for tests — directly hand an event to the routing path. */
762
850
  async handleEvent(account, event) {
763
851
  if (this.stopped) return;
852
+ if (event.type === "new" && typeof event.uid === "number" && account.name.toLowerCase() === this.cfg.bridgeAgentName.toLowerCase()) {
853
+ const ch = this.channels.get(account.id);
854
+ if (ch?.seenUids.has(event.uid)) return;
855
+ if (ch) rememberBounded(ch.seenUids, event.uid);
856
+ this.state.markSeen(account.id, event.uid);
857
+ void this.handleBridgeMail(account, event);
858
+ return;
859
+ }
764
860
  if (event.type === "new" && typeof event.uid === "number") {
765
861
  const ch = this.channels.get(account.id);
766
862
  if (ch?.seenUids.has(event.uid)) return;
@@ -860,7 +956,7 @@ var Dispatcher = class _Dispatcher {
860
956
  shouldWatch(account) {
861
957
  const bridgeName = this.cfg.bridgeAgentName.toLowerCase();
862
958
  const meta = account.metadata;
863
- if (account.name.toLowerCase() === bridgeName) return false;
959
+ if (account.name.toLowerCase() === bridgeName) return true;
864
960
  if (account.role === "bridge") return false;
865
961
  if (meta && meta.bridge === true) return false;
866
962
  if (meta && typeof meta.host === "string" && meta.host.length > 0) {
@@ -1333,6 +1429,107 @@ var Dispatcher = class _Dispatcher {
1333
1429
  sections.push("this thread will load that memory into context for you.");
1334
1430
  return sections.join("\n");
1335
1431
  }
1432
+ /**
1433
+ * Handle mail that lands in the host's OWN bridge inbox.
1434
+ *
1435
+ * Unlike normal sub-agent wakes (which spawn a fresh worker turn),
1436
+ * bridge wakes resume the operator's last interactive session via
1437
+ * the Claude Agent SDK's `resume` option — keeping the operator's
1438
+ * context intact and avoiding a duplicate "second Claude trying to
1439
+ * be Claude Code". When no fresh session is available (operator
1440
+ * hasn't run `claude` in >24h, or `~/.agenticmail/host-sessions.json`
1441
+ * was wiped), the dispatcher emits an `urgent` system event so the
1442
+ * web UI's notification surface + any future SMS escalation hook
1443
+ * can pick it up. See packages/claudecode/src/bridge-wake.ts.
1444
+ *
1445
+ * Two short-circuits:
1446
+ * 1. If lastSeenMs is within 30s, the operator is actively at the
1447
+ * keyboard right now — their own UserPromptSubmit / Stop hook
1448
+ * will surface the mail on their next keystroke. We skip the
1449
+ * resume entirely to avoid token waste.
1450
+ * 2. If a previous bridge-wake for this same UID is already
1451
+ * in-flight (same key in `inFlightBridgeWakes`), skip — IMAP
1452
+ * IDLE replays + SSE reconnects can fire the same event
1453
+ * twice and we don't want to double-resume.
1454
+ */
1455
+ inFlightBridgeWakes = /* @__PURE__ */ new Set();
1456
+ async handleBridgeMail(account, event) {
1457
+ if (typeof event.uid !== "number") return;
1458
+ if (this.inFlightBridgeWakes.has(event.uid)) return;
1459
+ this.inFlightBridgeWakes.add(event.uid);
1460
+ const startMs = Date.now();
1461
+ const subject = extractSubject(event);
1462
+ const from = extractFrom(event);
1463
+ const preview = event.preview ?? event.message?.preview ?? "";
1464
+ try {
1465
+ const saved = loadHostSession("claudecode");
1466
+ if (saved && Date.now() - saved.lastSeenMs < 3e4) {
1467
+ this.log("info", `[bridge-wake] skip \u2014 operator is live (lastSeen=${Date.now() - saved.lastSeenMs}ms ago); their hook will surface uid=${event.uid}`);
1468
+ this.postActivity("/dispatcher/bridge-skipped", {
1469
+ uid: event.uid,
1470
+ agentName: account.name,
1471
+ reason: "operator-live"
1472
+ });
1473
+ return;
1474
+ }
1475
+ if (!saved) {
1476
+ this.log("warn", `[bridge-wake] no fresh Claude Code session recorded \u2014 bridge mail uid=${event.uid} cannot resume. Escalating via system-event.`);
1477
+ this.postActivity("/dispatcher/bridge-escalation", {
1478
+ uid: event.uid,
1479
+ agentName: account.name,
1480
+ subject,
1481
+ from,
1482
+ preview,
1483
+ reason: "no-fresh-session"
1484
+ });
1485
+ return;
1486
+ }
1487
+ const mcpEnv = await this.buildMcpEnv();
1488
+ const prompt = composeBridgeWakePrompt({
1489
+ bridgeName: account.name,
1490
+ uid: event.uid,
1491
+ subject,
1492
+ from,
1493
+ preview
1494
+ });
1495
+ const result = await resumeBridgeSession(this.query, {
1496
+ bridge: account,
1497
+ sessionId: saved.sessionId,
1498
+ cwd: saved.workspace,
1499
+ prompt,
1500
+ mcpEnv
1501
+ }, this.log);
1502
+ if (result.ok) {
1503
+ this.postActivity("/dispatcher/bridge-resumed", {
1504
+ uid: event.uid,
1505
+ agentName: account.name,
1506
+ subject,
1507
+ from,
1508
+ durationMs: result.durationMs,
1509
+ resultPreview: result.text?.slice(0, 240)
1510
+ });
1511
+ } else {
1512
+ if (result.error === "session-expired") {
1513
+ this.log("warn", `[bridge-wake] session expired; forgetting and escalating uid=${event.uid}`);
1514
+ forgetHostSession("claudecode");
1515
+ }
1516
+ this.postActivity("/dispatcher/bridge-escalation", {
1517
+ uid: event.uid,
1518
+ agentName: account.name,
1519
+ subject,
1520
+ from,
1521
+ preview,
1522
+ reason: result.error ?? "resume-failed",
1523
+ errorMessage: result.errorMessage
1524
+ });
1525
+ }
1526
+ } catch (err) {
1527
+ this.log("error", `[bridge-wake] uid=${event.uid} threw: ${err.message}`);
1528
+ } finally {
1529
+ this.inFlightBridgeWakes.delete(event.uid);
1530
+ this.log("info", `[bridge-wake] finished uid=${event.uid} in ${Date.now() - startMs}ms`);
1531
+ }
1532
+ }
1336
1533
  /** Acquire a concurrency slot, run a worker, release the slot. */
1337
1534
  async spawnWorker(account, prompt, ctx) {
1338
1535
  const releaseAgentLock = await this.acquireAgentSerial(account.id);
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-B5JDOV32.js";
5
5
  import {
6
6
  Dispatcher
7
- } from "./chunk-N6OG2X3V.js";
7
+ } from "./chunk-ALWSCQFP.js";
8
8
  import "./chunk-7FQNH2YO.js";
9
9
 
10
10
  // src/dispatcher-bin.ts
@@ -370,6 +370,31 @@ declare class Dispatcher {
370
370
  subject?: string;
371
371
  uid?: number;
372
372
  }, prompt: string): string;
373
+ /**
374
+ * Handle mail that lands in the host's OWN bridge inbox.
375
+ *
376
+ * Unlike normal sub-agent wakes (which spawn a fresh worker turn),
377
+ * bridge wakes resume the operator's last interactive session via
378
+ * the Claude Agent SDK's `resume` option — keeping the operator's
379
+ * context intact and avoiding a duplicate "second Claude trying to
380
+ * be Claude Code". When no fresh session is available (operator
381
+ * hasn't run `claude` in >24h, or `~/.agenticmail/host-sessions.json`
382
+ * was wiped), the dispatcher emits an `urgent` system event so the
383
+ * web UI's notification surface + any future SMS escalation hook
384
+ * can pick it up. See packages/claudecode/src/bridge-wake.ts.
385
+ *
386
+ * Two short-circuits:
387
+ * 1. If lastSeenMs is within 30s, the operator is actively at the
388
+ * keyboard right now — their own UserPromptSubmit / Stop hook
389
+ * will surface the mail on their next keystroke. We skip the
390
+ * resume entirely to avoid token waste.
391
+ * 2. If a previous bridge-wake for this same UID is already
392
+ * in-flight (same key in `inFlightBridgeWakes`), skip — IMAP
393
+ * IDLE replays + SSE reconnects can fire the same event
394
+ * twice and we don't want to double-resume.
395
+ */
396
+ private inFlightBridgeWakes;
397
+ private handleBridgeMail;
373
398
  /** Acquire a concurrency slot, run a worker, release the slot. */
374
399
  private spawnWorker;
375
400
  /**
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Dispatcher
3
- } from "./chunk-N6OG2X3V.js";
3
+ } from "./chunk-ALWSCQFP.js";
4
4
  import "./chunk-7FQNH2YO.js";
5
5
  export {
6
6
  Dispatcher
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  Dispatcher,
8
8
  loadPersonaForAgent
9
- } from "./chunk-N6OG2X3V.js";
9
+ } from "./chunk-ALWSCQFP.js";
10
10
  import {
11
11
  createIntegrationRoutes
12
12
  } from "./chunk-QXFCUEN3.js";
package/dist/mail-hook.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
5
5
  import { homedir } from "os";
6
6
  import { join, dirname } from "path";
7
+ import { saveHostSession } from "@agenticmail/core";
7
8
  var AGENTICMAIL_DIR = join(homedir(), ".agenticmail");
8
9
  var CONFIG_PATH = join(AGENTICMAIL_DIR, "config.json");
9
10
  var CURSOR_PATH = join(AGENTICMAIL_DIR, "claudecode-hook-cursor.json");
@@ -92,6 +93,12 @@ async function main() {
92
93
  const input = await readStdinJson();
93
94
  const eventName = input?.hook_event_name ?? "UserPromptSubmit";
94
95
  const sessionId = typeof input?.session_id === "string" ? input.session_id : "";
96
+ if (sessionId) {
97
+ try {
98
+ saveHostSession("claudecode", { sessionId, workspace: process.cwd() });
99
+ } catch {
100
+ }
101
+ }
95
102
  if (eventName === "SessionStart") {
96
103
  process.stdout.write(JSON.stringify({
97
104
  hookSpecificOutput: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/claudecode",
3
- "version": "0.2.19",
3
+ "version": "0.2.20",
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",
@@ -47,7 +47,7 @@
47
47
  "prepublishOnly": "npm run build"
48
48
  },
49
49
  "dependencies": {
50
- "@agenticmail/core": "^0.9.5",
50
+ "@agenticmail/core": "^0.9.6",
51
51
  "@agenticmail/mcp": "^0.9.7",
52
52
  "@anthropic-ai/claude-agent-sdk": "^0.2.140"
53
53
  },