@agentconnect.md/daemon 1.0.0-rc.282 → 1.0.0-rc.284

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.
@@ -5,7 +5,7 @@ import { t as DAEMON_VERSION } from "./version-DGaRXjBK.js";
5
5
  import { C as _coercedNumber, T as NEVER, a as array, d as literal, f as looseObject, i as any, m as object, o as boolean, p as number$1, t as ZodNumber, v as string, x as url$4 } from "./schemas-CQkO9hC5.js";
6
6
  import { C as ZodIssueCode, S as RESERVED_MCP_SERVER_NAME, _ as buildEnvelope, a as agentChildEnv, b as MAX_FRAME_BYTES, c as persistRelays, d as normalizeGitUrl, f as RELAY_DAEMON_SUBPROTOCOL, g as SHARED_CONFIG_ACTION_ID, h as decodeRelayDaemonFrame, i as selectAgent, m as buildRelayDaemonFrame, n as findAgentFiles, o as loadConfig, p as RELAY_DAEMON_WS_PATH, r as loadAgents, s as persistDaemonId, v as decodeEnvelope, y as encode } from "./load-agents-DzCnR0cj.js";
7
7
  import { l as resolveRoot, o as mcpSocketPath, u as statePath } from "./paths-DEcUO8lC.js";
8
- import { S as resolveRuntimes, _ as writeMemoryFile, a as initGitInjection, b as cachedRuntimeNames, c as simpleGit, d as memoryProviderFor, f as MemoryConflictError, g as resolveInMemoryDir, h as listMemory, i as prepareWorkspace, l as createMemoryProvider, m as MemoryTooLargeError, n as installedRuntimes, o as probeGitVersion, p as MemoryPathError, r as prefetchWorkspace, s as sessionGitEnv, t as AcpHost, u as memoryKindOf, v as MEMORY_TOOL_NAMES, x as cachedRuntimeVersions, y as toolsForIntegrations } from "./acp-host-C-4zFhF8.js";
8
+ import { S as resolveRuntimes, _ as writeMemoryFile, a as initGitInjection, b as cachedRuntimeNames, c as simpleGit, d as memoryProviderFor, f as MemoryConflictError, g as resolveInMemoryDir, h as listMemory, i as prepareWorkspace, l as createMemoryProvider, m as MemoryTooLargeError, n as installedRuntimes, o as probeGitVersion, p as MemoryPathError, r as prefetchWorkspace, s as sessionGitEnv, t as AcpHost, u as memoryKindOf, v as MEMORY_TOOL_NAMES, x as cachedRuntimeVersions, y as toolsForIntegrations } from "./acp-host-CDuL4q1C.js";
9
9
  import { i as GitCredentialCache, n as gitcredSocketPath, r as writeGitcredShim, t as GitCredServer } from "./gitcred-server-CMC80O_1.js";
10
10
  import { B as ToolListChangedNotificationSchema, D as ListPromptsResultSchema, E as ListChangedOptionsBaseSchema, F as PromptListChangedNotificationSchema, G as isZ4Schema, H as isJSONRPCRequest, I as ReadResourceResultSchema, J as encodeFrame, K as safeParse, L as ResourceListChangedNotificationSchema, M as ListToolsResultSchema, O as ListResourceTemplatesResultSchema, P as McpError, R as SUPPORTED_PROTOCOL_VERSIONS, S as InitializeResultSchema, T as LATEST_PROTOCOL_VERSION, U as isJSONRPCResultResponse, V as isInitializedNotification, W as getObjectShape, _ as ElicitResultSchema, a as AjvJsonSchemaValidator, b as GetPromptResultSchema, c as mergeCapabilities, d as CompleteResultSchema, f as CreateMessageRequestSchema, g as ElicitRequestSchema, h as CreateTaskResultSchema, i as assertToolsCallTaskCapability, k as ListResourcesResultSchema, m as CreateMessageResultWithToolsSchema, n as serializeMessage, o as require_fast_deep_equal, p as CreateMessageResultSchema, q as decodeFrames, r as assertClientRequestTaskCapability, s as Protocol, t as ReadBuffer, u as CallToolResultSchema, v as EmptyResultSchema, w as JSONRPCMessageSchema, y as ErrorCode } from "./stdio-DnS8WcXi.js";
11
11
  import { URL as URL$1 } from "url";
@@ -6373,6 +6373,68 @@ var LocalStore = class {
6373
6373
  CREATE TABLE IF NOT EXISTS cron_runs (
6374
6374
  key TEXT PRIMARY KEY, lastRunAt INTEGER NOT NULL
6375
6375
  );
6376
+ -- §6.9 #353 durable inbox: an ADMITTED-but-QUEUED message persisted BEFORE the
6377
+ -- admission ACK, so a hard kill / agent move can't lose a message the caller was
6378
+ -- already told delivered:true. Replayed FIFO-by-sessionKey on startup and removed
6379
+ -- on every terminal path (success / reject / cancel / gate-drop). The id column is the
6380
+ -- message's STABLE deliveryId/msgId (§6.3) so re-append and replay are idempotent
6381
+ -- against the existing admission-idempotency maps. Webchat turns are NOT persisted
6382
+ -- here (their live sink can't be restored — see §6.9 #367). enqueuedAt is a
6383
+ -- monotonic decimal string (fixed-width ⇒ string order == numeric order) driving
6384
+ -- FIFO within a sessionKey.
6385
+ CREATE TABLE IF NOT EXISTS inbox (
6386
+ id TEXT PRIMARY KEY,
6387
+ sessionKey TEXT NOT NULL,
6388
+ agentId TEXT NOT NULL,
6389
+ msg TEXT NOT NULL,
6390
+ integrationId TEXT,
6391
+ callMeta TEXT,
6392
+ isQueueCmd INTEGER,
6393
+ enqueuedAt TEXT NOT NULL
6394
+ );
6395
+ CREATE INDEX IF NOT EXISTS inbox_fifo ON inbox (sessionKey, enqueuedAt);
6396
+ -- §3.4/§6.8 main-agent orchestration: a main agent fans out N subtasks to
6397
+ -- worker agents, then waits asynchronously and summarizes. The record is
6398
+ -- persisted BEFORE any delivery (record-first — a fast worker's reply must
6399
+ -- never arrive before the record exists, else §3.3 correlation drops it).
6400
+ -- daemon-local (never on the CP hot path); the deadline epoch is the durable
6401
+ -- SoT for the one-shot cron re-armed on startup. status: active|done|cancelled.
6402
+ CREATE TABLE IF NOT EXISTS orchestration (
6403
+ orchestrationId TEXT PRIMARY KEY,
6404
+ mainSessionKey TEXT NOT NULL,
6405
+ mainAgentId TEXT NOT NULL,
6406
+ -- The main's session coords, stored explicitly (not parsed from mainSessionKey,
6407
+ -- whose channel/thread may contain ':') so the deadline fire wakes the exact session.
6408
+ platform TEXT NOT NULL,
6409
+ channel TEXT NOT NULL,
6410
+ thread TEXT NOT NULL,
6411
+ integrationId TEXT,
6412
+ replyTarget TEXT,
6413
+ deadline INTEGER,
6414
+ status TEXT NOT NULL,
6415
+ createdAt INTEGER NOT NULL,
6416
+ updatedAt INTEGER NOT NULL
6417
+ );
6418
+ CREATE INDEX IF NOT EXISTS orchestration_active ON orchestration (status);
6419
+ -- One row per subtask. Stable correlationId = "<orchestrationId>.<idx>" is the
6420
+ -- delivery/report correlation key (§3.3). State machine (§6.8):
6421
+ -- pending → sending → delivered → succeeded | worker_error | timed_out
6422
+ -- busy/offline are RETRYABLE attempt states folded back to a delivery failure.
6423
+ -- Transitions are CAS + idempotent on (orchestrationId, correlationId).
6424
+ CREATE TABLE IF NOT EXISTS orchestration_subtask (
6425
+ orchestrationId TEXT NOT NULL,
6426
+ correlationId TEXT NOT NULL,
6427
+ idx INTEGER NOT NULL,
6428
+ toAgentId TEXT NOT NULL,
6429
+ text TEXT NOT NULL,
6430
+ status TEXT NOT NULL,
6431
+ result TEXT,
6432
+ deliveryReason TEXT,
6433
+ updatedAt TEXT NOT NULL,
6434
+ PRIMARY KEY (orchestrationId, correlationId)
6435
+ );
6436
+ CREATE INDEX IF NOT EXISTS orchestration_subtask_by_orch
6437
+ ON orchestration_subtask (orchestrationId, idx);
6376
6438
  `);
6377
6439
  this.migrateSessionUsage();
6378
6440
  this.migrateTranscriptToolBody();
@@ -6727,6 +6789,125 @@ var LocalStore = class {
6727
6789
  getCronLastRun(key) {
6728
6790
  return this.db.prepare("SELECT lastRunAt FROM cron_runs WHERE key = ?").get(key)?.lastRunAt;
6729
6791
  }
6792
+ /** §6.9 #353 durable inbox: persist an admitted message BEFORE its admission ACK. Keyed
6793
+ * by the stable id (deliveryId/msgId) with INSERT OR IGNORE, so a re-append of the same
6794
+ * delivery (P2 retry / replay) is a no-op — idempotent against the admission maps. */
6795
+ appendInbox(row) {
6796
+ this.db.prepare(`INSERT OR IGNORE INTO inbox (id, sessionKey, agentId, msg, integrationId, callMeta, isQueueCmd, enqueuedAt)
6797
+ VALUES (@id, @sessionKey, @agentId, @msg, @integrationId, @callMeta, @isQueueCmd, @enqueuedAt)`).run({
6798
+ id: row.id,
6799
+ sessionKey: row.sessionKey,
6800
+ agentId: row.agentId,
6801
+ msg: row.msg,
6802
+ integrationId: row.integrationId ?? null,
6803
+ callMeta: row.callMeta ?? null,
6804
+ isQueueCmd: row.isQueueCmd ?? null,
6805
+ enqueuedAt: row.enqueuedAt
6806
+ });
6807
+ }
6808
+ /** Remove an inbox row once its turn reaches a terminal state (success / reject / cancel /
6809
+ * gate-drop) so it is never replayed. No-op on an unknown id. */
6810
+ removeInbox(id) {
6811
+ this.db.prepare("DELETE FROM inbox WHERE id = ?").run(id);
6812
+ }
6813
+ /** All pending inbox rows, ordered FIFO-by-sessionKey (sessionKey, then enqueuedAt) for
6814
+ * startup replay (§6.9 #353). Order within a sessionKey is preserved by `enqueuedAt`. */
6815
+ listInboxBySessionKeyFifo() {
6816
+ return this.db.prepare("SELECT * FROM inbox ORDER BY sessionKey ASC, enqueuedAt ASC").all();
6817
+ }
6818
+ /**
6819
+ * RECORD-FIRST (§3.4): persist the orchestration header + all subtask rows (status
6820
+ * 'pending') in ONE transaction, BEFORE any delivery. A fast worker's reply that
6821
+ * arrives before this returns has nothing to correlate against and is dropped — so
6822
+ * this MUST complete before startOrchestration delivers anything. Idempotent via
6823
+ * INSERT OR IGNORE on the stable ids (replay-safe).
6824
+ */
6825
+ createOrchestration(orch, subtasks) {
6826
+ const insertOrch = this.db.prepare(`INSERT OR IGNORE INTO orchestration
6827
+ (orchestrationId, mainSessionKey, mainAgentId, platform, channel, thread,
6828
+ integrationId, replyTarget, deadline, status, createdAt, updatedAt)
6829
+ VALUES (@orchestrationId, @mainSessionKey, @mainAgentId, @platform, @channel, @thread,
6830
+ @integrationId, @replyTarget, @deadline, @status, @createdAt, @updatedAt)`);
6831
+ const insertSub = this.db.prepare(`INSERT OR IGNORE INTO orchestration_subtask
6832
+ (orchestrationId, correlationId, idx, toAgentId, text, status, result, deliveryReason, updatedAt)
6833
+ VALUES (@orchestrationId, @correlationId, @idx, @toAgentId, @text, @status, @result, @deliveryReason, @updatedAt)`);
6834
+ this.db.exec("BEGIN");
6835
+ try {
6836
+ insertOrch.run({
6837
+ orchestrationId: orch.orchestrationId,
6838
+ mainSessionKey: orch.mainSessionKey,
6839
+ mainAgentId: orch.mainAgentId,
6840
+ platform: orch.platform,
6841
+ channel: orch.channel,
6842
+ thread: orch.thread,
6843
+ integrationId: orch.integrationId ?? null,
6844
+ replyTarget: orch.replyTarget ?? null,
6845
+ deadline: orch.deadline ?? null,
6846
+ status: orch.status,
6847
+ createdAt: orch.createdAt,
6848
+ updatedAt: orch.updatedAt
6849
+ });
6850
+ for (const s of subtasks) insertSub.run({
6851
+ orchestrationId: s.orchestrationId,
6852
+ correlationId: s.correlationId,
6853
+ idx: s.idx,
6854
+ toAgentId: s.toAgentId,
6855
+ text: s.text,
6856
+ status: s.status,
6857
+ result: s.result ?? null,
6858
+ deliveryReason: s.deliveryReason ?? null,
6859
+ updatedAt: s.updatedAt
6860
+ });
6861
+ this.db.exec("COMMIT");
6862
+ } catch (err) {
6863
+ this.db.exec("ROLLBACK");
6864
+ throw err;
6865
+ }
6866
+ }
6867
+ getOrchestration(orchestrationId) {
6868
+ return this.db.prepare("SELECT * FROM orchestration WHERE orchestrationId = ?").get(orchestrationId);
6869
+ }
6870
+ getSubtasks(orchestrationId) {
6871
+ return this.db.prepare("SELECT * FROM orchestration_subtask WHERE orchestrationId = ? ORDER BY idx ASC").all(orchestrationId);
6872
+ }
6873
+ getSubtaskByCorrelation(orchestrationId, correlationId) {
6874
+ return this.db.prepare("SELECT * FROM orchestration_subtask WHERE orchestrationId = ? AND correlationId = ?").get(orchestrationId, correlationId);
6875
+ }
6876
+ /** All still-`active` orchestrations — for startup re-arm of deadlines + re-drive
6877
+ * of non-terminal subtasks (§6.8). */
6878
+ listActiveOrchestrations() {
6879
+ return this.db.prepare("SELECT * FROM orchestration WHERE status = 'active' ORDER BY createdAt ASC").all();
6880
+ }
6881
+ /** CAS subtask status — only advances when the current status is one of `from`.
6882
+ * Idempotent + monotonic: a stale/duplicate transition (current status not in
6883
+ * `from`) is a no-op and returns false. */
6884
+ setSubtaskStatus(orchestrationId, correlationId, from, to, updatedAt, extra) {
6885
+ const placeholders = from.map((_, i) => `@from${i}`).join(", ");
6886
+ const params = {
6887
+ orchestrationId,
6888
+ correlationId,
6889
+ to,
6890
+ updatedAt
6891
+ };
6892
+ from.forEach((f, i) => params[`from${i}`] = f);
6893
+ let setResult = "";
6894
+ if (extra && "result" in extra) {
6895
+ setResult += ", result=@result";
6896
+ params.result = extra.result ?? null;
6897
+ }
6898
+ if (extra && "deliveryReason" in extra) {
6899
+ setResult += ", deliveryReason=@deliveryReason";
6900
+ params.deliveryReason = extra.deliveryReason ?? null;
6901
+ }
6902
+ return this.db.prepare(`UPDATE orchestration_subtask SET status=@to, updatedAt=@updatedAt${setResult}
6903
+ WHERE orchestrationId=@orchestrationId AND correlationId=@correlationId AND status IN (${placeholders})`).run(params).changes > 0;
6904
+ }
6905
+ setOrchestrationStatus(orchestrationId, status, updatedAt) {
6906
+ this.db.prepare("UPDATE orchestration SET status=?, updatedAt=? WHERE orchestrationId=?").run(status, updatedAt, orchestrationId);
6907
+ }
6908
+ setOrchestrationDeadline(orchestrationId, deadline, updatedAt) {
6909
+ this.db.prepare("UPDATE orchestration SET deadline=?, updatedAt=? WHERE orchestrationId=?").run(deadline, updatedAt, orchestrationId);
6910
+ }
6730
6911
  close() {
6731
6912
  this.db.close();
6732
6913
  }
@@ -7128,6 +7309,8 @@ async function executeTool(ctx, name, args, deps) {
7128
7309
  return await deps.messageAgent({
7129
7310
  callerAgentId: ctx.agentId,
7130
7311
  platform: ctx.platform,
7312
+ callerChannel: ctx.channel,
7313
+ callerThread: ctx.thread,
7131
7314
  toAgentId,
7132
7315
  text,
7133
7316
  channel,
@@ -7135,6 +7318,50 @@ async function executeTool(ctx, name, args, deps) {
7135
7318
  ...correlationId !== void 0 ? { correlationId } : {}
7136
7319
  });
7137
7320
  }
7321
+ if (name === "startOrchestration") {
7322
+ const rawSubtasks = args.subtasks;
7323
+ if (!Array.isArray(rawSubtasks) || rawSubtasks.length === 0) throw new Error("missing required argument: subtasks (non-empty array)");
7324
+ const subtasks = rawSubtasks.map((s, i) => {
7325
+ if (typeof s !== "object" || s === null) throw new Error(`subtasks[${i}] must be an object`);
7326
+ const so = s;
7327
+ return {
7328
+ toAgentId: requireString(so, "toAgentId"),
7329
+ text: requireString(so, "text")
7330
+ };
7331
+ });
7332
+ const deadlineMs = optionalNumber(args, "deadlineMs");
7333
+ const replyTarget = optionalString(args, "replyTarget");
7334
+ return await deps.startOrchestration({
7335
+ mainAgentId: ctx.agentId,
7336
+ platform: ctx.platform,
7337
+ channel: ctx.channel,
7338
+ thread: ctx.thread,
7339
+ ...ctx.integrationId !== void 0 ? { integrationId: ctx.integrationId } : {},
7340
+ subtasks,
7341
+ ...deadlineMs !== void 0 ? { deadlineMs } : {},
7342
+ ...replyTarget !== void 0 ? { replyTarget } : {}
7343
+ });
7344
+ }
7345
+ if (name === "getOrchestration" || name === "cancelOrchestration") {
7346
+ const orchestrationId = requireString(args, "orchestrationId");
7347
+ const req = {
7348
+ mainAgentId: ctx.agentId,
7349
+ platform: ctx.platform,
7350
+ channel: ctx.channel,
7351
+ thread: ctx.thread,
7352
+ orchestrationId
7353
+ };
7354
+ if (name === "getOrchestration") {
7355
+ const rec = await deps.getOrchestration(req);
7356
+ if (rec === null) throw new Error(`no orchestration ${orchestrationId} owned by this session`);
7357
+ return rec;
7358
+ }
7359
+ if (!await deps.cancelOrchestration(req)) throw new Error(`no orchestration ${orchestrationId} owned by this session`);
7360
+ return {
7361
+ orchestrationId,
7362
+ cancelled: true
7363
+ };
7364
+ }
7138
7365
  const gw = ctx.integrationId ? deps.gatewayFor(ctx.integrationId) : void 0;
7139
7366
  if (!gw) throw new Error(`no live platform connection for integration ${ctx.integrationId ?? "(none)"}`);
7140
7367
  switch (name) {
@@ -7215,6 +7442,12 @@ function optionalString(args, key) {
7215
7442
  if (typeof v !== "string") throw new Error(`argument ${key} must be a string`);
7216
7443
  return v;
7217
7444
  }
7445
+ function optionalNumber(args, key) {
7446
+ const v = args[key];
7447
+ if (v === void 0 || v === null) return void 0;
7448
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new Error(`argument ${key} must be a finite number`);
7449
+ return v;
7450
+ }
7218
7451
  //#endregion
7219
7452
  //#region src/mcp/control-server.ts
7220
7453
  /**
@@ -214181,6 +214414,9 @@ var Daemon = class Daemon {
214181
214414
  return client.channelAgents(req);
214182
214415
  },
214183
214416
  messageAgent: (req) => this.messageAgent(req),
214417
+ startOrchestration: (req) => this.startOrchestration(req),
214418
+ getOrchestration: (req) => Promise.resolve(this.getOrchestrationForOwner(req)),
214419
+ cancelOrchestration: (req) => Promise.resolve(this.cancelOrchestrationForOwner(req)),
214184
214420
  memory: this.memory,
214185
214421
  recordOutbound: (ctx, channel, thread, text, ts) => this.store.appendTranscript({
214186
214422
  channel,
@@ -214295,6 +214531,8 @@ var Daemon = class Daemon {
214295
214531
  };
214296
214532
  this.watcher.on("add", debounced).on("change", debounced).on("unlink", debounced);
214297
214533
  this.log.info(`watching ${this.agentsDir} for agent changes`);
214534
+ this.replayInbox();
214535
+ this.rearmOrchestrationDeadlines();
214298
214536
  this.startCpClient(root);
214299
214537
  this.armIdleSweep();
214300
214538
  this.log.info("daemon ready");
@@ -215058,6 +215296,7 @@ var Daemon = class Daemon {
215058
215296
  }
215059
215297
  relayMsgAcks = /* @__PURE__ */ new Map();
215060
215298
  agentCallDeliveries = /* @__PURE__ */ new Map();
215299
+ orchestrationDeadlines = /* @__PURE__ */ new Map();
215061
215300
  /**
215062
215301
  * Dispatch one inbound relay item (`rd/msg` — a webchat op or a hook fire) and
215063
215302
  * return the `rd/ack` verdict — the relay-path entry, reusing the SAME turn
@@ -215225,10 +215464,19 @@ var Daemon = class Daemon {
215225
215464
  reason: "not_allowed"
215226
215465
  });
215227
215466
  }
215228
- const hopCount = 0;
215467
+ const callerKey = sessionKey(platform, req.callerChannel, req.callerThread, req.callerAgentId);
215468
+ const inbound = this.activeTurnCallMeta.get(callerKey);
215469
+ const hopCount = inbound ? inbound.hopCount + 1 : 0;
215470
+ if (hopCount > MAX_AGENT_CALL_HOPS) return record({
215471
+ delivered: false,
215472
+ targetSession,
215473
+ reason: "hop_limit"
215474
+ });
215475
+ const isReply = inbound !== void 0 && req.toAgentId === inbound.callFrom;
215476
+ const correlationId = req.correlationId !== void 0 ? req.correlationId : isReply ? inbound.correlationId : void 0;
215229
215477
  const callMeta = {
215230
215478
  callFrom: req.callerAgentId,
215231
- ...req.correlationId !== void 0 ? { correlationId: req.correlationId } : {},
215479
+ ...correlationId !== void 0 ? { correlationId } : {},
215232
215480
  hopCount,
215233
215481
  deliveryId
215234
215482
  };
@@ -215311,6 +215559,235 @@ var Daemon = class Daemon {
215311
215559
  return p === "telegram" || p === "webchat" || p === "discord" || p === "hook" ? p : "slack";
215312
215560
  }
215313
215561
  /**
215562
+ * Start an orchestration (§3.4/§6.8). ATOMIC ordering per §3.4:
215563
+ * (a) RECORD-FIRST — persist the orchestration header + every subtask (status
215564
+ * 'pending') in one transaction BEFORE any delivery, so a fast worker's reply can
215565
+ * never arrive before the record exists (§3.3 would otherwise drop it);
215566
+ * (b) deliver each subtask via the existing `messageAgent` path, CAS-recording
215567
+ * pending→sending→delivered | pending→sending→(delivered rollback)failed per subtask;
215568
+ * (c) schedule the one-shot, session-anchored, cancelable cron deadline (if requested)
215569
+ * — but only when at least one subtask actually delivered (all-failed ⇒ no wait).
215570
+ * The main identity + coords are the TRUSTED SessionContext (never tool input).
215571
+ */
215572
+ async startOrchestration(req) {
215573
+ const orchestrationId = randomUUID();
215574
+ const platform = this.narrowPlatform(req.platform);
215575
+ const mainSessionKey = sessionKey(platform, req.channel, req.thread, req.mainAgentId);
215576
+ const now = this.clock.now();
215577
+ const deadline = req.deadlineMs !== void 0 && req.deadlineMs > 0 ? now + Math.min(req.deadlineMs, 2147483647) : null;
215578
+ const subtaskRows = req.subtasks.map((s, idx) => ({
215579
+ orchestrationId,
215580
+ correlationId: `${orchestrationId}.${idx}`,
215581
+ idx,
215582
+ toAgentId: s.toAgentId,
215583
+ text: s.text,
215584
+ status: "pending",
215585
+ updatedAt: monotonicTs()
215586
+ }));
215587
+ const orch = {
215588
+ orchestrationId,
215589
+ mainSessionKey,
215590
+ mainAgentId: req.mainAgentId,
215591
+ platform,
215592
+ channel: req.channel,
215593
+ thread: req.thread,
215594
+ integrationId: req.integrationId ?? null,
215595
+ replyTarget: req.replyTarget ?? null,
215596
+ deadline,
215597
+ status: "active",
215598
+ createdAt: now,
215599
+ updatedAt: now
215600
+ };
215601
+ this.store.createOrchestration(orch, subtaskRows);
215602
+ const delivered = [];
215603
+ const failed = [];
215604
+ for (const s of subtaskRows) {
215605
+ this.store.setSubtaskStatus(orchestrationId, s.correlationId, ["pending"], "sending", monotonicTs());
215606
+ let result;
215607
+ try {
215608
+ result = await this.messageAgent({
215609
+ callerAgentId: req.mainAgentId,
215610
+ platform: req.platform,
215611
+ callerChannel: req.channel,
215612
+ callerThread: req.thread,
215613
+ toAgentId: s.toAgentId,
215614
+ text: s.text,
215615
+ channel: req.channel,
215616
+ thread: req.thread,
215617
+ correlationId: s.correlationId
215618
+ });
215619
+ } catch (err) {
215620
+ result = {
215621
+ delivered: false,
215622
+ targetSession: "",
215623
+ reason: "error"
215624
+ };
215625
+ this.log.warn(`orchestration ${orchestrationId}: delivery of ${s.correlationId} threw: ${formatErr(err)}`);
215626
+ }
215627
+ if (result.delivered) {
215628
+ this.store.setSubtaskStatus(orchestrationId, s.correlationId, ["sending"], "delivered", monotonicTs());
215629
+ delivered.push(s.correlationId);
215630
+ } else {
215631
+ const reason = result.reason ?? "undeliverable";
215632
+ this.store.setSubtaskStatus(orchestrationId, s.correlationId, ["sending"], "worker_error", monotonicTs(), { deliveryReason: reason });
215633
+ failed.push({
215634
+ correlationId: s.correlationId,
215635
+ reason
215636
+ });
215637
+ }
215638
+ }
215639
+ if (deadline !== null && delivered.length > 0) this.armOrchestrationDeadline(orchestrationId, deadline);
215640
+ else if (deadline !== null) this.store.setOrchestrationDeadline(orchestrationId, null, this.clock.now());
215641
+ this.log.info(`orchestration ${orchestrationId}: ${delivered.length} delivered, ${failed.length} failed` + (deadline !== null && delivered.length > 0 ? `, deadline in ${deadline - now}ms` : ""));
215642
+ return {
215643
+ orchestrationId,
215644
+ delivered,
215645
+ failed
215646
+ };
215647
+ }
215648
+ /** Arm (or re-arm) the one-shot, session-anchored deadline for an orchestration. On fire
215649
+ * it wakes the main's session DIRECTLY (dispatch to mainSessionKey) — NOT via fireTrigger
215650
+ * (which would post a channel anchor / open a new thread). Idempotent: replaces any
215651
+ * existing timer for this id. Clamped to setTimeout's 32-bit ceiling. */
215652
+ armOrchestrationDeadline(orchestrationId, deadlineEpoch) {
215653
+ const existing = this.orchestrationDeadlines.get(orchestrationId);
215654
+ if (existing !== void 0) this.clock.clearTimeout(existing);
215655
+ const delay = Math.min(Math.max(0, deadlineEpoch - this.clock.now()), 2147483647);
215656
+ const handle = this.clock.setTimeout(() => {
215657
+ this.orchestrationDeadlines.delete(orchestrationId);
215658
+ this.fireOrchestrationDeadline(orchestrationId);
215659
+ }, delay);
215660
+ this.orchestrationDeadlines.set(orchestrationId, handle);
215661
+ }
215662
+ /** Deadline fired (§3.5): mark every still-open subtask timed_out, then WAKE the main's
215663
+ * session so it re-reads getOrchestration and summarizes the partial result. The wake is
215664
+ * a direct dispatch to the stored session coords — no platform post, no new thread. */
215665
+ fireOrchestrationDeadline(orchestrationId) {
215666
+ const orch = this.store.getOrchestration(orchestrationId);
215667
+ if (!orch || orch.status !== "active") return;
215668
+ for (const s of this.store.getSubtasks(orchestrationId)) this.store.setSubtaskStatus(orchestrationId, s.correlationId, [
215669
+ "pending",
215670
+ "sending",
215671
+ "delivered"
215672
+ ], "timed_out", monotonicTs());
215673
+ this.store.setOrchestrationDeadline(orchestrationId, null, this.clock.now());
215674
+ this.wakeOrchestrationMain(orch, `orchestration ${orchestrationId} deadline reached — summarize what you have`);
215675
+ }
215676
+ /** Wake the orchestration's owning main session with a synthetic agent-source turn keyed
215677
+ * to the exact stored coords (so it lands in the same session that started it). Headless
215678
+ * is NOT set — the main needs its reply transport to post the summary. */
215679
+ wakeOrchestrationMain(orch, text) {
215680
+ const platform = this.narrowPlatform(orch.platform);
215681
+ const msg = {
215682
+ msgId: `orchestration:${orch.orchestrationId}:${monotonicTs()}`,
215683
+ traceId: orch.orchestrationId,
215684
+ source: "agent",
215685
+ platform,
215686
+ channel: orch.channel,
215687
+ thread: orch.thread,
215688
+ sender: {
215689
+ id: `orchestration:${orch.orchestrationId}`,
215690
+ isBot: true
215691
+ },
215692
+ text,
215693
+ mentionedBots: [],
215694
+ isDm: false
215695
+ };
215696
+ this.dispatch(orch.mainAgentId, msg, orch.integrationId ?? void 0).catch((err) => this.log.error(`orchestration ${orch.orchestrationId}: deadline wake dispatch failed: ${formatErr(err)}`));
215697
+ }
215698
+ /** §3.3 correlation-recording hook, called from dispatchOne when the MAIN receives a
215699
+ * messageAgent turn carrying a correlationId. Records the worker's result into the
215700
+ * orchestration ONLY if ALL four safety checks hold; any failure drops the report
215701
+ * (debug log) and never corrupts completion. Uses ONLY trusted values (callMeta.callFrom,
215702
+ * the receiving session key) — never a frame/prompt field. */
215703
+ recordWorkerReport(receivingSessionKey, callMeta, reportText) {
215704
+ const correlationId = callMeta.correlationId;
215705
+ if (correlationId === void 0) return;
215706
+ const dot = correlationId.lastIndexOf(".");
215707
+ if (dot <= 0) return;
215708
+ const orchestrationId = correlationId.slice(0, dot);
215709
+ const orch = this.store.getOrchestration(orchestrationId);
215710
+ if (!orch) return;
215711
+ if (orch.mainSessionKey !== receivingSessionKey) {
215712
+ this.log.debug(`orchestration ${orchestrationId}: report dropped — session mismatch`);
215713
+ return;
215714
+ }
215715
+ if (orch.status !== "active") {
215716
+ this.log.debug(`orchestration ${orchestrationId}: report dropped — orchestration ${orch.status}`);
215717
+ return;
215718
+ }
215719
+ const sub = this.store.getSubtaskByCorrelation(orchestrationId, correlationId);
215720
+ if (!sub) return;
215721
+ if (callMeta.callFrom !== sub.toAgentId) {
215722
+ this.log.debug(`orchestration ${orchestrationId}: report for ${correlationId} dropped — reporter "${callMeta.callFrom}" ≠ tasked worker "${sub.toAgentId}"`);
215723
+ return;
215724
+ }
215725
+ if (this.store.setSubtaskStatus(orchestrationId, correlationId, [
215726
+ "sending",
215727
+ "delivered",
215728
+ "timed_out"
215729
+ ], "succeeded", monotonicTs(), { result: reportText })) this.log.info(`orchestration ${orchestrationId}: recorded result for ${correlationId} from ${callMeta.callFrom}`);
215730
+ else this.log.debug(`orchestration ${orchestrationId}: duplicate/late report for ${correlationId} — no-op`);
215731
+ }
215732
+ getOrchestrationForOwner(req) {
215733
+ const orch = this.ownedOrchestration(req);
215734
+ if (!orch) return null;
215735
+ return {
215736
+ orchestrationId: orch.orchestrationId,
215737
+ status: orch.status,
215738
+ deadline: orch.deadline ?? null,
215739
+ replyTarget: orch.replyTarget ?? null,
215740
+ createdAt: orch.createdAt,
215741
+ subtasks: this.store.getSubtasks(orch.orchestrationId).map((s) => ({
215742
+ correlationId: s.correlationId,
215743
+ toAgentId: s.toAgentId,
215744
+ status: s.status,
215745
+ result: s.result ?? null,
215746
+ ...s.deliveryReason ? { deliveryReason: s.deliveryReason } : {}
215747
+ }))
215748
+ };
215749
+ }
215750
+ cancelOrchestrationForOwner(req) {
215751
+ const orch = this.ownedOrchestration(req);
215752
+ if (!orch) return false;
215753
+ const handle = this.orchestrationDeadlines.get(orch.orchestrationId);
215754
+ if (handle !== void 0) {
215755
+ this.clock.clearTimeout(handle);
215756
+ this.orchestrationDeadlines.delete(orch.orchestrationId);
215757
+ }
215758
+ this.store.setOrchestrationDeadline(orch.orchestrationId, null, this.clock.now());
215759
+ this.store.setOrchestrationStatus(orch.orchestrationId, "cancelled", this.clock.now());
215760
+ return true;
215761
+ }
215762
+ /** Resolve an orchestration IFF the requesting main+session OWNS it (§3.5a owner check).
215763
+ * Returns undefined on unknown id or any owner mismatch. */
215764
+ ownedOrchestration(req) {
215765
+ const orch = this.store.getOrchestration(req.orchestrationId);
215766
+ if (!orch) return void 0;
215767
+ const requesterKey = sessionKey(this.narrowPlatform(req.platform), req.channel, req.thread, req.mainAgentId);
215768
+ if (orch.mainSessionKey !== requesterKey || orch.mainAgentId !== req.mainAgentId) return void 0;
215769
+ return orch;
215770
+ }
215771
+ /** Startup re-arm (§3.5): re-arm the one-shot deadline for every still-active orchestration
215772
+ * from the durable `deadline` epoch. A deadline already in the past fires ~immediately.
215773
+ * Mirrors replayInbox's read-active-rows/re-schedule pattern; idempotent. */
215774
+ rearmOrchestrationDeadlines() {
215775
+ let active;
215776
+ try {
215777
+ active = this.store.listActiveOrchestrations();
215778
+ } catch (err) {
215779
+ this.log.warn(`orchestration: deadline re-arm read failed: ${err.message}`);
215780
+ return;
215781
+ }
215782
+ let armed = 0;
215783
+ for (const orch of active) {
215784
+ if (orch.deadline == null) continue;
215785
+ this.armOrchestrationDeadline(orch.orchestrationId, orch.deadline);
215786
+ armed++;
215787
+ }
215788
+ if (armed) this.log.info(`orchestration: re-armed ${armed} deadline(s) on startup`);
215789
+ }
215790
+ /**
215314
215791
  * Ack-verdict gate for one hook fire (`rd/msg` hook member) — the mirror of
215315
215792
  * {@link dispatchWebchatTurn}'s synchronous gates: the relay's rc/run-report
215316
215793
  * needs a REASONED rejection now, not a silently dropped fire-and-forget
@@ -215537,6 +216014,10 @@ var Daemon = class Daemon {
215537
216014
  }
215538
216015
  inflight = /* @__PURE__ */ new Set();
215539
216016
  serialQueue = /* @__PURE__ */ new Map();
216017
+ /** §6.9 #353: stable ids of messages currently backed by a durable inbox row (admitted,
216018
+ * not yet terminal). Guards startup replay from re-admitting a row whose entry is already
216019
+ * live in the gate (idempotency — a duplicate/in-flight id is not double-processed). */
216020
+ liveInboxIds = /* @__PURE__ */ new Set();
215540
216021
  /**
215541
216022
  * Resolve a command's target from the channel's latest session when the routing ladder
215542
216023
  * couldn't (no mention entity / thread / dm rule matched — e.g. a group `/status@bot`).
@@ -215868,6 +216349,49 @@ var Daemon = class Daemon {
215868
216349
  }
215869
216350
  }
215870
216351
  /**
216352
+ * §6.9 #353 durable inbox: persist an ADMITTED entry BEFORE its admission ACK/return, so a
216353
+ * hard kill / agent move can't lose a message the caller was already told delivered:true.
216354
+ * Called ONLY on the two genuine admission branches in dispatch() (claim + enqueue), never
216355
+ * on a rejected/gate-dropped admission — a queue-full or paused/draining drop persists
216356
+ * nothing. WEBCHAT turns are skipped (§6.9 #367): their `sink` is a live in-memory transport
216357
+ * that can't be restored across a restart and a dead browser socket can't be resumed, so a
216358
+ * durable row would be un-replayable. Sets `entry.inboxId` so every terminal path can delete
216359
+ * the row. The row id is the message's stable deliveryId/msgId (§6.3) — idempotent re-append.
216360
+ */
216361
+ persistInbox(entry, key) {
216362
+ if (entry.webchat) return;
216363
+ const id = entry.callMeta?.deliveryId ?? entry.msg.msgId;
216364
+ try {
216365
+ this.store.appendInbox({
216366
+ id,
216367
+ sessionKey: key,
216368
+ agentId: entry.agentId,
216369
+ msg: JSON.stringify(entry.msg),
216370
+ integrationId: entry.integrationId ?? null,
216371
+ callMeta: entry.callMeta ? JSON.stringify(entry.callMeta) : null,
216372
+ isQueueCmd: entry.isQueueCmd ? 1 : null,
216373
+ enqueuedAt: monotonicTs()
216374
+ });
216375
+ entry.inboxId = id;
216376
+ this.liveInboxIds.add(id);
216377
+ } catch (err) {
216378
+ this.log.warn(`durable inbox: append failed for ${id}: ${err.message}`);
216379
+ }
216380
+ }
216381
+ /** Remove an entry's durable inbox row once its turn reaches ANY terminal state (success,
216382
+ * reject/fail-stop, cancel, gate-drop) so it is not replayed on the next startup. No-op
216383
+ * for a non-persisted entry (webchat / never-admitted). NOT called on graceful-shutdown
216384
+ * settle: those admitted-but-unrun rows are intentionally LEFT for startup replay. */
216385
+ removeInbox(entry) {
216386
+ if (!entry.inboxId) return;
216387
+ this.liveInboxIds.delete(entry.inboxId);
216388
+ try {
216389
+ this.store.removeInbox(entry.inboxId);
216390
+ } catch (err) {
216391
+ this.log.warn(`durable inbox: remove failed for ${entry.inboxId}: ${err.message}`);
216392
+ }
216393
+ }
216394
+ /**
215871
216395
  * Terminate a QUEUED entry's webchat sink when it is gate-dropped/rejected without ever
215872
216396
  * entering dispatchOne (drain/pause gate-drop, fail-stop of the queued rest, `!cancel`/`!stop`
215873
216397
  * queue drop, shutdown). A webchat turn signals its client via the WebchatSink terminal
@@ -215915,9 +216439,11 @@ var Daemon = class Daemon {
215915
216439
  }
215916
216440
  q.push(entry);
215917
216441
  this.serialQueue.set(key, q);
216442
+ this.persistInbox(entry, key);
215918
216443
  this.log.debug(`dispatch: queued behind in-flight turn for session ${key} (depth ${q.length})`);
215919
216444
  return;
215920
216445
  }
216446
+ this.persistInbox(entry, key);
215921
216447
  this.inflight.add(key);
215922
216448
  this.runLoop(key, entry);
215923
216449
  });
@@ -215939,11 +216465,13 @@ var Daemon = class Daemon {
215939
216465
  while (entry) {
215940
216466
  if (entry !== firstEntry && (this.draining || this.drainingAgents.has(entry.agentId) || this.paused(entry.agentId))) {
215941
216467
  this.terminateQueuedSink(entry);
216468
+ if (!this.draining) this.removeInbox(entry);
215942
216469
  entry.resolve(null);
215943
216470
  const rest = this.serialQueue.get(key) ?? [];
215944
216471
  this.serialQueue.delete(key);
215945
216472
  for (const e of rest) {
215946
216473
  this.terminateQueuedSink(e);
216474
+ if (!this.draining) this.removeInbox(e);
215947
216475
  e.resolve(null);
215948
216476
  }
215949
216477
  this.inflight.delete(key);
@@ -215951,14 +216479,17 @@ var Daemon = class Daemon {
215951
216479
  }
215952
216480
  try {
215953
216481
  const sessionId = await this.dispatchOne(entry, key);
216482
+ this.removeInbox(entry);
215954
216483
  entry.resolve(sessionId);
215955
216484
  } catch (err) {
216485
+ if (!this.draining) this.removeInbox(entry);
215956
216486
  entry.reject(err);
215957
216487
  const rest = this.serialQueue.get(key) ?? [];
215958
216488
  this.serialQueue.delete(key);
215959
216489
  for (const e of rest) {
215960
216490
  const failStop = new FailStopError(key);
215961
216491
  this.terminateQueuedSink(e, failStop.message);
216492
+ if (!this.draining) this.removeInbox(e);
215962
216493
  e.reject(failStop);
215963
216494
  }
215964
216495
  this.inflight.delete(key);
@@ -215972,6 +216503,7 @@ var Daemon = class Daemon {
215972
216503
  }
215973
216504
  async dispatchOne(entry, key) {
215974
216505
  const { agentId, msg, integrationId, webchat, callMeta } = entry;
216506
+ if (callMeta?.correlationId !== void 0) this.recordWorkerReport(key, callMeta, msg.text);
215975
216507
  const agent = this.agents.get(agentId);
215976
216508
  const rec = new TranscriptRecorder();
215977
216509
  const replyConn = msg.headless || webchat ? void 0 : this.replyConnFor(agentId, integrationId);
@@ -216028,6 +216560,7 @@ var Daemon = class Daemon {
216028
216560
  } } : {}
216029
216561
  };
216030
216562
  this.pending.set(sessionId, p);
216563
+ if (callMeta) this.activeTurnCallMeta.set(key, callMeta);
216031
216564
  let finalPhase = "end";
216032
216565
  try {
216033
216566
  const host = await this.ensureHostAsync(agentId);
@@ -216103,6 +216636,7 @@ var Daemon = class Daemon {
216103
216636
  this.releasePermissions(sessionId);
216104
216637
  this.releaseElicits(sessionId);
216105
216638
  this.pending.delete(sessionId);
216639
+ if (callMeta) this.activeTurnCallMeta.delete(key);
216106
216640
  this.store.setSessionState(key, "idle", this.clock.now());
216107
216641
  this.emitSessionMetadataSnapshot({
216108
216642
  sessionId,
@@ -216130,6 +216664,7 @@ var Daemon = class Daemon {
216130
216664
  this.serialQueue.delete(key);
216131
216665
  for (const e of queued) {
216132
216666
  this.terminateQueuedSink(e, reason);
216667
+ this.removeInbox(e);
216133
216668
  e.reject(new FailStopError(key));
216134
216669
  }
216135
216670
  }
@@ -216591,6 +217126,17 @@ var Daemon = class Daemon {
216591
217126
  p.idleTimer = void 0;
216592
217127
  }
216593
217128
  pending = /* @__PURE__ */ new Map();
217129
+ /**
217130
+ * DAEMON-PRIVATE trusted call metadata of the CURRENTLY in-flight turn, keyed by the
217131
+ * turn's LOGICAL sessionKey (§6.7). Installed at the start of a `dispatchOne` turn that
217132
+ * carries `callMeta` and cleared in its `finally`; the tool is keyed by the static
217133
+ * session-level MCP token, so a nested `messageAgent` resolves the caller's sessionKey
217134
+ * (from its trusted SessionContext coords + agentId) and reads THIS map to auto-inherit
217135
+ * hop/origin (all child calls) and correlationId (replies only) — never trusting the
217136
+ * agent to pass them. A plain human/platform turn has no entry, so a `messageAgent` it
217137
+ * makes is a fresh call (hopCount 0, no inherited correlation).
217138
+ */
217139
+ activeTurnCallMeta = /* @__PURE__ */ new Map();
216594
217140
  permSeq = 0;
216595
217141
  pendingPerms = /* @__PURE__ */ new Map();
216596
217142
  /**
@@ -217068,6 +217614,49 @@ var Daemon = class Daemon {
217068
217614
  }
217069
217615
  this.serialQueue.clear();
217070
217616
  }
217617
+ /**
217618
+ * §6.9 #353 startup replay: re-admit durably-persisted admitted-but-not-completed inbox rows
217619
+ * through the SAME serial gate (`dispatch`), FIFO-by-sessionKey (rows come back ordered by
217620
+ * (sessionKey, enqueuedAt), so order within a sessionKey is preserved and the gate keeps them
217621
+ * serial). Runs once on start() after the store is open, agents are loaded into `this.agents`,
217622
+ * and platform connections/crons are up (so a re-admitted turn has its reply transport), as
217623
+ * normal inbound begins. Idempotency: a row whose id is already live in the gate (`liveInboxIds`)
217624
+ * is skipped — dispatch re-appends the same id with INSERT OR IGNORE so no double row is written,
217625
+ * and the re-admitted entry adopts the existing row for later removal. Rows for an agent that no
217626
+ * longer exists on this daemon are SKIPPED + logged (the simplest choice — another owner/daemon
217627
+ * may hold that agent; we neither drop the row nor block on it). Webchat turns were never
217628
+ * persisted, so none appear here.
217629
+ */
217630
+ replayInbox() {
217631
+ let rows;
217632
+ try {
217633
+ rows = this.store.listInboxBySessionKeyFifo();
217634
+ } catch (err) {
217635
+ this.log.warn(`durable inbox: replay read failed: ${err.message}`);
217636
+ return;
217637
+ }
217638
+ if (rows.length === 0) return;
217639
+ let replayed = 0;
217640
+ for (const row of rows) {
217641
+ if (this.liveInboxIds.has(row.id)) continue;
217642
+ if (!this.agents.has(row.agentId)) {
217643
+ this.log.warn(`durable inbox: skipping replay of ${row.id} — unknown agent "${row.agentId}"`);
217644
+ continue;
217645
+ }
217646
+ let msg;
217647
+ let callMeta;
217648
+ try {
217649
+ msg = JSON.parse(row.msg);
217650
+ callMeta = row.callMeta ? JSON.parse(row.callMeta) : void 0;
217651
+ } catch (err) {
217652
+ this.log.warn(`durable inbox: skipping corrupt row ${row.id}: ${err.message}`);
217653
+ continue;
217654
+ }
217655
+ this.dispatch(row.agentId, msg, row.integrationId ?? void 0, void 0, callMeta, row.isQueueCmd ? { isQueueCmd: true } : void 0).catch(() => {});
217656
+ replayed++;
217657
+ }
217658
+ if (replayed) this.log.info(`durable inbox: replayed ${replayed} admitted message(s) through the serial gate`);
217659
+ }
217071
217660
  /** daemon/restart + daemon/upgrade (§8.3): ack now, then drain + stop + exit so
217072
217661
  * the supervisor relaunches (the new binary, for upgrade). */
217073
217662
  scheduleFleetExit(kind, targetVersion) {
@@ -217423,6 +218012,8 @@ var Daemon = class Daemon {
217423
218012
  this.cancelTimers.clear();
217424
218013
  for (const t of this.slackRetryTimers.values()) this.clock.clearTimeout(t);
217425
218014
  this.slackRetryTimers.clear();
218015
+ for (const t of this.orchestrationDeadlines.values()) this.clock.clearTimeout(t);
218016
+ this.orchestrationDeadlines.clear();
217426
218017
  this.metrics?.stop();
217427
218018
  await this.watcher?.close();
217428
218019
  await this.drainForShutdown();
@@ -217473,4 +218064,4 @@ async function runForeground(opts, deps = {}) {
217473
218064
  //#endregion
217474
218065
  export { ClientTransport as i, CP_SUBPROTOCOL as n, CP_WS_PATH as r, runForeground as t };
217475
218066
 
217476
- //# sourceMappingURL=run-foreground-BV2scayf.js.map
218067
+ //# sourceMappingURL=run-foreground-D_sn_AMV.js.map