@sema-agent/core 5.47.0 → 5.49.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +4 -0
  3. package/dist/agents/agent-transcript-tool.js +10 -3
  4. package/dist/agents/send-message-tool.d.ts +43 -1
  5. package/dist/agents/send-message-tool.js +50 -11
  6. package/dist/agents/subagent.d.ts +18 -0
  7. package/dist/agents/subagent.js +102 -2
  8. package/dist/agents/teacher.d.ts +25 -1
  9. package/dist/agents/teacher.js +85 -12
  10. package/dist/config/defaults.d.ts +20 -0
  11. package/dist/config/defaults.js +5 -0
  12. package/dist/core/background-agent-store.d.ts +1 -0
  13. package/dist/core/background-agent-store.js +14 -0
  14. package/dist/core/mcp.d.ts +6 -1
  15. package/dist/core/mcp.js +34 -7
  16. package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
  17. package/dist/core/memory-engine/delegation-settlement.js +31 -4
  18. package/dist/core/memory-engine/dual-root.js +11 -0
  19. package/dist/core/memory-engine/engine.d.ts +6 -1
  20. package/dist/core/memory-engine/engine.js +136 -21
  21. package/dist/core/memory-engine/memory-backend-contract.js +33 -0
  22. package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
  23. package/dist/core/memory-engine/origin-clearance.js +10 -0
  24. package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
  25. package/dist/core/memory-engine/provenance-wording.js +1 -0
  26. package/dist/core/memory-engine/tools.js +6 -4
  27. package/dist/core/reminder-disclosure.d.ts +90 -0
  28. package/dist/core/reminder-disclosure.js +64 -0
  29. package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
  30. package/dist/core/runner/prepare-acquire-reconcile.js +1 -1
  31. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  32. package/dist/core/runner/prepare-hands-readface.js +1 -0
  33. package/dist/core/runner/prepare-task.d.ts +15 -0
  34. package/dist/core/runner/prepare-task.js +51 -33
  35. package/dist/core/runner/runtask.d.ts +26 -1
  36. package/dist/core/runner/runtask.js +21 -3
  37. package/dist/core/session-store.d.ts +59 -1
  38. package/dist/core/session-store.js +82 -14
  39. package/dist/core/session.d.ts +83 -1
  40. package/dist/core/strategy-store.d.ts +180 -3
  41. package/dist/core/strategy-store.js +172 -23
  42. package/dist/core/task-registry-agent.d.ts +28 -0
  43. package/dist/core/task-registry-agent.js +63 -2
  44. package/dist/core/task-registry.d.ts +21 -0
  45. package/dist/core/task-registry.js +4 -1
  46. package/dist/core/types.d.ts +66 -0
  47. package/dist/core/untrusted-text.d.ts +63 -0
  48. package/dist/core/untrusted-text.js +48 -0
  49. package/dist/core/wiring-manifest.d.ts +35 -0
  50. package/dist/core/wiring-manifest.js +21 -1
  51. package/dist/engine/harness/types.d.ts +36 -1
  52. package/dist/index.d.ts +7 -6
  53. package/dist/index.js +6 -5
  54. package/dist/internal/harness-types.d.ts +1 -0
  55. package/dist/stores/file/file-snapshot-store.js +7 -1
  56. package/dist/stores/file/index.d.ts +27 -3
  57. package/dist/stores/file/index.js +36 -1
  58. package/dist/stores/file/session-policy-store.d.ts +0 -13
  59. package/dist/stores/file/session-policy-store.js +7 -1
  60. package/dist/stores/file/session-store.d.ts +22 -5
  61. package/dist/stores/file/session-store.js +80 -13
  62. package/dist/stores/file/strategy-store.d.ts +97 -0
  63. package/dist/stores/file/strategy-store.js +340 -0
  64. package/dist/tools/fs/fs-pdf.d.ts +12 -1
  65. package/dist/tools/fs/fs-pdf.js +17 -3
  66. package/dist/tools/fs/fs-read.d.ts +2 -1
  67. package/dist/tools/fs/fs-read.js +33 -5
  68. package/dist/tools/fs/fs-shared.d.ts +6 -2
  69. package/dist/tools/fs/index.d.ts +7 -0
  70. package/dist/tools/fs/index.js +1 -1
  71. package/dist/tools/web.js +21 -2
  72. package/package.json +3 -2
  73. package/test/export-surface.snapshot.json +22 -1
@@ -35,7 +35,7 @@ export async function prepareAcquireReconcile(input) {
35
35
  let resumeAtBeforeParentId = null;
36
36
  for (let attempt = 0;; attempt++) {
37
37
  try {
38
- acquired = await sessions.acquire(spec.sessionId, spec.requireExistingSession ? { requireExisting: true } : undefined);
38
+ acquired = await sessions.acquire(spec.sessionId, spec.requireExistingSession ? { requireExisting: true } : input.placement !== undefined ? { placement: input.placement } : undefined);
39
39
  }
40
40
  catch (err) {
41
41
  if (spec.requireExistingSession && err?.code === "not_found") {
@@ -103,6 +103,10 @@ export interface PrepareHandsReadFaceInput {
103
103
  * (the Read cyber/dedup/offset/empty reminders, the gh rate-limit hint) stamp the same mark the
104
104
  * system-prompt declaration names. */
105
105
  reminderMark: string;
106
+ /** borrowed-mutable — design/319 (B ticket): the leg's disclosure trigger counters; the hands
107
+ * band's Read/notebook/PDF trailer outlets bump keys on it at tool-execute time (the driver owns
108
+ * the object and folds it into result stats). */
109
+ reminderDisclosureCounts: import("../reminder-disclosure.js").ReminderDisclosureCounts;
106
110
  }
107
111
  /** The phase's outputs (相 API 规则件 four-class form) — ALL settled before the return; the driver
108
112
  * binds them as fresh consts (R-5) except the inverted-closure trio and the two shellGated bits,
@@ -219,6 +219,7 @@ export async function prepareHandsMount(input) {
219
219
  ...(readDenyBuiltinCfg.exclude !== undefined ? { readDenyBuiltinExclude: readDenyBuiltinCfg.exclude } : {}),
220
220
  readFace: liveReadFace,
221
221
  reminderMark: input.reminderMark,
222
+ reminderDisclosureCounts: input.reminderDisclosureCounts,
222
223
  includeShell: handsIncludeShell,
223
224
  readOnly: handsReadOnly,
224
225
  ...(handsCwdRef ? { cwdRef: handsCwdRef } : {}),
@@ -147,6 +147,10 @@ export interface Prepared {
147
147
  * fresh mint). Every engine-authored `<system-reminder>` open tag in the run carries it, and the
148
148
  * system prompt's Harness declaration names it. Always present on a completed prepare. */
149
149
  reminderMark: string;
150
+ /** design/319 (B ticket) — the leg's reminder-disclosure trigger counters (mutated by the
151
+ * disclosure outlets at tool-execute time; read once at result assembly into
152
+ * `stats.mechanisms.reminderDisclosures` when any key is non-zero). Always present. */
153
+ reminderDisclosureCounts: import("../reminder-disclosure.js").ReminderDisclosureCounts;
150
154
  /** The ISOLATION-AWARE working-tree root for this task (a worktree's cwd when `isolation: "worktree"`, else
151
155
  * `deps.rootPath ?? executionEnv.cwd`) — the same value the hands/LSP/policy/restore use. The Runner's
152
156
  * rewind/snapshot path MUST key off THIS, not `deps.rootPath`, or a worktree-isolated turn snapshots the base
@@ -1224,6 +1228,17 @@ export interface RunInternals {
1224
1228
  * mirroring `insideFork`.
1225
1229
  */
1226
1230
  isDelegatedChild?: boolean;
1231
+ /**
1232
+ * Subagent transcript persistence — the child transcript session's PLACEMENT declaration, minted
1233
+ * by the background delegation lane (the trusted spawner: it knows the a* handle, scope and root)
1234
+ * and forwarded verbatim by this prepare's session acquire, so the session store CREATES the
1235
+ * child's transcript into its declared subagent partition (see {@link SessionStore.placements}).
1236
+ * Deliberately a TRUSTED internals seat and never a {@link TaskSpec} key: a public key would let
1237
+ * any caller push arbitrary sessions into the partition and poison store-side retention/
1238
+ * enumeration. Absent (sync children, forks, observers, every non-delegated run) ⇒ the acquire
1239
+ * carries no placement — byte-identical to before.
1240
+ */
1241
+ sessionPlacement?: import("../session.js").SessionPlacement;
1227
1242
  /**
1228
1243
  * G1+G2 合车复审修② (1.259.0) — the DEFAULT role-base persona for a DELEGATED child, threaded by
1229
1244
  * `createSubagentTool`'s execute (a core caller) when neither an agent-definition `systemPrompt` nor the
@@ -16,7 +16,7 @@ import { Value } from "typebox/value";
16
16
  import { uuidv7 } from "../../engine/session/uuid.js";
17
17
  import { brainToRuntime } from "../runtime.js";
18
18
  import { hasSessionFork } from "../session.js";
19
- import { createSubagentWorktreeHelper, forkGovernanceDenial } from "../../agents/subagent.js";
19
+ import { createSubagentWorktreeHelper, forkGovernanceDenial, resolveDelegationEntryCaps } from "../../agents/subagent.js";
20
20
  import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
21
21
  import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
22
22
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
@@ -88,10 +88,10 @@ import { isSelfOrchestrationActive, selfOrchestrationFailClosedReason } from "..
88
88
  import { createRunWorkflowTool, RUN_WORKFLOW_TOOL_NAME } from "../../orchestration/run-workflow-tool.js";
89
89
  import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
90
90
  import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
91
- import { resolveKey } from "../../tools/fs/safety.js";
91
+ import { fileArgPath, resolveKey } from "../../tools/fs/safety.js";
92
92
  import { BINDING_CHECKPOINT_VERSION, mintCheckpointId, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
93
93
  import { boundInputHashOf } from "../canonical-json.js";
94
- import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
94
+ import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
95
95
  import { durableParkGapFor } from "../park-selfcheck.js";
96
96
  import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "../usage-window-store.js";
97
97
  import { deliverEngineNotice } from "../types.js";
@@ -332,6 +332,33 @@ function cwdConflictsRestoreError(requestedCwd) {
332
332
  e.code = "config.cwd_conflicts_restore";
333
333
  return e;
334
334
  }
335
+ async function adoptReminderMark(session, sessionId, seedMark, spawnMark, onError) {
336
+ const sessionReminderMark = await (async () => {
337
+ try {
338
+ return await session.getReminderMark();
339
+ }
340
+ catch (err) {
341
+ onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId });
342
+ return undefined;
343
+ }
344
+ })();
345
+ const reminderMark = isValidReminderMark(seedMark)
346
+ ? seedMark
347
+ : isValidReminderMark(spawnMark)
348
+ ? spawnMark
349
+ : isValidReminderMark(sessionReminderMark)
350
+ ? sessionReminderMark
351
+ : mintReminderMark();
352
+ if (reminderMark !== sessionReminderMark) {
353
+ try {
354
+ await session.appendReminderMark(reminderMark);
355
+ }
356
+ catch (err) {
357
+ onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId });
358
+ }
359
+ }
360
+ return { reminderMark, reminderDisclosureCounts: {} };
361
+ }
335
362
  export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
336
363
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
337
364
  spec = doors.spec;
@@ -345,7 +372,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
345
372
  e.code = "resume.session_not_found";
346
373
  throw e;
347
374
  }
348
- const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
375
+ const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(internals?.sessionPlacement !== undefined ? { placement: internals.sessionPlacement } : {}), ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
349
376
  const sessionId = acquired.sessionId;
350
377
  const hostTaskId = spec.taskId ?? sessionId;
351
378
  const delegation = effectiveDelegationFacts(internals, resume?.seed.isDelegatedChild);
@@ -353,30 +380,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
353
380
  const taskScope = internals?.registryScope ?? spec.principal ?? "default";
354
381
  internals?.peerSelfRef?.addAxis("s", sessionId);
355
382
  internals?.peerSelfRef?.addAxis("t", hostTaskId);
356
- const sessionReminderMark = await (async () => {
357
- try {
358
- return await session.getReminderMark();
359
- }
360
- catch (err) {
361
- deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId });
362
- return undefined;
363
- }
364
- })();
365
- const reminderMark = isValidReminderMark(resume?.seed.reminderMark)
366
- ? resume.seed.reminderMark
367
- : isValidReminderMark(internals?.reminderMark)
368
- ? internals.reminderMark
369
- : isValidReminderMark(sessionReminderMark)
370
- ? sessionReminderMark
371
- : mintReminderMark();
372
- if (reminderMark !== sessionReminderMark) {
373
- try {
374
- await session.appendReminderMark(reminderMark);
375
- }
376
- catch (err) {
377
- deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "config", sessionId });
378
- }
379
- }
383
+ const { reminderMark, reminderDisclosureCounts } = await adoptReminderMark(session, sessionId, resume?.seed.reminderMark, internals?.reminderMark, deps.onError);
380
384
  const offloadScope = spec.principal ?? "default";
381
385
  if (internals?.registryScope !== undefined && internals.registryScope !== offloadScope) {
382
386
  deps.onError?.(new Error(`offload/tool-result namespace note: this run mounts in registry domain "${internals.registryScope}" but its offloaded tool results live under the TRUST identity "${offloadScope}" (principal${spec.principal === undefined ? " absent → default" : ""}). Before 2.13.0 the registry domain named this namespace too; if a deployment used registryScope as its ref-tenancy boundary, that boundary is now the principal — pass one, or read refs under "${offloadScope}".`), { phase: "config", sessionId });
@@ -800,8 +804,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
800
804
  spec.handsReadOnly !== true &&
801
805
  !(toolFaceSnapshot.exclude?.includes("Bash") ?? false);
802
806
  let autoModeDecider;
807
+ const delegationEntryCapsResolved = resolveDelegationEntryCaps(deps.delegationEntryCaps);
803
808
  const enrichSpecToolCtx = (ctx) => ({
804
809
  ...ctx,
810
+ delegationEntryCaps: delegationEntryCapsResolved,
805
811
  reportUsage,
806
812
  model: harnessRef.current?.getModel(),
807
813
  thinkingLevel: harnessRef.current?.getThinkingLevel(),
@@ -826,6 +832,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
826
832
  getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
827
833
  parentCwd: taskRootFinal,
828
834
  reminderMark,
835
+ reminderDisclosureCounts,
829
836
  ...(centerAdoption !== undefined ? { centerArtifactDigest: centerAdoption.artifact.artifactDigest } : {}),
830
837
  ...(centerAdoption?.sourceRevision !== undefined ? { centerSourceRevision: centerAdoption.sourceRevision } : {}),
831
838
  activeSkillScope: () => skillScope.active(),
@@ -1094,7 +1101,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1094
1101
  tools.push(createOutputTool(outputRef, spec.outputSchema, compiled.strict ? compiled.modelSchema : undefined));
1095
1102
  }
1096
1103
  mcp = lockedPreflight.mcp?.length
1097
- ? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer)
1104
+ ? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer, { reminderMark, counts: reminderDisclosureCounts })
1098
1105
  : { tools: [], toolAxes: [], warnings: [], serverInstructions: [], instructionsDelta: { pendingAdds: [], pendingRemovals: [] }, droppedTools: [], statuses: [], refresh: async () => [], dispose: async () => { } };
1099
1106
  for (const w of mcp.warnings)
1100
1107
  deps.onError?.(w, { phase: "mcp", sessionId });
@@ -1229,7 +1236,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1229
1236
  foldProtocolAxes(a2a.toolAxes, "A2A");
1230
1237
  const callIssuedAtRef = {};
1231
1238
  const memoryWriteGateRef = {};
1232
- const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, tools, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark };
1239
+ const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, tools, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark, reminderDisclosureCounts };
1233
1240
  const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
1234
1241
  const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher } = handsReadFace;
1235
1242
  resolvedReadFace = handsReadFace.resolvedReadFace;
@@ -1289,6 +1296,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1289
1296
  ...(deps.mailboxStore !== undefined ? { mailbox: deps.mailboxStore } : {}),
1290
1297
  ...(reviveSpawn !== undefined ? { reviveSpawn } : {}),
1291
1298
  onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: hostTaskId, site: f.site, message: f.error.message, ts: Date.now() })),
1299
+ onTranscriptIntegrityGap: (handle, scope) => deliverEngineNotice(deps.onNotice, {
1300
+ code: "delegation.transcript_integrity",
1301
+ message: `delegation transcript integrity: agent ${handle}'s durable row binds a transcript session the session store attests is gone — the declared transcript durability is being contradicted (check the session store wiring/retention)`,
1302
+ detail: { handle, ...(scope !== undefined ? { scope } : {}) },
1303
+ }),
1292
1304
  })));
1293
1305
  if (!(spec.tools ?? []).some((t) => t.name === AGENT_TRANSCRIPT_TOOL_NAME)) {
1294
1306
  tools.push(firstPartyOffload(createAgentTranscriptTool({
@@ -1299,6 +1311,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1299
1311
  scope: taskScope,
1300
1312
  ...(sessionId !== undefined ? { sessionId } : {}),
1301
1313
  enrichCtx: enrichSpecToolCtx,
1314
+ onTranscriptIntegrityGap: (handle, scope) => deliverEngineNotice(deps.onNotice, {
1315
+ code: "delegation.transcript_integrity",
1316
+ message: `delegation transcript integrity: agent ${handle}'s durable row binds a transcript session the session store attests is gone — the declared transcript durability is being contradicted (check the session store wiring/retention)`,
1317
+ detail: { handle, ...(scope !== undefined ? { scope } : {}) },
1318
+ }),
1302
1319
  })));
1303
1320
  }
1304
1321
  }
@@ -1974,8 +1991,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1974
1991
  const delivered = result?.isError !== true &&
1975
1992
  result?.details?.type !== "file_unchanged" &&
1976
1993
  !headText.startsWith("<system-reminder");
1977
- const fp = params?.file_path;
1978
- if (delivered && typeof fp === "string" && recallTaintJudge(fp)) {
1994
+ const fp = fileArgPath(params);
1995
+ if (delivered && fp !== undefined && recallTaintJudge(fp)) {
1979
1996
  mark(`the "${t.name}" tool delivered the content of an external-origin memory entry file`, "derived");
1980
1997
  }
1981
1998
  return result;
@@ -3334,6 +3351,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3334
3351
  ...(checkpointStore !== undefined ? { checkpointDurability: resolveDeclaredDurability(checkpointStore, "checkpointStore") } : {}),
3335
3352
  sessionDurability: resolveDeclaredDurability(sessions, "sessionStore"),
3336
3353
  backgroundAgentStoreWired: deps.backgroundAgentStore !== undefined,
3354
+ subagentTranscriptTier: resolveSubagentTranscriptTier(deps.backgroundAgentStore !== undefined, sessions),
3337
3355
  permissionRuleStoreWired: deps.permissionRuleStore !== undefined,
3338
3356
  permissionRuleSyncWired: deps.permissionRuleSyncWired === true,
3339
3357
  permissionRuleOrgGoverned: deps.permissionRuleOrg !== undefined,
@@ -4606,7 +4624,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4606
4624
  const effectiveReadFaceObserved = carrierReadFace();
4607
4625
  const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
4608
4626
  const preparedHolder = {};
4609
- const buildPrepared = () => ({ harness, session, sessionId, reminderMark, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4627
+ const buildPrepared = () => ({ harness, session, sessionId, reminderMark, reminderDisclosureCounts, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4610
4628
  const prepared = buildPrepared();
4611
4629
  preparedHolder.current = prepared;
4612
4630
  return prepared;
@@ -6,7 +6,7 @@ import { type SideQuerySpec, type SideQueryResult } from "../side-query.js";
6
6
  import type { SessionStore } from "../session.js";
7
7
  import { type RecoveredOrphan } from "../session-reconcile.js";
8
8
  import { type ApprovalSettledBy } from "../tool-policy.js";
9
- import type { AgentDefinition, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
9
+ import type { AgentDefinition, ModelRef, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
10
10
  /**
11
11
  * Config re-supplied to {@link Runner.resume} (design/45). A suspended task's tools / model / policy /
12
12
  * hooks cannot be reconstructed from a checkpoint token (the session stores neither tool implementations
@@ -189,6 +189,31 @@ export declare class Runner {
189
189
  };
190
190
  /** Acquire the lock for a sessionId; returns a release fn. New (undefined) sessions need no lock. */
191
191
  private acquireSessionLock;
192
+ /**
193
+ * Hot-swap the model catalog (and optionally the tier bindings) without restarting the process or
194
+ * rebuilding the Runner — the deployment seat that makes "switching models" a zero-restart
195
+ * operation for catalogs the constructor froze (the constructor runs {@link expandTiers} once and
196
+ * keeps a private expanded copy, so mutating a shared table after construction never took effect;
197
+ * this verb is the sanctioned generation change).
198
+ *
199
+ * Semantics:
200
+ * - **Atomic**: the candidate catalog is tier-expanded and validated FIRST (an illegal tier
201
+ * binding throws exactly like the constructor's boot error) — on any throw the current
202
+ * generation stays in force, untouched.
203
+ * - **In-flight tasks keep their generation** (natural snapshot): a running task resolved its
204
+ * `Model` object at prepare time and holds that reference; the swap changes what FUTURE
205
+ * prepares (and auto-sourced agents' string refs — {@link agentCatalog} copies per read)
206
+ * resolve. Same-name redirects therefore never re-route or re-price a task mid-run; the
207
+ * divergence window is exactly the in-flight tasks' lifetime, by design.
208
+ * - `tiers` omitted ⇒ the current tier bindings are kept (and re-applied over the new models);
209
+ * explicitly passed (including `undefined`) ⇒ replaced.
210
+ * - Success is announced via `config.models_swapped` (models/tiers counts — key material only,
211
+ * never the catalog itself).
212
+ */
213
+ swapModels(next: {
214
+ models: Record<string, Model>;
215
+ tiers?: Record<string, ModelRef>;
216
+ }): void;
192
217
  /** [1463]① — one-shot brain-routed utility query (see {@link runSideQuery} for the full contract):
193
218
  * preserves system/multi-turn messages/tool DEFINITIONS, routes through the deployment's brain with
194
219
  * the same model resolution as tasks, returns real usage/model. No session, no tool execution, no
@@ -1533,6 +1533,21 @@ export class Runner {
1533
1533
  });
1534
1534
  };
1535
1535
  }
1536
+ swapModels(next) {
1537
+ if (next.models === null || typeof next.models !== "object" || Array.isArray(next.models)) {
1538
+ throw new Error(`swapModels: models must be a plain Record<string, Model> (got ${next.models === null ? "null" : Array.isArray(next.models) ? "array" : typeof next.models}) — the current generation stays in force`);
1539
+ }
1540
+ const tiers = Object.hasOwn(next, "tiers") ? next.tiers : this.deps.tiers;
1541
+ const expanded = tiers && Object.keys(tiers).length > 0 ? expandTiers({ ...next.models }, tiers) : { ...next.models };
1542
+ this.deps = { ...this.deps, models: expanded, ...(tiers !== undefined ? { tiers } : {}) };
1543
+ if (tiers === undefined)
1544
+ delete this.deps.tiers;
1545
+ deliverEngineNotice(this.deps.onNotice, {
1546
+ code: "config.models_swapped",
1547
+ message: `model catalog swapped: ${Object.keys(next.models).length} model(s), ${tiers ? Object.keys(tiers).length : 0} tier binding(s); in-flight tasks finish on their resolved models, new tasks resolve against the new catalog`,
1548
+ detail: { models: Object.keys(next.models).length, tiers: tiers ? Object.keys(tiers).length : 0 },
1549
+ });
1550
+ }
1536
1551
  sideQuery(spec) {
1537
1552
  return runSideQuery(spec, { brain: this.deps.brain, models: this.deps.models, roles: this.deps.roles });
1538
1553
  }
@@ -2027,6 +2042,7 @@ export class Runner {
2027
2042
  };
2028
2043
  const peerSelfRef = internals?.peerSelfRef ?? createPeerSelfRef(internals?.registryScope ?? spec.principal ?? "default");
2029
2044
  const peerInboundChainRef = internals?.peerInboundChainRef ?? createPeerInboundChainRef();
2045
+ const modelCatalog = this.deps.models;
2030
2046
  const prepared = await prepareTask(spec, this.deps, this.sessions, prepareResume, {
2031
2047
  ...(internals ?? {}),
2032
2048
  peerSelfRef,
@@ -2128,7 +2144,7 @@ export class Runner {
2128
2144
  rs.telemetry.pricingConfigured = this.deps.pricing?.[prepared.model.id] !== undefined || prepared.model.cost !== undefined;
2129
2145
  if (spec.limits?.degrade) {
2130
2146
  try {
2131
- rs.degrade.degradeToModel = resolveModel(spec.limits.degrade.to, this.deps.models);
2147
+ rs.degrade.degradeToModel = resolveModel(spec.limits.degrade.to, modelCatalog);
2132
2148
  }
2133
2149
  catch (e) {
2134
2150
  this.deps.onError?.(e, { phase: "degraded", sessionId: prepared.sessionId });
@@ -2171,7 +2187,7 @@ export class Runner {
2171
2187
  let m = toModel;
2172
2188
  if (!m) {
2173
2189
  try {
2174
- m = resolveModel(info.to, this.deps.models);
2190
+ m = resolveModel(info.to, modelCatalog);
2175
2191
  }
2176
2192
  catch {
2177
2193
  m = undefined;
@@ -3446,7 +3462,8 @@ export class Runner {
3446
3462
  gates: [...prepared.humanReviewRef.gates],
3447
3463
  };
3448
3464
  }
3449
- if (rs.counters.finalVerifyInjections > 0 || rs.attach.attachmentsInjected > 0 || rs.counters.repetitionCuts > 0 || rs.counters.repetitionSpared > 0 || rs.counters.approachNoticesSent > 0) {
3465
+ const reminderDisclosuresActive = Object.keys(prepared.reminderDisclosureCounts).length > 0;
3466
+ if (rs.counters.finalVerifyInjections > 0 || rs.attach.attachmentsInjected > 0 || rs.counters.repetitionCuts > 0 || rs.counters.repetitionSpared > 0 || rs.counters.approachNoticesSent > 0 || reminderDisclosuresActive) {
3450
3467
  stats.mechanisms = {
3451
3468
  ...(rs.counters.finalVerifyInjections > 0 ? { finalVerifyInjected: true } : {}),
3452
3469
  ...(rs.counters.finalVerifyInjections > 0 ? { finalVerifyInjections: rs.counters.finalVerifyInjections } : {}),
@@ -3455,6 +3472,7 @@ export class Runner {
3455
3472
  ...(rs.counters.repetitionCuts > 0 ? { repetitionCuts: rs.counters.repetitionCuts } : {}),
3456
3473
  ...(rs.counters.repetitionSpared > 0 ? { repetitionSpared: rs.counters.repetitionSpared } : {}),
3457
3474
  ...(rs.counters.repetitionEvents.length > 0 ? { repetitionEvents: rs.counters.repetitionEvents } : {}),
3475
+ ...(reminderDisclosuresActive ? { reminderDisclosures: { ...prepared.reminderDisclosureCounts } } : {}),
3458
3476
  };
3459
3477
  }
3460
3478
  const result = assembleResult(spec, prepared.sessionId, final, stats, {
@@ -1,5 +1,5 @@
1
1
  import { type SessionRepo } from "../internal/harness.js";
2
- import { type AcquiredSession, type SessionStore, type SessionStoreSummary } from "./session.js";
2
+ import { type AcquiredSession, type PlacedSessionRow, type SessionPlacement, type SessionStore, type SessionStoreSummary } from "./session.js";
3
3
  import type { StoreDurability } from "./checkpoint-store.js";
4
4
  import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
5
5
  export { SESSION_DEFAULT_TTL_DAYS };
@@ -36,6 +36,21 @@ export interface TtlSessionStoreOptions {
36
36
  * fail-closed as process-local (under-promise, never over-promise).
37
37
  */
38
38
  durability?: StoreDurability;
39
+ /**
40
+ * Subagent transcript persistence — the store's per-placement durability declaration (see
41
+ * {@link SessionStore.placements}). Same declaration rule as {@link durability}: this cache layer
42
+ * cannot inspect whether the injected repo persists placed sessions, so the DEPLOYMENT that wired
43
+ * the repo states the fact (`FileStorageBackend` passes `{subagent:{durability:"durable"}}`).
44
+ * Declaring it also arms the cache-layer placement obligations (list exclusion, claim refusal,
45
+ * release-is-deletion, sweep-never-deletes) — they key on the repo-persisted tuple, which a repo
46
+ * that ignores `placement` never mints, so declaring over such a repo is the deployment lying to
47
+ * itself (declaration-制, wiring-manifest posture).
48
+ */
49
+ placements?: {
50
+ subagent?: {
51
+ durability: StoreDurability;
52
+ };
53
+ };
39
54
  }
40
55
  /**
41
56
  * TTL-cached {@link SessionStore} over a pluggable {@link SessionRepo}.
@@ -59,6 +74,18 @@ export declare class TtlSessionStore implements SessionStore {
59
74
  readonly retention: "none";
60
75
  /** design/173 §2.3 — see {@link TtlSessionStoreOptions.durability} for the declaration rules. */
61
76
  readonly durability?: StoreDurability;
77
+ /** Subagent transcript persistence — see {@link TtlSessionStoreOptions.placements}. */
78
+ readonly placements?: {
79
+ subagent?: {
80
+ durability: StoreDurability;
81
+ };
82
+ };
83
+ /** Placed-partition enumeration, present only when the backing repo can enumerate placements
84
+ * (see {@link SessionStore.listPlaced} — absence means the partition-GC leg is unavailable). */
85
+ readonly listPlaced?: (kind: "subagent", opts?: {
86
+ olderThanMs?: number;
87
+ scope?: string;
88
+ }) => Promise<PlacedSessionRow[]>;
62
89
  private repo;
63
90
  private entries;
64
91
  /** Session-ownership rows (the multi-tenant admission capability face — see SessionStore.ownerOf).
@@ -68,6 +95,20 @@ export declare class TtlSessionStore implements SessionStore {
68
95
  private owners;
69
96
  /** In-flight acquisitions keyed by id, so concurrent acquire(sameId) share one session. */
70
97
  private pending;
98
+ /** Subagent transcript persistence (codex 323-r2) — placed ids whose durable deletion FAILED: the
99
+ * placed fact for the retry lives HERE, not on a retained cache entry, because the repo may have
100
+ * torn down the shared session authority before its removal failed (the File repo closes the
101
+ * append fd pre-rmSync) — a retained entry would serve a session whose log is closed while the
102
+ * file still exists (append → log_closed, transcript revival broken until restart). The entry is
103
+ * dropped instead, so a re-acquire replays a FRESH authority from disk; success clears the row. */
104
+ private pendingPlacedDeletes;
105
+ /** Subagent transcript persistence (codex 323-r3) — placed deletions IN FLIGHT: acquire awaits the
106
+ * settlement before resolving the id, because the repo's delete tears down the shared session
107
+ * authority mid-flight — an acquire racing the await window could capture (or rebuild and then
108
+ * lose) an authority the delete is about to close, and the caller would hold a session whose
109
+ * first append answers log_closed while the file still exists. The gate is per-id and bounded by
110
+ * the delete's own settlement (both outcomes release it). */
111
+ private deletingPlaced;
71
112
  /** Sessions pinned by a design/45 checkpoint — skipped by idle sweep until unpinned (B6/§5). */
72
113
  private pinned;
73
114
  private defaultTtlMs;
@@ -77,7 +118,24 @@ export declare class TtlSessionStore implements SessionStore {
77
118
  /** Get an existing session by id (resuming from the repo if needed), or create one. */
78
119
  acquire(sessionId?: string, opts?: {
79
120
  requireExisting?: boolean;
121
+ placement?: SessionPlacement;
80
122
  }): Promise<AcquiredSession>;
123
+ /**
124
+ * Placement obligation 6 (claim closure): an EXISTING placed session refuses the claim-form
125
+ * acquire — no `requireExisting` (the read/revive form, obligation 5) and no creation placement
126
+ * (the trusted-internals form; the creating call and its dedup/retry re-acquires stay legal, and
127
+ * placement immutability means the argument can never REWRITE anything — the persisted tuple wins).
128
+ *
129
+ * …and the MIRROR direction (codex 323-r1 F1 — the pre-claim SQUAT race): the child session id is
130
+ * published on the spawn frame BEFORE the child's prepare creates the session, so a concurrent
131
+ * claim-form acquire could win first creation and hand the trusted placement caller an ORDINARY
132
+ * session — host-listable, never really-deleted, the declared full tier falsified in silence. On
133
+ * a store that DECLARES the partition (an ignoring store legally drops placements), an acquire
134
+ * carrying a placement must therefore come back with a PERSISTED tuple whose join identity
135
+ * matches the declaration — a missing or foreign tuple refuses loudly, on every resolution path
136
+ * (cache hit, pending-promise inherit, open, idempotent re-create).
137
+ */
138
+ private assertPlacementAdmission;
81
139
  /** Resume an existing session from the repo, or create it if the backend has no such id (unless
82
140
  * `requireExisting`, in which case a genuinely-missing id fails loud instead — design/114 Phase3). */
83
141
  private openOrCreate;
@@ -1,4 +1,4 @@
1
- import { InMemorySessionRepo, uuidv7, } from "../internal/harness.js";
1
+ import { InMemorySessionRepo, SessionError, uuidv7, } from "../internal/harness.js";
2
2
  import { isSessionConflict } from "./session.js";
3
3
  const DAY_MS = 24 * 60 * 60 * 1000;
4
4
  import { SESSION_DEFAULT_TTL_DAYS } from "../config/defaults.js";
@@ -6,10 +6,14 @@ export { SESSION_DEFAULT_TTL_DAYS };
6
6
  export class TtlSessionStore {
7
7
  retention = "none";
8
8
  durability;
9
+ placements;
10
+ listPlaced;
9
11
  repo;
10
12
  entries = new Map();
11
13
  owners = new Map();
12
14
  pending = new Map();
15
+ pendingPlacedDeletes = new Set();
16
+ deletingPlaced = new Map();
13
17
  pinned = new Set();
14
18
  defaultTtlMs;
15
19
  evictPolicy;
@@ -21,6 +25,12 @@ export class TtlSessionStore {
21
25
  this.durability = durability;
22
26
  this.defaultTtlMs = (opts.defaultTtlDays ?? SESSION_DEFAULT_TTL_DAYS) * DAY_MS;
23
27
  this.evictPolicy = opts.evict ?? (opts.repo ? "forget" : "delete");
28
+ if (opts.placements !== undefined)
29
+ this.placements = opts.placements;
30
+ const repoListPlaced = this.repo.listPlaced;
31
+ if (typeof repoListPlaced === "function") {
32
+ this.listPlaced = (kind, o) => repoListPlaced.call(this.repo, kind, o);
33
+ }
24
34
  if (opts.sweepIntervalMs !== undefined && opts.sweepIntervalMs > 0) {
25
35
  this.sweepTimer = setInterval(() => this.sweep(), opts.sweepIntervalMs);
26
36
  this.sweepTimer.unref?.();
@@ -29,8 +39,12 @@ export class TtlSessionStore {
29
39
  async acquire(sessionId, opts) {
30
40
  this.sweep();
31
41
  if (sessionId) {
42
+ const inflightPlacedDelete = this.deletingPlaced.get(sessionId);
43
+ if (inflightPlacedDelete !== undefined)
44
+ await inflightPlacedDelete;
32
45
  const existing = this.entries.get(sessionId);
33
46
  if (existing) {
47
+ this.assertPlacementAdmission(sessionId, existing.placement, opts);
34
48
  existing.lastActiveAt = Date.now();
35
49
  return { session: existing.session, sessionId };
36
50
  }
@@ -39,26 +53,46 @@ export class TtlSessionStore {
39
53
  }
40
54
  const inflight = this.pending.get(sessionId);
41
55
  if (inflight) {
42
- return { session: await inflight, sessionId };
56
+ const session = await inflight;
57
+ this.assertPlacementAdmission(sessionId, this.entries.get(sessionId)?.placement, opts);
58
+ return { session, sessionId };
43
59
  }
44
- const p = this.openOrCreate(sessionId, false);
60
+ const p = this.openOrCreate(sessionId, false, opts?.placement);
45
61
  this.pending.set(sessionId, p);
46
62
  try {
47
- return { session: await p, sessionId };
63
+ const session = await p;
64
+ this.assertPlacementAdmission(sessionId, this.entries.get(sessionId)?.placement, opts);
65
+ return { session, sessionId };
48
66
  }
49
67
  finally {
50
68
  this.pending.delete(sessionId);
51
69
  }
52
70
  }
53
- const session = await this.createAndStore(undefined);
71
+ const session = await this.createAndStore(undefined, opts?.placement);
54
72
  const meta = await session.getMetadata();
55
73
  return { session, sessionId: meta.id };
56
74
  }
57
- async openOrCreate(id, requireExisting = false) {
75
+ assertPlacementAdmission(sessionId, placement, opts) {
76
+ if (placement !== undefined && opts?.requireExisting !== true && opts?.placement === undefined) {
77
+ throw new SessionError("placement_refused", `session ${sessionId} belongs to the ${placement.kind} placement partition — a claim-form acquire cannot attach to it (read it with requireExisting, or address the agent through its a* handle)`);
78
+ }
79
+ if (opts?.placement !== undefined && opts.requireExisting !== true && this.placements?.subagent !== undefined) {
80
+ const want = opts.placement;
81
+ if (placement === undefined) {
82
+ throw new SessionError("placement_refused", `session ${sessionId} exists OUTSIDE the ${want.kind} placement partition it was declared into — refusing to run a subagent transcript on it (a concurrent claim-form create won this id, or the repo dropped the tuple)`);
83
+ }
84
+ if (placement.kind !== want.kind || placement.scope !== want.scope || placement.handle !== want.handle) {
85
+ throw new SessionError("placement_refused", `session ${sessionId}'s persisted placement does not match the declared one (persisted ${placement.kind}/${placement.scope ?? "-"}/${placement.handle ?? "-"} vs declared ${want.kind}/${want.scope ?? "-"}/${want.handle ?? "-"}) — refusing a cross-tuple attach`);
86
+ }
87
+ }
88
+ }
89
+ async openOrCreate(id, requireExisting = false, placement) {
58
90
  try {
59
91
  const session = await this.repo.open({ id, createdAt: "" });
60
92
  const meta = await session.getMetadata();
61
- this.entries.set(id, { session, lastActiveAt: Date.now(), createdAt: meta.createdAt, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}) });
93
+ if (meta.placement === undefined)
94
+ this.pendingPlacedDeletes.delete(id);
95
+ this.entries.set(id, { session, lastActiveAt: Date.now(), createdAt: meta.createdAt, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}), ...(meta.placement !== undefined ? { placement: meta.placement } : {}) });
62
96
  return session;
63
97
  }
64
98
  catch (err) {
@@ -68,13 +102,15 @@ export class TtlSessionStore {
68
102
  if (requireExisting && isNotFound(err)) {
69
103
  throw err;
70
104
  }
71
- return this.createAndStore(id);
105
+ return this.createAndStore(id, placement);
72
106
  }
73
107
  }
74
- async createAndStore(id) {
75
- const session = await this.repo.create(id ? { id } : {});
108
+ async createAndStore(id, placement) {
109
+ const session = await this.repo.create({ ...(id !== undefined ? { id } : {}), ...(placement !== undefined ? { placement } : {}) });
76
110
  const meta = await session.getMetadata();
77
- this.entries.set(meta.id, { session, lastActiveAt: Date.now(), createdAt: meta.createdAt, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}) });
111
+ if (meta.placement === undefined)
112
+ this.pendingPlacedDeletes.delete(meta.id);
113
+ this.entries.set(meta.id, { session, lastActiveAt: Date.now(), createdAt: meta.createdAt, ...(meta.forkedFrom !== undefined ? { forkedFrom: meta.forkedFrom } : {}), ...(meta.placement !== undefined ? { placement: meta.placement } : {}) });
78
114
  return session;
79
115
  }
80
116
  touch(sessionId) {
@@ -92,6 +128,7 @@ export class TtlSessionStore {
92
128
  }
93
129
  async list() {
94
130
  return [...this.entries.entries()]
131
+ .filter(([, e]) => e.placement === undefined)
95
132
  .map(([sessionId, e]) => ({
96
133
  sessionId,
97
134
  lastActiveAt: e.lastActiveAt,
@@ -137,12 +174,43 @@ export class TtlSessionStore {
137
174
  return;
138
175
  }
139
176
  const e = this.entries.get(sessionId);
140
- if (!e) {
177
+ if (e !== undefined && e.placement === undefined)
178
+ this.pendingPlacedDeletes.delete(sessionId);
179
+ let placed = e !== undefined ? e.placement !== undefined : this.pendingPlacedDeletes.has(sessionId);
180
+ if (!placed && e === undefined && this.placements !== undefined) {
181
+ const probe = this.repo.placementOf;
182
+ if (typeof probe === "function") {
183
+ try {
184
+ placed = (await probe.call(this.repo, sessionId)) !== undefined;
185
+ }
186
+ catch {
187
+ placed = false;
188
+ }
189
+ }
190
+ }
191
+ if (!e && !placed) {
192
+ return;
193
+ }
194
+ if (placed) {
195
+ this.pendingPlacedDeletes.add(sessionId);
196
+ this.entries.delete(sessionId);
197
+ let settleGate;
198
+ this.deletingPlaced.set(sessionId, new Promise((r) => (settleGate = r)));
199
+ try {
200
+ const meta = e !== undefined ? await e.session.getMetadata() : { id: sessionId, createdAt: "" };
201
+ await this.repo.delete(meta);
202
+ }
203
+ finally {
204
+ this.deletingPlaced.delete(sessionId);
205
+ settleGate();
206
+ }
207
+ this.pendingPlacedDeletes.delete(sessionId);
208
+ this.owners.delete(sessionId);
141
209
  return;
142
210
  }
143
211
  this.entries.delete(sessionId);
144
212
  if (this.evictPolicy === "delete") {
145
- await this.repo.delete(await e.session.getMetadata());
213
+ await this.repo.delete(e !== undefined ? await e.session.getMetadata() : { id: sessionId, createdAt: "" });
146
214
  this.owners.delete(sessionId);
147
215
  }
148
216
  }
@@ -174,7 +242,7 @@ export class TtlSessionStore {
174
242
  }
175
243
  if (now - e.lastActiveAt > this.defaultTtlMs) {
176
244
  this.entries.delete(id);
177
- if (this.evictPolicy === "delete") {
245
+ if (this.evictPolicy === "delete" && e.placement === undefined) {
178
246
  void this.repo.delete({ id, createdAt: "" }).then(() => {
179
247
  this.owners.delete(id);
180
248
  }, () => { });