@agentproto/runtime 5.11.1 → 5.12.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
@@ -10389,6 +10389,20 @@ function buildDaemonSelfMountEntry(daemonMcpUrl, opts) {
10389
10389
  ref += `${ref.includes("?") ? "&" : "?"}callerSessionId=${encodeURIComponent(opts.callerSessionId)}`;
10390
10390
  return { name: "agentproto", transport: "http", ref };
10391
10391
  }
10392
+ function stampDaemonCallerSessionId(mcpServers, opts) {
10393
+ if (!mcpServers) return void 0;
10394
+ const daemonQ = `${opts.daemonMcpUrl}?`;
10395
+ return mcpServers.map((entry) => {
10396
+ if (entry.transport !== "http" || typeof entry.ref !== "string") return entry;
10397
+ const targetsDaemon = entry.ref === opts.daemonMcpUrl || entry.ref.startsWith(daemonQ);
10398
+ if (!targetsDaemon || entry.ref.includes("callerSessionId=")) return entry;
10399
+ const sep7 = entry.ref.includes("?") ? "&" : "?";
10400
+ return {
10401
+ ...entry,
10402
+ ref: `${entry.ref}${sep7}callerSessionId=${encodeURIComponent(opts.callerSessionId)}`
10403
+ };
10404
+ });
10405
+ }
10392
10406
  function slugifyMcpImportName(alias, fallbackId) {
10393
10407
  const slug = alias.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10394
10408
  return slug || fallbackId;
@@ -10935,16 +10949,9 @@ async function spawnAgentSession(deps2, input) {
10935
10949
  }
10936
10950
  }
10937
10951
  if (daemonMcpUrl && mcpServers) {
10938
- const daemonQ = `${daemonMcpUrl}?`;
10939
- mcpServers = mcpServers.map((entry) => {
10940
- if (entry.transport !== "http" || typeof entry.ref !== "string") return entry;
10941
- const targetsDaemon = entry.ref === daemonMcpUrl || entry.ref.startsWith(daemonQ);
10942
- if (!targetsDaemon || entry.ref.includes("callerSessionId=")) return entry;
10943
- const sep7 = entry.ref.includes("?") ? "&" : "?";
10944
- return {
10945
- ...entry,
10946
- ref: `${entry.ref}${sep7}callerSessionId=${encodeURIComponent(mintedSessionId)}`
10947
- };
10952
+ mcpServers = stampDaemonCallerSessionId(mcpServers, {
10953
+ daemonMcpUrl,
10954
+ callerSessionId: mintedSessionId
10948
10955
  });
10949
10956
  }
10950
10957
  if (mcpServers === void 0 && parentSessionId && buildOrchestratorMcp && input.orchestrator !== false && input.sandbox === void 0) {
@@ -17288,8 +17295,16 @@ var init_session_end_reason = __esm({
17288
17295
  // `registry.closeWithOutcome`, never a plain `kill()`.
17289
17296
  "steward-completed",
17290
17297
  // verdict:"done" — the steward closed it as finished work.
17291
- "steward-abandoned"
17298
+ "steward-abandoned",
17292
17299
  // verdict other than "done" — the steward closed it as not completed.
17300
+ // `restartAgentSession` (session-restart-core.ts) minted a fresh-id
17301
+ // continuation while this row was still alive (running/starting) — the
17302
+ // OLD row is superseded (see its `continuedTo`) and closed cleanly here,
17303
+ // never left running alongside its replacement. Deliberate, not a crash:
17304
+ // never eligible for `restart-scheduler.ts`'s crash-restart (status is
17305
+ // "killed", not "error") and never triggers `notifyParentOnCrash`
17306
+ // (`supervisor-notify.ts` gates on `status:"error"`/`reason:"crashed"`).
17307
+ "restarted"
17293
17308
  ];
17294
17309
  PROVIDER_LIMIT_ERROR_RE = /\bhit your (?:session|usage) limit\b/i;
17295
17310
  }
@@ -26383,7 +26398,11 @@ var DELIBERATE_END_REASONS = /* @__PURE__ */ new Set([
26383
26398
  "operator-completed",
26384
26399
  "operator-stopped",
26385
26400
  "steward-completed",
26386
- "steward-abandoned"
26401
+ "steward-abandoned",
26402
+ // Superseded by a fresh-id restart continuation (session-restart-core.ts)
26403
+ // while still alive — never auto-revive the OLD row, it already has a
26404
+ // living replacement (`continuedTo`).
26405
+ "restarted"
26387
26406
  ]);
26388
26407
  var POLLABLE_STATUSES = /* @__PURE__ */ new Set(["active", "orphaned"]);
26389
26408
  function agentprotoHome3() {
@@ -26601,8 +26620,16 @@ function createSentinelRuntime(opts) {
26601
26620
  try {
26602
26621
  await opts.registry.sendMessage(msg, { source: "sentinel", origin: sentinel.id });
26603
26622
  } catch (err) {
26604
- if (!(err instanceof SessionNotAliveError)) throw err;
26605
- await handleDeadSession(sentinel, event, msg);
26623
+ if (err instanceof SessionNotAliveError) {
26624
+ await handleDeadSession(sentinel, event, msg);
26625
+ return;
26626
+ }
26627
+ if (opts.sessionInfo && !opts.isSessionAlive(target.sessionId) && !opts.sessionInfo(target.sessionId)) {
26628
+ parkEvent(sentinel, event, `target session ${target.sessionId} no longer exists`);
26629
+ markOrphaned(sentinel);
26630
+ return;
26631
+ }
26632
+ throw err;
26606
26633
  }
26607
26634
  }
26608
26635
  function checkSubjectTerminalExpiry(sentinel, event) {
@@ -26820,6 +26847,8 @@ function createSentinelRuntime(opts) {
26820
26847
  // src/session-restart-core.ts
26821
26848
  init_resume_strategies();
26822
26849
  init_spawn_defaults();
26850
+ init_session_spawn();
26851
+ init_session_continue_fresh();
26823
26852
  init_launch_config();
26824
26853
  init_resume_context_digest();
26825
26854
  init_eligibility_manifest();
@@ -27123,9 +27152,25 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
27123
27152
  authSpec = resumeAuth.authSpec;
27124
27153
  authEcho = resumeAuth.authEcho;
27125
27154
  }
27155
+ const buildRestartMcpServers = (newSessionId) => {
27156
+ if (!opts.daemonMcpUrl) {
27157
+ if (prev.mcpServers && prev.mcpServers.length > 0) {
27158
+ console.warn(
27159
+ `[restartAgentSession] no daemonMcpUrl wired for restart of ${prev.id} \u2014 carrying mcpServers forward WITHOUT re-stamping callerSessionId (the restarted session ${newSessionId} will keep speaking to the daemon under the OLD session's identity).`
27160
+ );
27161
+ }
27162
+ return prev.mcpServers;
27163
+ }
27164
+ const stripped = stripOwnCallerStamp(prev.mcpServers, prev.id);
27165
+ return stampDaemonCallerSessionId(stripped, {
27166
+ daemonMcpUrl: opts.daemonMcpUrl,
27167
+ callerSessionId: newSessionId
27168
+ });
27169
+ };
27126
27170
  const spawnWithResume = async (resumeSessionId) => {
27127
27171
  let liveSessionId;
27128
27172
  const restartedSessionId = mintSessionId();
27173
+ const restartMcpServers = buildRestartMcpServers(restartedSessionId);
27129
27174
  if (sandboxId !== void 0) {
27130
27175
  return spawnInSandbox(sandboxId, restartedSessionId);
27131
27176
  }
@@ -27168,7 +27213,7 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
27168
27213
  // Legacy AIP-45 mode: prefer an explicit override; otherwise carry the
27169
27214
  // prior descriptor's mode forward so a plain restart preserves it.
27170
27215
  ...effMode ? { mode: effMode } : {},
27171
- ...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
27216
+ ...restartMcpServers ? { mcpServers: restartMcpServers } : {},
27172
27217
  ...authSpec ? { auth: authSpec } : {},
27173
27218
  ...launchConfig.options ? { options: launchConfig.options } : {},
27174
27219
  // Spawn-time guarantees survive a restart: the adapter runs under the
@@ -27202,7 +27247,7 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
27202
27247
  ...resolved.authDescriptor?.provider !== void 0 ? { adapterProvider: resolved.authDescriptor.provider } : {},
27203
27248
  ...resolved.authDescriptor?.modelDerivedApiKey !== void 0 ? { modelDerivedApiKey: resolved.authDescriptor.modelDerivedApiKey } : {},
27204
27249
  ...prev.label ? { label: prev.label } : {},
27205
- ...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
27250
+ ...restartMcpServers ? { mcpServers: restartMcpServers } : {},
27206
27251
  ...prevBoundary ? { meta: boundaryMeta(prevBoundary) } : {},
27207
27252
  ...effModel ? { model: effModel } : {},
27208
27253
  // Decomposed config-axis echoes (SPEC §3.7) — carried forward from `prev`
@@ -27228,6 +27273,16 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
27228
27273
  ...prev.origin ? { origin: prev.origin } : {},
27229
27274
  ...prev.parentSessionId ? { parentSessionId: prev.parentSessionId } : {},
27230
27275
  ...prev.depth !== void 0 ? { depth: prev.depth } : {},
27276
+ // Immutable spawn-time flags survive a restart exactly like
27277
+ // `continueAgentSessionFresh` carries `keepAlive`/`notifyParentOnCrash`
27278
+ // forward (session-continue-fresh.ts) — these are never axes a restart
27279
+ // overrides, only inherited, so dropping them here is what let a
27280
+ // persistent supervisor's restart silently become idle-reapable and
27281
+ // lose its crash-detect opt-ins.
27282
+ ...prev.keepAlive ? { keepAlive: true } : {},
27283
+ ...prev.notifyParentOnCrash ? { notifyParentOnCrash: true } : {},
27284
+ ...prev.sentinelAutoWatch === false ? { sentinelAutoWatch: false } : {},
27285
+ ...prev.restartPolicy ? { restartPolicy: prev.restartPolicy } : {},
27231
27286
  // Verifiability echo (never the credential) — see the auth
27232
27287
  // resolution block above. Absent when no credential resolved,
27233
27288
  // same as session-spawn.ts.
@@ -27264,6 +27319,7 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
27264
27319
  `restartAgentSession: sandbox provider "${providerSlug}" not found \u2014 cannot re-attach sandbox "${boxSandboxId}".`
27265
27320
  );
27266
27321
  }
27322
+ const sandboxMcpServers = buildRestartMcpServers(restartedSessionId);
27267
27323
  const spec = { provider: providerSlug, config: {} };
27268
27324
  const lifecyclePolicy = resolveLifecyclePolicy(spec, true);
27269
27325
  let host;
@@ -27293,7 +27349,7 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
27293
27349
  } : {},
27294
27350
  ...effEffort ? { effort: effEffort } : {},
27295
27351
  ...prev.label ? { label: prev.label } : {},
27296
- ...prev.mcpServers ? { mcpServers: toBoxMcpServerMounts(prev.mcpServers) } : {},
27352
+ ...sandboxMcpServers ? { mcpServers: toBoxMcpServerMounts(sandboxMcpServers) } : {},
27297
27353
  ...authSpec ? { auth: sandboxAuthForBox(authSpec) } : {}
27298
27354
  });
27299
27355
  remoteSessionId = remoteDesc.id;
@@ -27322,7 +27378,7 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
27322
27378
  ...resolved?.authDescriptor?.provider !== void 0 ? { adapterProvider: resolved.authDescriptor.provider } : {},
27323
27379
  ...resolved?.authDescriptor?.modelDerivedApiKey !== void 0 ? { modelDerivedApiKey: resolved.authDescriptor.modelDerivedApiKey } : {},
27324
27380
  ...prev.label ? { label: prev.label } : {},
27325
- ...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
27381
+ ...sandboxMcpServers ? { mcpServers: sandboxMcpServers } : {},
27326
27382
  ...effModel ? { model: effModel } : {},
27327
27383
  ...effEffort ? { effort: effEffort } : {},
27328
27384
  ...effPosture !== void 0 ? { posture: effPosture } : {},
@@ -27333,6 +27389,10 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
27333
27389
  ...prev.origin ? { origin: prev.origin } : {},
27334
27390
  ...prev.parentSessionId ? { parentSessionId: prev.parentSessionId } : {},
27335
27391
  ...prev.depth !== void 0 ? { depth: prev.depth } : {},
27392
+ ...prev.keepAlive ? { keepAlive: true } : {},
27393
+ ...prev.notifyParentOnCrash ? { notifyParentOnCrash: true } : {},
27394
+ ...prev.sentinelAutoWatch === false ? { sentinelAutoWatch: false } : {},
27395
+ ...prev.restartPolicy ? { restartPolicy: prev.restartPolicy } : {},
27336
27396
  ...authEcho?.fingerprint ? {
27337
27397
  auth: {
27338
27398
  mode: authEcho.authMode,
@@ -27374,6 +27434,9 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
27374
27434
  digestRecovered = result.hasContent;
27375
27435
  }
27376
27436
  }
27437
+ if (sandboxId === void 0) {
27438
+ registry.kill(prev.id, "SIGTERM", "restarted");
27439
+ }
27377
27440
  const ts = (/* @__PURE__ */ new Date()).toISOString();
27378
27441
  const emit = (axis, value) => {
27379
27442
  registry.emitConfigChanged({
@@ -32562,7 +32625,8 @@ function registerSessionTools(rawServer, opts) {
32562
32625
  forceAgentResume: true,
32563
32626
  overrides,
32564
32627
  ...listCatalogModels ? { listCatalogModels } : {},
32565
- ...opts.resolveSandboxProvider ? { resolveSandboxProvider: opts.resolveSandboxProvider } : {}
32628
+ ...opts.resolveSandboxProvider ? { resolveSandboxProvider: opts.resolveSandboxProvider } : {},
32629
+ ...opts.daemonMcpUrl ? { daemonMcpUrl: opts.daemonMcpUrl } : {}
32566
32630
  });
32567
32631
  return {
32568
32632
  content: [
@@ -32772,7 +32836,8 @@ function registerSessionTools(rawServer, opts) {
32772
32836
  }
32773
32837
  }
32774
32838
  const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev, {
32775
- ...opts.resolveSandboxProvider ? { resolveSandboxProvider: opts.resolveSandboxProvider } : {}
32839
+ ...opts.resolveSandboxProvider ? { resolveSandboxProvider: opts.resolveSandboxProvider } : {},
32840
+ ...opts.daemonMcpUrl ? { daemonMcpUrl: opts.daemonMcpUrl } : {}
32776
32841
  });
32777
32842
  return {
32778
32843
  content: [
@@ -39190,10 +39255,273 @@ async function handleSentinelInbound(hookKey, req, deps2) {
39190
39255
 
39191
39256
  // src/sentinel-tools.ts
39192
39257
  init_session_message();
39193
- function resolveUntil(input, prSugar) {
39258
+ var SESSION_SLUG = "session";
39259
+ var SESSION_DEFAULT_TYPES = [
39260
+ "session.turn.ended",
39261
+ "session.awaiting_input",
39262
+ "session.exited"
39263
+ ];
39264
+ var SESSION_SUBJECT_RE = /^session:(.+)$/;
39265
+ function parseSessionSubject(subject) {
39266
+ return SESSION_SUBJECT_RE.exec(subject)?.[1];
39267
+ }
39268
+ var RING_CAP = 2e3;
39269
+ var ringsByBus = /* @__PURE__ */ new WeakMap();
39270
+ function appendEvent(ring, event) {
39271
+ const seq = ring.nextSeq++;
39272
+ ring.entries.push({ seq, event });
39273
+ if (ring.entries.length > RING_CAP) ring.entries.splice(0, ring.entries.length - RING_CAP);
39274
+ return seq;
39275
+ }
39276
+ function parseCursor(cursor) {
39277
+ if (!cursor) return void 0;
39278
+ const i = cursor.lastIndexOf(":");
39279
+ if (i < 0) return void 0;
39280
+ const seq = Number(cursor.slice(i + 1));
39281
+ return Number.isFinite(seq) ? { epoch: cursor.slice(0, i), seq } : void 0;
39282
+ }
39283
+ function formatCursor(ring, seq) {
39284
+ return `${ring.epoch}:${seq}`;
39285
+ }
39286
+ function mintEventId(parts) {
39287
+ return `evt_${createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 24)}`;
39288
+ }
39289
+ function truncate3(text13, max = 200) {
39290
+ const flat = text13.replace(/\s+/g, " ").trim();
39291
+ return flat.length <= max ? flat : `${flat.slice(0, max - 1)}\u2026`;
39292
+ }
39293
+ function makeEvent2(input) {
39294
+ return {
39295
+ specversion: "1.0",
39296
+ id: mintEventId(input.idParts),
39297
+ source: `//agentproto.local/sentinel/${SESSION_SLUG}`,
39298
+ type: input.type,
39299
+ subject: input.subject,
39300
+ time: input.time,
39301
+ datacontenttype: "application/json",
39302
+ data: input.data,
39303
+ summary: input.summary,
39304
+ subjects: [input.subject],
39305
+ terminal: input.terminal
39306
+ };
39307
+ }
39308
+ function eventsForSessionEvent(ev) {
39309
+ switch (ev.type) {
39310
+ case "session:turn-end": {
39311
+ const subject = `session:${ev.sessionId}`;
39312
+ const who = ev.label ? `${ev.label} (${ev.sessionId})` : ev.sessionId;
39313
+ if (ev.awaitingInput) {
39314
+ const question = ev.question?.text;
39315
+ return [
39316
+ makeEvent2({
39317
+ idParts: [ev.sessionId, "awaiting_input", ev.ts],
39318
+ type: "session.awaiting_input",
39319
+ subject,
39320
+ time: ev.ts,
39321
+ terminal: false,
39322
+ data: { sessionId: ev.sessionId, ...question ? { question } : {} },
39323
+ summary: `${who} is awaiting input${question ? `: ${truncate3(question)}` : ""}`
39324
+ })
39325
+ ];
39326
+ }
39327
+ const detail = ev.error ?? (ev.reason && ev.reason !== "completed" ? ev.reason : void 0);
39328
+ return [
39329
+ makeEvent2({
39330
+ idParts: [ev.sessionId, "turn-end", ev.ts],
39331
+ type: "session.turn.ended",
39332
+ subject,
39333
+ time: ev.ts,
39334
+ terminal: false,
39335
+ data: {
39336
+ sessionId: ev.sessionId,
39337
+ ...ev.reason ? { reason: ev.reason } : {},
39338
+ ...ev.error ? { error: ev.error } : {}
39339
+ },
39340
+ summary: `${who} ended a turn${detail ? `: ${truncate3(detail)}` : ""}`
39341
+ })
39342
+ ];
39343
+ }
39344
+ case "session:awaiting-input": {
39345
+ const subject = `session:${ev.sessionId}`;
39346
+ const who = ev.label ? `${ev.label} (${ev.sessionId})` : ev.sessionId;
39347
+ const question = ev.question?.text;
39348
+ return [
39349
+ makeEvent2({
39350
+ idParts: [ev.sessionId, "awaiting_input", ev.ts],
39351
+ type: "session.awaiting_input",
39352
+ subject,
39353
+ time: ev.ts,
39354
+ terminal: false,
39355
+ data: { sessionId: ev.sessionId, ...question ? { question } : {} },
39356
+ summary: `${who} is awaiting input${question ? `: ${truncate3(question)}` : ""}`
39357
+ })
39358
+ ];
39359
+ }
39360
+ case "session:exited": {
39361
+ const subject = `session:${ev.sessionId}`;
39362
+ const who = ev.label ? `${ev.label} (${ev.sessionId})` : ev.sessionId;
39363
+ return [
39364
+ makeEvent2({
39365
+ idParts: [ev.sessionId, "exited", ev.ts],
39366
+ type: "session.exited",
39367
+ subject,
39368
+ time: ev.ts,
39369
+ terminal: true,
39370
+ data: {
39371
+ sessionId: ev.sessionId,
39372
+ status: ev.status,
39373
+ ...ev.exitCode !== void 0 ? { exitCode: ev.exitCode } : {},
39374
+ ...ev.reason ? { reason: ev.reason } : {}
39375
+ },
39376
+ summary: `${who} exited (${ev.reason ?? ev.status})`
39377
+ })
39378
+ ];
39379
+ }
39380
+ default:
39381
+ return [];
39382
+ }
39383
+ }
39384
+ function getRing(bus) {
39385
+ const existing = ringsByBus.get(bus);
39386
+ if (existing) return existing;
39387
+ const ring = { epoch: randomUUID(), entries: [], nextSeq: 0 };
39388
+ ringsByBus.set(bus, ring);
39389
+ bus.onAny((ev) => {
39390
+ for (const event of eventsForSessionEvent(ev)) appendEvent(ring, event);
39391
+ });
39392
+ return ring;
39393
+ }
39394
+ function identityOf(handle) {
39395
+ const state = handle.state;
39396
+ return typeof state?.sessionId === "string" ? state.sessionId : handle.remoteId;
39397
+ }
39398
+ function synthesizeExitEvent(ring, subject, sessionId, tag, info, goneReason) {
39399
+ const data = info ? {
39400
+ sessionId,
39401
+ status: info.status ?? "exited",
39402
+ ...info.exitCode !== void 0 ? { exitCode: info.exitCode } : {},
39403
+ ...info.endedReason ? { reason: info.endedReason } : {}
39404
+ } : { sessionId, status: "exited", ...goneReason ? { reason: goneReason } : {} };
39405
+ const summary = info ? `${info.label ?? sessionId} had already exited${info.endedReason ? ` (${info.endedReason})` : ""}` : `${sessionId} no longer exists${goneReason ? ` (${goneReason})` : ""}`;
39406
+ appendEvent(
39407
+ ring,
39408
+ makeEvent2({
39409
+ idParts: [sessionId, "exited", tag],
39410
+ type: "session.exited",
39411
+ subject,
39412
+ time: (/* @__PURE__ */ new Date()).toISOString(),
39413
+ terminal: true,
39414
+ data,
39415
+ summary
39416
+ })
39417
+ );
39418
+ }
39419
+ function resyncIfStale(ring, handle, sessionId, subject, getSession, tag, goneTag) {
39420
+ const parsed = parseCursor(handle.cursor);
39421
+ if (parsed && parsed.epoch === ring.epoch) return parsed.seq;
39422
+ const lastSeq = ring.nextSeq - 1;
39423
+ const info = getSession(sessionId);
39424
+ if (!info) {
39425
+ synthesizeExitEvent(ring, subject, sessionId, goneTag, void 0, "session no longer exists");
39426
+ } else if (!info.alive) {
39427
+ synthesizeExitEvent(ring, subject, sessionId, tag, info);
39428
+ }
39429
+ return lastSeq;
39430
+ }
39431
+ function sessionSentinelProvider(deps2) {
39432
+ return {
39433
+ slug: SESSION_SLUG,
39434
+ name: "Session Lifecycle",
39435
+ version: "0.1.0",
39436
+ description: "In-process sentinel over this daemon's own session lifecycle \u2014 turn-end, awaiting-input, exit. No credentials, no external system: the subject IS another session (`session:<id>`).",
39437
+ requiresSetup: false,
39438
+ capabilities: {
39439
+ subjects: [SESSION_SLUG],
39440
+ push: false,
39441
+ poll: true,
39442
+ durable: false,
39443
+ needsPublicUrl: false,
39444
+ requiresAuth: false,
39445
+ typicalLatencyMs: 15e3
39446
+ },
39447
+ async check() {
39448
+ return true;
39449
+ },
39450
+ async create(spec, _delivery) {
39451
+ if (spec.match.length !== 1) {
39452
+ throw new Error(`${SESSION_SLUG}: exactly one match clause is supported (one sentinel per session)`);
39453
+ }
39454
+ const subject = spec.match[0].subject;
39455
+ const sessionId = parseSessionSubject(subject);
39456
+ if (!sessionId) {
39457
+ throw new Error(`${SESSION_SLUG}: subject "${subject}" is not a "session:<id>" subject`);
39458
+ }
39459
+ const info = deps2.getSession(sessionId);
39460
+ if (!info) {
39461
+ throw new Error(`${SESSION_SLUG}: no session "${sessionId}"`);
39462
+ }
39463
+ const ring = getRing(deps2.sessionEvents);
39464
+ const lastSeq = ring.nextSeq - 1;
39465
+ if (!info.alive) {
39466
+ synthesizeExitEvent(ring, subject, sessionId, "already-ended-at-create", info);
39467
+ }
39468
+ return { provider: SESSION_SLUG, remoteId: sessionId, cursor: formatCursor(ring, lastSeq), state: { sessionId } };
39469
+ },
39470
+ async attach(handle, _delivery) {
39471
+ const sessionId = identityOf(handle);
39472
+ if (!sessionId) return { ...handle };
39473
+ const subject = `session:${sessionId}`;
39474
+ const ring = getRing(deps2.sessionEvents);
39475
+ const lastSeq = resyncIfStale(
39476
+ ring,
39477
+ handle,
39478
+ sessionId,
39479
+ subject,
39480
+ deps2.getSession,
39481
+ "already-ended-at-attach",
39482
+ "gone-at-attach"
39483
+ );
39484
+ return { ...handle, cursor: formatCursor(ring, lastSeq) };
39485
+ },
39486
+ async cancel(_handle) {
39487
+ },
39488
+ async status(handle) {
39489
+ const sessionId = identityOf(handle);
39490
+ if (!sessionId) return { ok: false, detail: "no session identity on handle" };
39491
+ const info = deps2.getSession(sessionId);
39492
+ if (!info) return { ok: false, detail: `session "${sessionId}" no longer exists` };
39493
+ return { ok: true };
39494
+ },
39495
+ async poll(handle, limit) {
39496
+ const sessionId = identityOf(handle);
39497
+ if (!sessionId) return { events: [], cursor: handle.cursor ?? "" };
39498
+ const ring = getRing(deps2.sessionEvents);
39499
+ const subject = `session:${sessionId}`;
39500
+ const lastSeq = resyncIfStale(
39501
+ ring,
39502
+ handle,
39503
+ sessionId,
39504
+ subject,
39505
+ deps2.getSession,
39506
+ "already-ended-at-poll",
39507
+ "gone-at-poll"
39508
+ );
39509
+ const matching = ring.entries.filter((e) => e.seq > lastSeq && e.event.subject === subject);
39510
+ const slice = matching.slice(0, limit);
39511
+ const cursor = slice.length > 0 ? formatCursor(ring, slice[slice.length - 1].seq) : formatCursor(ring, lastSeq);
39512
+ return { events: slice.map((e) => e.event), cursor };
39513
+ },
39514
+ defaultTypes(subject) {
39515
+ return parseSessionSubject(subject) ? [...SESSION_DEFAULT_TYPES] : [];
39516
+ }
39517
+ };
39518
+ }
39519
+
39520
+ // src/sentinel-tools.ts
39521
+ function resolveUntil(input, defaultsToTerminal) {
39194
39522
  if (input === "never") return { kind: "never" };
39195
39523
  if (input === "subject_terminal") return { kind: "subject_terminal" };
39196
- return prSugar ? { kind: "subject_terminal" } : { kind: "never" };
39524
+ return defaultsToTerminal ? { kind: "subject_terminal" } : { kind: "never" };
39197
39525
  }
39198
39526
  async function createSentinelWatch(deps2, input) {
39199
39527
  if (!input.subject && !input.prUrl) {
@@ -39215,7 +39543,8 @@ async function createSentinelWatch(deps2, input) {
39215
39543
  } else {
39216
39544
  subject = input.subject;
39217
39545
  }
39218
- const until = resolveUntil(input.until, prSugar);
39546
+ const sessionSubject = !prSugar && parseSessionSubject(subject) !== void 0;
39547
+ const until = resolveUntil(input.until, prSugar || sessionSubject);
39219
39548
  const sessionId = input.sessionId ?? deps2.defaultSessionId;
39220
39549
  if (!sessionId) {
39221
39550
  return {
@@ -39227,10 +39556,10 @@ async function createSentinelWatch(deps2, input) {
39227
39556
  if (!deps2.isSessionAlive(sessionId)) {
39228
39557
  return { ok: false, error: "session_not_alive", message: `session "${sessionId}" is not alive` };
39229
39558
  }
39230
- const providerSlug = input.provider ?? await autoSelectProviderSlug({
39559
+ const providerSlug = input.provider ?? (sessionSubject ? SESSION_SLUG : await autoSelectProviderSlug({
39231
39560
  resolveProvider: deps2.resolveProvider,
39232
39561
  ...deps2.publicUrl ? { publicUrl: deps2.publicUrl } : {}
39233
- });
39562
+ }));
39234
39563
  const provider = await deps2.resolveProvider(providerSlug);
39235
39564
  if (!provider) {
39236
39565
  return { ok: false, error: "unknown_provider", message: `provider "${providerSlug}" is not available` };
@@ -39303,15 +39632,15 @@ function registerSentinelTools(server, opts) {
39303
39632
  const { store, runtime, resolveProvider, isSessionAlive, callerSessionId, activeIntervalMs } = opts;
39304
39633
  server.tool(
39305
39634
  "sentinel_watch",
39306
- "Watch a GitHub subject (a PR, by default) and deliver matching events as a `system`/`notice` message into a session's AIP-46 inbox. Provide either `prUrl` (sugar: `https://github.com/o/r/pull/N` -> subject `github:o/r#N`, the default PR type set, and `until: subject_terminal`) or a raw `subject` (e.g. `github:o/r#N`, `github:o/r`). `sessionId` defaults to the calling session. `provider` picks the backend: `local-gh` (poll every ~15-60s over the host's authenticated `gh` CLI, zero infra), `webhook` (near-real-time push via a GitHub repo hook; needs a public daemon URL \u2014 a named tunnel or AGENTPROTO_PUBLIC_URL \u2014 and a `gh` token with admin:repo_hook) or `agentpush` (hosted, durable subscription: events queue server-side across daemon downtime; needs an agentpush API key; one subject per sentinel); see `list_sentinel_adapters` for readiness. Omitted, it defaults to `agentpush` when set up, else `webhook` when a stable public URL exists and webhook is ready, else `local-gh`.",
39635
+ "Watch a subject \u2014 a GitHub PR/repo (default) or ANOTHER SESSION's own lifecycle \u2014 and deliver matching events as a `system`/`notice` message into a session's AIP-46 inbox. Provide either `prUrl` (sugar: `https://github.com/o/r/pull/N` -> subject `github:o/r#N`, the default PR type set, and `until: subject_terminal`) or a raw `subject`: a GitHub one (`github:o/r#N`, `github:o/r`), or `session:<id>` to watch another session \u2014 woken on its turn-end, awaiting-input, or exit, even if it never calls `message_parent`, with the watch self-expiring once it exits (`until` defaults to `subject_terminal` for this scheme too). Prefer this over `session_follow` for a single, known target that should auto-expire on exit; prefer `session_follow` for a broad selector (`all`/`cwdPrefix`) with coalesced digests across many sessions. `sessionId` (the DELIVERY target) defaults to the calling session \u2014 distinct from the `<id>` inside a `session:<id>` subject (the WATCHED session). `provider` picks the backend: `local-gh` (poll every ~15-60s over the host's authenticated `gh` CLI, zero infra), `webhook` (near-real-time push via a GitHub repo hook; needs a public daemon URL \u2014 a named tunnel or AGENTPROTO_PUBLIC_URL \u2014 and a `gh` token with admin:repo_hook), `agentpush` (hosted, durable subscription: events queue server-side across daemon downtime; needs an agentpush API key; one subject per sentinel) or `session` (in-process, zero infra, no credentials \u2014 the only backend that understands a `session:<id>` subject, and picked automatically for one); see `list_sentinel_adapters` for readiness. Omitted for a GitHub subject, it defaults to `agentpush` when set up, else `webhook` when a stable public URL exists and webhook is ready, else `local-gh`.",
39307
39636
  {
39308
- subject: z.string().optional().describe('Raw subject, e.g. "github:owner/repo#42". Mutually exclusive with prUrl.'),
39637
+ subject: z.string().optional().describe('Raw subject, e.g. "github:owner/repo#42" or "session:sess_abc123". Mutually exclusive with prUrl.'),
39309
39638
  prUrl: z.string().optional().describe("https://github.com/owner/repo/pull/N \u2014 sugar for subject+types+until."),
39310
- sessionId: z.string().optional().describe("Target session. Defaults to the calling session."),
39639
+ sessionId: z.string().optional().describe("Delivery target session. Defaults to the calling session."),
39311
39640
  types: z.array(z.string()).optional().describe("Type globs. Defaults to the provider's defaultTypes(subject)."),
39312
39641
  urgency: urgencyField2.optional().describe('Inbox delivery urgency. Default "next-turn".'),
39313
- until: untilField.optional().describe('Lifetime. Default "subject_terminal" for prUrl, "never" for a raw subject.'),
39314
- provider: z.string().optional().describe('Provider slug: "local-gh", "webhook" or "agentpush". Default: "agentpush" when set up, else "webhook" when a stable public URL is configured and webhook is ready, else "local-gh".')
39642
+ until: untilField.optional().describe('Lifetime. Default "subject_terminal" for prUrl and for a "session:<id>" subject, "never" for any other raw subject.'),
39643
+ provider: z.string().optional().describe('Provider slug: "local-gh", "webhook", "agentpush" or "session". Default for a GitHub subject: "agentpush" when set up, else "webhook" when a stable public URL is configured and webhook is ready, else "local-gh". A "session:<id>" subject always defaults to "session".')
39315
39644
  },
39316
39645
  async (input) => {
39317
39646
  const result = await createSentinelWatch(
@@ -39593,7 +39922,7 @@ function registerSessionFollowTools(server, opts) {
39593
39922
  const { store, hasSession, callerSessionId } = opts;
39594
39923
  server.tool(
39595
39924
  "session_follow",
39596
- "Wake a session (the follower, default: you) when OTHER sessions end a turn, await input, exit/crash, or get a PR opened/merged \u2014 no polling. `selector` picks sessions: `sessionIds`, `all: true` (evaluated at event time, so sessions spawned later are covered; root sessions only unless `rootOnly: false`), or `cwdPrefix`. `exclude` removes ids/labels. Events are coalesced within `batchMs` into one automatic digest message that never interrupts a busy follower. Your own events and your children's are never delivered to you. Upserts by `key` when given. Survives daemon restarts.",
39925
+ "Wake a session (the follower, default: you) when OTHER sessions end a turn, await input, exit/crash, or get a PR opened/merged \u2014 no polling. `selector` picks sessions: `sessionIds`, `all: true` (evaluated at event time, so sessions spawned later are covered; root sessions only unless `rootOnly: false`), or `cwdPrefix`. `exclude` removes ids/labels. Events are coalesced within `batchMs` into one automatic digest message that never interrupts a busy follower. Your own events are never delivered to you; your descendants' only with `excludeFollowerChildren: false`. Upserts by `key` when given. Survives daemon restarts.",
39597
39926
  {
39598
39927
  follower: z.string().optional().describe("Session to wake. Defaults to the calling session."),
39599
39928
  key: z.string().optional().describe("Stable key: re-posting the same key updates that follow in place."),
@@ -39602,7 +39931,9 @@ function registerSessionFollowTools(server, opts) {
39602
39931
  events: z.array(z.enum(FOLLOW_EVENTS)).optional().describe("Event kinds to deliver. Default: all of turn-end, awaiting-input, exited, crashed, pr-opened, pr-merged."),
39603
39932
  batchMs: z.number().int().optional().describe("Coalescing window in ms. Default 15000."),
39604
39933
  skipEmptyTurns: z.boolean().optional().describe("Drop silent no-op turns. Default true."),
39605
- excludeFollowerChildren: z.boolean().optional().describe("Ignore the follower's own descendants. Default true.")
39934
+ excludeFollowerChildren: z.boolean().optional().describe(
39935
+ "Ignore the follower's own descendants. Default true. `false` delivers them (any depth, even under `rootOnly`) \u2014 what a supervisor wants for its own children."
39936
+ )
39606
39937
  },
39607
39938
  async (input) => {
39608
39939
  const result = upsertSessionFollow(
@@ -43678,7 +44009,7 @@ function nextFireDate(cronInstance) {
43678
44009
  return cronInstance.nextRun() ?? null;
43679
44010
  }
43680
44011
  function createCronScheduler(opts) {
43681
- const { sessionEvents, registry, resolveAgentAdapter, dispatchTool, workspace } = opts;
44012
+ const { sessionEvents, registry, resolveAgentAdapter, daemonMcpUrl, dispatchTool, workspace } = opts;
43682
44013
  const observeTurn = opts.observeTurn;
43683
44014
  const observeTimeoutMs = opts.observeTimeoutMs ?? DEFAULT_OBSERVE_TIMEOUT_MS;
43684
44015
  const persistPath = opts.persistPath ?? DEFAULT_PERSIST_PATH3();
@@ -43781,7 +44112,10 @@ function createCronScheduler(opts) {
43781
44112
  // result, which only an agent-cli session has — never let
43782
44113
  // decideRestartStrategy hand back a PTY-native resume (e.g.
43783
44114
  // claude-code's `claude --resume`) here.
43784
- { forceAgentResume: true }
44115
+ {
44116
+ forceAgentResume: true,
44117
+ ...daemonMcpUrl ? { daemonMcpUrl } : {}
44118
+ }
43785
44119
  );
43786
44120
  action.sessionId = restarted.desc.id;
43787
44121
  await registry.sendPrompt(restarted.desc.id, action.prompt);
@@ -54580,7 +54914,8 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
54580
54914
  const restarted = await restartAgentSession(registry, resolveAgentAdapter, prev, {
54581
54915
  forceAgentResume: true,
54582
54916
  overrides,
54583
- ...listCatalogModels ? { listCatalogModels } : {}
54917
+ ...listCatalogModels ? { listCatalogModels } : {},
54918
+ ...daemonMcpUrl ? { daemonMcpUrl } : {}
54584
54919
  });
54585
54920
  json3(200, {
54586
54921
  ...sessionDescriptorForHttp(restarted.desc),
@@ -57868,7 +58203,10 @@ var BUILTIN_SENTINEL_PROVIDERS = {
57868
58203
  [WEBHOOK_SLUG]: () => webhookSentinelProvider(),
57869
58204
  [AGENTPUSH_SLUG]: (creds) => agentpushSentinelProvider({ creds })
57870
58205
  };
57871
- var BUILTIN_SENTINEL_SLUGS = [LOCAL_GH_SLUG, WEBHOOK_SLUG, AGENTPUSH_SLUG];
58206
+ var BUILTIN_SENTINEL_SLUGS = [LOCAL_GH_SLUG, WEBHOOK_SLUG, AGENTPUSH_SLUG, SESSION_SLUG];
58207
+ function configureSessionSentinelProvider(deps2) {
58208
+ BUILTIN_SENTINEL_PROVIDERS[SESSION_SLUG] = () => sessionSentinelProvider(deps2);
58209
+ }
57872
58210
  var slugToCamel3 = (slug) => slug.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
57873
58211
  function isSentinelProvider(x) {
57874
58212
  if (!x || typeof x !== "object") return false;
@@ -58673,11 +59011,11 @@ function followCoversSession(follow, sessionId, desc, registry, eventLabel) {
58673
59011
  if (follow.exclude?.sessionIds?.includes(sessionId)) return false;
58674
59012
  const label = desc?.label ?? eventLabel;
58675
59013
  if (label && follow.exclude?.labels?.includes(label)) return false;
58676
- if (follow.excludeFollowerChildren && descendsFrom(registry, desc, follow.follower)) {
58677
- return false;
58678
- }
59014
+ const ownDescendant = descendsFrom(registry, desc, follow.follower);
59015
+ if (ownDescendant && follow.excludeFollowerChildren) return false;
58679
59016
  const sel = follow.selector;
58680
59017
  if (sel.sessionIds?.includes(sessionId)) return true;
59018
+ if (ownDescendant && (sel.all === true || sel.cwdPrefix !== void 0 && sel.cwdPrefix !== "")) return true;
58681
59019
  const broad = sel.all === true || sel.cwdPrefix !== void 0 && sel.cwdPrefix !== "";
58682
59020
  if (!broad) return false;
58683
59021
  if (sel.cwdPrefix && !cwdUnder(desc?.cwd, sel.cwdPrefix)) return false;
@@ -66451,6 +66789,13 @@ var SENTINEL_CATALOG = [
66451
66789
  description: "Hosted event subscriptions over agentpush \u2014 GitHub App installs, Telegram, generic hooks. Events queue server-side and survive daemon downtime; delivered by poll (default, no public URL) or push. Needs an agentpush workspace API key (`setup_sentinel_provider`, or an imported `agentpush` MCP alias).",
66452
66790
  packageName: "@agentproto/runtime",
66453
66791
  hint: "any source \xB7 durable \xB7 needs API key"
66792
+ },
66793
+ {
66794
+ slug: SESSION_SLUG,
66795
+ name: "Session Lifecycle",
66796
+ description: 'In-process watcher over this daemon\'s own session lifecycle (turn-end, awaiting-input, exit) for a "session:<id>" subject. No credentials, no external system \u2014 wired up by the daemon itself, not configurable via `setup_sentinel_provider`.',
66797
+ packageName: "@agentproto/runtime",
66798
+ hint: "session \xB7 zero-infra \xB7 in-process"
66454
66799
  }
66455
66800
  ];
66456
66801
  function toSentinelInfo(handle) {
@@ -67102,7 +67447,8 @@ function makeRestartForRouting(deps2, config) {
67102
67447
  }
67103
67448
  const restarted = await restartPreferInPlace(deps2.sessions, deps2.resolveAgentAdapter, desc, {
67104
67449
  forceAgentResume: true,
67105
- allowDeliberateEnd: config.allowDeliberateEnd
67450
+ allowDeliberateEnd: config.allowDeliberateEnd,
67451
+ ...deps2.daemonMcpUrl ? { daemonMcpUrl: deps2.daemonMcpUrl } : {}
67106
67452
  });
67107
67453
  return restarted.desc.id;
67108
67454
  };
@@ -67393,6 +67739,20 @@ async function createGateway(opts) {
67393
67739
  }
67394
67740
  } : {}
67395
67741
  });
67742
+ configureSessionSentinelProvider({
67743
+ sessionEvents,
67744
+ getSession: (sessionId) => {
67745
+ const desc = sessions.get(sessionId);
67746
+ if (!desc) return void 0;
67747
+ return {
67748
+ alive: desc.alive === true,
67749
+ status: desc.status,
67750
+ ...desc.label ? { label: desc.label } : {},
67751
+ ...desc.endedReason ? { endedReason: desc.endedReason } : {},
67752
+ ...desc.exitCode !== void 0 ? { exitCode: desc.exitCode } : {}
67753
+ };
67754
+ }
67755
+ });
67396
67756
  const restartScheduler = createRestartScheduler({ registry: sessions, sessionEvents });
67397
67757
  const supervisor = createCompletionPolicySupervisor({
67398
67758
  registry: sessions,
@@ -67434,6 +67794,7 @@ async function createGateway(opts) {
67434
67794
  const cronScheduler = createCronScheduler({
67435
67795
  sessionEvents,
67436
67796
  registry: sessions,
67797
+ daemonMcpUrl,
67437
67798
  ...opts.resolveAgentAdapter ? { resolveAgentAdapter: opts.resolveAgentAdapter } : {},
67438
67799
  ...opts.listAgentAdapters ? { listAgentAdapters: opts.listAgentAdapters } : {},
67439
67800
  getAuthProfile: getAuthProfile,
@@ -67609,11 +67970,11 @@ async function createGateway(opts) {
67609
67970
  return desc.processAlive !== false;
67610
67971
  };
67611
67972
  const restartSentinelSession = makeRestartForRouting(
67612
- { sessions, resolveAgentAdapter: opts.resolveAgentAdapter },
67973
+ { sessions, resolveAgentAdapter: opts.resolveAgentAdapter, daemonMcpUrl },
67613
67974
  { name: "restartSentinelSession", allowDeliberateEnd: false }
67614
67975
  );
67615
67976
  const restartInboundSession = makeRestartForRouting(
67616
- { sessions, resolveAgentAdapter: opts.resolveAgentAdapter },
67977
+ { sessions, resolveAgentAdapter: opts.resolveAgentAdapter, daemonMcpUrl },
67617
67978
  { name: "restartInboundSession", allowDeliberateEnd: true }
67618
67979
  );
67619
67980
  const sentinelRuntime = createSentinelRuntime({