@agentproto/runtime 2.2.0 → 2.4.0

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/index.mjs CHANGED
@@ -310,11 +310,16 @@ function createTranscriptWriter(opts) {
310
310
  }, DEBOUNCE_MS);
311
311
  };
312
312
  return {
313
- recordPrompt(sessionId, message) {
313
+ recordPrompt(sessionId, message, opts2) {
314
314
  const state = getState(sessionId);
315
315
  flushBuffers(sessionId, state);
316
316
  const text9 = typeof message === "string" ? message : JSON.stringify(message);
317
- writeRecord(sessionId, state, { kind: "user-prompt", sessionId, text: text9 });
317
+ writeRecord(sessionId, state, {
318
+ kind: "user-prompt",
319
+ sessionId,
320
+ text: text9,
321
+ ...opts2?.source ? { source: opts2.source } : {}
322
+ });
318
323
  },
319
324
  recordEvent(sessionId, evt) {
320
325
  const state = getState(sessionId);
@@ -404,6 +409,22 @@ function createTranscriptWriter(opts) {
404
409
  options: evt.options
405
410
  });
406
411
  break;
412
+ // Durable counterpart to "agent-prompt" — recorded when
413
+ // `respondPermission` (or a dying session's cancel-in-flight sweep)
414
+ // resolves the ask, keyed by the same `toolCallId` so a reader can
415
+ // tell an answered request from a still-pending one without relying
416
+ // on the in-memory pending-permissions map (see sessions.ts's
417
+ // `resolvePendingPermission` / `cancelPendingPermissionsForSession`).
418
+ case "permission-resolved":
419
+ flushBuffers(sessionId, state);
420
+ writeRecord(sessionId, state, {
421
+ kind: "permission-resolved",
422
+ sessionId,
423
+ toolCallId: evt.toolCallId,
424
+ decision: evt.decision,
425
+ ...evt.optionId ? { optionId: evt.optionId } : {}
426
+ });
427
+ break;
407
428
  case "turn-end":
408
429
  flushBuffers(sessionId, state);
409
430
  writeRecord(sessionId, state, { kind: "turn-end", sessionId, reason: evt.reason });
@@ -2552,7 +2573,7 @@ function resolveAuthSpec(input) {
2552
2573
  }
2553
2574
  if (!provider) return void 0;
2554
2575
  const sub = input.descriptor.authSubscription;
2555
- const supportsSub = sub !== void 0 && (gatewayRoute === void 0 || isNativeGatewayPreset);
2576
+ const supportsSub = (sub !== void 0 || input.descriptor.modelDerivedApiKey === true) && (gatewayRoute === void 0 || isNativeGatewayPreset);
2556
2577
  const enforce = input.descriptor.authEnforce ?? "when-configured";
2557
2578
  const external = supportsSub && sub?.external === true;
2558
2579
  const subCredAvailable = input.subscriptionCredential !== void 0 || external && input.externalSubscriptionVerified === true;
@@ -2584,7 +2605,7 @@ function resolveAuthSpec(input) {
2584
2605
  credentialSource = subCredAvailable ? "cli-local-login" : "none";
2585
2606
  externalCredential = true;
2586
2607
  } else if (mode === "subscription") {
2587
- setEnv = sub.setEnv;
2608
+ setEnv = sub?.setEnv ?? apiKeyEnv;
2588
2609
  credential = input.subscriptionCredential;
2589
2610
  credentialSource = credential !== void 0 ? input.subscriptionCredentialSource ?? "explicit-config" : "none";
2590
2611
  } else {
@@ -3658,7 +3679,8 @@ function resolveModelId(id) {
3658
3679
  }
3659
3680
  function methodsForDirect(descriptor) {
3660
3681
  const methods = [];
3661
- if (descriptor?.authSubscription) methods.push("oauth-bearer");
3682
+ if (descriptor?.authSubscription || descriptor?.modelDerivedApiKey)
3683
+ methods.push("oauth-bearer");
3662
3684
  if (descriptor?.provider || descriptor?.modelDerivedApiKey) methods.push("api-key");
3663
3685
  return methods;
3664
3686
  }
@@ -5116,6 +5138,14 @@ async function spawnAgentSession(deps2, input) {
5116
5138
  };
5117
5139
  });
5118
5140
  }
5141
+ if (mcpServers === void 0 && parentSessionId && buildOrchestratorMcp && input.orchestrator !== false && input.sandbox === void 0) {
5142
+ const injection = buildOrchestratorMcp({
5143
+ tools: ["message_parent"],
5144
+ role: role.name
5145
+ });
5146
+ mcpServers = [injection.entry];
5147
+ bindOrchestratorLifecycle = injection.bindLifecycle;
5148
+ }
5119
5149
  const spawnDefaults = resolveSpawnDefaults(configDefaults, input.adapter, {
5120
5150
  skills: input.skills,
5121
5151
  options: input.options,
@@ -5277,9 +5307,8 @@ async function spawnAgentSession(deps2, input) {
5277
5307
  details: { adapter: input.adapter, gateway }
5278
5308
  };
5279
5309
  }
5280
- let effectivePrompt = input.prompt ? `${composeRoleContext(role, input.promptAppend, roleRegistry)}
5281
-
5282
- ${input.prompt}` : input.prompt;
5310
+ const parentContextLine = parentSessionId ? `You were spawned by session ${parentSessionId} (also available in the ${PARENT_SESSION_ID_ENV} env var). When you finish \u2014 or hit a blocker you cannot resolve \u2014 report back to it via the message_parent tool if one is available (no session id needed; the daemon resolves your parent).` : void 0;
5311
+ let effectivePrompt = input.prompt ? [composeRoleContext(role, input.promptAppend, roleRegistry), parentContextLine, input.prompt].filter((p) => !!p).join("\n\n") : input.prompt;
5283
5312
  const explicitTitle = input.title?.trim() ? input.title.trim() : void 0;
5284
5313
  const spawnLabel = input.label?.trim() ? input.label.trim() : void 0;
5285
5314
  const initialTitle = explicitTitle ?? spawnLabel ?? (input.prompt ? deriveSessionTitle(input.prompt) : void 0);
@@ -5536,7 +5565,12 @@ ${asyncPrompt}`;
5536
5565
  // collide with, so this is the entire env for this spawn.
5537
5566
  env: {
5538
5567
  [SESSION_ID_ENV]: mintedSessionId,
5539
- [WORKSPACE_SLUG_ENV]: resolvedSlug
5568
+ [WORKSPACE_SLUG_ENV]: resolvedSlug,
5569
+ // Lineage (PARENT_SESSION_ID_ENV's doc, sessions.ts) — mirrors
5570
+ // the descriptor's `parentSessionId` so the child can discover
5571
+ // who spawned it without a registry round-trip. Absent on a
5572
+ // parentless root spawn.
5573
+ ...parentSessionId ? { [PARENT_SESSION_ID_ENV]: parentSessionId } : {}
5540
5574
  },
5541
5575
  onActivity: () => {
5542
5576
  if (liveSessionId) registry.pulseActivity(liveSessionId);
@@ -6721,8 +6755,8 @@ function composeSessionObservers(observers) {
6721
6755
  );
6722
6756
  };
6723
6757
  return {
6724
- recordPrompt(sessionId, message) {
6725
- forEachSafe((o) => o.recordPrompt(sessionId, message));
6758
+ recordPrompt(sessionId, message, opts) {
6759
+ forEachSafe((o) => o.recordPrompt(sessionId, message, opts));
6726
6760
  },
6727
6761
  recordEvent(sessionId, evt) {
6728
6762
  forEachSafe((o) => o.recordEvent(sessionId, evt));
@@ -6740,10 +6774,10 @@ function composeSessionObservers(observers) {
6740
6774
  }
6741
6775
  function filterSessionObserver(inner, shouldObserve) {
6742
6776
  return {
6743
- recordPrompt(sessionId, message) {
6777
+ recordPrompt(sessionId, message, opts) {
6744
6778
  if (!shouldObserve(sessionId)) return;
6745
6779
  try {
6746
- inner.recordPrompt(sessionId, message);
6780
+ inner.recordPrompt(sessionId, message, opts);
6747
6781
  } catch {
6748
6782
  }
6749
6783
  },
@@ -7446,6 +7480,7 @@ function mintSessionId() {
7446
7480
  }
7447
7481
  var SESSION_ID_ENV = "AGENTPROTO_SESSION_ID";
7448
7482
  var WORKSPACE_SLUG_ENV = "AGENTPROTO_WORKSPACE_SLUG";
7483
+ var PARENT_SESSION_ID_ENV = "AGENTPROTO_PARENT_SESSION_ID";
7449
7484
  var SessionNotAliveError = class extends Error {
7450
7485
  sessionId;
7451
7486
  status;
@@ -7471,6 +7506,8 @@ function toSessionSummary(desc) {
7471
7506
  lastOutputAt: desc.lastOutputAt,
7472
7507
  lastActivityAt: desc.lastActivityAt,
7473
7508
  processAlive: desc.processAlive,
7509
+ watchers: desc.watchers,
7510
+ childrenBusy: desc.childrenBusy,
7474
7511
  label: desc.label,
7475
7512
  title: desc.title,
7476
7513
  renamedByUser: desc.renamedByUser,
@@ -7498,6 +7535,7 @@ function toSessionSummary(desc) {
7498
7535
  turnsCompleted: desc.turnsCompleted,
7499
7536
  busy: desc.busy,
7500
7537
  blockedOn: desc.blockedOn,
7538
+ stalledSinceMs: desc.stalledSinceMs,
7501
7539
  origin: desc.origin,
7502
7540
  parentSessionId: desc.parentSessionId,
7503
7541
  depth: desc.depth,
@@ -7625,6 +7663,28 @@ function createSessionsRegistry(opts) {
7625
7663
  const sessionEvents = opts?.sessionEvents;
7626
7664
  const resolveAgentAdapter = opts?.resolveAgentAdapter;
7627
7665
  const sessions = /* @__PURE__ */ new Map();
7666
+ const watchersById = /* @__PURE__ */ new Map();
7667
+ const stampWatchers = (desc) => {
7668
+ desc.watchers = watchersById.get(desc.id) ?? 0;
7669
+ };
7670
+ const childrenBusyCounts = () => {
7671
+ const all = Array.from(sessions.values());
7672
+ const parentOf = new Map(all.map((s) => [s.desc.id, s.desc.parentSessionId]));
7673
+ const counts = /* @__PURE__ */ new Map();
7674
+ for (const s of all) {
7675
+ const d = s.desc;
7676
+ const midTurn = d.busy === true && (d.status === "running" || d.status === "starting");
7677
+ if (!midTurn) continue;
7678
+ const seen = /* @__PURE__ */ new Set([d.id]);
7679
+ let pid = d.parentSessionId;
7680
+ while (pid && !seen.has(pid)) {
7681
+ seen.add(pid);
7682
+ counts.set(pid, (counts.get(pid) ?? 0) + 1);
7683
+ pid = parentOf.get(pid);
7684
+ }
7685
+ }
7686
+ return counts;
7687
+ };
7628
7688
  const pendingPermissions = /* @__PURE__ */ new Map();
7629
7689
  let persistTimer = null;
7630
7690
  let nextSubId = 1;
@@ -7688,6 +7748,17 @@ function createSessionsRegistry(opts) {
7688
7748
  ...rt.desc.endedReason ? { reason: rt.desc.endedReason } : {}
7689
7749
  });
7690
7750
  };
7751
+ const clearStalledFlag = (rt) => {
7752
+ if (rt.desc.stalledSinceMs === void 0) return;
7753
+ rt.desc.stalledSinceMs = void 0;
7754
+ schedulePersist();
7755
+ sessionEvents?.emit({
7756
+ type: "session:stall-cleared",
7757
+ sessionId: rt.desc.id,
7758
+ ...rt.desc.label ? { label: rt.desc.label } : {},
7759
+ ts: (/* @__PURE__ */ new Date()).toISOString()
7760
+ });
7761
+ };
7691
7762
  const refreshAwaitingPermission = (rt) => {
7692
7763
  let has = false;
7693
7764
  for (const p of pendingPermissions.values()) {
@@ -7748,6 +7819,11 @@ function createSessionsRegistry(opts) {
7748
7819
  ts: (/* @__PURE__ */ new Date()).toISOString()
7749
7820
  });
7750
7821
  }
7822
+ transcriptWriter.recordEvent(rt.desc.id, {
7823
+ kind: "permission-resolved",
7824
+ toolCallId: id,
7825
+ decision: "cancelled"
7826
+ });
7751
7827
  }
7752
7828
  delete rt.desc.awaitingPermission;
7753
7829
  };
@@ -7803,6 +7879,12 @@ function createSessionsRegistry(opts) {
7803
7879
  ts: (/* @__PURE__ */ new Date()).toISOString()
7804
7880
  });
7805
7881
  }
7882
+ transcriptWriter.recordEvent(pending.sessionId, {
7883
+ kind: "permission-resolved",
7884
+ toolCallId: id,
7885
+ decision: input.decision,
7886
+ ...chosenOptionId ? { optionId: chosenOptionId } : {}
7887
+ });
7806
7888
  schedulePersist();
7807
7889
  if (!okResolved) {
7808
7890
  return { ok: true, permission: pending, decision: input.decision, ...chosenOptionId ? { optionId: chosenOptionId } : {} };
@@ -8435,7 +8517,7 @@ function createSessionsRegistry(opts) {
8435
8517
  return;
8436
8518
  }
8437
8519
  }
8438
- const runAgentTurn = async (rt, message) => {
8520
+ const runAgentTurn = async (rt, message, turnOpts) => {
8439
8521
  if (!rt.agentSession) {
8440
8522
  throw new Error("runAgentTurn: session has no agentSession");
8441
8523
  }
@@ -8458,6 +8540,7 @@ ${message}`;
8458
8540
  rt.desc.awaitingInput = false;
8459
8541
  rt.desc.awaitingQuestion = void 0;
8460
8542
  releaseBlockedOn(rt.desc);
8543
+ clearStalledFlag(rt);
8461
8544
  let turnCompleted = false;
8462
8545
  let sawTurnEnd = false;
8463
8546
  let abnormalReason;
@@ -8471,7 +8554,11 @@ ${message}`;
8471
8554
  `\x1B[2m\u2500\u2500 \u25B6 ${typeof message === "string" ? message : JSON.stringify(message)} \u2500\u2500\x1B[0m`,
8472
8555
  "stdout"
8473
8556
  );
8474
- transcriptWriter.recordPrompt(rt.desc.id, message);
8557
+ transcriptWriter.recordPrompt(
8558
+ rt.desc.id,
8559
+ message,
8560
+ turnOpts?.promptSource ? { source: turnOpts.promptSource } : void 0
8561
+ );
8475
8562
  const wrapped = typeof message === "string" ? { type: "text", text: message } : message;
8476
8563
  for await (const evt of rt.agentSession.send(wrapped)) {
8477
8564
  transcriptWriter.recordEvent(rt.desc.id, evt);
@@ -8511,6 +8598,7 @@ ${message}`;
8511
8598
  rt.desc.busy = false;
8512
8599
  rt.emitter.emit("busy", false);
8513
8600
  releaseBlockedOn(rt.desc);
8601
+ clearStalledFlag(rt);
8514
8602
  if (pendingToolCallIds.size > 0) {
8515
8603
  for (const toolCallId of pendingToolCallIds) {
8516
8604
  const synthetic = {
@@ -8882,7 +8970,11 @@ ${message}`;
8882
8970
  schedulePersist();
8883
8971
  recordConversationLink(rt);
8884
8972
  if (input.initialPrompt) {
8885
- void runAgentTurn(rt, input.initialPrompt).catch((err) => {
8973
+ void runAgentTurn(
8974
+ rt,
8975
+ input.initialPrompt,
8976
+ desc.parentSessionId ? { promptSource: `agent:${desc.parentSessionId}` } : void 0
8977
+ ).catch((err) => {
8886
8978
  appendLine(
8887
8979
  rt,
8888
8980
  `[turn error] ${err instanceof Error ? err.message : String(err)}`,
@@ -9010,7 +9102,11 @@ ${message}`;
9010
9102
  schedulePersist();
9011
9103
  recordConversationLink(rt);
9012
9104
  if (outcome.initialPrompt) {
9013
- void runAgentTurn(rt, outcome.initialPrompt).catch((err) => {
9105
+ void runAgentTurn(
9106
+ rt,
9107
+ outcome.initialPrompt,
9108
+ rt.desc.parentSessionId ? { promptSource: `agent:${rt.desc.parentSessionId}` } : void 0
9109
+ ).catch((err) => {
9014
9110
  appendLine(
9015
9111
  rt,
9016
9112
  `[turn error] ${err instanceof Error ? err.message : String(err)}`,
@@ -9268,7 +9364,7 @@ ${message}`;
9268
9364
  }
9269
9365
  if (rtPre) await maybeResumeAgent(rtPre);
9270
9366
  const rt = validateAgentTurn(id, "sendPrompt");
9271
- await runAgentTurn(rt, message);
9367
+ await runAgentTurn(rt, message, opts2?.source ? { promptSource: opts2.source } : void 0);
9272
9368
  },
9273
9369
  async enqueuePrompt(id, message, opts2) {
9274
9370
  const rtPre = sessions.get(id);
@@ -9280,7 +9376,7 @@ ${message}`;
9280
9376
  }
9281
9377
  await maybeResumeAgent(rtPre);
9282
9378
  const rt = validateAgentTurn(id, "enqueuePrompt");
9283
- void runAgentTurn(rt, message).catch((err) => {
9379
+ void runAgentTurn(rt, message, opts2?.source ? { promptSource: opts2.source } : void 0).catch((err) => {
9284
9380
  appendLine(
9285
9381
  rtPre,
9286
9382
  `[error] ${err instanceof Error ? err.message : String(err)}`,
@@ -9444,13 +9540,17 @@ ${message}`;
9444
9540
  const rt = sessions.get(id);
9445
9541
  if (!rt) return;
9446
9542
  rt.desc.lastActivityAt = (/* @__PURE__ */ new Date()).toISOString();
9543
+ clearStalledFlag(rt);
9447
9544
  schedulePersist();
9448
9545
  },
9449
9546
  list(opts2) {
9450
9547
  const includeArchived = opts2?.includeArchived ?? false;
9548
+ const childrenBusy = childrenBusyCounts();
9451
9549
  return Array.from(sessions.values()).map((s) => s.desc).filter((desc) => includeArchived || !desc.archived).sort((a, b) => b.startedAt.localeCompare(a.startedAt)).map((desc) => {
9452
9550
  stampProcessAlive(desc);
9453
9551
  stampInterrupted(desc);
9552
+ stampWatchers(desc);
9553
+ desc.childrenBusy = childrenBusy.get(desc.id) ?? 0;
9454
9554
  return desc;
9455
9555
  });
9456
9556
  },
@@ -9458,11 +9558,14 @@ ${message}`;
9458
9558
  const includeArchived = opts2?.includeArchived ?? false;
9459
9559
  const limit = Math.max(1, Math.min(200, opts2?.limit ?? 50));
9460
9560
  const offset = Math.max(0, opts2?.offset ?? 0);
9561
+ const childrenBusy = childrenBusyCounts();
9461
9562
  const all = Array.from(sessions.values()).map((s) => s.desc).filter((desc) => includeArchived || !desc.archived).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
9462
9563
  const slice = all.slice(offset, offset + limit);
9463
9564
  const summaries = slice.map((desc) => {
9464
9565
  stampProcessAlive(desc);
9465
9566
  stampInterrupted(desc);
9567
+ stampWatchers(desc);
9568
+ desc.childrenBusy = childrenBusy.get(desc.id) ?? 0;
9466
9569
  return toSessionSummary(desc);
9467
9570
  });
9468
9571
  return { summaries, total: all.length };
@@ -9472,9 +9575,19 @@ ${message}`;
9472
9575
  if (desc) {
9473
9576
  stampProcessAlive(desc);
9474
9577
  stampInterrupted(desc);
9578
+ stampWatchers(desc);
9579
+ desc.childrenBusy = childrenBusyCounts().get(desc.id) ?? 0;
9475
9580
  }
9476
9581
  return desc;
9477
9582
  },
9583
+ incWatchers(id) {
9584
+ watchersById.set(id, (watchersById.get(id) ?? 0) + 1);
9585
+ },
9586
+ decWatchers(id) {
9587
+ const next = (watchersById.get(id) ?? 0) - 1;
9588
+ if (next > 0) watchersById.set(id, next);
9589
+ else watchersById.delete(id);
9590
+ },
9478
9591
  attach(id, onLine) {
9479
9592
  const rt = sessions.get(id);
9480
9593
  if (!rt) return null;
@@ -9641,6 +9754,30 @@ ${message}`;
9641
9754
  emitExited(rt);
9642
9755
  return true;
9643
9756
  },
9757
+ markStalled(id, stalledSinceMs) {
9758
+ const rt = sessions.get(id);
9759
+ if (!rt) return false;
9760
+ if (rt.desc.kind !== "agent-cli" || rt.desc.status !== "running") return false;
9761
+ if (rt.desc.busy !== true || rt.desc.blockedOn !== void 0) return false;
9762
+ if (rt.desc.stalledSinceMs !== void 0) return false;
9763
+ rt.desc.stalledSinceMs = stalledSinceMs;
9764
+ schedulePersist();
9765
+ sessionEvents?.emit({
9766
+ type: "session:stalled",
9767
+ sessionId: rt.desc.id,
9768
+ stalledSinceMs,
9769
+ ...rt.desc.label ? { label: rt.desc.label } : {},
9770
+ ts: (/* @__PURE__ */ new Date()).toISOString()
9771
+ });
9772
+ return true;
9773
+ },
9774
+ clearStalled(id) {
9775
+ const rt = sessions.get(id);
9776
+ if (!rt) return false;
9777
+ const wasFlagged = rt.desc.stalledSinceMs !== void 0;
9778
+ clearStalledFlag(rt);
9779
+ return wasFlagged;
9780
+ },
9644
9781
  isResuming(id) {
9645
9782
  return !!sessions.get(id)?.resumePromise;
9646
9783
  },
@@ -10707,12 +10844,17 @@ function registerFsTools(server, opts) {
10707
10844
  const anchor = makeAnchor(opts.workspace);
10708
10845
  server.tool(
10709
10846
  "file_read",
10710
- "Read a UTF-8 file from the workspace.",
10711
- { path: z.string().describe("Workspace-relative path to the file.") },
10712
- async ({ path }) => {
10847
+ `Read a file from the workspace. Defaults to UTF-8 text; pass encoding: "base64" for binary files (images, audio, video, \u2026) \u2014 UTF-8 decoding a non-text file replaces invalid byte sequences (e.g. a PNG's 0x89 header byte) with U+FFFD, corrupting the content.`,
10848
+ {
10849
+ path: z.string().describe("Workspace-relative path to the file."),
10850
+ encoding: z.enum(["utf8", "base64"]).optional().describe(
10851
+ '"utf8" (default) for text files, "base64" for binary files.'
10852
+ )
10853
+ },
10854
+ async ({ path, encoding }) => {
10713
10855
  const abs = anchor(path);
10714
10856
  const buf = await readFile(abs);
10715
- return text(buf.toString("utf8"));
10857
+ return text(buf.toString(encoding === "base64" ? "base64" : "utf8"));
10716
10858
  }
10717
10859
  );
10718
10860
  server.tool(
@@ -10946,6 +11088,7 @@ function registerAgentTools(server, opts) {
10946
11088
  buildOrchestratorMcp,
10947
11089
  callerScope,
10948
11090
  callerSessionId,
11091
+ mcpBridgeOrigin,
10949
11092
  webhookNotifier,
10950
11093
  daemonMcpUrl,
10951
11094
  loadRoleRegistry: loadRoleRegistry2,
@@ -11223,6 +11366,12 @@ function registerAgentTools(server, opts) {
11223
11366
  {
11224
11367
  ...spawnInput,
11225
11368
  adapter,
11369
+ // Auto-stamp the source channel (#session-visibility): when the
11370
+ // caller didn't pass an explicit `origin`, fall back to the connecting
11371
+ // client's `?origin=` label so a bridge-client spawn (cowork/vscode/
11372
+ // codex) is attributed instead of landing as a bare root. An explicit
11373
+ // `input.origin` (already in `spawnInput`) always wins.
11374
+ ...!spawnInput.origin && mcpBridgeOrigin ? { origin: mcpBridgeOrigin } : {},
11226
11375
  // The trusted caller id (from `?callerSessionId=`) becomes the
11227
11376
  // implicit auto-parent — attach-by-default without the caller
11228
11377
  // passing its own id. An explicit `parentSessionId` still outranks
@@ -11288,8 +11437,10 @@ function registerAgentTools(server, opts) {
11288
11437
  const sessionId = resolveSessionIdArg(input);
11289
11438
  if (!sessionId) return missingSessionIdError("agent_prompt");
11290
11439
  try {
11440
+ const promptSource = callerScope?.ownerSessionId ?? callerSessionId;
11291
11441
  await registry.enqueuePrompt(sessionId, input.prompt, {
11292
- interrupt: input.interrupt
11442
+ interrupt: input.interrupt,
11443
+ ...promptSource ? { source: `agent:${promptSource}` } : {}
11293
11444
  });
11294
11445
  return {
11295
11446
  content: [
@@ -11316,6 +11467,67 @@ function registerAgentTools(server, opts) {
11316
11467
  }
11317
11468
  }
11318
11469
  );
11470
+ server.tool(
11471
+ "message_parent",
11472
+ "Report a message UP to the session that spawned you (your parent/supervisor) \u2014 a result, a progress update, or a blocker. No session id needed: the daemon resolves your recorded parent from your own session identity (also visible as the AGENTPROTO_PARENT_SESSION_ID env var). Delivered as a prompt when the parent is idle, or queued onto its next turn when it's mid-turn (never interrupts). Errors if this session has no recorded parent or the parent is gone.",
11473
+ {
11474
+ message: z.string().min(1).describe("The message to deliver to your parent session (plain text).")
11475
+ },
11476
+ async (input) => {
11477
+ const fail = (text9) => ({
11478
+ content: [{ type: "text", text: text9 }],
11479
+ isError: true
11480
+ });
11481
+ const selfId = callerScope?.ownerSessionId ?? callerSessionId;
11482
+ if (!selfId) {
11483
+ return fail(
11484
+ "message_parent: cannot identify the calling session \u2014 this tool needs gateway access attributed to a session (a scoped orchestrator gateway, or a daemon `/mcp` URL carrying `?callerSessionId=`). A human/root caller has no parent to message."
11485
+ );
11486
+ }
11487
+ const self = registry.get(selfId);
11488
+ if (!self) {
11489
+ return fail(`message_parent: calling session "${selfId}" is not in the registry.`);
11490
+ }
11491
+ const parentId = self.parentSessionId;
11492
+ if (!parentId || parentId === selfId) {
11493
+ return fail(
11494
+ "message_parent: this session has no recorded parent \u2014 it was spawned at the root, so there is no one to report up to."
11495
+ );
11496
+ }
11497
+ const parent = registry.get(parentId);
11498
+ if (!parent) {
11499
+ return fail(`message_parent: parent session "${parentId}" no longer exists.`);
11500
+ }
11501
+ if (parent.status !== "running" && parent.status !== "starting") {
11502
+ return fail(
11503
+ `message_parent: parent session "${parentId}" is not running (status: ${parent.status}) \u2014 the message cannot be delivered.`
11504
+ );
11505
+ }
11506
+ const who = self.label ?? selfId;
11507
+ const notice = `[child-message] ${who} (${selfId}): ${input.message}`;
11508
+ const done = (delivery) => ({
11509
+ content: [
11510
+ {
11511
+ type: "text",
11512
+ text: JSON.stringify({ ok: true, parentSessionId: parentId, delivery }, null, 2)
11513
+ }
11514
+ ]
11515
+ });
11516
+ if (!parent.busy) {
11517
+ try {
11518
+ await registry.enqueuePrompt(parentId, notice, {});
11519
+ return done("enqueued");
11520
+ } catch {
11521
+ }
11522
+ }
11523
+ if (!registry.stampPendingChildCrashNotice(parentId, notice)) {
11524
+ return fail(
11525
+ `message_parent: parent session "${parentId}" vanished mid-delivery.`
11526
+ );
11527
+ }
11528
+ return done("queued-next-turn");
11529
+ }
11530
+ );
11319
11531
  server.tool(
11320
11532
  "agent_output",
11321
11533
  "Tail the recent output of a session. Returns the last N lines of the ring buffer (stdout + stderr inter-leaved, newest last). Use this to read an agent's reply after `agent_prompt`.",
@@ -12406,6 +12618,15 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
12406
12618
  ...accessProfileEcho ? { accessProfile: accessProfileEcho } : {},
12407
12619
  ...effMode ? { mode: effMode } : {},
12408
12620
  ...resolved.commandPreview ? { commandPreview: resolved.commandPreview } : {},
12621
+ // Lineage carry-forward (#session-visibility). A restart is a NEW
12622
+ // descriptor, but it is the same logical session continued — so its
12623
+ // origin (which channel spawned it: cowork/vscode/codex/cron) and its
12624
+ // parent/depth must survive, exactly as continue-fresh already carries
12625
+ // them (session-continue-fresh.ts). Dropping them here is what left a
12626
+ // restarted session a bare top-level root with no source trace.
12627
+ ...prev.origin ? { origin: prev.origin } : {},
12628
+ ...prev.parentSessionId ? { parentSessionId: prev.parentSessionId } : {},
12629
+ ...prev.depth !== void 0 ? { depth: prev.depth } : {},
12409
12630
  // Verifiability echo (never the credential) — see the auth
12410
12631
  // resolution block above. Absent when no credential resolved,
12411
12632
  // same as session-spawn.ts.
@@ -13981,6 +14202,13 @@ function registerSessionTools(rawServer, opts) {
13981
14202
  rows: input.rows ?? 24,
13982
14203
  ...prev.name ? { name: prev.name } : {},
13983
14204
  ...prev.label ? { label: prev.label } : {},
14205
+ // Lineage carry-forward (#session-visibility) — same reasoning as
14206
+ // the agent branch in session-restart-core.ts: a restart keeps the
14207
+ // logical session's origin/parent/depth rather than resetting it to
14208
+ // a bare root.
14209
+ ...prev.origin ? { origin: prev.origin } : {},
14210
+ ...prev.parentSessionId ? { parentSessionId: prev.parentSessionId } : {},
14211
+ ...prev.depth !== void 0 ? { depth: prev.depth } : {},
13984
14212
  resumedFrom: prev.id,
13985
14213
  resumeVia: describeResumePath(augmented)
13986
14214
  });
@@ -19551,11 +19779,13 @@ async function monitorSessionWait(opts) {
19551
19779
  return new Promise((resolve22) => {
19552
19780
  const unsubs = [];
19553
19781
  let settled = false;
19782
+ for (const id of resolvedIds) registry.incWatchers(id);
19554
19783
  const finish = (result) => {
19555
19784
  if (settled) return;
19556
19785
  settled = true;
19557
19786
  clearTimeout(timer);
19558
19787
  for (const u of unsubs) u();
19788
+ for (const id of resolvedIds) registry.decWatchers(id);
19559
19789
  resolve22(result);
19560
19790
  };
19561
19791
  const relevantTypes = targetEvent === "any" ? ["session:turn-end", "session:awaiting-input", "session:exited"] : targetEvent === "turn-end" ? ["session:turn-end", "session:awaiting-input"] : targetEvent === "awaiting-input" ? ["session:awaiting-input"] : ["session:exited"];
@@ -20914,11 +21144,18 @@ async function startHttpServer(opts) {
20914
21144
  const raw = new URLSearchParams(url.slice(qIdx + 1)).get("callerSessionId");
20915
21145
  return raw && raw.length > 0 ? raw : void 0;
20916
21146
  }
21147
+ function parseOriginQuery(url) {
21148
+ const qIdx = url.indexOf("?");
21149
+ if (qIdx === -1) return void 0;
21150
+ const raw = new URLSearchParams(url.slice(qIdx + 1)).get("origin");
21151
+ return raw && raw.length > 0 ? raw : void 0;
21152
+ }
20917
21153
  async function handleMcp(req, res) {
20918
21154
  if (!authorizeMcp(req, res)) return;
20919
21155
  const denyTools = parseDenyToolsQuery(req.url ?? "");
20920
21156
  const callerSessionId = parseCallerSessionIdQuery(req.url ?? "");
20921
- const server2 = await opts.mcpServerFactory(denyTools, callerSessionId);
21157
+ const origin = parseOriginQuery(req.url ?? "");
21158
+ const server2 = await opts.mcpServerFactory(denyTools, callerSessionId, origin);
20922
21159
  await serveMcp(req, res, server2);
20923
21160
  }
20924
21161
  async function handleOrchestratorMcp(req, res) {
@@ -20968,7 +21205,8 @@ async function startHttpServer(opts) {
20968
21205
  resumeSessionsOnBoot: opts.meta.resumeSessionsOnBoot === true,
20969
21206
  idleReapAfterMs: opts.meta.idleReapAfterMs ?? 0,
20970
21207
  crashDetectIntervalMs: opts.meta.crashDetectIntervalMs ?? 0,
20971
- restartSweepIntervalMs: opts.meta.restartSweepIntervalMs ?? 0
21208
+ restartSweepIntervalMs: opts.meta.restartSweepIntervalMs ?? 0,
21209
+ turnStallAfterMs: opts.meta.turnStallAfterMs ?? 0
20972
21210
  })
20973
21211
  );
20974
21212
  }
@@ -24414,6 +24652,49 @@ function runCrashDetectPass(opts) {
24414
24652
  return summary;
24415
24653
  }
24416
24654
 
24655
+ // src/stall-watchdog.ts
24656
+ function lastActivityMsOf(desc) {
24657
+ const tsStr = desc.lastActivityAt ?? desc.startedAt;
24658
+ const ts = tsStr ? Date.parse(tsStr) : Number.NaN;
24659
+ return Number.isFinite(ts) ? ts : null;
24660
+ }
24661
+ function isStallCandidate(desc, nowMs, thresholdMs) {
24662
+ if (desc.kind !== "agent-cli") return false;
24663
+ if (desc.status !== "running") return false;
24664
+ if (desc.busy !== true) return false;
24665
+ if (desc.blockedOn !== void 0) return false;
24666
+ if (desc.stalledSinceMs !== void 0) return false;
24667
+ const lastMs = lastActivityMsOf(desc);
24668
+ if (lastMs === null) return false;
24669
+ return nowMs - lastMs > thresholdMs;
24670
+ }
24671
+ function runStallWatchdogPass(opts) {
24672
+ const { registry, isServed } = opts;
24673
+ const thresholdMs = opts.turnStallAfterMs;
24674
+ if (!thresholdMs || thresholdMs <= 0) {
24675
+ return { enabled: false, candidates: 0, stalled: 0, ids: [] };
24676
+ }
24677
+ const nowMs = opts.now ? opts.now() : Date.now();
24678
+ const all = registry.list({ includeArchived: true });
24679
+ const candidates = all.filter(
24680
+ (d) => (isServed?.(d) ?? true) && isStallCandidate(d, nowMs, thresholdMs)
24681
+ );
24682
+ const summary = {
24683
+ enabled: true,
24684
+ candidates: candidates.length,
24685
+ stalled: 0,
24686
+ ids: []
24687
+ };
24688
+ for (const d of candidates) {
24689
+ const lastMs = lastActivityMsOf(d) ?? nowMs;
24690
+ if (registry.markStalled(d.id, lastMs)) {
24691
+ summary.stalled++;
24692
+ summary.ids.push(d.id);
24693
+ }
24694
+ }
24695
+ return summary;
24696
+ }
24697
+
24417
24698
  // src/restart-scheduler.ts
24418
24699
  function isEligibleForRestart(desc) {
24419
24700
  const policy = desc.restartPolicy;
@@ -25513,6 +25794,13 @@ function createAppRegistry(opts) {
25513
25794
  listApps() {
25514
25795
  return [...state.apps];
25515
25796
  },
25797
+ removeApp(appId) {
25798
+ const idx = state.apps.findIndex((a) => a.appId === appId);
25799
+ if (idx === -1) return void 0;
25800
+ const [removed] = state.apps.splice(idx, 1);
25801
+ persist();
25802
+ return removed;
25803
+ },
25516
25804
  createRun(input) {
25517
25805
  const run = {
25518
25806
  appRunId: `apprun_${randomUUID()}`,
@@ -25569,6 +25857,31 @@ function createAppRegistry(opts) {
25569
25857
  }
25570
25858
  };
25571
25859
  }
25860
+ var EMPTY_CATALOG = { apps: [] };
25861
+ function defaultAppCatalogPath() {
25862
+ return join(homedir(), ".agentproto", "app-catalog.json");
25863
+ }
25864
+ async function loadAppCatalogFile(path) {
25865
+ const catalogPath = path ?? defaultAppCatalogPath();
25866
+ let raw;
25867
+ try {
25868
+ raw = await readFile(catalogPath, "utf8");
25869
+ } catch {
25870
+ return EMPTY_CATALOG;
25871
+ }
25872
+ try {
25873
+ const parsed = JSON.parse(raw);
25874
+ if (!Array.isArray(parsed.apps)) return EMPTY_CATALOG;
25875
+ const apps = parsed.apps.filter((e) => {
25876
+ if (typeof e !== "object" || e === null) return false;
25877
+ const rec = e;
25878
+ return typeof rec.appId === "string" && typeof rec.dir === "string";
25879
+ });
25880
+ return { apps };
25881
+ } catch {
25882
+ return EMPTY_CATALOG;
25883
+ }
25884
+ }
25572
25885
 
25573
25886
  // src/app-tools.ts
25574
25887
  var DEFAULT_AGENT_ADAPTER = "mastra-agent";
@@ -25601,7 +25914,12 @@ async function readAppRefs(dir) {
25601
25914
  const toRefs = (v) => Array.isArray(v) ? v.filter(
25602
25915
  (e) => typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.path === "string"
25603
25916
  ).map((e) => ({ id: e.id, path: resolveRef(dir, e.path) })) : [];
25604
- return { agents: toRefs(data.agents), workflows: toRefs(data.workflows) };
25917
+ let ui;
25918
+ if (typeof data.ui === "object" && data.ui !== null && typeof data.ui.path === "string") {
25919
+ const uiData = data.ui;
25920
+ ui = { ...uiData, path: resolveRef(dir, uiData.path) };
25921
+ }
25922
+ return { agents: toRefs(data.agents), workflows: toRefs(data.workflows), ...ui ? { ui } : {} };
25605
25923
  }
25606
25924
  async function performInstall(dir, appRegistry, listRegisteredToolIds, resolveAgentAdapter) {
25607
25925
  let handle;
@@ -25639,20 +25957,31 @@ async function performInstall(dir, appRegistry, listRegisteredToolIds, resolveAg
25639
25957
  const unvalidatedAgentTools = [
25640
25958
  ...new Set(handle.agents.flatMap((e) => (e.agent.tools ?? []).map(refIdOf)))
25641
25959
  ];
25960
+ const ui = refs.ui ? {
25961
+ path: refs.ui.path,
25962
+ ...handle.ui?.title !== void 0 ? { title: handle.ui.title } : {},
25963
+ ...handle.ui?.description !== void 0 ? { description: handle.ui.description } : {},
25964
+ ...handle.ui?.tools !== void 0 ? { tools: handle.ui.tools } : {},
25965
+ ...handle.ui?.csp !== void 0 ? { csp: handle.ui.csp } : {}
25966
+ } : void 0;
25642
25967
  const record2 = appRegistry.upsertApp({
25643
25968
  appId: handle.id,
25644
25969
  dir,
25645
25970
  ...handle.version ? { version: handle.version } : {},
25646
25971
  ...handle.name ? { name: handle.name } : {},
25972
+ ...handle.description ? { description: handle.description } : {},
25647
25973
  agents: refs.agents,
25648
25974
  workflows: refs.workflows,
25649
25975
  unvalidatedAgentTools,
25650
- ...handle.requires ? { requires: handle.requires } : {}
25976
+ ...handle.requires ? { requires: handle.requires } : {},
25977
+ ...ui ? { ui } : {},
25978
+ ...handle.artifacts ? { artifacts: handle.artifacts } : {},
25979
+ ...handle.dev ? { dev: handle.dev } : {}
25651
25980
  });
25652
25981
  return { ok: true, record: record2 };
25653
25982
  }
25654
25983
  function registerAppTools(server, opts) {
25655
- const { registry, resolveAgentAdapter, listRegisteredToolIds, workflowRunner } = opts;
25984
+ const { registry, resolveAgentAdapter, listRegisteredToolIds, workflowRunner, dispatchTool, callImportedTool } = opts;
25656
25985
  const appRegistry = opts.appRegistry ?? createAppRegistry({
25657
25986
  ...opts.persistPath !== void 0 ? { persistPath: opts.persistPath } : {},
25658
25987
  ...opts.persist !== void 0 ? { persist: opts.persist } : {}
@@ -25901,6 +26230,167 @@ function registerAppTools(server, opts) {
25901
26230
  return textResult(result);
25902
26231
  }
25903
26232
  );
26233
+ server.tool(
26234
+ "app_tool_call",
26235
+ "Call one of an installed app's UI-exposed tools \u2014 the allowlist set at `defineApp({ ui: { tools: [...] } })` time (`app_install`'s `record.ui.tools`). A tool id prefixed `imported:<alias>/<toolName>` dispatches through an imported MCP server (same proxy `mcp_imported_call` uses); every other id dispatches through the daemon's own registered tools (same reach-in routine/cron `target.tool` dispatch uses).",
26236
+ {
26237
+ appId: z.string(),
26238
+ tool: z.string().describe("A tool id from the app's `ui.tools` allowlist."),
26239
+ args: z.record(z.string(), z.unknown()).optional().describe("Tool arguments. Default: empty object.")
26240
+ },
26241
+ async (input) => {
26242
+ const installed = appRegistry.getApp(input.appId);
26243
+ if (!installed || !installed.ui) {
26244
+ return errorResult(`app_tool_call: app "${input.appId}" is not installed or has no UI.`);
26245
+ }
26246
+ const allowlist = installed.ui.tools ?? [];
26247
+ if (!allowlist.includes(input.tool)) {
26248
+ return errorResult(
26249
+ `app_tool_call: tool "${input.tool}" is not in app "${input.appId}"'s ui.tools allowlist: ${allowlist.length > 0 ? allowlist.join(", ") : "(empty)"}`
26250
+ );
26251
+ }
26252
+ const args = input.args ?? {};
26253
+ try {
26254
+ if (input.tool.startsWith("imported:")) {
26255
+ if (!callImportedTool) return notEnabled("app_tool_call");
26256
+ const rest = input.tool.slice("imported:".length);
26257
+ const slash = rest.indexOf("/");
26258
+ if (slash === -1) {
26259
+ return errorResult(
26260
+ `app_tool_call: malformed imported tool id "${input.tool}" \u2014 expected "imported:<alias>/<toolName>".`
26261
+ );
26262
+ }
26263
+ const result2 = await callImportedTool(rest.slice(0, slash), rest.slice(slash + 1), args);
26264
+ return textResult(result2);
26265
+ }
26266
+ if (!dispatchTool) return notEnabled("app_tool_call");
26267
+ const result = await dispatchTool(input.tool, args);
26268
+ return textResult(result);
26269
+ } catch (err) {
26270
+ return errorResult(`app_tool_call: ${err instanceof Error ? err.message : String(err)}`);
26271
+ }
26272
+ }
26273
+ );
26274
+ server.tool(
26275
+ "app_uninstall",
26276
+ "Remove an installed app's record. Refuses if the app is applied to any scope (unapply first via app_unapply) or has a running app_run (stop it first via app_stop).",
26277
+ { appId: z.string() },
26278
+ async (input) => {
26279
+ const applied = appRegistry.listApplied().filter((m) => m.appId === input.appId);
26280
+ if (applied.length > 0) {
26281
+ return errorResult(
26282
+ `app_uninstall: app "${input.appId}" is applied to scope(s) ${applied.map((m) => m.scopeId).join(", ")} \u2014 unapply from scopes first.`
26283
+ );
26284
+ }
26285
+ const runningRuns = appRegistry.listRuns().filter((r) => r.appId === input.appId && r.status === "running");
26286
+ if (runningRuns.length > 0) {
26287
+ return errorResult(
26288
+ `app_uninstall: app "${input.appId}" has running app_run(s) ${runningRuns.map((r) => r.appRunId).join(", ")} \u2014 stop app runs first.`
26289
+ );
26290
+ }
26291
+ const removed = appRegistry.removeApp(input.appId);
26292
+ if (!removed) {
26293
+ return errorResult(`app_uninstall: no installed app "${input.appId}".`);
26294
+ }
26295
+ return textResult({ appId: removed.appId });
26296
+ }
26297
+ );
26298
+ server.tool(
26299
+ "app_catalog",
26300
+ "List browsable apps from the catalog file (default `~/.agentproto/app-catalog.json`, tolerates a missing file), merged with installed-app status \u2014 every entry reports `installed` and `hasUi`. Installed apps absent from the catalog file are included too.",
26301
+ {
26302
+ scopeId: z.string().optional().describe("Reserved for future scope-aware filtering. Currently unused.")
26303
+ },
26304
+ async () => {
26305
+ const catalog = await loadAppCatalogFile(opts.catalogPath);
26306
+ const installedApps = appRegistry.listApps();
26307
+ const installedById = new Map(installedApps.map((a) => [a.appId, a]));
26308
+ const seen = /* @__PURE__ */ new Set();
26309
+ const entries = catalog.apps.map((entry) => {
26310
+ const installed = installedById.get(entry.appId);
26311
+ seen.add(entry.appId);
26312
+ const name = entry.name ?? installed?.name;
26313
+ const description = entry.description ?? installed?.description;
26314
+ return {
26315
+ appId: entry.appId,
26316
+ ...name ? { name } : {},
26317
+ ...description ? { description } : {},
26318
+ dir: entry.dir,
26319
+ ...entry.category ? { category: entry.category } : {},
26320
+ installed: installed !== void 0,
26321
+ hasUi: installed?.ui !== void 0
26322
+ };
26323
+ });
26324
+ for (const app of installedApps) {
26325
+ if (seen.has(app.appId)) continue;
26326
+ entries.push({
26327
+ appId: app.appId,
26328
+ ...app.name ? { name: app.name } : {},
26329
+ ...app.description ? { description: app.description } : {},
26330
+ dir: app.dir,
26331
+ installed: true,
26332
+ hasUi: app.ui !== void 0
26333
+ });
26334
+ }
26335
+ return textResult(entries);
26336
+ }
26337
+ );
26338
+ }
26339
+ function appUiToolId(appId) {
26340
+ const slug = appId.replace(/^@[^/]+\//, "").replace(/[^a-z0-9]/g, "_");
26341
+ return `app_ui_${slug}`;
26342
+ }
26343
+ function createUiHtmlCache() {
26344
+ const cache = /* @__PURE__ */ new Map();
26345
+ return {
26346
+ async get(path, version) {
26347
+ const cached = cache.get(path);
26348
+ if (cached && cached.version === version) return cached.html;
26349
+ const html = await readFile(path, "utf8");
26350
+ cache.set(path, { version, html });
26351
+ return html;
26352
+ }
26353
+ };
26354
+ }
26355
+ async function makeInstalledAppUiApps(appRegistry, cache, existingToolNames) {
26356
+ const apps = [];
26357
+ const seen = new Set(existingToolNames);
26358
+ for (const app of appRegistry.listApps()) {
26359
+ const ui = app.ui;
26360
+ if (!ui) continue;
26361
+ const toolId = appUiToolId(app.appId);
26362
+ if (seen.has(toolId)) {
26363
+ console.warn(
26364
+ `[app-ui-apps] skipping UI panel for app "${app.appId}": tool id "${toolId}" collides with an existing tool or another installed app's panel.`
26365
+ );
26366
+ continue;
26367
+ }
26368
+ let html;
26369
+ try {
26370
+ html = await cache.get(ui.path, app.updatedAt);
26371
+ } catch (err) {
26372
+ console.warn(
26373
+ `[app-ui-apps] skipping UI panel for app "${app.appId}": could not read "${ui.path}": ${err instanceof Error ? err.message : String(err)}`
26374
+ );
26375
+ continue;
26376
+ }
26377
+ seen.add(toolId);
26378
+ apps.push({
26379
+ id: toolId,
26380
+ title: ui.title ?? app.name ?? app.appId,
26381
+ ...ui.description ? { description: ui.description } : {},
26382
+ inputSchema: z.object({}),
26383
+ execute: async () => ({ appId: app.appId, tools: ui.tools ?? [] }),
26384
+ html,
26385
+ ...ui.csp ? {
26386
+ csp: {
26387
+ ...ui.csp.connectDomains ? { connectDomains: [...ui.csp.connectDomains] } : {},
26388
+ ...ui.csp.resourceDomains ? { resourceDomains: [...ui.csp.resourceDomains] } : {}
26389
+ }
26390
+ } : {}
26391
+ });
26392
+ }
26393
+ return apps;
25904
26394
  }
25905
26395
  function createSessionEventBus() {
25906
26396
  const ee = new EventEmitter();
@@ -27825,6 +28315,11 @@ var DEFAULT_ORCHESTRATOR_TOOLS = [
27825
28315
  "agent_prompt",
27826
28316
  "agent_output",
27827
28317
  "agent_kill",
28318
+ // Child→parent report-back. NOT a delegation tool (takes no session id;
28319
+ // the daemon resolves the caller's own recorded parent, and it can reach
28320
+ // nothing else) — it's also the sole tool of the minimal report-only
28321
+ // scope `session-spawn.ts` mints for a gateway-less child with a parent.
28322
+ "message_parent",
27828
28323
  "session_monitor",
27829
28324
  "session_events_poll",
27830
28325
  "session_list",
@@ -29081,7 +29576,8 @@ function registerDaemonHealthTools(server, opts) {
29081
29576
  resumeSessionsOnBoot: opts.resumeSessionsOnBoot,
29082
29577
  idleReapAfterMs: opts.idleReapAfterMs,
29083
29578
  crashDetectIntervalMs: opts.crashDetectIntervalMs ?? 0,
29084
- restartSweepIntervalMs: opts.restartSweepIntervalMs ?? 0
29579
+ restartSweepIntervalMs: opts.restartSweepIntervalMs ?? 0,
29580
+ turnStallAfterMs: opts.turnStallAfterMs ?? 0
29085
29581
  });
29086
29582
  }
29087
29583
  );
@@ -31226,6 +31722,7 @@ async function isAgentCliAuthConfigured(slug, descriptor, model) {
31226
31722
 
31227
31723
  // src/index.ts
31228
31724
  var DEFAULT_CRASH_DETECT_INTERVAL_MS = 3e4;
31725
+ var DEFAULT_TURN_STALL_AFTER_MS = 5 * 6e4;
31229
31726
  var DEFAULT_ALWAYS_ON_TOOLS = [
31230
31727
  "daemon_health",
31231
31728
  "agent_start",
@@ -31236,7 +31733,8 @@ var DEFAULT_ALWAYS_ON_TOOLS = [
31236
31733
  "session_monitor",
31237
31734
  "session_events_poll",
31238
31735
  "permissions_list",
31239
- "permissions_respond"
31736
+ "permissions_respond",
31737
+ "app_tool_call"
31240
31738
  ];
31241
31739
  async function createGateway(opts) {
31242
31740
  const startedAt = Date.now();
@@ -31244,6 +31742,7 @@ async function createGateway(opts) {
31244
31742
  const idleReapAfterMs = typeof opts.idleReapAfterMs === "number" && opts.idleReapAfterMs > 0 ? opts.idleReapAfterMs : 0;
31245
31743
  const crashDetectIntervalMs = typeof opts.crashDetectIntervalMs === "number" ? opts.crashDetectIntervalMs > 0 ? opts.crashDetectIntervalMs : 0 : DEFAULT_CRASH_DETECT_INTERVAL_MS;
31246
31744
  const restartSweepIntervalMs = typeof opts.restartSweepIntervalMs === "number" && opts.restartSweepIntervalMs > 0 ? opts.restartSweepIntervalMs : 0;
31745
+ const turnStallAfterMs = typeof opts.turnStallAfterMs === "number" ? opts.turnStallAfterMs > 0 ? opts.turnStallAfterMs : 0 : DEFAULT_TURN_STALL_AFTER_MS;
31247
31746
  const workspace = resolve(opts.workspace);
31248
31747
  if (!existsSync(workspace)) {
31249
31748
  throw new Error(`runtime: workspace dir does not exist: ${workspace}`);
@@ -31420,7 +31919,8 @@ async function createGateway(opts) {
31420
31919
  cronScheduler,
31421
31920
  dispatchTool
31422
31921
  });
31423
- const appRegistry = createAppRegistry();
31922
+ const appRegistry = createAppRegistry({ persist });
31923
+ const appUiHtmlCache = createUiHtmlCache();
31424
31924
  const workflowRunner = opts.resolveAgentAdapter ? createWorkflowRunner({
31425
31925
  registry: sessions,
31426
31926
  sessionEvents,
@@ -31541,7 +32041,7 @@ async function createGateway(opts) {
31541
32041
  ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
31542
32042
  ...opts.listHarnessCapabilities ? { listHarnessCapabilities: opts.listHarnessCapabilities } : {}
31543
32043
  });
31544
- const mcpServerFactory = async (denyTools, callerSessionId) => {
32044
+ const mcpServerFactory = async (denyTools, callerSessionId, origin) => {
31545
32045
  const { server: rawServer } = await createMcpServer({
31546
32046
  specs: opts.specs,
31547
32047
  workspace,
@@ -31560,7 +32060,8 @@ async function createGateway(opts) {
31560
32060
  resumeSessionsOnBoot: opts.resumeSessionsOnBoot === true,
31561
32061
  idleReapAfterMs,
31562
32062
  crashDetectIntervalMs,
31563
- restartSweepIntervalMs
32063
+ restartSweepIntervalMs,
32064
+ turnStallAfterMs
31564
32065
  });
31565
32066
  registerCommandTools(server, {
31566
32067
  workspace,
@@ -31584,6 +32085,9 @@ async function createGateway(opts) {
31584
32085
  // a spawn made by an agent session attaches under it by default (see
31585
32086
  // spawn-attach.ts). Absent on a human/root `/mcp` call → no auto-parent.
31586
32087
  ...callerSessionId ? { callerSessionId } : {},
32088
+ // `?origin=` query (#session-visibility) — the connecting client's source
32089
+ // label, used as the default origin for a spawn that doesn't set its own.
32090
+ ...origin ? { mcpBridgeOrigin: origin } : {},
31587
32091
  webhookNotifier,
31588
32092
  daemonMcpUrl,
31589
32093
  resolveSandboxProvider: resolveSandboxProviderResolved,
@@ -31623,6 +32127,16 @@ async function createGateway(opts) {
31623
32127
  registry: sessions,
31624
32128
  listRegisteredToolIds,
31625
32129
  appRegistry,
32130
+ dispatchTool,
32131
+ // Same proxy `mcp_imported_call` (session-tools.ts) dispatches
32132
+ // through — unwraps `{ok,result}|{ok:false,error}` into a plain
32133
+ // return-or-throw for `app_tool_call`'s `imported:<alias>/<toolName>`
32134
+ // ids.
32135
+ callImportedTool: async (alias, tool, args) => {
32136
+ const out = await mcpProxy.callTool(alias, tool, args);
32137
+ if (!out.ok) throw new Error(`app_tool_call: imported "${alias}".${tool}: ${out.error}`);
32138
+ return out.result;
32139
+ },
31626
32140
  ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
31627
32141
  ...workflowRunner ? { workflowRunner } : {}
31628
32142
  });
@@ -31634,7 +32148,7 @@ async function createGateway(opts) {
31634
32148
  }
31635
32149
  return rows;
31636
32150
  };
31637
- registerMcpApps(server, [
32151
+ const builtinPanelApps = [
31638
32152
  makeSessionsPanelApp({ listSessions: listSessionsFiltered }),
31639
32153
  makeAgentsOverviewApp({ listSessions: listSessionsFiltered }),
31640
32154
  makeBureauSessionsApp({ listSessions: listSessionsFiltered }),
@@ -31684,7 +32198,13 @@ async function createGateway(opts) {
31684
32198
  }
31685
32199
  })
31686
32200
  ] : []
31687
- ]);
32201
+ ];
32202
+ const installedAppUiApps = await makeInstalledAppUiApps(
32203
+ appRegistry,
32204
+ appUiHtmlCache,
32205
+ new Set(builtinPanelApps.map((app) => app.id))
32206
+ );
32207
+ registerMcpApps(server, [...builtinPanelApps, ...installedAppUiApps]);
31688
32208
  registerSummarizeSessionTool(server, {
31689
32209
  getSession: (id) => sessions.get(id),
31690
32210
  tailLines: (id, lastN) => {
@@ -31837,7 +32357,8 @@ async function createGateway(opts) {
31837
32357
  resumeSessionsOnBoot: opts.resumeSessionsOnBoot === true,
31838
32358
  idleReapAfterMs,
31839
32359
  crashDetectIntervalMs,
31840
- restartSweepIntervalMs
32360
+ restartSweepIntervalMs,
32361
+ turnStallAfterMs
31841
32362
  },
31842
32363
  cronScheduler,
31843
32364
  routineRegistrar
@@ -31882,6 +32403,27 @@ async function createGateway(opts) {
31882
32403
  }, crashDetectIntervalMs);
31883
32404
  crashDetectTimer.unref?.();
31884
32405
  }
32406
+ let turnStallTimer = null;
32407
+ if (turnStallAfterMs > 0) {
32408
+ const rawInterval = process.env.AGENTPROTO_TURN_STALL_INTERVAL_MS;
32409
+ const parsedInterval = rawInterval ? Number.parseInt(rawInterval, 10) : NaN;
32410
+ const intervalMs = Number.isFinite(parsedInterval) && parsedInterval > 0 ? parsedInterval : Math.min(turnStallAfterMs, 6e4);
32411
+ turnStallTimer = setInterval(() => {
32412
+ try {
32413
+ const summary = runStallWatchdogPass({ registry: sessions, turnStallAfterMs });
32414
+ if (summary.stalled > 0) {
32415
+ console.log(
32416
+ `[stall-watchdog] flagged ${summary.stalled}/${summary.candidates} stalled agent session(s): ${summary.ids.join(", ")}`
32417
+ );
32418
+ }
32419
+ } catch (err) {
32420
+ console.warn(
32421
+ `[stall-watchdog] sweep failed: ${err instanceof Error ? err.message : String(err)}`
32422
+ );
32423
+ }
32424
+ }, intervalMs);
32425
+ turnStallTimer.unref?.();
32426
+ }
31885
32427
  let restartSweepTimer = null;
31886
32428
  if (restartSweepIntervalMs > 0) {
31887
32429
  restartSweepTimer = setInterval(() => {
@@ -31942,6 +32484,7 @@ async function createGateway(opts) {
31942
32484
  heartbeat.stop();
31943
32485
  if (idleReapTimer) clearInterval(idleReapTimer);
31944
32486
  if (crashDetectTimer) clearInterval(crashDetectTimer);
32487
+ if (turnStallTimer) clearInterval(turnStallTimer);
31945
32488
  if (restartSweepTimer) clearInterval(restartSweepTimer);
31946
32489
  restartScheduler.dispose();
31947
32490
  inboundWatcher?.shutdown();
@@ -32001,6 +32544,6 @@ var export_providersPath = providers_store_exports.providersPath;
32001
32544
  var export_removeProviderKey = providers_store_exports.removeProviderKey;
32002
32545
  var export_setProviderKey = providers_store_exports.setProviderKey;
32003
32546
 
32004
- export { AnthropicRemainingQuotaReader, AuthResolutionError, BUCKETS_ROOT, CLAUDE_CODE_OAUTH_SOURCE, DEFAULT_BUCKET, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_WORKTREE_ISOLATION, INBOUND_PROVIDERS, LEGACY_SESSIONS_FILE, PAIRINGS_VERSION, POSTURE_NATIVE_ALIASES, POSTURE_PREAMBLES, export_PROVIDER_ENV_VARS as PROVIDER_ENV_VARS, ResumeDisabledError, SESSION_ID_ENV, SubscriptionSourceError, TASK_STATUSES, WORKSPACE_SLUG_ENV, WORKTREE_ISOLATION_ENV, activityCounts, appendConversationRecord, attachSandbox, bucketDir, bucketSessionsFile, bucketTranscriptDir, buildCatalogModels, buildCatalogProviderModels, buildMcpConfigSnippet, buildRouteAwareLaunchConfig, canonicalForModeId, claudeProjectSlug, composeMode, composeSessionObservers, conversationIndexPath, createActivityProjector, createFileStepCache, createGateway, createInboundEndpointStore, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createPrProvenanceReconciler, createReconnectLogGate, createScopeTokenRegistry, createSupervisorTaskGateRunner, createTaskLedger, createWorkspaceFs, credentialFingerprint, daemonRegistryDir, decideWorktreeIsolation, declaredPresetToProviderPreset, decomposeMode, deleteUserPreset, deriveSessionUsage, enrichRollupWithProviderQuota, enrichWithRemainingQuota, evaluateCostBudget, fileConversationStore, filterActivities, findConversationRecord, findNativeMode, formatToolCall, formatToolResult, getMcpCredentialDeps, export_getProviderKey as getProviderKey, getUserPreset, export_injectProviderKeysIntoEnv as injectProviderKeysIntoEnv, isAgentCliAuthConfigured, isClosedTaskStatus, isSafeBucketSlug, isTerminalActivityState, listBuckets, listPresets, listUserPresets, export_loadProviders as loadProviders, loadQuotaStore, loadUserPresets, loadWorktreeIsolation, locateConversationByNativePath, locateConversationBySessionId, makeBrowserAdapterLister, migrateLegacySessionsFile, migrationMarkerPath, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeInbound, normalizeModeId, normalizeSkillsOption, normalizeWorktreeField, parseAnthropicRateLimitHeaders, parseDuration, parseTaskStatus, parseWindow, parseWorktreeIsolationMode, policyToActivities, policyWatchesSession, prToActivities, projectSessionUsage, export_providerEnvVar as providerEnvVar, export_providersPath as providersPath, readConversationIndex, readDaemonRegistry, readProfileQuota, readRegisteredSlugs, readRuntimeMeta, readUsageSnapshots, reapOrphanedDescendants, recordProfileQuota, registerBuiltinRoutes, registerPairingTools, registerSandboxAttachTool, export_removeProviderKey as removeProviderKey, resolveAuthSpec, resolveBucketSlug, resolveNativeLink, resolvePosture, resolveSpawnDefaults, resolveSubscriptionCredential, rollupUsage, runCrashDetectPass, runEagerResumePass, runIdleReapPass, saveUserPreset, setMcpCredentialDeps, export_setProviderKey as setProviderKey, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, toWorktreeStatusView, turnToActivities, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, userPresetsPath, verifyInboundSignature, workflowToActivities, writeDaemonRegistryEntry };
32547
+ export { AnthropicRemainingQuotaReader, AuthResolutionError, BUCKETS_ROOT, CLAUDE_CODE_OAUTH_SOURCE, DEFAULT_BUCKET, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_WORKTREE_ISOLATION, INBOUND_PROVIDERS, LEGACY_SESSIONS_FILE, PAIRINGS_VERSION, POSTURE_NATIVE_ALIASES, POSTURE_PREAMBLES, export_PROVIDER_ENV_VARS as PROVIDER_ENV_VARS, ResumeDisabledError, SESSION_ID_ENV, SubscriptionSourceError, TASK_STATUSES, WORKSPACE_SLUG_ENV, WORKTREE_ISOLATION_ENV, activityCounts, appendConversationRecord, attachSandbox, bucketDir, bucketSessionsFile, bucketTranscriptDir, buildCatalogModels, buildCatalogProviderModels, buildMcpConfigSnippet, buildRouteAwareLaunchConfig, canonicalForModeId, claudeProjectSlug, composeMode, composeSessionObservers, conversationIndexPath, createActivityProjector, createFileStepCache, createGateway, createInboundEndpointStore, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createPrProvenanceReconciler, createReconnectLogGate, createScopeTokenRegistry, createSupervisorTaskGateRunner, createTaskLedger, createWorkspaceFs, credentialFingerprint, daemonRegistryDir, decideWorktreeIsolation, declaredPresetToProviderPreset, decomposeMode, deleteUserPreset, deriveSessionUsage, enrichRollupWithProviderQuota, enrichWithRemainingQuota, evaluateCostBudget, fileConversationStore, filterActivities, findConversationRecord, findNativeMode, formatToolCall, formatToolResult, getMcpCredentialDeps, export_getProviderKey as getProviderKey, getUserPreset, export_injectProviderKeysIntoEnv as injectProviderKeysIntoEnv, isAgentCliAuthConfigured, isClosedTaskStatus, isSafeBucketSlug, isTerminalActivityState, listBuckets, listPresets, listUserPresets, export_loadProviders as loadProviders, loadQuotaStore, loadUserPresets, loadWorktreeIsolation, locateConversationByNativePath, locateConversationBySessionId, makeBrowserAdapterLister, migrateLegacySessionsFile, migrationMarkerPath, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeInbound, normalizeModeId, normalizeSkillsOption, normalizeWorktreeField, parseAnthropicRateLimitHeaders, parseDuration, parseTaskStatus, parseWindow, parseWorktreeIsolationMode, policyToActivities, policyWatchesSession, prToActivities, projectSessionUsage, export_providerEnvVar as providerEnvVar, export_providersPath as providersPath, readConversationIndex, readDaemonRegistry, readProfileQuota, readRegisteredSlugs, readRuntimeMeta, readUsageSnapshots, reapOrphanedDescendants, recordProfileQuota, registerBuiltinRoutes, registerPairingTools, registerSandboxAttachTool, export_removeProviderKey as removeProviderKey, resolveAuthSpec, resolveBucketSlug, resolveNativeLink, resolvePosture, resolveSpawnDefaults, resolveSubscriptionCredential, rollupUsage, runCrashDetectPass, runEagerResumePass, runIdleReapPass, runStallWatchdogPass, saveUserPreset, setMcpCredentialDeps, export_setProviderKey as setProviderKey, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, toWorktreeStatusView, turnToActivities, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, userPresetsPath, verifyInboundSignature, workflowToActivities, writeDaemonRegistryEntry };
32005
32548
  //# sourceMappingURL=index.mjs.map
32006
32549
  //# sourceMappingURL=index.mjs.map