@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.
- package/CHANGELOG.md +115 -0
- package/dist/agents/agent-transcript-tool.d.ts +4 -0
- package/dist/agents/agent-transcript-tool.js +10 -3
- package/dist/agents/send-message-tool.d.ts +43 -1
- package/dist/agents/send-message-tool.js +50 -11
- package/dist/agents/subagent.d.ts +18 -0
- package/dist/agents/subagent.js +102 -2
- package/dist/agents/teacher.d.ts +25 -1
- package/dist/agents/teacher.js +85 -12
- package/dist/config/defaults.d.ts +20 -0
- package/dist/config/defaults.js +5 -0
- package/dist/core/background-agent-store.d.ts +1 -0
- package/dist/core/background-agent-store.js +14 -0
- package/dist/core/mcp.d.ts +6 -1
- package/dist/core/mcp.js +34 -7
- package/dist/core/memory-engine/delegation-settlement.d.ts +27 -0
- package/dist/core/memory-engine/delegation-settlement.js +31 -4
- package/dist/core/memory-engine/dual-root.js +11 -0
- package/dist/core/memory-engine/engine.d.ts +6 -1
- package/dist/core/memory-engine/engine.js +136 -21
- package/dist/core/memory-engine/memory-backend-contract.js +33 -0
- package/dist/core/memory-engine/origin-clearance.d.ts +19 -0
- package/dist/core/memory-engine/origin-clearance.js +10 -0
- package/dist/core/memory-engine/provenance-wording.d.ts +15 -1
- package/dist/core/memory-engine/provenance-wording.js +1 -0
- package/dist/core/memory-engine/tools.js +6 -4
- package/dist/core/reminder-disclosure.d.ts +90 -0
- package/dist/core/reminder-disclosure.js +64 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
- package/dist/core/runner/prepare-hands-readface.js +1 -0
- package/dist/core/runner/prepare-task.d.ts +15 -0
- package/dist/core/runner/prepare-task.js +51 -33
- package/dist/core/runner/runtask.d.ts +26 -1
- package/dist/core/runner/runtask.js +21 -3
- package/dist/core/session-store.d.ts +59 -1
- package/dist/core/session-store.js +82 -14
- package/dist/core/session.d.ts +83 -1
- package/dist/core/strategy-store.d.ts +180 -3
- package/dist/core/strategy-store.js +172 -23
- package/dist/core/task-registry-agent.d.ts +28 -0
- package/dist/core/task-registry-agent.js +63 -2
- package/dist/core/task-registry.d.ts +21 -0
- package/dist/core/task-registry.js +4 -1
- package/dist/core/types.d.ts +66 -0
- package/dist/core/untrusted-text.d.ts +63 -0
- package/dist/core/untrusted-text.js +48 -0
- package/dist/core/wiring-manifest.d.ts +35 -0
- package/dist/core/wiring-manifest.js +21 -1
- package/dist/engine/harness/types.d.ts +36 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/internal/harness-types.d.ts +1 -0
- package/dist/stores/file/file-snapshot-store.js +7 -1
- package/dist/stores/file/index.d.ts +27 -3
- package/dist/stores/file/index.js +36 -1
- package/dist/stores/file/session-policy-store.d.ts +0 -13
- package/dist/stores/file/session-policy-store.js +7 -1
- package/dist/stores/file/session-store.d.ts +22 -5
- package/dist/stores/file/session-store.js +80 -13
- package/dist/stores/file/strategy-store.d.ts +97 -0
- package/dist/stores/file/strategy-store.js +340 -0
- package/dist/tools/fs/fs-pdf.d.ts +12 -1
- package/dist/tools/fs/fs-pdf.js +17 -3
- package/dist/tools/fs/fs-read.d.ts +2 -1
- package/dist/tools/fs/fs-read.js +33 -5
- package/dist/tools/fs/fs-shared.d.ts +6 -2
- package/dist/tools/fs/index.d.ts +7 -0
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/web.js +21 -2
- package/package.json +3 -2
- 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
|
|
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
|
|
1978
|
-
if (delivered &&
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
}, () => { });
|