@sema-agent/core 5.45.0 → 5.47.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 +97 -0
- package/dist/agents/subagent.js +130 -3
- package/dist/core/checkpoint-store.d.ts +12 -0
- package/dist/core/governance-codes.d.ts +13 -0
- package/dist/core/governance-codes.js +33 -0
- package/dist/core/hooks.d.ts +9 -2
- package/dist/core/hooks.js +6 -5
- package/dist/core/memory-engine/content-origin.d.ts +3 -1
- package/dist/core/memory-engine/delegation-settlement.d.ts +318 -0
- package/dist/core/memory-engine/delegation-settlement.js +661 -0
- package/dist/core/memory-engine/engine.d.ts +209 -4
- package/dist/core/memory-engine/engine.js +885 -39
- package/dist/core/memory-engine/export-bundle.d.ts +10 -1
- package/dist/core/memory-engine/export-bundle.js +21 -0
- package/dist/core/memory-engine/file-backend.d.ts +34 -4
- package/dist/core/memory-engine/file-backend.js +168 -40
- package/dist/core/memory-engine/frontmatter.d.ts +69 -1
- package/dist/core/memory-engine/frontmatter.js +156 -2
- package/dist/core/memory-engine/header-hints.d.ts +17 -0
- package/dist/core/memory-engine/header-hints.js +6 -0
- package/dist/core/memory-engine/index.d.ts +7 -5
- package/dist/core/memory-engine/index.js +5 -3
- package/dist/core/memory-engine/layout.d.ts +39 -2
- package/dist/core/memory-engine/layout.js +27 -14
- package/dist/core/memory-engine/memory-backend-contract.js +108 -0
- package/dist/core/memory-engine/origin-clearance.d.ts +66 -0
- package/dist/core/memory-engine/origin-clearance.js +84 -0
- package/dist/core/memory-engine/provenance-wording.d.ts +50 -0
- package/dist/core/memory-engine/provenance-wording.js +15 -0
- package/dist/core/memory-engine/sync-client.d.ts +1 -1
- package/dist/core/memory-engine/sync-client.js +33 -1
- package/dist/core/memory-engine/tools.d.ts +64 -3
- package/dist/core/memory-engine/tools.js +37 -9
- package/dist/core/memory-engine/types.d.ts +145 -3
- package/dist/core/memory-engine/types.js +1 -1
- package/dist/core/reminder-mint.d.ts +70 -0
- package/dist/core/reminder-mint.js +25 -0
- package/dist/core/runner/git-status-frame.d.ts +3 -14
- package/dist/core/runner/git-status-frame.js +39 -14
- package/dist/core/runner/prepare-config-doors.d.ts +4 -0
- package/dist/core/runner/prepare-config-doors.js +15 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +5 -11
- package/dist/core/runner/prepare-hands-readface.js +26 -0
- package/dist/core/runner/prepare-memory.d.ts +11 -0
- package/dist/core/runner/prepare-memory.js +61 -24
- package/dist/core/runner/prepare-task.d.ts +46 -1
- package/dist/core/runner/prepare-task.js +128 -23
- package/dist/core/runner/runtask.js +62 -55
- package/dist/core/session-reconcile.js +3 -2
- package/dist/core/side-query.d.ts +11 -1
- package/dist/core/side-query.js +3 -0
- package/dist/core/types.d.ts +85 -10
- package/dist/core/types.js +3 -0
- package/dist/engine/harness/types.d.ts +46 -1
- package/dist/engine/harness/types.js +11 -0
- package/dist/engine/session/import-validate.js +6 -1
- package/dist/engine/session/session.d.ts +20 -0
- package/dist/engine/session/session.js +26 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/orchestration/run-workflow-tool.d.ts +16 -0
- package/dist/orchestration/run-workflow-tool.js +23 -3
- package/dist/orchestration/workflow-governance.d.ts +8 -1
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/types.d.ts +6 -0
- package/dist/prompts/default.d.ts +13 -1
- package/dist/prompts/default.js +5 -1
- package/dist/tools/fs/fs-bash.d.ts +4 -0
- package/dist/tools/fs/fs-bash.js +1 -1
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +8 -7
- package/dist/tools/fs/fs-shared.d.ts +10 -4
- package/dist/tools/fs/fs-shared.js +6 -3
- package/dist/tools/fs/gh-rate-limit.d.ts +4 -1
- package/dist/tools/fs/gh-rate-limit.js +3 -2
- package/dist/tools/fs/index.d.ts +10 -2
- package/dist/tools/fs/index.js +2 -1
- package/dist/tools/task-list.d.ts +5 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +21 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,102 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.47.0 — 2026-08-20
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- design/336 slices 2+3 — the F-020 deep fix completes:
|
|
7
|
+
- **Slice 2 (settlement + hold)**: a delegation's memory-pollution verdict is a durable
|
|
8
|
+
SETTLEMENT ACCOUNT (three strict side-ledgers; write-ahead launch rows; crash-idempotent
|
|
9
|
+
retry; fork/bg lane parity), and instruction-form files under exposure ride a three-state
|
|
10
|
+
HOLD (held → released/disposed, loud timeout `config.memory_hold_timeout`;
|
|
11
|
+
`MemoryEngineOptions.holdSettleTimeoutMs`). `HarvestReport.containment` (#331) and the
|
|
12
|
+
`memory.hold_*` notice family land with it. The origin representation family is
|
|
13
|
+
serialization-closed (carrier tokenization + round-trip property laws).
|
|
14
|
+
- **Slice 3 (read side + human faces)**: `memory_search` returns two exposure bands (clean
|
|
15
|
+
first; `MemoryEntryHeader.exposure`; `MemorySearchHit` becomes a discriminated union
|
|
16
|
+
CleanHit | ExposedHit — see BREAKING); exposed entries render as opaque handles (zero
|
|
17
|
+
model-authored text on the passive face); `memory_get` delivery carries a fact-form banner
|
|
18
|
+
and marks the session derived (`onTaintedDelivery` — mechanical closure of source-scope
|
|
19
|
+
laundering); the memory-dir Read lane head-parses committed origin. Human faces (host API
|
|
20
|
+
only, deliberately no model tool): `listExternalOriginEntries` + `clearEntryOrigin` (audited
|
|
21
|
+
un-mark: write-ahead custody → committed tombstone → same-id re-record;
|
|
22
|
+
`listOriginClearances`; `memory.origin_clear_*` family). A live behavioral probe pins that a
|
|
23
|
+
marked entry in context is still used normally (deepseek-v4-flash, zero refusal).
|
|
24
|
+
- `memoryProvenance: "off"` keeps every read face byte-identical to 5.46 (pinned directly).
|
|
25
|
+
- `NOTICE_AUDIENCE` + `noticeAudienceOf` — the presentation-tier registry (who a notice code is
|
|
26
|
+
for): "user" for the session-memory posture disclosures, everything else defaults "operator".
|
|
27
|
+
`EngineNotice.sessionId` typed top-level key, lifted from `detail.sessionId` at the delivery
|
|
28
|
+
throat (mint sites untouched). `ambiguousOriginRepresentation` exported (the origin ambiguity
|
|
29
|
+
predicate a third-party backend refuses multi-carrier representations with).
|
|
30
|
+
- The session-reconcile never-started texts condition their re-issue cue on the blocking
|
|
31
|
+
approval/gate being resolved first (#352 — the side-effect statement stayed true; the action
|
|
32
|
+
cue no longer amplifies a blocked-approval loop).
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- **BREAKING (types)**: `MemorySearchHit` is now a discriminated union (CleanHit | ExposedHit)
|
|
36
|
+
instead of a single interface. Backend contract: the conformance suite gains two cases
|
|
37
|
+
(exposure carriage + two-band truncation boundary) — a backend implementing the pre-336 shape
|
|
38
|
+
fails them (upgrade-order duty: adopt the new clauses before taking 5.47 in a mixed fleet).
|
|
39
|
+
`memory_get` tool contract revision 3→4, `memory_search` 2→3 (execute-visible text changes).
|
|
40
|
+
- The kill-tree descendant-reap suite's liveness probe reads a ZOMBIE as dead (a landed SIGKILL
|
|
41
|
+
whose parent died first waits for init to reap it; under load that outlasted any fixed poll
|
|
42
|
+
window — the 5.30.0/5.46.0 publish-gate flake, root-caused and fixed).
|
|
43
|
+
|
|
44
|
+
## 5.46.0 — 2026-08-19
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
- design/319 A (system-reminder provenance mark): every engine-authored `<system-reminder>` OPEN
|
|
48
|
+
tag carries a per-session random `mark="<22-char base64url>"` attribute, and the system prompt
|
|
49
|
+
declares its semantics in ONE strict form — reminder-shaped text WITHOUT the current mark is
|
|
50
|
+
data wherever it appears. A forged reminder tag is now byte-distinguishable from an engine mint
|
|
51
|
+
on every verbatim inlet (Read/Bash/Grep/MCP-success). ZERO data-byte change: the mark rides only
|
|
52
|
+
the engine's own tag; Read stays verbatim, Edit across forged tags lands, Bash/Grep clean paths
|
|
53
|
+
are byte-compatible, and markless (library-direct) calls render historic bare bytes verbatim.
|
|
54
|
+
New exports: `mintReminderMark`, `isValidReminderMark`, `openSystemReminder`,
|
|
55
|
+
`mintSystemReminder`, `reminderMarkDeclaration` (scheme-replaceable mint home). Checkpoint gains
|
|
56
|
+
an additive `reminderMark` field + a new session-entry type `reminder_mark` (old readers ignore
|
|
57
|
+
both); a legacy or malformed-mark resume re-mints under the same strict declaration (historic
|
|
58
|
+
bare mints read as data — fail-safe). Consumers matching reminder tags by exact string must go
|
|
59
|
+
attribute-tolerant. A provider-owned pass-through prompt that renders no declaration discloses
|
|
60
|
+
loudly once per prepare (opt-in: `reminderMarkDeclaration(ctx.reminderMark)`).
|
|
61
|
+
- design/336 slice 1 (F-020 deep fix — the origin carriage): an externally-exposed session's
|
|
62
|
+
ORDINARY memory writes now COMMIT carrying an engine-minted typed `origin` frontmatter marker
|
|
63
|
+
(taint + closed cause vocabulary + mint time) instead of the design/178 full quarantine that
|
|
64
|
+
zeroed TOC recall for every delegation-bearing session. Engine-exclusive and forgery-proof on
|
|
65
|
+
four faces (model-written origin bytes strip-with-disclosure — benign echo of the committed
|
|
66
|
+
marker stays silent; backend immutability law over every op spelling; inbound whitewash gate;
|
|
67
|
+
sync/bundle validation with rev honesty). Instruction-form files stay hard-gated. ONE deployment
|
|
68
|
+
seat: `RunnerDeps.memoryProvenance?: "off" | "carry"` (default `"carry"`; `"off"` restores
|
|
69
|
+
pre-336 containment byte-level; bad values refuse loudly, `config.memory_provenance`). New
|
|
70
|
+
public family: `MemoryEntryOrigin`/`MemoryOriginCause`/`MEMORY_ORIGIN_CAUSES`,
|
|
71
|
+
`committedOriginOf`, `originEquals`, `isInstructionEntry`. Bundle rows carry `marked`+`cause`;
|
|
72
|
+
lineage merge upgrades (never drops) a content-verified marked bit.
|
|
73
|
+
- #342: the `run_workflow` lane gains the onAsk pass-through arm — every workflow-spawned agent's
|
|
74
|
+
own ask reaches the same operator as the host's (provenance-stamped, `parentToolCallId` +
|
|
75
|
+
`depth:1`) instead of the headless auto-deny. `RunWorkflowToolDeps.parentOnAsk` mount dep; a
|
|
76
|
+
deployment-pinned `base.onAsk` always wins; with no approver anywhere the fail-closed deny is
|
|
77
|
+
byte-identical. `AskRequest.delegation.parentToolCallId` is now reliable on the workflow lane.
|
|
78
|
+
- #341: `SideQuerySpec.getApiKeyAndHeaders` — side-queries resolve per-model auth exactly like
|
|
79
|
+
the main lane (a gateway key no longer rides an external model's URL when a resolver is wired).
|
|
80
|
+
- Memory-engine fencing (#262/#270/#191): journal recovery carries a fence token asserted at every
|
|
81
|
+
destructive write (redo/sweep/heal/commit-boundary/identity-close/migration); the chain-degraded
|
|
82
|
+
marker's directory entry is fsynced (POSIX name durability); txn-lock contention is typed
|
|
83
|
+
(EACCES vs EEXIST). #339 (stale-steal residual window) consolidated into design/186 v2 §3.1.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
- The read-face LIST seats (`additionalDirectories`, `additionalReadDirectories`,
|
|
87
|
+
`readDenyPatterns` deps+spec) refuse non-array values loudly (`config.read_face_seat`) instead
|
|
88
|
+
of iterating a bare string character by character — `"/tmp/x"` no longer admits the
|
|
89
|
+
one-character root `"/"` into the write-capable allowlist, and a bare deny-"pattern" string no
|
|
90
|
+
longer dissolves into meaningless entries with zero refusal (#344, merged-code review). The
|
|
91
|
+
deny seats' ENTRY law stays with `compileReadDeny` (the `ReadDenyEntry` union's object form —
|
|
92
|
+
verify's frozen-posture carry — still completes).
|
|
93
|
+
|
|
94
|
+
### Errata (5.44.0 changelog)
|
|
95
|
+
- The "Tool contract revisions move: search 1→2, get 2→3" line overstated the rule — revisions
|
|
96
|
+
bump on execute-visible text changes, per the in-code note.
|
|
97
|
+
- The #318 scope line ("single explicit FILE target's own spelling") was narrower than the shipped
|
|
98
|
+
behavior; see the path-prefix handling in `search.ts` (`relOut`).
|
|
99
|
+
|
|
3
100
|
## 5.45.0 — 2026-08-19
|
|
4
101
|
|
|
5
102
|
### Added
|
package/dist/agents/subagent.js
CHANGED
|
@@ -3,6 +3,8 @@ import { isAbsolute } from "node:path";
|
|
|
3
3
|
import { withDelegationProvenance } from "../core/tool-policy.js";
|
|
4
4
|
import { isHighSurrogate, isLowSurrogate } from "../core/surrogate-safe-slice.js";
|
|
5
5
|
import { newDelegationProvenanceAggregate, reduceDelegationAttestation } from "../core/memory-engine/delegation-provenance.js";
|
|
6
|
+
import { registerDelegationLaunch, replayExternalSettlementEffects, settleDelegation } from "../core/memory-engine/delegation-settlement.js";
|
|
7
|
+
import { enqueueMemoryAnnouncement } from "../core/memory-engine/layout.js";
|
|
6
8
|
import { resolveModel, resolveModelDisplayLabel } from "../core/roles.js";
|
|
7
9
|
import { OUTPUT_TOOL_NAME, REPORT_BLOCKED_TOOL_NAME } from "../core/runner/synthetic-tools.js";
|
|
8
10
|
import { TOOL_SEARCH_NAME } from "../core/runner/tool-disclosure.js";
|
|
@@ -1915,7 +1917,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1915
1917
|
const wt = await finishWorktree();
|
|
1916
1918
|
return { isError: true, content: `Sub-agent not started: source session not found or fork unavailable.${wt ? `\n${wt}` : ""}`, details: { error: "source not found", forkLane: ctx.hostSessionFork ? "host" : "child-runner", ...(ctx.sessionId !== undefined ? { sourceSessionId: ctx.sessionId } : {}) } };
|
|
1917
1919
|
}
|
|
1918
|
-
const forkInternals = { ...childInternals, insideFork: true };
|
|
1920
|
+
const forkInternals = { ...childInternals, insideFork: true, ...(ctx.reminderMark !== undefined ? { reminderMark: ctx.reminderMark } : {}) };
|
|
1919
1921
|
stepRecorder.lockTo(forkedId);
|
|
1920
1922
|
const forkWorktreeNote = worktreeDir !== undefined ? forkWorktreeTranslationNote(ctx.parentCwd, worktreeDir) : undefined;
|
|
1921
1923
|
const forkObjective = `${FORK_DIRECTIVE_FRAME}${prompt}${forkWorktreeNote !== undefined ? `\n\n${forkWorktreeNote}` : ""}`;
|
|
@@ -1935,6 +1937,59 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1935
1937
|
const shortDesc = `fork: ${(typeof a.description === "string" && a.description.trim() ? a.description.trim() : prompt).slice(0, 180)}`;
|
|
1936
1938
|
const bgOwner = sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
|
|
1937
1939
|
const bgScope = treeScope;
|
|
1940
|
+
const forkSettlementSeat = ctx.delegationSettlement?.();
|
|
1941
|
+
const forkSettleId = forkSettlementSeat !== undefined ? `bg-${uuidv7()}` : undefined;
|
|
1942
|
+
if (forkSettlementSeat !== undefined && forkSettleId !== undefined) {
|
|
1943
|
+
try {
|
|
1944
|
+
registerDelegationLaunch(forkSettlementSeat.controlDir, { settleId: forkSettleId, sessionId: forkSettlementSeat.sessionId, now: Date.now, ...(ctx.toolCallId !== undefined ? { toolUseId: ctx.toolCallId } : {}) });
|
|
1945
|
+
}
|
|
1946
|
+
catch (e) {
|
|
1947
|
+
dropHostAbortListener();
|
|
1948
|
+
const wt = await finishWorktree();
|
|
1949
|
+
return {
|
|
1950
|
+
isError: true,
|
|
1951
|
+
content: `Sub-agent not started in background: the delegation settlement account could not record the launch (${(e instanceof Error ? e.message : String(e)).slice(0, 300)}) — fail-closed; repair the memory control plane or retry.${wt ? `\n${wt}` : ""}`,
|
|
1952
|
+
details: { error: "settlement_write_ahead_failed" },
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
const settleForkRow = (status) => {
|
|
1957
|
+
if (forkSettlementSeat === undefined || forkSettleId === undefined)
|
|
1958
|
+
return;
|
|
1959
|
+
const att = status !== undefined ? childAttestation(status) : undefined;
|
|
1960
|
+
const verdict = att === "external" ? "external" : att === "clean" ? "clean" : "unattestable";
|
|
1961
|
+
let settled = false;
|
|
1962
|
+
let lastErr;
|
|
1963
|
+
for (let attempt = 0; attempt < 3 && !settled; attempt++) {
|
|
1964
|
+
try {
|
|
1965
|
+
settleDelegation(forkSettlementSeat.controlDir, { settleId: forkSettleId, status: verdict, now: Date.now });
|
|
1966
|
+
settled = true;
|
|
1967
|
+
}
|
|
1968
|
+
catch (e) {
|
|
1969
|
+
lastErr = e;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
if (settled && verdict === "external") {
|
|
1973
|
+
try {
|
|
1974
|
+
replayExternalSettlementEffects(forkSettlementSeat.controlDir, { carry: true, now: Date.now });
|
|
1975
|
+
}
|
|
1976
|
+
catch {
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if (!settled) {
|
|
1980
|
+
const detail = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
1981
|
+
try {
|
|
1982
|
+
enqueueMemoryAnnouncement(forkSettlementSeat.controlDir, {
|
|
1983
|
+
kind: "gate",
|
|
1984
|
+
at: Date.now(),
|
|
1985
|
+
items: [`delegation settlement: the terminal observation for a background fork of session ${JSON.stringify(forkSettlementSeat.sessionId)} could NOT be recorded (verdict ${verdict}) — the row stays pending and expires as UNPROVEN at the settlement window (fail-closed floor): ${detail.slice(0, 200)}`],
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
catch {
|
|
1989
|
+
console.warn(`[sema] delegation settlement terminal write failed (fork, verdict ${verdict}, session ${forkSettlementSeat.sessionId}): ${detail}`);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
};
|
|
1938
1993
|
let taskId;
|
|
1939
1994
|
try {
|
|
1940
1995
|
taskId = bg.registry.registerBackgroundAgent({
|
|
@@ -1962,6 +2017,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
1962
2017
|
catch {
|
|
1963
2018
|
}
|
|
1964
2019
|
const wt = await finishWorktree();
|
|
2020
|
+
if (forkSettlementSeat !== undefined && forkSettleId !== undefined) {
|
|
2021
|
+
try {
|
|
2022
|
+
settleDelegation(forkSettlementSeat.controlDir, { settleId: forkSettleId, status: "void", now: Date.now, note: "registration failed before invoke (not dispatched)" });
|
|
2023
|
+
}
|
|
2024
|
+
catch {
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
1965
2027
|
return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
|
|
1966
2028
|
}
|
|
1967
2029
|
childInternals.peerSelfRef?.addAxis("h", taskId);
|
|
@@ -2106,6 +2168,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2106
2168
|
void opts.runner
|
|
2107
2169
|
.runTask(bgForkSpec, bgForkInternals)
|
|
2108
2170
|
.then(async (child) => {
|
|
2171
|
+
settleForkRow(child.status);
|
|
2109
2172
|
dropHostAbortListener();
|
|
2110
2173
|
await finishWorktree();
|
|
2111
2174
|
try {
|
|
@@ -2202,6 +2265,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2202
2265
|
}
|
|
2203
2266
|
})
|
|
2204
2267
|
.catch((e) => {
|
|
2268
|
+
settleForkRow("failed");
|
|
2205
2269
|
dropHostAbortListener();
|
|
2206
2270
|
void finishWorktree();
|
|
2207
2271
|
const killed = abort.signal.aborted;
|
|
@@ -2268,7 +2332,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2268
2332
|
: `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.`,
|
|
2269
2333
|
],
|
|
2270
2334
|
}),
|
|
2271
|
-
details: { type: "agent", subagent_type: FORK_SUBAGENT_TYPE, status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt },
|
|
2335
|
+
details: { type: "agent", subagent_type: FORK_SUBAGENT_TYPE, status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt, ...(forkSettleId !== undefined ? { settle_id: forkSettleId } : {}) },
|
|
2272
2336
|
};
|
|
2273
2337
|
}
|
|
2274
2338
|
const bgIgnoredNote = a.run_in_background === true
|
|
@@ -2368,6 +2432,60 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2368
2432
|
const shortDesc = reviveRow?.description ?? String(a.description ?? "sub-agent").slice(0, 200);
|
|
2369
2433
|
const bgOwner = reviveRow !== undefined ? reviveRow.owner : sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
|
|
2370
2434
|
const bgScope = treeScope;
|
|
2435
|
+
const settlementSeat = ctx.delegationSettlement?.();
|
|
2436
|
+
const bgSettleId = settlementSeat !== undefined ? `bg-${uuidv7()}` : undefined;
|
|
2437
|
+
if (settlementSeat !== undefined && bgSettleId !== undefined) {
|
|
2438
|
+
try {
|
|
2439
|
+
registerDelegationLaunch(settlementSeat.controlDir, { settleId: bgSettleId, sessionId: settlementSeat.sessionId, now: Date.now, ...(ctx.toolCallId !== undefined ? { toolUseId: ctx.toolCallId } : {}) });
|
|
2440
|
+
}
|
|
2441
|
+
catch (e) {
|
|
2442
|
+
dropHostAbortListener();
|
|
2443
|
+
await cancelObserver();
|
|
2444
|
+
const wt = await finishWorktree();
|
|
2445
|
+
return {
|
|
2446
|
+
isError: true,
|
|
2447
|
+
content: `Sub-agent not started in background: the delegation settlement account could not record the launch (${(e instanceof Error ? e.message : String(e)).slice(0, 300)}) — fail-closed; repair the memory control plane or retry.${wt ? `\n${wt}` : ""}`,
|
|
2448
|
+
details: { error: "settlement_write_ahead_failed" },
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
const settleDelegationRow = (status) => {
|
|
2453
|
+
if (settlementSeat === undefined || bgSettleId === undefined)
|
|
2454
|
+
return;
|
|
2455
|
+
const att = status !== undefined ? childAttestation(status) : undefined;
|
|
2456
|
+
const verdict = att === "external" ? "external" : att === "clean" ? "clean" : "unattestable";
|
|
2457
|
+
let settled = false;
|
|
2458
|
+
let lastErr;
|
|
2459
|
+
for (let attempt = 0; attempt < 3 && !settled; attempt++) {
|
|
2460
|
+
try {
|
|
2461
|
+
settleDelegation(settlementSeat.controlDir, { settleId: bgSettleId, status: verdict, now: Date.now });
|
|
2462
|
+
settled = true;
|
|
2463
|
+
}
|
|
2464
|
+
catch (e) {
|
|
2465
|
+
lastErr = e;
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
if (settled && verdict === "external") {
|
|
2469
|
+
try {
|
|
2470
|
+
replayExternalSettlementEffects(settlementSeat.controlDir, { carry: true, now: Date.now });
|
|
2471
|
+
}
|
|
2472
|
+
catch {
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
if (!settled) {
|
|
2476
|
+
const detail = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
2477
|
+
try {
|
|
2478
|
+
enqueueMemoryAnnouncement(settlementSeat.controlDir, {
|
|
2479
|
+
kind: "gate",
|
|
2480
|
+
at: Date.now(),
|
|
2481
|
+
items: [`delegation settlement: the terminal observation for a background delegation of session ${JSON.stringify(settlementSeat.sessionId)} could NOT be recorded (verdict ${verdict}) — the row stays pending and expires as UNPROVEN at the settlement window (fail-closed floor): ${detail.slice(0, 200)}`],
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
catch {
|
|
2485
|
+
console.warn(`[sema] delegation settlement terminal write failed (verdict ${verdict}, session ${settlementSeat.sessionId}): ${detail} — the pending row expires as unproven at the settlement window`);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
};
|
|
2371
2489
|
let taskId;
|
|
2372
2490
|
try {
|
|
2373
2491
|
taskId = bg.registry.registerBackgroundAgent(reviveRow !== undefined
|
|
@@ -2427,6 +2545,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2427
2545
|
dropHostAbortListener();
|
|
2428
2546
|
await cancelObserver();
|
|
2429
2547
|
const wt = await finishWorktree();
|
|
2548
|
+
if (settlementSeat !== undefined && bgSettleId !== undefined) {
|
|
2549
|
+
try {
|
|
2550
|
+
settleDelegation(settlementSeat.controlDir, { settleId: bgSettleId, status: "void", now: Date.now, note: "registration failed before invoke (not dispatched)" });
|
|
2551
|
+
}
|
|
2552
|
+
catch {
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2430
2555
|
return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
|
|
2431
2556
|
}
|
|
2432
2557
|
childInternals.peerSelfRef?.addAxis("h", taskId);
|
|
@@ -2744,6 +2869,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2744
2869
|
: opts.runner.runTask(bgSpec, bgInternals);
|
|
2745
2870
|
void bgChildPromise
|
|
2746
2871
|
.then(async (child) => {
|
|
2872
|
+
settleDelegationRow(child.status);
|
|
2747
2873
|
closeObserverWindow(child.status);
|
|
2748
2874
|
dropHostAbortListener();
|
|
2749
2875
|
await finishWorktree();
|
|
@@ -2938,6 +3064,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
2938
3064
|
}
|
|
2939
3065
|
})
|
|
2940
3066
|
.catch(async (e) => {
|
|
3067
|
+
settleDelegationRow("failed");
|
|
2941
3068
|
closeObserverWindow("failed");
|
|
2942
3069
|
dropHostAbortListener();
|
|
2943
3070
|
void finishWorktree();
|
|
@@ -3043,7 +3170,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
|
|
|
3043
3170
|
: `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.`,
|
|
3044
3171
|
],
|
|
3045
3172
|
}),
|
|
3046
|
-
details: { type: "agent", status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt },
|
|
3173
|
+
details: { type: "agent", status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt, ...(bgSettleId !== undefined ? { settle_id: bgSettleId } : {}) },
|
|
3047
3174
|
};
|
|
3048
3175
|
}
|
|
3049
3176
|
let child;
|
|
@@ -807,6 +807,18 @@ export interface CheckpointState {
|
|
|
807
807
|
* scope for any in-row invariant). Absent = the row was minted ungoverned. */
|
|
808
808
|
realApproval?: true;
|
|
809
809
|
};
|
|
810
|
+
/**
|
|
811
|
+
* design/319 (A ticket) — the session's reminder provenance mark
|
|
812
|
+
* ({@link import("./reminder-mint.js").mintReminderMark}), persisted so a durable resume keeps
|
|
813
|
+
* minting the SAME mark its transcript's engine reminders and system-prompt declaration carry
|
|
814
|
+
* ("resume reuses" — one declaration, one mark). Additive on the existing checkpoint version —
|
|
815
|
+
* a reader that ignores it is unchanged (the :646 precedent; no version bump). Resume goes
|
|
816
|
+
* through the VERIFY port ({@link import("./reminder-mint.js").isValidReminderMark}): a missing
|
|
817
|
+
* field (pre-319 row) or an unrecognized value-shape (corrupt / foreign / old-scheme after a
|
|
818
|
+
* scheme change) RE-MINTS instead of adopting — the declaration stays strict, and the historic
|
|
819
|
+
* legs' reminders simply read as data (fail-safe downgrade, design/319 a-2).
|
|
820
|
+
*/
|
|
821
|
+
reminderMark?: string;
|
|
810
822
|
/**
|
|
811
823
|
* design/78 Slice-1: the SAFE-tier self-repair loop's durable state (`failureTrace`/`diagnostics`/
|
|
812
824
|
* `rejectedHypotheses`/`attemptCount`/`oracleTier` — all JSON/`structuredClone`-safe, no fn/Date). Set ONLY
|
|
@@ -67,3 +67,16 @@ export type RuleSyncDropReason = keyof typeof RULE_SYNC_DROP_CODES;
|
|
|
67
67
|
/** The subset that may appear on a LOCAL quarantined row (design/182 §8.1 `quarantine` instruction /
|
|
68
68
|
* fence arm / local screening). `own_actor_forged` is inbound-only by construction. */
|
|
69
69
|
export type RuleQuarantineReason = Exclude<RuleSyncDropReason, "own_actor_forged">;
|
|
70
|
+
/**
|
|
71
|
+
* The presentation-tier registry (the server's wire whitelist retires into this
|
|
72
|
+
* table once shipped): WHO a notice code is for. `"user"` = a session-scoped disclosure the end
|
|
73
|
+
* user of that session should see (safe to project onto the session's event stream); `"operator"`
|
|
74
|
+
* = a deployment/config/ops fact for whoever runs the process. Presentation tier is a property of
|
|
75
|
+
* the CODE (closed set, one code one tier) — never of an individual emission, which is why this is
|
|
76
|
+
* a registry and not an EngineNotice field. Codes absent from the table read as `"operator"`
|
|
77
|
+
* (the conservative default: never push an unclassified code at an end user).
|
|
78
|
+
* Orthogonal to {@link NON_GOVERNANCE_MEMORY_CODES} (a retry-semantics table, not presentation).
|
|
79
|
+
*/
|
|
80
|
+
export declare const NOTICE_AUDIENCE: Readonly<Record<string, "user" | "operator">>;
|
|
81
|
+
/** The audience for `code` — table lookup with the conservative `"operator"` default. */
|
|
82
|
+
export declare function noticeAudienceOf(code: string): "user" | "operator";
|
|
@@ -29,6 +29,28 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
|
|
|
29
29
|
"memory.erasure_index_residue",
|
|
30
30
|
"memory.export_incomplete",
|
|
31
31
|
"memory.import_rejected",
|
|
32
|
+
"memory.hold_opened",
|
|
33
|
+
"memory.hold_released",
|
|
34
|
+
"memory.hold_disposed",
|
|
35
|
+
"memory.settlement_resolve_unattributed",
|
|
36
|
+
"memory.settlement_resolve_invalid",
|
|
37
|
+
"memory.settlement_resolve_unknown",
|
|
38
|
+
"memory.session_account_resolve_unattributed",
|
|
39
|
+
"memory.session_account_resolve_unknown",
|
|
40
|
+
"memory.hold_resolve_unattributed",
|
|
41
|
+
"memory.hold_resolve_invalid",
|
|
42
|
+
"memory.hold_resolve_unknown",
|
|
43
|
+
"memory.hold_resolve_invalid_state",
|
|
44
|
+
"memory.settlement_record_failed",
|
|
45
|
+
"memory.session_account_failed",
|
|
46
|
+
"memory.origin_clear_unattributed",
|
|
47
|
+
"memory.origin_clear_invalid",
|
|
48
|
+
"memory.origin_clear_unknown",
|
|
49
|
+
"memory.origin_clear_not_marked",
|
|
50
|
+
"memory.origin_clear_challenged",
|
|
51
|
+
"memory.origin_clear_pending",
|
|
52
|
+
"memory.origin_clear_conflict",
|
|
53
|
+
"memory.origin_clear_failed",
|
|
32
54
|
]);
|
|
33
55
|
export function governanceRetryClass(code) {
|
|
34
56
|
if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
|
|
@@ -46,3 +68,14 @@ export const RULE_SYNC_DROP_CODES = {
|
|
|
46
68
|
below_gc_frontier: "local-quarantined",
|
|
47
69
|
server_rejected: "local-quarantined",
|
|
48
70
|
};
|
|
71
|
+
export const NOTICE_AUDIENCE = {
|
|
72
|
+
"memory.session_polluted": "user",
|
|
73
|
+
"memory.harvest_quarantined": "user",
|
|
74
|
+
"memory.delegation_static_mark_waived": "user",
|
|
75
|
+
"memory.hold_opened": "user",
|
|
76
|
+
"memory.hold_released": "user",
|
|
77
|
+
"memory.hold_disposed": "user",
|
|
78
|
+
};
|
|
79
|
+
export function noticeAudienceOf(code) {
|
|
80
|
+
return NOTICE_AUDIENCE[code] ?? "operator";
|
|
81
|
+
}
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -583,8 +583,11 @@ export interface UserPromptSubmitResult {
|
|
|
583
583
|
* NOTE (council design/74 #6): this does NOT escape a literal `</system-reminder>` in `text` — callers MUST
|
|
584
584
|
* pass trusted, first-party strings (every current caller does: fixed gate/limit messages). If a future
|
|
585
585
|
* caller needs to relay UNTRUSTED content (tool output, user data), it must sanitize the close tag first
|
|
586
|
-
* (or use the `delimitUntrusted` fence), or a crafted payload could break out of the reminder framing.
|
|
587
|
-
|
|
586
|
+
* (or use the `delimitUntrusted` fence), or a crafted payload could break out of the reminder framing.
|
|
587
|
+
* design/319 (A ticket): `mark` is the session's reminder provenance mark — run-scoped callers thread it
|
|
588
|
+
* so the open tag carries the value the system-prompt declaration names (rendered by the mint home; the
|
|
589
|
+
* body is byte-untouched). Absent ⇒ the historic bare open tag (a caller outside a run). */
|
|
590
|
+
export declare function formatHookFeedback(text: string, mark?: string): string;
|
|
588
591
|
/** The outcome of the two-phase tool gate, mapped onto the harness `tool_call` hook return shape. */
|
|
589
592
|
export interface ToolGateResult {
|
|
590
593
|
/** Block execution (the loop emits an error tool result with `reason`). */
|
|
@@ -799,6 +802,10 @@ export interface ToolGateInput {
|
|
|
799
802
|
* per-leg mint; a host driving the gate directly may omit it, and the contexts then carry no
|
|
800
803
|
* identity (honest absence — this layer never fabricates a leg). */
|
|
801
804
|
identity?: HookInvocationIdentity;
|
|
805
|
+
/** design/319 (A ticket) — the run's reminder provenance mark: the gate's own model-facing deny
|
|
806
|
+
* reasons ride `formatHookFeedback`, so their open tags must carry the same mark the run's
|
|
807
|
+
* system-prompt declaration names. Absent (a host driving the gate directly) ⇒ bare tags. */
|
|
808
|
+
reminderMark?: string;
|
|
802
809
|
preToolUse?: Hooks["preToolUse"];
|
|
803
810
|
/** The read-only env capability face put on every {@link HookToolContext} this gate call
|
|
804
811
|
* builds ({@link HookEnvCapabilities}). Built ONCE per task by the runner (after the env is minted) and
|
package/dist/core/hooks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { decisionText, describeThrown, refuseOutOfContractDecision } from "./tool-policy.js";
|
|
2
2
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
3
3
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
4
|
+
import { mintSystemReminder } from "./reminder-mint.js";
|
|
4
5
|
import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js";
|
|
5
6
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
6
7
|
import { createSafeNotifier } from "./safe-notify.js";
|
|
@@ -90,8 +91,8 @@ export function mintHookInvocationIdentity(facts) {
|
|
|
90
91
|
...(facts.parentToolCallId !== undefined ? { parentToolCallId: facts.parentToolCallId } : {}),
|
|
91
92
|
}));
|
|
92
93
|
}
|
|
93
|
-
export function formatHookFeedback(text) {
|
|
94
|
-
return
|
|
94
|
+
export function formatHookFeedback(text, mark) {
|
|
95
|
+
return mintSystemReminder(text, mark);
|
|
95
96
|
}
|
|
96
97
|
const PARK_FAILURE_CAUSE_MAX = 600;
|
|
97
98
|
function withParkFailureCause(reason, parkFailed) {
|
|
@@ -304,7 +305,7 @@ export async function runToolGate(input) {
|
|
|
304
305
|
if (input.permissionDenied) {
|
|
305
306
|
await notifier.notifyAsync(() => input.permissionDenied?.({ toolName, input: cloneObserverInput(currentInput), toolCallId, reason, source: "hook", ...(input.identity !== undefined ? { identity: input.identity } : {}) }), "toolGate.permissionDenied");
|
|
306
307
|
}
|
|
307
|
-
return { block: true, reason: formatHookFeedback(reason), preToolContext };
|
|
308
|
+
return { block: true, reason: formatHookFeedback(reason, input.reminderMark), preToolContext };
|
|
308
309
|
}
|
|
309
310
|
if (r) {
|
|
310
311
|
if (r.additionalContext) {
|
|
@@ -313,7 +314,7 @@ export async function runToolGate(input) {
|
|
|
313
314
|
if (r.action === "deny") {
|
|
314
315
|
return {
|
|
315
316
|
block: true,
|
|
316
|
-
reason: formatHookFeedback(decisionText(r) ?? `tool "${toolName}" blocked by a PreToolUse hook
|
|
317
|
+
reason: formatHookFeedback(decisionText(r) ?? `tool "${toolName}" blocked by a PreToolUse hook`, input.reminderMark),
|
|
317
318
|
preToolContext,
|
|
318
319
|
};
|
|
319
320
|
}
|
|
@@ -813,7 +814,7 @@ export async function runToolGate(input) {
|
|
|
813
814
|
const denyApprover = denySettledBy !== undefined ? resolvedApprover : undefined;
|
|
814
815
|
return {
|
|
815
816
|
block: true,
|
|
816
|
-
reason: formatHookFeedback(denyReason),
|
|
817
|
+
reason: formatHookFeedback(denyReason, input.reminderMark),
|
|
817
818
|
...(denySettledBy !== undefined ? { settledBy: denySettledBy } : {}),
|
|
818
819
|
...(denyApprover !== undefined ? { approver: denyApprover } : {}),
|
|
819
820
|
preToolContext,
|
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* into long-term memory? Three tiers:
|
|
7
7
|
*
|
|
8
8
|
* - "external" — network-retrieval families and external protocol channels. Invocation marks the
|
|
9
|
-
* session's memory
|
|
9
|
+
* session's memory EXTERNALLY EXPOSED (one-way, durable; what the mark does at
|
|
10
|
+
* harvest is the provenance mode's question — design/336: tag-admit with an origin
|
|
11
|
+
* marker under "carry", full quarantine under "off").
|
|
10
12
|
* - "execution" — general execution (shell/exec). A NAMED residual channel: it can reach external
|
|
11
13
|
* content indirectly, but the two industry anchors this tier is calibrated against
|
|
12
14
|
* gate on network/protocol families only, and excluding every shell session would
|