@sema-agent/core 5.65.0 → 7.0.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 +58 -0
- package/dist/agents/subagent.d.ts +2 -2
- package/dist/agents/subagent.js +11 -0
- package/dist/agents/verify.d.ts +1 -1
- package/dist/brain/anthropic.js +1 -1
- package/dist/brain/errors.d.ts +29 -0
- package/dist/brain/errors.js +20 -0
- package/dist/brain/open-responses.js +2 -2
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +1 -0
- package/dist/brain/status-sink.js +12 -1
- package/dist/brain/stream-engine.js +17 -6
- package/dist/core/auto-compaction.d.ts +26 -0
- package/dist/core/auto-compaction.js +7 -2
- package/dist/core/auto-mode-arming.d.ts +138 -0
- package/dist/core/auto-mode-arming.js +181 -0
- package/dist/core/auto-mode-defaults.d.ts +13 -0
- package/dist/core/auto-mode-defaults.js +5 -0
- package/dist/core/auto-mode-prompt.d.ts +14 -3
- package/dist/core/auto-mode-prompt.js +10 -7
- package/dist/core/auto-mode-rebuild.d.ts +75 -0
- package/dist/core/auto-mode-rebuild.js +41 -0
- package/dist/core/auto-mode.d.ts +15 -0
- package/dist/core/auto-mode.js +4 -2
- package/dist/core/checkpoint-store.d.ts +18 -0
- package/dist/core/context-edit.d.ts +47 -5
- package/dist/core/context-guard.d.ts +1 -1
- package/dist/core/file-history-retention.d.ts +106 -0
- package/dist/core/file-history-retention.js +36 -0
- package/dist/core/file-history-store.d.ts +768 -0
- package/dist/core/file-history-store.js +880 -0
- package/dist/core/governance-codes.d.ts +13 -3
- package/dist/core/governance-codes.js +15 -0
- package/dist/core/hooks.d.ts +39 -7
- package/dist/core/hooks.js +38 -21
- package/dist/core/lsp.d.ts +2 -2
- package/dist/core/mcp.d.ts +29 -7
- package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
- package/dist/core/memory-engine/consolidation-driver.js +71 -4
- package/dist/core/memory-engine/consolidation.d.ts +25 -2
- package/dist/core/memory-engine/consolidation.js +4 -1
- package/dist/core/memory-engine/distiller.d.ts +84 -1
- package/dist/core/memory-engine/distiller.js +68 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +328 -15
- package/dist/core/memory-engine/engine.js +355 -29
- package/dist/core/memory-engine/file-backend.d.ts +30 -0
- package/dist/core/memory-engine/file-backend.js +14 -13
- package/dist/core/memory-engine/frontmatter.d.ts +22 -1
- package/dist/core/memory-engine/frontmatter.js +3 -0
- package/dist/core/memory-engine/header-hints.d.ts +5 -0
- package/dist/core/memory-engine/index.d.ts +5 -4
- package/dist/core/memory-engine/index.js +5 -4
- package/dist/core/memory-engine/layout.d.ts +88 -2
- package/dist/core/memory-engine/layout.js +112 -3
- package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
- package/dist/core/memory-engine/provenance-wording.js +3 -0
- package/dist/core/memory-engine/tools.d.ts +89 -8
- package/dist/core/memory-engine/tools.js +263 -22
- package/dist/core/memory-engine/types.d.ts +64 -1
- package/dist/core/memory-recall.d.ts +6 -0
- package/dist/core/memory.d.ts +27 -1
- package/dist/core/memory.js +16 -2
- package/dist/core/permission-rule-consent.d.ts +62 -2
- package/dist/core/permission-rule-consent.js +105 -14
- package/dist/core/permission-rule-model.d.ts +118 -16
- package/dist/core/permission-rule-model.js +57 -9
- package/dist/core/permission-rule-session.d.ts +124 -0
- package/dist/core/permission-rule-session.js +121 -0
- package/dist/core/permission-rule-store.d.ts +65 -2
- package/dist/core/permission-rule-store.js +75 -16
- package/dist/core/permission-rule-sync.d.ts +9 -0
- package/dist/core/permission-rule-sync.js +52 -19
- package/dist/core/retention-policy.d.ts +9 -0
- package/dist/core/retention-policy.js +5 -2
- package/dist/core/retention.d.ts +13 -2
- package/dist/core/runner/assemble-result.d.ts +19 -1
- package/dist/core/runner/assemble-result.js +17 -2
- package/dist/core/runner/compaction-call-options.d.ts +93 -0
- package/dist/core/runner/compaction-call-options.js +3 -0
- package/dist/core/runner/memory-capture-optout.d.ts +80 -0
- package/dist/core/runner/memory-capture-optout.js +53 -0
- package/dist/core/runner/prepare-config-doors.d.ts +5 -0
- package/dist/core/runner/prepare-config-doors.js +16 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
- package/dist/core/runner/prepare-hands-readface.js +99 -7
- package/dist/core/runner/prepare-memory.d.ts +88 -0
- package/dist/core/runner/prepare-memory.js +305 -24
- package/dist/core/runner/prepare-task.d.ts +141 -1
- package/dist/core/runner/prepare-task.js +478 -81
- package/dist/core/runner/runtask.d.ts +9 -20
- package/dist/core/runner/runtask.js +133 -96
- package/dist/core/runner/session-file-state-replay.d.ts +18 -10
- package/dist/core/runner/session-file-state-replay.js +52 -1
- package/dist/core/runner/tool-disclosure.js +2 -1
- package/dist/core/runner/turn-attachments.d.ts +22 -12
- package/dist/core/session-store.d.ts +1 -1
- package/dist/core/session-store.js +6 -1
- package/dist/core/session.d.ts +34 -1
- package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-history-store-contract.js +720 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
- package/dist/core/task-registry-shared.js +11 -1
- package/dist/core/tool-errors.js +1 -0
- package/dist/core/tool-policy.d.ts +172 -1
- package/dist/core/tool-policy.js +32 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +31 -2
- package/dist/core/types.d.ts +795 -89
- package/dist/core/types.js +4 -3
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/untrusted-text.js +8 -0
- package/dist/core/workflow-run-store-contract.js +6 -2
- package/dist/core/workflow-run-store.d.ts +4 -1
- package/dist/engine/compaction/compaction.d.ts +88 -10
- package/dist/engine/compaction/compaction.js +109 -30
- package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
- package/dist/engine/execution-env/node-execution-env.js +28 -0
- package/dist/engine/harness/agent-harness.d.ts +52 -1
- package/dist/engine/harness/agent-harness.js +36 -1
- package/dist/engine/harness/types.d.ts +26 -1
- package/dist/engine/llm/types.d.ts +50 -4
- package/dist/engine/loop/agent-loop.d.ts +5 -1
- package/dist/engine/loop/agent-loop.js +25 -0
- package/dist/engine/loop/types.d.ts +19 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
- package/dist/engine/session/session.js +1 -1
- package/dist/index.d.ts +20 -9
- package/dist/index.js +16 -7
- package/dist/orchestration/run-workflow-tool.d.ts +20 -2
- package/dist/orchestration/run-workflow-tool.js +22 -3
- package/dist/orchestration/workflow-governance.d.ts +59 -1
- package/dist/orchestration/workflow-governance.js +61 -8
- package/dist/orchestration/workflow-meta.d.ts +4 -2
- package/dist/orchestration/workflow-primitives.js +56 -13
- package/dist/orchestration/workflow-types.d.ts +78 -2
- package/dist/orchestration/workflow.d.ts +20 -0
- package/dist/orchestration/workflow.js +163 -14
- package/dist/prompt-assembly/event-registry.js +1 -1
- package/dist/prompts/default.d.ts +7 -7
- package/dist/stores/file/file-history-store.d.ts +368 -0
- package/dist/stores/file/file-history-store.js +1248 -0
- package/dist/stores/file/index.d.ts +22 -13
- package/dist/stores/file/index.js +4 -4
- package/dist/stores/file/permission-rule-store.d.ts +11 -0
- package/dist/stores/file/permission-rule-store.js +23 -9
- package/dist/stores/file/strategy-store.d.ts +3 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
- package/dist/tools/fs/bash-readonly-classifier.js +106 -4
- package/dist/tools/fs/fs-bash.js +9 -5
- package/dist/tools/fs/fs-shared.d.ts +52 -1
- package/dist/tools/fs/fs-shared.js +14 -0
- package/dist/tools/fs/fs-write.d.ts +5 -5
- package/dist/tools/fs/fs-write.js +71 -14
- package/dist/tools/fs/index.d.ts +6 -1
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/web.js +2 -1
- package/package.json +5 -1
- package/test/export-surface.snapshot.json +169 -23
- package/dist/core/file-snapshot-store.d.ts +0 -165
- package/dist/core/file-snapshot-store.js +0 -259
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
- package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
- package/dist/stores/file/file-snapshot-store.d.ts +0 -58
- package/dist/stores/file/file-snapshot-store.js +0 -353
|
@@ -11,14 +11,18 @@ function refuseLocalOwnerSync() {
|
|
|
11
11
|
const SYNC_MAX_ATTEMPTS = 8;
|
|
12
12
|
const DISCLOSED_REFUSED_TOMBSTONES = 5;
|
|
13
13
|
export async function syncPermissionRules(opts) {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const principal = opts.principal;
|
|
15
|
+
const owner = opts.owner;
|
|
16
|
+
const provider = opts.provider;
|
|
17
|
+
const transport = opts.transport;
|
|
18
|
+
if (owner !== undefined) {
|
|
19
|
+
if (owner.kind === "local-owner")
|
|
16
20
|
refuseLocalOwnerSync();
|
|
17
|
-
if (
|
|
18
|
-
throw new Error(`contradictory identity: principal "${escapeForDisclosure(
|
|
21
|
+
if (owner.principal !== principal) {
|
|
22
|
+
throw new Error(`contradictory identity: principal "${escapeForDisclosure(principal)}" and owner principal "${escapeForDisclosure(owner.principal)}" disagree`);
|
|
19
23
|
}
|
|
20
24
|
}
|
|
21
|
-
if (typeof
|
|
25
|
+
if (typeof principal !== "string" || principal === "") {
|
|
22
26
|
throw new Error("syncPermissionRules requires a verified principal — an unauthenticated deployment has no cloud bucket to sync");
|
|
23
27
|
}
|
|
24
28
|
const traceClock = opts.now ?? Date.now;
|
|
@@ -30,7 +34,7 @@ export async function syncPermissionRules(opts) {
|
|
|
30
34
|
emitTrace(tracer, () => ({
|
|
31
35
|
kind: "permission.rule_sync_resurrected",
|
|
32
36
|
version: 1,
|
|
33
|
-
principal
|
|
37
|
+
principal,
|
|
34
38
|
rule: r.rule,
|
|
35
39
|
scopeKind: r.scope.kind,
|
|
36
40
|
ts: traceClock(),
|
|
@@ -40,7 +44,7 @@ export async function syncPermissionRules(opts) {
|
|
|
40
44
|
emitTrace(tracer, () => ({
|
|
41
45
|
kind: "permission.rule_sync_dropped",
|
|
42
46
|
version: 1,
|
|
43
|
-
principal
|
|
47
|
+
principal,
|
|
44
48
|
rule: d.rule,
|
|
45
49
|
scopeKind: d.scope.kind,
|
|
46
50
|
reason: d.reason,
|
|
@@ -49,20 +53,20 @@ export async function syncPermissionRules(opts) {
|
|
|
49
53
|
}
|
|
50
54
|
return result;
|
|
51
55
|
};
|
|
52
|
-
const store =
|
|
56
|
+
const store = provider.forPrincipal(principal);
|
|
53
57
|
const writer = writerOf(store);
|
|
54
58
|
if (writer === undefined) {
|
|
55
59
|
throw new Error("the resolved permission-rule store has no write face — a sync round cannot land through it");
|
|
56
60
|
}
|
|
57
61
|
const raw = await writer.readRaw();
|
|
58
62
|
const request = {
|
|
59
|
-
principal
|
|
63
|
+
principal,
|
|
60
64
|
replica: raw.actor,
|
|
61
65
|
state: { rules: raw.rules, tombstones: raw.tombstones },
|
|
62
66
|
...(raw.observedVector !== undefined ? { observedVector: raw.observedVector } : {}),
|
|
63
67
|
};
|
|
64
|
-
const rawResponse = await
|
|
65
|
-
const response = parseRuleSyncResponse(rawResponse,
|
|
68
|
+
const rawResponse = await transport(PERMISSION_RULE_SYNC_PATH, request);
|
|
69
|
+
const response = parseRuleSyncResponse(rawResponse, principal);
|
|
66
70
|
const warnings = [...response.warnings];
|
|
67
71
|
const dropped = [];
|
|
68
72
|
const inboundTombstones = [];
|
|
@@ -87,6 +91,9 @@ export async function syncPermissionRules(opts) {
|
|
|
87
91
|
for (const t of response.textRefusedTombstones) {
|
|
88
92
|
dropped.push({ rule: t.rule, scope: t.scope, dot: t.dot, reason: "invalid_rule_text" });
|
|
89
93
|
}
|
|
94
|
+
for (const t of response.sessionScopeTombstones) {
|
|
95
|
+
dropped.push({ rule: t.rule, scope: t.scope, dot: t.dot, reason: "session_scope_not_durable" });
|
|
96
|
+
}
|
|
90
97
|
if (response.textRefusedTombstones.length > 0) {
|
|
91
98
|
const named = response.textRefusedTombstones
|
|
92
99
|
.slice(0, DISCLOSED_REFUSED_TOMBSTONES)
|
|
@@ -127,6 +134,10 @@ export async function syncPermissionRules(opts) {
|
|
|
127
134
|
}
|
|
128
135
|
const localDots = new Set(raw.rules.flatMap((r) => r.adds.map((a) => JSON.stringify([a.dot.actor, a.dot.counter]))));
|
|
129
136
|
for (const d of response.dropped) {
|
|
137
|
+
if (d.scope.kind === "session") {
|
|
138
|
+
warnings.push(`the server's dropped list names a session-scope row (${escapeForDisclosure(d.rule)}) — the durable store never holds one, so there is no local row it could mean; ignored`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
130
141
|
if (localDots.has(JSON.stringify([d.dot.actor, d.dot.counter]))) {
|
|
131
142
|
const reason = d.reason === "below_gc_frontier" ? "below_gc_frontier" : "server_rejected";
|
|
132
143
|
quarantine.push({ rule: d.rule, scope: d.scope, dots: [d.dot], reason });
|
|
@@ -186,7 +197,7 @@ export async function syncPermissionRules(opts) {
|
|
|
186
197
|
const dotKeyOf = (d) => JSON.stringify([d.actor, d.counter]);
|
|
187
198
|
const preDots = new Set(current.rules.flatMap((r) => r.adds.map((a) => dotKeyOf(a.dot))));
|
|
188
199
|
const newAdds = landedRaw.rules.flatMap((r) => r.adds).filter((a) => !preDots.has(dotKeyOf(a.dot))).length;
|
|
189
|
-
const tombKeyOf = (t) => JSON.stringify([t.rule, t.scope.kind === "
|
|
200
|
+
const tombKeyOf = (t) => JSON.stringify([t.rule, t.scope.kind, t.scope.kind === "project" ? t.scope.root : t.scope.kind === "session" ? t.scope.sessionId : null, dotKeyOf(t.deletedBy)]);
|
|
190
201
|
const preTombs = new Set(current.tombstones.map(tombKeyOf));
|
|
191
202
|
const newTombstones = landedRaw.tombstones.filter((t) => !preTombs.has(tombKeyOf(t))).length;
|
|
192
203
|
const landedReport = res.sync;
|
|
@@ -224,13 +235,23 @@ function pickDot(v) {
|
|
|
224
235
|
const counter = d?.counter;
|
|
225
236
|
return typeof actor === "string" && actor !== "" && typeof counter === "number" && Number.isFinite(counter) ? { actor, counter } : undefined;
|
|
226
237
|
}
|
|
227
|
-
function
|
|
238
|
+
function pickScopeCaptured(v) {
|
|
228
239
|
const s = v;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
240
|
+
const kind = s?.kind;
|
|
241
|
+
if (kind === "global")
|
|
242
|
+
return { kind, scope: { kind: "global" } };
|
|
243
|
+
if (kind === "project") {
|
|
244
|
+
const root = s?.root;
|
|
245
|
+
return { kind, scope: typeof root === "string" && root !== "" ? { kind: "project", root } : undefined };
|
|
246
|
+
}
|
|
247
|
+
if (kind === "session") {
|
|
248
|
+
const sessionId = s?.sessionId;
|
|
249
|
+
return { kind, scope: typeof sessionId === "string" && sessionId !== "" ? { kind: "session", sessionId } : undefined };
|
|
250
|
+
}
|
|
251
|
+
return { kind, scope: undefined };
|
|
252
|
+
}
|
|
253
|
+
function pickScope(v) {
|
|
254
|
+
return pickScopeCaptured(v).scope;
|
|
234
255
|
}
|
|
235
256
|
function pickAdd(v) {
|
|
236
257
|
const a = v;
|
|
@@ -321,9 +342,11 @@ export function parseRuleSyncResponse(raw, expectedPrincipal) {
|
|
|
321
342
|
}
|
|
322
343
|
const tombstones = [];
|
|
323
344
|
const textRefusedTombstones = [];
|
|
345
|
+
const sessionScopeTombstones = [];
|
|
324
346
|
for (const entry of mergedTombstones) {
|
|
325
347
|
const t = entry;
|
|
326
|
-
const
|
|
348
|
+
const pickedScope = pickScopeCaptured(t?.scope);
|
|
349
|
+
const scope = pickedScope.scope;
|
|
327
350
|
const ruleText = t?.rule;
|
|
328
351
|
const deletedBy = pickDot(t?.deletedBy);
|
|
329
352
|
const removedRaw = t?.removedDots;
|
|
@@ -338,6 +361,15 @@ export function parseRuleSyncResponse(raw, expectedPrincipal) {
|
|
|
338
361
|
removedDots.push(dot);
|
|
339
362
|
}
|
|
340
363
|
}
|
|
364
|
+
if (pickedScope.kind === "session") {
|
|
365
|
+
if (typeof ruleText === "string" && deletedBy !== undefined && scope !== undefined) {
|
|
366
|
+
sessionScopeTombstones.push({ rule: ruleText, scope, dot: deletedBy });
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
warnings.push("the response carried a session-scope tombstone whose identity could not be read — dropped per-row (a session row never lands here, and nothing is withheld for it)");
|
|
370
|
+
}
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
341
373
|
if (typeof ruleText !== "string" || scope === undefined || deletedBy === undefined || removedDots.length === 0) {
|
|
342
374
|
tombstones.push(undefined);
|
|
343
375
|
continue;
|
|
@@ -380,6 +412,7 @@ export function parseRuleSyncResponse(raw, expectedPrincipal) {
|
|
|
380
412
|
...(gcFrontier !== undefined ? { gcFrontier } : {}),
|
|
381
413
|
dropped,
|
|
382
414
|
textRefusedTombstones,
|
|
415
|
+
sessionScopeTombstones,
|
|
383
416
|
warnings,
|
|
384
417
|
};
|
|
385
418
|
}
|
|
@@ -31,6 +31,15 @@ export interface RetentionPolicyKnobs {
|
|
|
31
31
|
/** Row-count bound, whole rows (roster's write-time cap). */
|
|
32
32
|
maxEntries?: number;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The family's typed refusal, exported so a seam whose bound has its own ADMISSIBLE RANGE refuses in
|
|
36
|
+
* the same grammar under the same code instead of minting a second one. The one in-tree case:
|
|
37
|
+
* file-history's boundary cap requires `>= 1` where the row stores accept `0` ("keep no rows" is a
|
|
38
|
+
* coherent policy for a ledger; for a history whose every commit mints a boundary it is not — the
|
|
39
|
+
* cap would reap the boundary its own commit just published), so that door cannot reuse
|
|
40
|
+
* {@link assertRetentionPolicy}'s "0 or more" wording without telling an operator something false.
|
|
41
|
+
*/
|
|
42
|
+
export declare function refuseRetentionPolicy(label: string, knob: string, value: unknown, requirement: string): never;
|
|
34
43
|
/**
|
|
35
44
|
* Refuse a malformed retention policy at the door. `undefined` on any field keeps its documented
|
|
36
45
|
* meaning — that arm is simply not applied — so a deployment that sets nothing is untouched.
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
function
|
|
2
|
-
const e = new Error(`${label}: ${knob} must be ${requirement} (got ${String(value)}) — a retention bound that cannot be evaluated silently decides what to delete instead of bounding it`);
|
|
1
|
+
export function refuseRetentionPolicy(label, knob, value, requirement) {
|
|
2
|
+
const e = new Error(`${label}: ${knob} must be ${requirement} (got ${typeof value === "string" ? JSON.stringify(value) : String(value)}) — a retention bound that cannot be evaluated silently decides what to delete instead of bounding it`);
|
|
3
3
|
e.code = "config.retention_policy_invalid";
|
|
4
4
|
throw e;
|
|
5
5
|
}
|
|
6
|
+
function invalidPolicy(label, knob, value, requirement) {
|
|
7
|
+
refuseRetentionPolicy(label, knob, value, requirement);
|
|
8
|
+
}
|
|
6
9
|
export function assertRetentionPolicy(label, opts) {
|
|
7
10
|
if (opts === undefined)
|
|
8
11
|
return;
|
package/dist/core/retention.d.ts
CHANGED
|
@@ -27,7 +27,12 @@ export interface RetentionPolicy {
|
|
|
27
27
|
* - **idempotent**: a retried invocation after a crash re-converges (already-deleted rows are
|
|
28
28
|
* counted, not errors);
|
|
29
29
|
* - **tombstoned**: deletions leave tombstones so replicas/backups converge instead of resurrecting;
|
|
30
|
-
* - **audited**: every destructive call
|
|
30
|
+
* - **audited**: every destructive call is recorded append-only. The IDENTITY of that record —
|
|
31
|
+
* which occurrence, which of the three operations, at what wall clock — belongs to the side that
|
|
32
|
+
* WRITES the row, not to the receipt: an implementation records it in the same transaction as the
|
|
33
|
+
* deletion (recording after the delete would leave an unwitnessed delete on a crash), and it is
|
|
34
|
+
* the only side that knows its own op, sweep round, and policy generation. The receipt below is
|
|
35
|
+
* therefore the CALL's three counts, deliberately not an audit row.
|
|
31
36
|
*/
|
|
32
37
|
export interface ManagedRetentionCapability {
|
|
33
38
|
/** Enumerate the retention domains (tenant/scope keys) this store holds data for. */
|
|
@@ -54,7 +59,13 @@ export interface ManagedRetentionCapability {
|
|
|
54
59
|
cutoffMs: number;
|
|
55
60
|
}): Promise<RetentionReceipt>;
|
|
56
61
|
}
|
|
57
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* The three counts one destructive retention call produced. NOT an audit row and deliberately
|
|
64
|
+
* identity-free (see the capability's `audited` clause): a scheduler that needs to tell two
|
|
65
|
+
* same-shaped calls apart reads its store's audit records, which the implementation writes with its
|
|
66
|
+
* own occurrence id, operation, and timestamp inside the deleting transaction. Adding those here
|
|
67
|
+
* would ask a return value to know its own call context.
|
|
68
|
+
*/
|
|
58
69
|
export interface RetentionReceipt {
|
|
59
70
|
domain: string;
|
|
60
71
|
/** Rows destroyed by THIS call (idempotent retries count already-gone rows as 0, not errors). */
|
|
@@ -100,6 +100,11 @@ export type BudgetAxis = "tokens" | "cost";
|
|
|
100
100
|
*/
|
|
101
101
|
export interface ResultFlags {
|
|
102
102
|
threw: unknown;
|
|
103
|
+
/** #499 — the run's ENGINE-minted identity (`Prepared.runId`), echoed verbatim on
|
|
104
|
+
* `TaskResult.runId`. Pure pass-through on every terminal: assembly neither mints nor derives it
|
|
105
|
+
* (a caller reaching assembly at all has completed prepare, so the value exists), and a
|
|
106
|
+
* hand-constructed flags bag without one simply produces a result without one. */
|
|
107
|
+
runId?: string;
|
|
103
108
|
/** design/99 MF-25: the EFFECTIVE (resolved) model id that served the task — echoed on `TaskResult.model`. */
|
|
104
109
|
model?: string;
|
|
105
110
|
/** RB-368: some spend was accounted while the serving model had NO configured price table
|
|
@@ -118,6 +123,15 @@ export interface ResultFlags {
|
|
|
118
123
|
* path the terminal is `completed`, and this is what tells that completion apart from a natural
|
|
119
124
|
* one — the model did not finish; the person stopped it and the run awaits their direction). */
|
|
120
125
|
haltedOnUserRejection?: boolean;
|
|
126
|
+
/** design/373 (#504) — a `TaskStream.halt` (the bare user interrupt, CC Esc form) was accepted
|
|
127
|
+
* while the run was live; echoed on `TaskResult.haltedByUser`. Pure pass-through on every
|
|
128
|
+
* terminal (the sibling `haltedOnUserRejection` law: the fact is about the leg that ran) PLUS
|
|
129
|
+
* one status decision it owns (slot 9.7 below): a run whose ONLY stop was this halt reads
|
|
130
|
+
* `"completed"`, never the `!final` failure and never the aborted terminal — whether the halt
|
|
131
|
+
* landed before the model said anything (empty result) or cut a turn whose partial text the
|
|
132
|
+
* brain kept (that text IS the result). The person stopped the run, which is the verb working,
|
|
133
|
+
* not a defect. A REAL abort/limit that also fired still owns the terminal above it. */
|
|
134
|
+
userHalted?: boolean;
|
|
121
135
|
/** design/174 final-round: call ids of answered-but-never-collected questions, echoed on
|
|
122
136
|
* `TaskResult.strandedHumanAnswers`. Pure pass-through; empty/absent ⇒ the field is omitted. The
|
|
123
137
|
* optional `onError` alert is NOT the disclosure — this mandatory result face is. */
|
|
@@ -240,7 +254,11 @@ export declare function errorCodeOf(err: unknown): string | undefined;
|
|
|
240
254
|
* discriminant guarantees only one is ever set)
|
|
241
255
|
* 9. `abortedLive` | stopReason aborted → `"limits.max_walltime_exceeded"` | `"limits.max_turns_exceeded"` | undefined
|
|
242
256
|
* (audit A-1: hoisted ABOVE no-final — the loop exits cleanly on abort, so `final` may be a
|
|
243
|
-
* normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`)
|
|
257
|
+
* normal message or absent; a plain user interrupt carries NO errorCode — switch on `status`),
|
|
258
|
+
* EXCEPT when the aborted `final` is the bare halt's OWN cut artifact (`haltOwnsAbortedFinal`)
|
|
259
|
+
* 9.7 `userHalted` + (no `final` | the halt's own aborted `final`) → status `"completed"` (#504: a
|
|
260
|
+
* bare user halt — the verb working, not a defect; below the aborts on purpose, a real
|
|
261
|
+
* abort/limit that also fired owns the terminal and the halt rides as the pass-through seat)
|
|
244
262
|
* 10. no `final` → status `"failed"`
|
|
245
263
|
* 11. stopReason error → brain `[code]` prefix (or `"conflict"` from the storage-layer flag)
|
|
246
264
|
* 12. else → `"completed"`
|
|
@@ -57,12 +57,18 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
57
57
|
const result = text;
|
|
58
58
|
let errorMessage;
|
|
59
59
|
let errorCode;
|
|
60
|
+
let apiFailure;
|
|
60
61
|
let blockedReason;
|
|
61
62
|
let salvagedOutput;
|
|
62
63
|
let checkpointToken;
|
|
63
64
|
let checkpointId;
|
|
64
65
|
let checkpointGate;
|
|
65
66
|
let workspaceRestoreMode;
|
|
67
|
+
const haltOwnsAbortedFinal = flags.userHalted === true &&
|
|
68
|
+
final?.stopReason === "aborted" &&
|
|
69
|
+
!flags.abortedLive &&
|
|
70
|
+
flags.abortedForTimeout !== true &&
|
|
71
|
+
flags.abortedForTurns !== true;
|
|
66
72
|
const isDegenerate = final?.stopReason === "error" && isDegenerateCutMessage(final);
|
|
67
73
|
if (flags.outputInvalid) {
|
|
68
74
|
status = "failed";
|
|
@@ -127,11 +133,14 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
127
133
|
checkpointGate = flags.reviewRef.gate;
|
|
128
134
|
workspaceRestoreMode = flags.reviewRef.restoreMode;
|
|
129
135
|
}
|
|
130
|
-
else if (flags.abortedLive || final?.stopReason === "aborted") {
|
|
136
|
+
else if ((flags.abortedLive || final?.stopReason === "aborted") && !haltOwnsAbortedFinal) {
|
|
131
137
|
status = "failed";
|
|
132
138
|
errorCode = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
|
|
133
139
|
errorMessage = final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted");
|
|
134
140
|
}
|
|
141
|
+
else if (flags.userHalted === true && (!final || haltOwnsAbortedFinal)) {
|
|
142
|
+
status = "completed";
|
|
143
|
+
}
|
|
135
144
|
else if (!final) {
|
|
136
145
|
status = "failed";
|
|
137
146
|
errorMessage = "no assistant message produced";
|
|
@@ -139,6 +148,12 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
139
148
|
else if (final.stopReason === "error") {
|
|
140
149
|
status = "failed";
|
|
141
150
|
errorMessage = final.errorMessage ?? "model error";
|
|
151
|
+
if (final.isApiErrorMessage === true) {
|
|
152
|
+
apiFailure = {
|
|
153
|
+
...(final.apiErrorStatus !== undefined ? { status: final.apiErrorStatus } : {}),
|
|
154
|
+
...(final.requestId !== undefined ? { requestId: final.requestId } : {}),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
142
157
|
const lifted = extractErrorCode(errorMessage);
|
|
143
158
|
if (lifted) {
|
|
144
159
|
errorCode = lifted;
|
|
@@ -165,5 +180,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
165
180
|
void _internalCompaction;
|
|
166
181
|
if (flags.unpricedSpend)
|
|
167
182
|
delete publicStats.costMicroUsd;
|
|
168
|
-
return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
183
|
+
return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(flags.userHalted === true ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
|
|
169
184
|
}
|
|
@@ -29,6 +29,99 @@ import type { Prepared } from "./prepare-task.js";
|
|
|
29
29
|
* task has no hands (no `readTaskFile` ⇒ nothing can be re-read).
|
|
30
30
|
*/
|
|
31
31
|
export declare function buildWorkingFileAttachments(spec: TaskSpec, prepared: Prepared): MaybeCompactOptions["workingFileAttachments"];
|
|
32
|
+
/**
|
|
33
|
+
* #500 — `MaybeCompactOptions.contextInstructionFiles`: the seat that delivers the deployment's own
|
|
34
|
+
* instruction-file content (the CLAUDE.md family) to the SUMMARIZER, assembled once here and spread
|
|
35
|
+
* on all three lanes. Without it the compaction boundary is the one place a long task loses the
|
|
36
|
+
* standing instructions every other turn has been reading — and it loses them exactly where the
|
|
37
|
+
* summary REPLACES the transcript that carried them, so the loss is durable.
|
|
38
|
+
*
|
|
39
|
+
* WHICH BYTES, and why the neighbours are not candidates. `Prepared.projectInstructionContent` =
|
|
40
|
+
* the RAW loader text (`ProjectMemoryLoad.content`, exactly the string prepare-task feeds
|
|
41
|
+
* `composeMemoryBlock(projectMem, "project")`), captured beside the existing `instructionSources`
|
|
42
|
+
* capture under the SAME non-blank predicate that composes the project layer:
|
|
43
|
+
* · the assembled `memoryBlock` is NOT a candidate. It is the memory ENGINE's composed layers —
|
|
44
|
+
* bytes the model itself authored in-band this session through the memory tools — with the
|
|
45
|
+
* project layer appended at the tail. The seat's contract is host/systemPrompt tier and says in
|
|
46
|
+
* terms: never from tool output, model text, or any other in-band channel. Handing
|
|
47
|
+
* model-authored bytes an envelope whose preamble tells the summarizer they may instruct it is
|
|
48
|
+
* the laundering shape, not a purity nit. (Mechanically moot too — `memoryBlock` is a
|
|
49
|
+
* prepare-task local and never reaches `Prepared`.)
|
|
50
|
+
* · the COMPOSED project block is not it either, and this one is checkable rather than a
|
|
51
|
+
* judgement: `contextInstructionFilesSection` neutralizes the whole authority family
|
|
52
|
+
* (ENGINE_AUTHORITY_ENVELOPE_TAGS) over its payload, and `user_memory`/`scope` ARE in that
|
|
53
|
+
* family — so feeding a composed block would have the engine rewrite the tags of its own
|
|
54
|
+
* composition. Raw text in, one fence at the mint.
|
|
55
|
+
* · `prepared.instructionSources` carries path + contentHash only, by design (core never learns a
|
|
56
|
+
* file-name policy), so it cannot serve this seat at all.
|
|
57
|
+
*
|
|
58
|
+
* PROVENANCE, stated plainly because "the host supplies it" is the easy half of the truth. On this
|
|
59
|
+
* engine the value is REPOSITORY-CONTROLLED text: the loading block one file over calls a CLAUDE.md
|
|
60
|
+
* of unknown authorship untrusted in so many words, the MAIN lane's own framing tells the model it is
|
|
61
|
+
* "repository-controlled DATA, not instructions to obey", and this seat hands the same bytes to the
|
|
62
|
+
* summarizer with instruction standing for the summarization task. Delivery THROUGH a host callback
|
|
63
|
+
* is not host AUTHORSHIP: a prior agent, or anyone who can land a commit, can write a
|
|
64
|
+
* "## Compact Instructions" section, and the mint's neutralization stops tag breakout — not a
|
|
65
|
+
* sentence asking for a fact to be left out of the summary that replaces the transcript.
|
|
66
|
+
* Why it is nevertheless this value: the seat's own contract names this path ("hosts pass the same
|
|
67
|
+
* instruction-file content their prompt assembly mounts... on the runner path"), the parity form this
|
|
68
|
+
* channel exists to reach reads the user's and the project's instruction files at every boundary with
|
|
69
|
+
* exactly this standing, and the two alternatives are worse for reasons above rather than better.
|
|
70
|
+
* The standing is also narrower than the framing gap suggests — the section's preamble scopes it to
|
|
71
|
+
* guidance about HOW to summarize and classifies the rest as background — and a deployment that does
|
|
72
|
+
* not want it has two exits that need no code: do not wire the loader, or return content it vouches
|
|
73
|
+
* for. Recorded as an open design candidate rather than built here, because it is a contract change
|
|
74
|
+
* and not a wiring one: a SEPARATE deployment-vouched summarization-instructions seat, leaving
|
|
75
|
+
* repository-derived context at data tier.
|
|
76
|
+
*
|
|
77
|
+
* RULING ① — BOTH summary forms, no per-form split. Fork eligibility is decided per pass INSIDE
|
|
78
|
+
* maybeCompact (main-model-only, recorded-request-present, not lossy), so a call site cannot say
|
|
79
|
+
* "independent form only"; the closest approximation — pass the seat only when `prepared.compModel`
|
|
80
|
+
* is set, since an independent compaction model has no main prefix to fork — leaves the hole the
|
|
81
|
+
* wrong way round: the configuration it does NOT cover (no compaction model) is precisely the one
|
|
82
|
+
* where a fork degrades mid-pass to the independent form, which would then run WITHOUT the files.
|
|
83
|
+
* Feeding both forms closes that case by construction, and the library face already handles the fork
|
|
84
|
+
* half (the section rides the APPENDED instruction message, never the cached prefix, so the fork's
|
|
85
|
+
* whole economic point is untouched).
|
|
86
|
+
*
|
|
87
|
+
* RULING ② — no new size knob. The bound is the library's own `fitContextInstructionFilesSection`
|
|
88
|
+
* (independent form: cap against the compaction model's window, disclosed elision, and a complete
|
|
89
|
+
* yield when not even the framing fits). Its three KNOWN LIMITS are accepted here KNOWINGLY, as the
|
|
90
|
+
* pre-condition of this wiring rather than something it discovered: a token-dense payload (CJK,
|
|
91
|
+
* emoji) can pass a chars-domain fit and still overflow the gateway's tokenizer; an unknown
|
|
92
|
+
* `contextWindow` leaves the section unbounded (the conversation clamp declines to guess there too);
|
|
93
|
+
* and the FORK form cannot fit at all — its bound is a prompt-too-long that degrades ONCE to the
|
|
94
|
+
* independent form, which the engine's own note records as real but NOT total (an oversize rejection
|
|
95
|
+
* the classifier cannot read as prompt-too-long returns terminally instead).
|
|
96
|
+
*
|
|
97
|
+
* HOW BIG THAT ACCEPTANCE ACTUALLY IS — measured, because the sentence above is easy to read more
|
|
98
|
+
* comfortably than it deserves. The fitted form is NOT the default path: when a fork is eligible and
|
|
99
|
+
* the model answers in its envelope, the fork IS the pass, and its section is delivered WHOLE at any
|
|
100
|
+
* size. The independent form — the only one that fits — runs when there is no fork to take or the
|
|
101
|
+
* fork's answer did not conform. Both facts are pinned in the REF-A2 suite (a 55 KB payload: whole on
|
|
102
|
+
* the fork, elided on the independent fallback). And when a limit does bite, the failure is not a
|
|
103
|
+
* quiet degradation: the section is a FIXED part of the request, the reactive retry loop may drop
|
|
104
|
+
* only CONVERSATION groups, so a request the SECTION overflowed cannot converge and the boundary
|
|
105
|
+
* fails — the task loses that compaction (or that prompt-too-long recovery, or its end-of-task
|
|
106
|
+
* summary) rather than sending a shorter one. The exposure is what the wiring widened: this was
|
|
107
|
+
* previously reachable only by hosts calling the library directly, and is now reachable by every
|
|
108
|
+
* deployment that wires `loadProjectMemory` with a large instruction file and an unknown or small
|
|
109
|
+
* window. Making the section participate in the retry loop is the structural answer; it is a change
|
|
110
|
+
* to that loop's contract, tracked at the library face, and a runner-side knob would only move the
|
|
111
|
+
* decision to a place with less information.
|
|
112
|
+
*
|
|
113
|
+
* WHAT IT COSTS, measured rather than rounded: every deployment that wires `loadProjectMemory` now
|
|
114
|
+
* sends its instruction-file content plus EXACTLY 716 chars of framing (the preamble and the fence)
|
|
115
|
+
* on EVERY summarization request, both forms, once per pass — where a real project instruction file
|
|
116
|
+
* in this family runs to tens of KB, so the payload, not the framing, is the bill. The number is
|
|
117
|
+
* pinned in the REF-A2 suite against the engine's own renderer, so a preamble edit shows up as a
|
|
118
|
+
* changed assertion rather than as prose drifting away from the code it describes.
|
|
119
|
+
* Deployments that never wired the loader pay nothing: the key is absent, not
|
|
120
|
+
* `undefined`, so their requests are byte-identical to the pre-wiring shape on the independent and
|
|
121
|
+
* UPDATE passes (the FORK form appends its ~860-char authority clause with or without the seat, and
|
|
122
|
+
* always did).
|
|
123
|
+
*/
|
|
124
|
+
export declare function contextInstructionFilesOption(prepared: Prepared): Partial<Pick<MaybeCompactOptions, "contextInstructionFiles">>;
|
|
32
125
|
/**
|
|
33
126
|
* design/169-A — the CC-form fork seam, assembled identically on all three lanes (REF-A2): the
|
|
34
127
|
* summary request forks the main lane's latest real request (recorded by prepare-task's provider
|
|
@@ -14,6 +14,9 @@ export function buildWorkingFileAttachments(spec, prepared) {
|
|
|
14
14
|
...(typeof spec.compaction?.attachWorkingFiles === "object" ? spec.compaction.attachWorkingFiles : undefined),
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
+
export function contextInstructionFilesOption(prepared) {
|
|
18
|
+
return prepared.projectInstructionContent !== undefined ? { contextInstructionFiles: prepared.projectInstructionContent } : {};
|
|
19
|
+
}
|
|
17
20
|
export function forkContextOption(prepared, disable) {
|
|
18
21
|
return disable ? {} : { forkContext: prepared.lastBrainContext };
|
|
19
22
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/383 §3 — the session capture opt-out's ENTITLEMENT ADJUDICATOR (slice 3's core): one pure
|
|
3
|
+
* function every ingress shares — the prepare-time declaration, the resume-time standing-record
|
|
4
|
+
* re-check (§3.3), and the Runner flip verb's fresh-resolve closure — so the posture table cannot
|
|
5
|
+
* fork across call sites.
|
|
6
|
+
*
|
|
7
|
+
* The separation law (§3.3) is the module's spine: THIS adjudicator answers "may the declaration /
|
|
8
|
+
* the continued run proceed"; the one-way control-plane RECORD answers "how does this session run"
|
|
9
|
+
* (record wins for capture — nothing here ever re-enables capture for a recorded session; the
|
|
10
|
+
* governed/capture-required continuation arm refuses to RUN instead, keeping both promises: zero
|
|
11
|
+
* capture and zero out-of-policy running, availability paid).
|
|
12
|
+
*
|
|
13
|
+
* The two fault polarities, and why both are deliberate (the code-side home of the §3.1 cost
|
|
14
|
+
* table):
|
|
15
|
+
* - `"open"` (and absent): a resolver fault ALLOWS the opt-out (+ named disclosure). Wrongly
|
|
16
|
+
* allowing costs one uncaptured session — recoverable. Wrongly denying captures a session whose
|
|
17
|
+
* user explicitly said no — irreversible (read, foldable into products, syncable). The
|
|
18
|
+
* fail-safe faces the irreversible side's OPPOSITE, which is why this member is the documented
|
|
19
|
+
* exception to the caps family's deny-on-outage degrade.
|
|
20
|
+
* - `"governed"`: the deployment has EXPLICITLY declared the compliance side's irreversibility
|
|
21
|
+
* (a retention-required principal acquiring an opt-out through an outage window) the heavier
|
|
22
|
+
* one, so a fault/absent verdict refuses the run — fail-closed, the declared direction.
|
|
23
|
+
* A mutation flipping the governed fault arm to the open polarity must turn the G-姿态 escape pin
|
|
24
|
+
* red (m12).
|
|
25
|
+
*/
|
|
26
|
+
/** The screened posture (prepare-config-doors folds absent → `"open"`). */
|
|
27
|
+
export type MemoryCapturePosture = "open" | "governed" | "capture-required";
|
|
28
|
+
/** The per-principal entitlement input as RESOLVED for this adjudication:
|
|
29
|
+
* - `value` — `RuntimeCaps.allowMemoryOptOut` when the resolver answered a boolean; `undefined`
|
|
30
|
+
* when absent OR when the resolver faulted (the F2 fault-transit contract: a fault must never
|
|
31
|
+
* be encoded as an explicit value);
|
|
32
|
+
* - `faulted` — the resolve THREW / timed out / answered a non-boolean (the non-boolean case is
|
|
33
|
+
* a fault by the member's own contract, never truthiness-read). Distinguished from a clean
|
|
34
|
+
* `undefined` only for DISCLOSURE under `"open"` (both allow there) — under `"governed"` both
|
|
35
|
+
* are the same refusal (verdict absent = face fault). */
|
|
36
|
+
export interface CaptureEntitlementInput {
|
|
37
|
+
value: boolean | undefined;
|
|
38
|
+
faulted: boolean;
|
|
39
|
+
}
|
|
40
|
+
export type CaptureOptOutVerdict = {
|
|
41
|
+
allowed: true;
|
|
42
|
+
/** Present ⇔ the open-posture fault arm fired: the opt-out takes effect AND the caller owes
|
|
43
|
+
* the named `onError({phase:"config"})` disclosure line (this string). */
|
|
44
|
+
faultDisclosure?: string;
|
|
45
|
+
} | {
|
|
46
|
+
allowed: false;
|
|
47
|
+
/** Always `memory.capture_optout_denied` (terminal). The caller throws it; a DECLARATION
|
|
48
|
+
* denial must leave zero record residue (§2.1 order invariant — adjudicate BEFORE marking). */
|
|
49
|
+
code: "memory.capture_optout_denied";
|
|
50
|
+
detail: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Adjudicate one capture opt-out ingress. `subject` names what is being adjudicated, for the
|
|
54
|
+
* refusal detail only: `"declaration"` (a `capture:"off"` spelling / the flip verb) or
|
|
55
|
+
* `"standing-record"` (a resumed session whose one-way record stands — §3.3: under
|
|
56
|
+
* governed/capture-required the question is whether the run may CONTINUE; the record's zero-capture
|
|
57
|
+
* promise is not on the table).
|
|
58
|
+
*/
|
|
59
|
+
export declare function adjudicateCaptureOptOut(input: {
|
|
60
|
+
posture: MemoryCapturePosture;
|
|
61
|
+
entitlement: CaptureEntitlementInput;
|
|
62
|
+
subject: "declaration" | "standing-record";
|
|
63
|
+
}): CaptureOptOutVerdict;
|
|
64
|
+
/** The typed terminal refusal builder (one spelling for every ingress). */
|
|
65
|
+
export declare function captureOptOutDeniedError(detail: string): Error & {
|
|
66
|
+
code: string;
|
|
67
|
+
};
|
|
68
|
+
/** The typed could-not-record refusal (§2.8 — never a success notice's sibling; the caller mints
|
|
69
|
+
* {@link import("../memory-engine/engine.js").memoryCaptureOptOutUnpersistedNotice} beside it).
|
|
70
|
+
*
|
|
71
|
+
* `ingress` is REQUIRED (internal builder, both call sites in prepare-memory state it): the two
|
|
72
|
+
* arms' throws settle different things and the message must say which. The declaration arm's throw
|
|
73
|
+
* is whitelisted out of prepare-memory's fail-open catch and refuses the RUN; the host flip verb's
|
|
74
|
+
* throw rejects only the verb promise — `captureOptedOut` is assigned below it — so the run
|
|
75
|
+
* continues with capture still ON. One sentence for both said "refusing to run", which was false
|
|
76
|
+
* in the direction that matters: it told the caller the session had stopped capturing when it had
|
|
77
|
+
* not. */
|
|
78
|
+
export declare function captureOptOutUnpersistedError(sessionId: string, ingress: "declaration" | "flip-verb"): Error & {
|
|
79
|
+
code: string;
|
|
80
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export function adjudicateCaptureOptOut(input) {
|
|
2
|
+
const { posture, entitlement, subject } = input;
|
|
3
|
+
const what = subject === "standing-record" ? "continuing a session whose one-way capture opt-out record stands" : "the session memory-capture opt-out declaration";
|
|
4
|
+
if (posture === "capture-required") {
|
|
5
|
+
return {
|
|
6
|
+
allowed: false,
|
|
7
|
+
code: "memory.capture_optout_denied",
|
|
8
|
+
detail: `${what} is refused: this deployment declares memoryCapturePolicy "capture-required" (forced retention, resolver-independent). ` +
|
|
9
|
+
(subject === "standing-record"
|
|
10
|
+
? `The standing record keeps its zero-capture promise — the session is refused rather than run out of policy; start a new session without the opt-out.`
|
|
11
|
+
: `Remove the memory.capture declaration and resubmit.`),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (posture === "governed") {
|
|
15
|
+
if (entitlement.value === true)
|
|
16
|
+
return { allowed: true };
|
|
17
|
+
return {
|
|
18
|
+
allowed: false,
|
|
19
|
+
code: "memory.capture_optout_denied",
|
|
20
|
+
detail: entitlement.value === false
|
|
21
|
+
? `${what} is refused: the entitlement resolver answered allowMemoryOptOut=false for this principal (governed posture — the deployment requires capture for it).`
|
|
22
|
+
: `${what} is refused: memoryCapturePolicy "governed" requires an explicit per-principal allowMemoryOptOut verdict and none was resolvable (${entitlement.faulted ? "resolver fault" : "verdict absent"}) — fail-closed by the deployment's own declaration (a retention-required principal must not acquire an opt-out through an outage window).`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (entitlement.value === false) {
|
|
26
|
+
return {
|
|
27
|
+
allowed: false,
|
|
28
|
+
code: "memory.capture_optout_denied",
|
|
29
|
+
detail: `${what} is refused: the entitlement resolver answered allowMemoryOptOut=false for this principal.`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (entitlement.faulted) {
|
|
33
|
+
return {
|
|
34
|
+
allowed: true,
|
|
35
|
+
faultDisclosure: `the runtime-caps resolve faulted while adjudicating a memory-capture opt-out; under the "open" posture the opt-out takes effect anyway ` +
|
|
36
|
+
`(privacy-side irreversibility wins the fault arm — an explicit allowMemoryOptOut=false is the only denial). Repair the resolver if denial was intended.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return { allowed: true };
|
|
40
|
+
}
|
|
41
|
+
export function captureOptOutDeniedError(detail) {
|
|
42
|
+
const e = new Error(`memory capture opt-out denied: ${detail}`);
|
|
43
|
+
e.code = "memory.capture_optout_denied";
|
|
44
|
+
return e;
|
|
45
|
+
}
|
|
46
|
+
export function captureOptOutUnpersistedError(sessionId, ingress) {
|
|
47
|
+
const e = new Error(`memory capture opt-out could not be durably recorded for session ${sessionId} (control-plane write failed) — ` +
|
|
48
|
+
(ingress === "declaration"
|
|
49
|
+
? `refusing to run under an opt-out that would silently stop holding at the next resume. Nothing ran and nothing was captured.`
|
|
50
|
+
: `the opt-out request is REJECTED and not applied: the run continues with memory capture still ON. Nothing was recorded, so an identical retry fails the same way.`));
|
|
51
|
+
e.code = "memory.capture_optout_unpersisted";
|
|
52
|
+
return e;
|
|
53
|
+
}
|
|
@@ -196,6 +196,11 @@ export interface PrepareConfigDoorsResult {
|
|
|
196
196
|
* the synchronous pre-await stretch, screened (#123), absent folded to the `"carry"` default.
|
|
197
197
|
* The memory phase constructs every engine with THIS value — never a live deps re-read. */
|
|
198
198
|
memoryProvenance: "off" | "carry";
|
|
199
|
+
/** owned — the NORMALIZED memory-capture posture (design/383 §3.1): the deps seat read ONCE,
|
|
200
|
+
* screened (#123 — a bad value refuses, never folds), absent folded to `"open"`. The capture
|
|
201
|
+
* opt-out adjudicator consumes THIS value on every leg (declaration, resume record re-check,
|
|
202
|
+
* and the flip verb's frozen closure) — never a live deps re-read. */
|
|
203
|
+
memoryCapturePolicy: "open" | "governed" | "capture-required";
|
|
199
204
|
/** owned — validated deployment governance windows (undefined = ungoverned). */
|
|
200
205
|
usageWindows: readonly UsageWindow[] | undefined;
|
|
201
206
|
/** owned, out-param cell — created EMPTY here; the brain-call wiring later installs into
|
|
@@ -225,6 +225,21 @@ export function prepareConfigDoors(input) {
|
|
|
225
225
|
throw e;
|
|
226
226
|
}
|
|
227
227
|
const memoryProvenance = memoryProvenanceRaw === "off" ? "off" : "carry";
|
|
228
|
+
const memoryCapturePolicyRaw = deps.memoryCapturePolicy;
|
|
229
|
+
if (memoryCapturePolicyRaw !== undefined && memoryCapturePolicyRaw !== "open" && memoryCapturePolicyRaw !== "governed" && memoryCapturePolicyRaw !== "capture-required") {
|
|
230
|
+
const got = typeof memoryCapturePolicyRaw === "string"
|
|
231
|
+
? JSON.stringify(memoryCapturePolicyRaw.length > 64 ? `${memoryCapturePolicyRaw.slice(0, 64)}…` : memoryCapturePolicyRaw)
|
|
232
|
+
: memoryCapturePolicyRaw === null
|
|
233
|
+
? "null"
|
|
234
|
+
: Array.isArray(memoryCapturePolicyRaw)
|
|
235
|
+
? "an array"
|
|
236
|
+
: typeof memoryCapturePolicyRaw;
|
|
237
|
+
const e = new Error(`RunnerDeps.memoryCapturePolicy must be "open", "governed" or "capture-required" when present (got ${got}) — ` +
|
|
238
|
+
`an unevaluable capture posture is refused loudly, never folded to any posture (the fault arms differ per posture, so a guess would pick a polarity nobody configured).`);
|
|
239
|
+
e.code = "config.memory_capture_policy";
|
|
240
|
+
throw e;
|
|
241
|
+
}
|
|
242
|
+
const memoryCapturePolicy = memoryCapturePolicyRaw === "governed" ? "governed" : memoryCapturePolicyRaw === "capture-required" ? "capture-required" : "open";
|
|
228
243
|
if (spec.resumeAtMode !== undefined) {
|
|
229
244
|
if (spec.resumeAt === undefined) {
|
|
230
245
|
const e = new Error(`resumeAtMode "${spec.resumeAtMode}" requires resumeAt (there is no branch target to position against)`);
|
|
@@ -420,6 +435,7 @@ export function prepareConfigDoors(input) {
|
|
|
420
435
|
modelGate,
|
|
421
436
|
memoryDelegationEvidence,
|
|
422
437
|
memoryProvenance,
|
|
438
|
+
memoryCapturePolicy,
|
|
423
439
|
usageWindows,
|
|
424
440
|
brainCallGuardrailRef,
|
|
425
441
|
brainCallGuardrailMs,
|