@sema-agent/core 5.25.0 → 5.27.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-definition.js +5 -0
- package/dist/agents/agent-transcript-tool.d.ts +5 -2
- package/dist/agents/agent-transcript-tool.js +2 -1
- package/dist/agents/send-message-tool.d.ts +4 -1
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +6 -2
- package/dist/agents/subagent.js +5 -0
- package/dist/core/checkpoint-store.d.ts +7 -2
- package/dist/core/hooks.d.ts +39 -4
- package/dist/core/hooks.js +18 -14
- package/dist/core/memory-engine/dual-root.js +3 -1
- package/dist/core/memory-engine/engine.d.ts +53 -6
- package/dist/core/memory-engine/engine.js +43 -11
- package/dist/core/memory-engine/file-backend.d.ts +81 -0
- package/dist/core/memory-engine/file-backend.js +250 -24
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/types.d.ts +8 -1
- package/dist/core/memory-vector.d.ts +6 -1
- package/dist/core/memory-vector.js +14 -4
- package/dist/core/memory.js +1 -6
- package/dist/core/permission-rule-consent.js +8 -1
- package/dist/core/permission-rule-model.d.ts +70 -5
- package/dist/core/permission-rule-model.js +58 -0
- package/dist/core/runner/compaction-call-options.d.ts +4 -4
- package/dist/core/runner/compaction-call-options.js +3 -4
- package/dist/core/runner/prepare-memory.d.ts +34 -15
- package/dist/core/runner/prepare-memory.js +99 -26
- package/dist/core/runner/prepare-task.d.ts +9 -3
- package/dist/core/runner/prepare-task.js +60 -15
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +24 -0
- package/dist/core/task-registry-agent.d.ts +4 -3
- package/dist/core/task-registry-agent.js +3 -3
- package/dist/core/task-registry-monitor.js +6 -5
- package/dist/core/task-registry.d.ts +6 -3
- package/dist/core/tool-policy.d.ts +9 -2
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-budget.js +3 -3
- package/dist/core/tool-result-store.d.ts +164 -9
- package/dist/core/tool-result-store.js +82 -23
- package/dist/core/types.d.ts +103 -7
- package/dist/core/untrusted-text.d.ts +6 -2
- package/dist/core/untrusted-text.js +1 -1
- package/dist/engine/loop/types.d.ts +10 -3
- package/dist/engine/session/import-validate.js +2 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/orchestration/run-workflow-tool.d.ts +5 -3
- package/dist/orchestration/workflow.d.ts +9 -6
- package/dist/orchestration/workflow.js +2 -0
- package/dist/prompts/default.d.ts +11 -0
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/checkpoint-store.d.ts +2 -1
- package/dist/stores/file/fs-atomic.d.ts +1 -1
- package/dist/stores/file/index.d.ts +1 -1
- package/dist/stores/file/tool-result-store.d.ts +45 -9
- package/dist/stores/file/tool-result-store.js +76 -9
- package/dist/tools/fs/fs-shared.js +5 -4
- package/package.json +1 -1
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
const CJK_RUN = /[\u3040-\u30FF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7A3\uF900-\uFAFF]+/g;
|
|
1
2
|
export function termSet(s) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const lower = s.toLowerCase();
|
|
4
|
+
const out = new Set();
|
|
5
|
+
for (const run of lower.split(/[^a-z0-9]+/))
|
|
6
|
+
if (run)
|
|
7
|
+
out.add(run);
|
|
8
|
+
for (const m of lower.matchAll(CJK_RUN)) {
|
|
9
|
+
const chars = [...m[0]];
|
|
10
|
+
for (const c of chars)
|
|
11
|
+
out.add(c);
|
|
12
|
+
for (let i = 0; i + 1 < chars.length; i++)
|
|
13
|
+
out.add(chars[i] + chars[i + 1]);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
6
16
|
}
|
|
7
17
|
export function jaccardDistance(query, text) {
|
|
8
18
|
const t = termSet(text);
|
package/dist/core/memory.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { uuidv7 } from "../internal/harness.js";
|
|
2
2
|
import { parseScopeKey } from "./memory-engine/scope-contract.js";
|
|
3
|
+
import { termSet } from "./memory-vector.js";
|
|
3
4
|
import { sanitizeUntrustedText } from "./untrusted-text.js";
|
|
4
5
|
export function supportsConsolidation(store) {
|
|
5
6
|
return (typeof store.searchScored === "function" &&
|
|
@@ -148,12 +149,6 @@ export function guardedMemoryStore(inner, utilityGate) {
|
|
|
148
149
|
function renderBullet(e) {
|
|
149
150
|
return `- (${e.ts} UTC) ${e.text}`;
|
|
150
151
|
}
|
|
151
|
-
function termSet(s) {
|
|
152
|
-
return new Set(s
|
|
153
|
-
.toLowerCase()
|
|
154
|
-
.split(/[^a-z0-9]+/)
|
|
155
|
-
.filter(Boolean));
|
|
156
|
-
}
|
|
157
152
|
export class InMemoryMemoryStore {
|
|
158
153
|
byScope = new Map();
|
|
159
154
|
cursorByScope = new Map();
|
|
@@ -281,8 +281,15 @@ export async function prepareCcImport(opts) {
|
|
|
281
281
|
skipped.push({ rule: String(entry), reason: "settings entry is not a string" });
|
|
282
282
|
continue;
|
|
283
283
|
}
|
|
284
|
-
if (!entry.startsWith("Bash("))
|
|
284
|
+
if (!entry.startsWith("Bash(")) {
|
|
285
|
+
const reason = /^[A-Za-z][A-Za-z0-9_]*\(.*\)$/.test(entry)
|
|
286
|
+
? "unsupported.tool: only Bash(...) command rules import in v1 — this entry stays in the settings file, unimported"
|
|
287
|
+
: /^[A-Za-z][A-Za-z0-9_-]*$/.test(entry)
|
|
288
|
+
? "unsupported.form: a bare tool-name entry is a name-set item, not a command rule — it stays in the settings file, unimported"
|
|
289
|
+
: "unsupported.form: not a Bash(...) command rule — it stays in the settings file, unimported";
|
|
290
|
+
skipped.push({ rule: entry, reason });
|
|
285
291
|
continue;
|
|
292
|
+
}
|
|
286
293
|
const parsed = parseAllowRuleText(entry);
|
|
287
294
|
if ("reject" in parsed) {
|
|
288
295
|
skipped.push({ rule: entry, reason: `${parsed.reject.code}: ${parsed.reject.message}` });
|
|
@@ -120,6 +120,53 @@ export declare const MAX_RULE_TEXT_CHARS = 512;
|
|
|
120
120
|
* an EXACT rule naming a whole interpreter command line stays legal, since it authorizes one command.
|
|
121
121
|
*/
|
|
122
122
|
export declare const BARE_INTERPRETER_NAMES: ReadonlySet<string>;
|
|
123
|
+
/**
|
|
124
|
+
* design/185 §1 — the reviewed command/subcommand grammar the PREFIX suggestion is generated from
|
|
125
|
+
* (exactly the "reviewed command/subcommand grammar" the generator's history note names as the one
|
|
126
|
+
* thing that would let it produce a prefix).
|
|
127
|
+
*
|
|
128
|
+
* A flat set of BODIES — word sequences, each at least two words. A prefix candidate exists for a
|
|
129
|
+
* command iff some body here is a word-boundary prefix of its folded form, and the LONGEST hit wins:
|
|
130
|
+
* the deeper body is the narrower rule, so listing (or not listing) a deeper body is how this table
|
|
131
|
+
* sets suggestion granularity per branch. No groups, no denylist, and no fallback arm: a head outside
|
|
132
|
+
* the table, an unreviewed subcommand, a runtime-defined name (a git alias, a `git-<x>`/`cargo-<x>`
|
|
133
|
+
* external subcommand, a gh extension, a kubectl plugin), a flag or operand in a body position and a
|
|
134
|
+
* quoted token all fail the same way — by not being listed. Closure comes from positive enumeration
|
|
135
|
+
* itself, never from an exclusion list racing names that only exist at runtime.
|
|
136
|
+
*
|
|
137
|
+
* Review criteria — every row must pass BOTH axes (the same principle as the interpreter refusal
|
|
138
|
+
* above: the rule text must not read narrower than what it grants):
|
|
139
|
+
* · "runs what it is told to": a body whose use is fetching or naming a program to execute
|
|
140
|
+
* (`npm exec`, `docker run`, `kubectl exec`, `gh extension`, `git submodule foreach`, the install
|
|
141
|
+
* family) is refused — one click cannot be read as having granted arbitrary execution. Running the
|
|
142
|
+
* WORKSPACE'S OWN pinned content (`npm run`, `npm ci`, `cargo run`, `cargo test`) is inside the
|
|
143
|
+
* boundary: the scripts and lockfiles those execute are checked into the repository being worked on.
|
|
144
|
+
* · "rewrites what others execute": a body whose main use is writing configuration that changes what
|
|
145
|
+
* OTHER commands later run (`git config` — hooksPath/pager/alias; `kubectl config` —
|
|
146
|
+
* exec-credential; `npm config`/`npm set` — script-shell; `go env` — persisted GOFLAGS/GOBIN) is
|
|
147
|
+
* refused — its readable width and its real width differ by a whole composition surface.
|
|
148
|
+
* Past both axes there is deliberately NO "dangerousness" axis: `git push:*` and `git rebase:*` are
|
|
149
|
+
* wide but readable, and a person nodding at that text is granting exactly that.
|
|
150
|
+
*
|
|
151
|
+
* Residual width, stated rather than hidden (a reviewed trade, not an oversight): a prefix rule
|
|
152
|
+
* admits ANY arguments after its body, and some listed bodies carry flags that name a program to
|
|
153
|
+
* execute (`go build`/`go test`/`go vet -toolexec`, `git fetch --upload-pack`, `git rebase -x`,
|
|
154
|
+
* `git grep -O`, `git push --receive-pack`). The axes judge a body's MAIN use, not every flag —
|
|
155
|
+
* per-flag grammar is the road this module's history rejected twice, and applied consistently it
|
|
156
|
+
* would empty the table. Three standing fences hold that residue: the org deny/ask layer runs ahead
|
|
157
|
+
* of the rule lane and cannot be silenced by it; a mandated ask (egress/irreversibility marks,
|
|
158
|
+
* shellGate:"always") is not rule-clearable either; and the narrower exact candidate — plus minting
|
|
159
|
+
* no rule at all — is always on the same card.
|
|
160
|
+
*
|
|
161
|
+
* Maintenance: adding a row is a reviewed change — keep the per-group reasoning beside it current.
|
|
162
|
+
* The integrity pins (every body ≥ 2 words, lowercase word shape, no interpreter heads, no
|
|
163
|
+
* duplicates) are enforced by this module's test suite. This table and the read-only classifier's
|
|
164
|
+
* allowlists are DIFFERENT instruments and must never be merged or cross-referenced: that one is a
|
|
165
|
+
* machine auto-allow face whose criterion is "provably read-only"; this one is a human suggestion
|
|
166
|
+
* face whose criterion is "width a person can read off the rule text". One guards against a machine
|
|
167
|
+
* loosening; the other against a person being misled.
|
|
168
|
+
*/
|
|
169
|
+
export declare const SUGGESTION_LEXICON: readonly string[];
|
|
123
170
|
/**
|
|
124
171
|
* Parse one rule text into its canonical shape, or refuse it with a reason.
|
|
125
172
|
*
|
|
@@ -184,11 +231,29 @@ export interface RuleSuggestion {
|
|
|
184
231
|
/**
|
|
185
232
|
* The 1-2 candidates offered on an approval card for `command`.
|
|
186
233
|
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
234
|
+
* **Array order is a documented CONTRACT, not an implementation accident**: display order = array
|
|
235
|
+
* order = narrowest first. The EXACT form (this whole command line) is always index 0 whenever
|
|
236
|
+
* anything is offered at all; a broader reviewed PREFIX form — at most one — follows at index 1.
|
|
237
|
+
* Selection indices and redemption tickets are index-keyed against this order (a card's
|
|
238
|
+
* `selectedCandidate` and its `rt.<index>.` tickets), so consumers may rely on it.
|
|
239
|
+
*
|
|
240
|
+
* The prefix candidate comes ONLY from {@link SUGGESTION_LEXICON} — the longest reviewed body that is
|
|
241
|
+
* a word-boundary prefix of the folded command. History, and why there is no heuristic arm: two
|
|
242
|
+
* rounds of guessing produced two different wrong answers — a bare program name (`rm -f x` →
|
|
243
|
+
* `Bash(rm:*)`), then an operand mistaken for a subcommand (`rm harmless.txt` →
|
|
244
|
+
* `Bash(rm harmless.txt:*)`, which admits a second, unnamed target) — and both failed the same way:
|
|
245
|
+
* nothing in the command TEXT distinguishes a subcommand from an operand without a per-command
|
|
246
|
+
* grammar. The lexicon IS that grammar, per reviewed row; anything it does not list (a bare verb, an
|
|
247
|
+
* interpreter head, an unreviewed subcommand, a runtime-defined name) yields no prefix, with no
|
|
248
|
+
* fallback. Naive spacing note: `folded` keeps quoted whitespace, so splitting on single spaces can
|
|
249
|
+
* shear a quoted segment — harmless in this direction, because the sheared pieces carry quote
|
|
250
|
+
* characters and can never equal a bare lexicon word; every suspicious shape lands on "no prefix".
|
|
251
|
+
*
|
|
252
|
+
* Every produced candidate must survive the round trip — parse as a rule AND admit the very command
|
|
253
|
+
* it was minted from. True by construction (a word-boundary lexicon prefix of a folded simple command
|
|
254
|
+
* is exactly the matcher's two arms); enforced anyway, fail-closed: a candidate that would not
|
|
255
|
+
* round-trip is silently not offered, since offering an option redemption would refuse is worse than
|
|
256
|
+
* offering one fewer.
|
|
192
257
|
*
|
|
193
258
|
* Returns an empty array for anything the rule lane cannot speak for (compounds, redirections,
|
|
194
259
|
* substitutions) — the card then simply carries no "don't ask again" option, which is the honest answer.
|
|
@@ -6,6 +6,54 @@ export const BARE_INTERPRETER_NAMES = new Set([
|
|
|
6
6
|
"node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php",
|
|
7
7
|
"osascript", "env", "eval", "exec", "xargs", "nohup", "sudo", "doas", "su", "ssh",
|
|
8
8
|
]);
|
|
9
|
+
export const SUGGESTION_LEXICON = [
|
|
10
|
+
"git status", "git log", "git diff", "git show", "git branch", "git checkout", "git switch",
|
|
11
|
+
"git add", "git commit", "git push", "git pull", "git fetch", "git merge", "git rebase",
|
|
12
|
+
"git tag", "git blame", "git describe", "git cherry-pick", "git restore", "git reset",
|
|
13
|
+
"git rev-parse", "git ls-files", "git grep",
|
|
14
|
+
"git stash list", "git stash show", "git stash push", "git stash pop", "git stash drop",
|
|
15
|
+
"git remote show", "git worktree list", "git submodule update", "git submodule status",
|
|
16
|
+
"npm run", "npm test", "npm ci", "npm ls", "npm view", "npm outdated", "npm audit",
|
|
17
|
+
"npm pack", "npm publish", "npm version", "npm why",
|
|
18
|
+
"pnpm run", "pnpm test", "pnpm ls", "pnpm outdated", "pnpm audit", "pnpm why",
|
|
19
|
+
"yarn run", "yarn test", "yarn workspaces list",
|
|
20
|
+
"cargo build", "cargo test", "cargo run", "cargo check", "cargo clippy", "cargo fmt",
|
|
21
|
+
"cargo doc", "cargo tree", "cargo bench", "cargo update", "cargo metadata",
|
|
22
|
+
"go build", "go test", "go vet", "go fmt", "go doc",
|
|
23
|
+
"go mod tidy", "go mod download", "go mod verify", "go mod graph",
|
|
24
|
+
"docker ps", "docker images", "docker logs", "docker inspect", "docker build", "docker pull",
|
|
25
|
+
"docker push", "docker stop", "docker start", "docker restart", "docker rm", "docker rmi",
|
|
26
|
+
"docker compose up", "docker compose down", "docker compose ps", "docker compose logs",
|
|
27
|
+
"docker compose build", "docker compose pull",
|
|
28
|
+
"kubectl get", "kubectl describe", "kubectl logs", "kubectl apply", "kubectl delete",
|
|
29
|
+
"kubectl diff", "kubectl explain", "kubectl top",
|
|
30
|
+
"kubectl rollout status", "kubectl rollout restart", "kubectl rollout history",
|
|
31
|
+
"gh pr view", "gh pr list", "gh pr diff", "gh pr checks", "gh pr status", "gh pr create",
|
|
32
|
+
"gh pr merge", "gh issue view", "gh issue list", "gh issue create", "gh repo view",
|
|
33
|
+
"gh repo clone", "gh release list", "gh release view", "gh run list", "gh run view",
|
|
34
|
+
"gh run watch", "gh workflow list", "gh workflow view", "gh auth status",
|
|
35
|
+
"gh search repos", "gh search issues", "gh search prs", "gh search code",
|
|
36
|
+
];
|
|
37
|
+
const LEXICON_BODIES = SUGGESTION_LEXICON.map((b) => b.split(" "));
|
|
38
|
+
function longestReviewedBody(tokens) {
|
|
39
|
+
let best;
|
|
40
|
+
for (const words of LEXICON_BODIES) {
|
|
41
|
+
if (words.length > tokens.length)
|
|
42
|
+
continue;
|
|
43
|
+
if (best !== undefined && words.length <= best.length)
|
|
44
|
+
continue;
|
|
45
|
+
let hit = true;
|
|
46
|
+
for (let i = 0; i < words.length; i++) {
|
|
47
|
+
if (words[i] !== tokens[i]) {
|
|
48
|
+
hit = false;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (hit)
|
|
53
|
+
best = words;
|
|
54
|
+
}
|
|
55
|
+
return best?.join(" ");
|
|
56
|
+
}
|
|
9
57
|
function foldSpacing(s) {
|
|
10
58
|
let out = "";
|
|
11
59
|
let quote;
|
|
@@ -131,5 +179,15 @@ export function suggestRulesForCommand(command) {
|
|
|
131
179
|
const exact = parseAllowRuleText(formatAllowRuleText(folded, "exact"));
|
|
132
180
|
if ("rule" in exact)
|
|
133
181
|
out.push({ rule: exact.rule.rule, match: "exact", command: exact.rule.command });
|
|
182
|
+
if (out.length === 1) {
|
|
183
|
+
const body = longestReviewedBody(folded.split(" "));
|
|
184
|
+
if (body !== undefined) {
|
|
185
|
+
const text = formatAllowRuleText(body, "prefix");
|
|
186
|
+
const parsed = parseAllowRuleText(text);
|
|
187
|
+
if ("rule" in parsed && parsed.rule.match === "prefix" && ruleAdmitsCommand(parsed.rule, command)) {
|
|
188
|
+
out.push({ rule: parsed.rule.rule, match: "prefix", command: parsed.rule.command });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
134
192
|
return out;
|
|
135
193
|
}
|
|
@@ -95,10 +95,10 @@ export declare function buildStaleOffloadPointer(toolName: string, ref: string,
|
|
|
95
95
|
* The session transcript is NEVER touched — this runs on the outgoing {@link Context} only.
|
|
96
96
|
* Error results, image/document-bearing blocks' non-text parts, already-offloaded previews, and
|
|
97
97
|
* replacements that would save < `minSavingsChars` are left verbatim. The full text is persisted
|
|
98
|
-
* under a deterministic content-digested ref (
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* readable back via `read_tool_result`.
|
|
98
|
+
* under a deterministic content-digested ref (`buildToolResultRef(sessionId, toolCallId,
|
|
99
|
+
* toolResultContentSegment(text))` — a `~`-separated three-segment mint whose digest is its OWN
|
|
100
|
+
* segment; write-once, so re-projection on every turn re-puts a no-op; the digest exists because
|
|
101
|
+
* tool-call ids carry no cross-turn uniqueness contract), readable back via `read_tool_result`.
|
|
102
102
|
*/
|
|
103
103
|
export declare function projectStaleToolResults(context: Context, cfg: ResolvedStaleToolResultOffload, store: ToolResultStore, sessionId: string,
|
|
104
104
|
/** Run-scoped cache of refs already persisted by THIS run (独立复审 MED,已修): without it the
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { emitTrace } from "../trace.js";
|
|
3
|
-
import { buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX } from "../tool-result-store.js";
|
|
2
|
+
import { buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX, toolResultContentSegment, toolResultProvenanceOf, } from "../tool-result-store.js";
|
|
4
3
|
export function buildWorkingFileAttachments(spec, prepared) {
|
|
5
4
|
if (spec.compaction?.attachWorkingFiles === false || !prepared.readTaskFile)
|
|
6
5
|
return undefined;
|
|
@@ -73,13 +72,13 @@ export async function projectStaleToolResults(context, cfg, store, sessionId, wr
|
|
|
73
72
|
const text = toolResultText(msg);
|
|
74
73
|
if (text.startsWith(PERSISTED_OUTPUT_PREFIX))
|
|
75
74
|
continue;
|
|
76
|
-
const ref = buildToolResultRef(sessionId,
|
|
75
|
+
const ref = buildToolResultRef(sessionId, msg.toolCallId, toolResultContentSegment(text));
|
|
77
76
|
const pointer = buildStaleOffloadPointer(toolName, ref, text.length);
|
|
78
77
|
if (text.length - pointer.length < cfg.minSavingsChars)
|
|
79
78
|
continue;
|
|
80
79
|
if (!writtenRefs.has(ref)) {
|
|
81
80
|
try {
|
|
82
|
-
await store.put(ref, text);
|
|
81
|
+
await store.put(ref, text, toolResultProvenanceOf(sessionId));
|
|
83
82
|
writtenRefs.add(ref);
|
|
84
83
|
}
|
|
85
84
|
catch {
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* design/157 B15 一期 (P6 相位抽取) — prepareTask's long-term-memory phase, verbatim. The ONLY inputs
|
|
3
|
-
* are the five fields below (measured, not assumed: spec.memory + deps.* + the three ambient values);
|
|
4
|
-
* the ONLY outputs are the memory-engine session and the composed injection block. `deps.onError`
|
|
5
|
-
* call order and payloads are part of the contract (event-sequence snapshot pin recorded across the
|
|
6
|
-
* move). Throws pass through unchanged: a `config.memory_*`-coded violation is a DELIBERATE refusal
|
|
7
|
-
* (design/142 S1 硬门) and must keep failing prepare loudly.
|
|
8
|
-
*/
|
|
9
1
|
import type { BeforeWriteHook, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
|
|
10
2
|
import type { Prepared } from "./prepare-task.js";
|
|
11
3
|
export interface PrepareMemoryInput {
|
|
@@ -18,15 +10,42 @@ export interface PrepareMemoryInput {
|
|
|
18
10
|
};
|
|
19
11
|
/**
|
|
20
12
|
* #181-F5 — whether a tool NAMED `Write` (the tool the CC `# Memory` instruction names) is on the
|
|
21
|
-
* ASSEMBLED roster this run
|
|
22
|
-
* `tools.some(t => t.name === "Write") && !exclude.includes("Write")
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
13
|
+
* ASSEMBLED roster this run, not excluded, AND can actually REACH the host-side memory root:
|
|
14
|
+
* prepare-task's `tools.some(t => t.name === "Write") && !exclude.includes("Write")` plus the
|
|
15
|
+
* remote-env conjunct (a hand-band Write on a remote ExecutionEnv writes the sandbox filesystem,
|
|
16
|
+
* not the host memory root — it does not count unless the deployment declares the mount shared
|
|
17
|
+
* via `memoryPersistenceCapable: true`). Name occupancy IS the channel declaration for the local
|
|
18
|
+
* arms — a hands-less run that mounts its own `Write` counts, and an `excludeTools: ["Write"]`
|
|
19
|
+
* run reads as unmounted whatever the hands band did. Threaded into `engine.inject` so a run with
|
|
20
|
+
* a writable memory scope but no write channel is not instructed to call a tool that is not on
|
|
21
|
+
* its roster (or cannot reach the store); the RB-276 index seed follows the same gate.
|
|
28
22
|
*/
|
|
29
23
|
writeToolsMounted: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The session-wide persistence verdict: `TaskSpec.memoryPersistenceCapable` (the deployment's own
|
|
26
|
+
* statement — the only honest channel for a custom memory writer persisting through its closure,
|
|
27
|
+
* which no inference can see) when set, else the KNOWN-store-path inference: a mounted,
|
|
28
|
+
* non-excluded file-write tool (Write/Edit/NotebookEdit) or a write-capable shell. A generic
|
|
29
|
+
* write-effect tool does NOT count — a mail sender's side effect is not a memory store, and
|
|
30
|
+
* counting it re-opened the silent-confabulation hole. `false` ⇒ this phase mounts
|
|
31
|
+
* {@link MEMORY_READONLY_NOTICE} where the engine is silent.
|
|
32
|
+
*/
|
|
33
|
+
rosterCanPersist: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The raw `TaskSpec.memoryPersistenceCapable` DECLARATION, kept separate from the composed
|
|
36
|
+
* {@link rosterCanPersist} verdict because the two drive different arms:
|
|
37
|
+
* - `true` (declared) RETRACTS the engine's own read-only notice — the deployment vouches for a
|
|
38
|
+
* persistence channel the engine cannot see (a custom writer persisting through its closure),
|
|
39
|
+
* so "you cannot save" beside it would be a false claim. An INFERRED-true roster must NOT
|
|
40
|
+
* retract: over a writeScope-null layering the engine will refuse those very file writes
|
|
41
|
+
* (`read_only_layering`), so the roster's write tools prove nothing about THIS store and
|
|
42
|
+
* stripping the notice re-opens the silent-confabulation hole for exactly the state the
|
|
43
|
+
* notice was built for.
|
|
44
|
+
* - `false` (declared) also CLOSES the file-tool write channel into the writable memory root
|
|
45
|
+
* (the write gate refuses), so the mounted notice's "the engine will not accept writes into
|
|
46
|
+
* the memory store" stays a true statement instead of a disclosure the store then contradicts.
|
|
47
|
+
*/
|
|
48
|
+
memoryPersistenceDeclared?: boolean;
|
|
30
49
|
/**
|
|
31
50
|
* design/178 ②-1 — whether the `memory_search`/`memory_get` pair PASSED its early mount conjuncts
|
|
32
51
|
* (exclusion + name occupancy, decided in prepare-task BEFORE this phase). True ⇒ this phase builds
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { sep } from "node:path";
|
|
1
2
|
import { admitMemoryScopes } from "../memory-admission.js";
|
|
2
|
-
import { adoptLegacyRepoDirs, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
|
|
3
|
+
import { adoptLegacyRepoDirs, canonicalize, deriveRepoControlPlaneDir, deriveProjectControlDir, deriveProjectMemoryDir, deriveRepoMemoryDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, isContainedIn, lookupProjectIdHint, recordProjectIdHint, resolveMemoryEngineRoot } from "../memory-engine/layout.js";
|
|
3
4
|
import { classifyScopePlanes, derivePersonalControlDir, derivePersonalMemoryDir, mergeHarvestReports, mergeInjections, needsDualRoots, parsedProjectPlane } from "../memory-engine/dual-root.js";
|
|
4
5
|
import { normalizeMemorySpec } from "../memory.js";
|
|
5
|
-
import { MEMORY_PREFERENCE_DISCIPLINE, MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
|
|
6
|
+
import { MEMORY_ANNOUNCEMENT_READONLY_CODA, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_RECALL_DISCIPLINE, MemoryEngine } from "../memory-engine/engine.js";
|
|
6
7
|
import { createMemoryEngineTools } from "../memory-engine/tools.js";
|
|
7
8
|
import { assertScopeContractPlacement, parseScopeKey, resolveProjectId } from "../memory-engine/scope-contract.js";
|
|
8
9
|
import { FileMemoryEngineBackend } from "../memory-engine/file-backend.js";
|
|
@@ -113,17 +114,24 @@ export async function prepareMemory(input) {
|
|
|
113
114
|
}
|
|
114
115
|
const personalMemoryDir = derivePersonalMemoryDir(engineRoot);
|
|
115
116
|
const personalControlDir = derivePersonalControlDir(engineRoot);
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
const effectiveControlDir = (b, fallback) => {
|
|
118
|
+
const pinnedCtl = b.controlPlaneRoot;
|
|
119
|
+
return typeof pinnedCtl === "string" && pinnedCtl ? pinnedCtl : fallback;
|
|
120
|
+
};
|
|
121
|
+
const choosePersonalBackend = () => typeof pinned === "string" && pinned ? new FileMemoryEngineBackend(personalMemoryDir, { controlDir: personalControlDir }) : backend;
|
|
122
|
+
const createPersonalEngine = (personalBackend) => {
|
|
120
123
|
return {
|
|
121
124
|
engine: new MemoryEngine({ backend: personalBackend, memoryDir: personalMemoryDir, controlDir: personalControlDir, onIncident: onEngineIncident }),
|
|
122
125
|
backend: personalBackend,
|
|
123
126
|
};
|
|
124
127
|
};
|
|
125
128
|
const onEngineIncident = (err) => deps.onError?.(err, { phase: "memory", sessionId });
|
|
126
|
-
const
|
|
129
|
+
const adoptionRestricted = input.memoryPersistenceDeclared === false;
|
|
130
|
+
const retrievalBackend = (b, planeRestricted) => planeRestricted
|
|
131
|
+
? (b.restrictedAdoptionView?.({ audit: false }) ??
|
|
132
|
+
b.retrievalView?.() ??
|
|
133
|
+
b)
|
|
134
|
+
: (b.retrievalView?.() ?? b);
|
|
127
135
|
const planeScopes = (scopes, write) => [...new Set([...scopes, ...(write !== null ? [write] : [])])];
|
|
128
136
|
const pollutedOpts = (engine) => {
|
|
129
137
|
const rec = engine.sessionPollution(sessionId);
|
|
@@ -131,34 +139,62 @@ export async function prepareMemory(input) {
|
|
|
131
139
|
};
|
|
132
140
|
let writeEngine;
|
|
133
141
|
let writeHandle;
|
|
142
|
+
let readOnlyEngine;
|
|
143
|
+
let readOnlyHandle;
|
|
134
144
|
let injectFn;
|
|
135
145
|
let harvestBoth;
|
|
136
146
|
let toolPlanes;
|
|
147
|
+
const admitNothingOpts = input.memoryPersistenceDeclared === false
|
|
148
|
+
? { admitNothing: { reason: "harvest admitted nothing: memory persistence is declared unavailable for this session (memoryPersistenceCapable: false)" } }
|
|
149
|
+
: {};
|
|
137
150
|
if (dual) {
|
|
151
|
+
const personalBackendChosen = choosePersonalBackend();
|
|
152
|
+
{
|
|
153
|
+
const projCtl = effectiveControlDir(backend, identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot));
|
|
154
|
+
const persCtl = effectiveControlDir(personalBackendChosen, personalControlDir);
|
|
155
|
+
const overlap = (a, b) => isContainedIn(a, b) || isContainedIn(b, a);
|
|
156
|
+
const refuse = (what, a, b) => {
|
|
157
|
+
const e = new Error(`memory dual-root configuration error: ${what} overlap (${canonicalize(a)} vs ${canonicalize(b)}) — the planes' data roots and control planes must all be disjoint.`);
|
|
158
|
+
e.code = "config.memory_dual_root_overlap";
|
|
159
|
+
throw e;
|
|
160
|
+
};
|
|
161
|
+
if (overlap(memoryDir, personalMemoryDir))
|
|
162
|
+
refuse("the project and personal memory roots", memoryDir, personalMemoryDir);
|
|
163
|
+
if (overlap(projCtl, persCtl))
|
|
164
|
+
refuse("the project and personal control planes", projCtl, persCtl);
|
|
165
|
+
for (const [ctlName, ctl] of [["project control plane", projCtl], ["personal control plane", persCtl]]) {
|
|
166
|
+
for (const [rootName, dataRoot] of [["project memory root", memoryDir], ["personal memory mount", personalMemoryDir]]) {
|
|
167
|
+
if (overlap(ctl, dataRoot))
|
|
168
|
+
refuse(`the ${ctlName} and the ${rootName}`, ctl, dataRoot);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
138
172
|
const projectEngine = new MemoryEngine({
|
|
139
173
|
backend,
|
|
140
174
|
memoryDir,
|
|
141
175
|
controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
|
|
142
176
|
onIncident: onEngineIncident,
|
|
143
177
|
});
|
|
144
|
-
const personal = createPersonalEngine();
|
|
178
|
+
const personal = createPersonalEngine(personalBackendChosen);
|
|
145
179
|
const personalEngine = personal.engine;
|
|
146
180
|
const p = planes;
|
|
147
|
-
const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null);
|
|
148
|
-
const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null);
|
|
181
|
+
const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null, { adoptionRestricted });
|
|
182
|
+
const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null, { adoptionRestricted });
|
|
149
183
|
const writeIsPersonal = p.writePlane === "personal";
|
|
150
184
|
writeEngine = writeIsPersonal ? personalEngine : projectEngine;
|
|
151
185
|
writeHandle = writeIsPersonal ? personalHandle : projectHandle;
|
|
186
|
+
readOnlyEngine = writeIsPersonal ? projectEngine : personalEngine;
|
|
187
|
+
readOnlyHandle = writeIsPersonal ? projectHandle : personalHandle;
|
|
152
188
|
injectFn = () => mergeInjections(projectEngine.inject(projectHandle, { writeToolMounted: input.writeToolsMounted }), personalEngine.inject(personalHandle, { writeToolMounted: input.writeToolsMounted }));
|
|
153
189
|
toolPlanes = [
|
|
154
190
|
{
|
|
155
|
-
backend: retrievalBackend(backend),
|
|
191
|
+
backend: retrievalBackend(backend, adoptionRestricted || p.writePlane !== "project"),
|
|
156
192
|
scopes: planeScopes(p.project, p.writePlane === "project" ? memorySpec.writeScope : null),
|
|
157
193
|
recordRetrieved: (ids) => projectEngine.recordRetrieved(ids),
|
|
158
194
|
challengeExclusions: () => projectEngine.readChallengeExclusions(),
|
|
159
195
|
},
|
|
160
196
|
{
|
|
161
|
-
backend: retrievalBackend(personal.backend),
|
|
197
|
+
backend: retrievalBackend(personal.backend, adoptionRestricted || p.writePlane !== "personal"),
|
|
162
198
|
scopes: planeScopes(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null),
|
|
163
199
|
recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
|
|
164
200
|
challengeExclusions: () => personalEngine.readChallengeExclusions(),
|
|
@@ -167,7 +203,7 @@ export async function prepareMemory(input) {
|
|
|
167
203
|
harvestBoth = async () => {
|
|
168
204
|
const writeFirst = writeIsPersonal ? [personalEngine, personalHandle] : [projectEngine, projectHandle];
|
|
169
205
|
const readOther = writeIsPersonal ? [projectEngine, projectHandle] : [personalEngine, personalHandle];
|
|
170
|
-
const writeReport = await writeFirst[0].harvest(writeFirst[1], { ...pollutedOpts(writeFirst[0]), sessionId });
|
|
206
|
+
const writeReport = await writeFirst[0].harvest(writeFirst[1], { ...pollutedOpts(writeFirst[0]), sessionId, ...admitNothingOpts });
|
|
171
207
|
let readReport;
|
|
172
208
|
let readFailure;
|
|
173
209
|
try {
|
|
@@ -183,16 +219,16 @@ export async function prepareMemory(input) {
|
|
|
183
219
|
};
|
|
184
220
|
}
|
|
185
221
|
else if (personalOnly) {
|
|
186
|
-
const personal = createPersonalEngine();
|
|
222
|
+
const personal = createPersonalEngine(choosePersonalBackend());
|
|
187
223
|
const personalEngine = personal.engine;
|
|
188
|
-
const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope);
|
|
224
|
+
const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
|
|
189
225
|
writeEngine = personalEngine;
|
|
190
226
|
writeHandle = handle;
|
|
191
227
|
injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
192
|
-
harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId });
|
|
228
|
+
harvestBoth = () => personalEngine.harvest(handle, { ...pollutedOpts(personalEngine), sessionId, ...admitNothingOpts });
|
|
193
229
|
toolPlanes = [
|
|
194
230
|
{
|
|
195
|
-
backend: retrievalBackend(personal.backend),
|
|
231
|
+
backend: retrievalBackend(personal.backend, adoptionRestricted || memorySpec.writeScope === null),
|
|
196
232
|
scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
|
|
197
233
|
recordRetrieved: (ids) => personalEngine.recordRetrieved(ids),
|
|
198
234
|
challengeExclusions: () => personalEngine.readChallengeExclusions(),
|
|
@@ -206,21 +242,39 @@ export async function prepareMemory(input) {
|
|
|
206
242
|
controlDir: identityKey !== undefined ? deriveProjectControlDir(engineRoot, identityKey) : deriveRepoControlPlaneDir(engineRoot, repoRoot),
|
|
207
243
|
onIncident: onEngineIncident,
|
|
208
244
|
});
|
|
209
|
-
const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope);
|
|
245
|
+
const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
|
|
210
246
|
writeEngine = engine;
|
|
211
247
|
writeHandle = handle;
|
|
212
248
|
injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted });
|
|
213
|
-
harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId });
|
|
249
|
+
harvestBoth = () => engine.harvest(handle, { ...pollutedOpts(engine), sessionId, ...admitNothingOpts });
|
|
214
250
|
toolPlanes = [
|
|
215
251
|
{
|
|
216
|
-
backend: retrievalBackend(backend),
|
|
252
|
+
backend: retrievalBackend(backend, adoptionRestricted || memorySpec.writeScope === null),
|
|
217
253
|
scopes: planeScopes(memorySpec.scopes, memorySpec.writeScope),
|
|
218
254
|
recordRetrieved: (ids) => engine.recordRetrieved(ids),
|
|
219
255
|
challengeExclusions: () => engine.readChallengeExclusions(),
|
|
220
256
|
},
|
|
221
257
|
];
|
|
222
258
|
}
|
|
223
|
-
memoryWriteGateRef.current = (w) =>
|
|
259
|
+
memoryWriteGateRef.current = (w) => {
|
|
260
|
+
if (readOnlyEngine !== undefined && readOnlyHandle !== undefined) {
|
|
261
|
+
const ro = readOnlyEngine.gateWrite(readOnlyHandle, w.key, w.content);
|
|
262
|
+
if (!ro.ok)
|
|
263
|
+
return ro;
|
|
264
|
+
}
|
|
265
|
+
if (input.memoryPersistenceDeclared === false) {
|
|
266
|
+
const root = writeHandle.writableRoot;
|
|
267
|
+
if (w.key === root || w.key.startsWith(`${root}${sep}`)) {
|
|
268
|
+
return {
|
|
269
|
+
ok: false,
|
|
270
|
+
code: "read_only_layering",
|
|
271
|
+
reason: `memory persistence is declared unavailable for this session (memoryPersistenceCapable: false) — writes into the memory store are refused. Nothing was written.`,
|
|
272
|
+
muted: false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return writeEngine.gateWrite(writeHandle, w.key, w.content);
|
|
277
|
+
};
|
|
224
278
|
const harvestSafe = async (phase = "terminal") => {
|
|
225
279
|
try {
|
|
226
280
|
const report = await harvestBoth();
|
|
@@ -261,7 +315,10 @@ export async function prepareMemory(input) {
|
|
|
261
315
|
memoryTools = createMemoryEngineTools({ planes: toolPlanes });
|
|
262
316
|
}
|
|
263
317
|
catch (err) {
|
|
264
|
-
if (typeof err.code === "string" &&
|
|
318
|
+
if (typeof err.code === "string" &&
|
|
319
|
+
(err.code.startsWith("config.memory_scope") ||
|
|
320
|
+
err.code.startsWith("config.memory_project") ||
|
|
321
|
+
err.code === "config.memory_dual_root_overlap")) {
|
|
265
322
|
throw err;
|
|
266
323
|
}
|
|
267
324
|
deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "memory", sessionId });
|
|
@@ -275,15 +332,31 @@ export async function prepareMemory(input) {
|
|
|
275
332
|
}
|
|
276
333
|
if (memoryEngineSession) {
|
|
277
334
|
const injection = memoryEngineSession.inject();
|
|
278
|
-
|
|
279
|
-
|
|
335
|
+
let blockBody = injection.block;
|
|
336
|
+
if (!input.rosterCanPersist && injection.instruction === "" && injection.readOnlyNotice === undefined) {
|
|
337
|
+
blockBody = blockBody.trim() ? `${MEMORY_READONLY_NOTICE}\n\n${blockBody}` : MEMORY_READONLY_NOTICE;
|
|
338
|
+
}
|
|
339
|
+
else if (!input.rosterCanPersist && injection.instruction !== "") {
|
|
340
|
+
blockBody = [MEMORY_READONLY_NOTICE, injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
341
|
+
}
|
|
342
|
+
else if (input.memoryPersistenceDeclared === true && injection.readOnlyNotice !== undefined) {
|
|
343
|
+
blockBody = [injection.index, injection.announceBlock].filter((s) => Boolean(s && s.trim())).join("\n\n");
|
|
344
|
+
}
|
|
345
|
+
if (!input.rosterCanPersist &&
|
|
346
|
+
(injection.announceBlock?.trim() ?? "") !== "" &&
|
|
347
|
+
blockBody.includes(injection.announceBlock) &&
|
|
348
|
+
!blockBody.trimEnd().endsWith(MEMORY_ANNOUNCEMENT_READONLY_CODA)) {
|
|
349
|
+
blockBody = `${blockBody}\n\n${MEMORY_ANNOUNCEMENT_READONLY_CODA}`;
|
|
350
|
+
}
|
|
351
|
+
if (blockBody.trim())
|
|
352
|
+
memoryBlock = blockBody;
|
|
280
353
|
if (memoryTools !== undefined) {
|
|
281
354
|
memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_RECALL_DISCIPLINE}` : MEMORY_RECALL_DISCIPLINE;
|
|
282
355
|
}
|
|
283
|
-
if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted) {
|
|
356
|
+
if (memoryEngineSession.handle.writeScope !== null && input.writeToolsMounted && input.rosterCanPersist) {
|
|
284
357
|
memoryBlock = memoryBlock !== undefined ? `${memoryBlock}\n\n${MEMORY_PREFERENCE_DISCIPLINE}` : MEMORY_PREFERENCE_DISCIPLINE;
|
|
285
358
|
}
|
|
286
|
-
if (memoryBlock !== undefined && injection.indexSeed !== undefined)
|
|
359
|
+
if (memoryBlock !== undefined && injection.indexSeed !== undefined && input.rosterCanPersist)
|
|
287
360
|
seedFiles = [injection.indexSeed];
|
|
288
361
|
}
|
|
289
362
|
return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, ...(memoryTools !== undefined ? { memoryTools } : {}), ...(seedFiles !== undefined ? { seedFiles } : {}) };
|
|
@@ -354,7 +354,10 @@ export interface Prepared {
|
|
|
354
354
|
* deferred; a caller with no accessor has no deferred family to describe. */
|
|
355
355
|
staticFaceFor?: (name: string) => boolean;
|
|
356
356
|
/**
|
|
357
|
-
* design/138 S1 — the MemoryEngine session
|
|
357
|
+
* design/138 S1 — the MemoryEngine session. Present when `deps.memoryBackend` + `spec.memory.enabled`
|
|
358
|
+
* hold AND the engine mount succeeded: a materialize failure without a `config.memory_*` code is
|
|
359
|
+
* fail-open (reported via `deps.onError`, the task runs memory-less), leaving this absent even though
|
|
360
|
+
* both flags hold.
|
|
358
361
|
* `harvest` is the swallow-guarded boundary hook (task terminal in runtask + the checkpoint mint
|
|
359
362
|
* point in commitSuspendSaga): it runs the FULL gate set (containment/secret/caps/deletion fuse),
|
|
360
363
|
* commits entry patches to the backend, self-heals the derived index, and re-baselines (a second
|
|
@@ -1296,8 +1299,11 @@ export interface RunInternals {
|
|
|
1296
1299
|
* design/99 (nested-subagent live tree) — an OPT-IN, DISPLAY-ONLY event sink a deployment sets on the TOP run to
|
|
1297
1300
|
* receive a subagent's live `task_progress` ticks (which otherwise stay in the child's ISOLATED stream). Threaded
|
|
1298
1301
|
* recursively down the delegation tree (via `ctx.forwardEvent`), so every nested subagent's ticks bubble to the
|
|
1299
|
-
* SAME sink. The Runner forwards
|
|
1300
|
-
*
|
|
1302
|
+
* SAME sink. The Runner's ctx wrapper forwards `task_progress` always; when the run's spec sets
|
|
1303
|
+
* `forwardSubagentEvents: true` it ALSO forwards the child's content events (`text_delta` / `reasoning_delta` /
|
|
1304
|
+
* `tool_start` / `tool_end` — the subagent viewing pane, carrying the same UNTRUSTED-RAW/consumer-must-redact
|
|
1305
|
+
* contract as the main stream's tool events). Either way the child stream is NEVER merged into the parent's
|
|
1306
|
+
* MODEL context (this is purely a render channel). Absent unless the deployment opted in.
|
|
1301
1307
|
*/
|
|
1302
1308
|
onForwardEvent?: (event: TaskEvent) => void;
|
|
1303
1309
|
/**
|