@sema-agent/core 5.35.0 → 5.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist/agents/subagent.d.ts +10 -0
  3. package/dist/agents/subagent.js +29 -2
  4. package/dist/core/auto-compaction.d.ts +23 -0
  5. package/dist/core/auto-compaction.js +8 -0
  6. package/dist/core/checkpoint-store.d.ts +16 -0
  7. package/dist/core/context-guard.d.ts +41 -0
  8. package/dist/core/context-guard.js +76 -0
  9. package/dist/core/governance-codes.js +4 -0
  10. package/dist/core/memory-engine/engine.d.ts +142 -0
  11. package/dist/core/memory-engine/engine.js +265 -3
  12. package/dist/core/memory-engine/file-backend.d.ts +490 -16
  13. package/dist/core/memory-engine/file-backend.js +1099 -36
  14. package/dist/core/memory-engine/index.d.ts +2 -2
  15. package/dist/core/memory-engine/index.js +1 -1
  16. package/dist/core/memory-engine/layout.d.ts +42 -2
  17. package/dist/core/memory-engine/layout.js +76 -12
  18. package/dist/core/memory-engine/memory-backend-contract.d.ts +13 -0
  19. package/dist/core/memory-engine/memory-backend-contract.js +89 -0
  20. package/dist/core/park-selfcheck.d.ts +5 -0
  21. package/dist/core/protocol-table.d.ts +4 -4
  22. package/dist/core/runner/assemble-result.d.ts +8 -0
  23. package/dist/core/runner/assemble-result.js +4 -1
  24. package/dist/core/runner/git-status-frame.d.ts +219 -0
  25. package/dist/core/runner/git-status-frame.js +212 -0
  26. package/dist/core/runner/prepare-memory.d.ts +11 -1
  27. package/dist/core/runner/prepare-memory.js +48 -2
  28. package/dist/core/runner/prepare-task.d.ts +21 -0
  29. package/dist/core/runner/prepare-task.js +28 -35
  30. package/dist/core/runner/runtask.js +270 -5
  31. package/dist/core/task-registry-agent.d.ts +15 -0
  32. package/dist/core/task-registry-agent.js +9 -0
  33. package/dist/core/task-registry.d.ts +3 -0
  34. package/dist/core/task-registry.js +4 -1
  35. package/dist/core/types.d.ts +122 -7
  36. package/dist/engine/harness/types.d.ts +65 -1
  37. package/dist/engine/harness/types.js +20 -0
  38. package/dist/engine/session/import-validate.js +10 -1
  39. package/dist/engine/session/session.d.ts +37 -1
  40. package/dist/engine/session/session.js +56 -1
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.js +1 -1
  43. package/dist/internal/harness-types.d.ts +1 -0
  44. package/dist/internal/harness.d.ts +2 -0
  45. package/dist/internal/harness.js +2 -0
  46. package/dist/prompt-assembly/epoch.js +1 -1
  47. package/dist/prompt-assembly/event-registry.js +1 -0
  48. package/dist/prompts/default.d.ts +20 -7
  49. package/dist/prompts/default.js +2 -7
  50. package/package.json +1 -1
  51. package/test/export-surface.snapshot.json +13 -1
@@ -0,0 +1,219 @@
1
+ import type { AgentMessage, ExecutionEnv, GitAnnouncementKind, SessionTreeEntry } from "../../internal/harness-types.js";
2
+ /** Bound into the hash domain: bump when the frame's wording/structure changes so the upgraded
3
+ * renderer re-announces on its first leg instead of being suppressed by a pre-upgrade hash. */
4
+ export declare const GIT_STATUS_FRAME_FORMAT_VERSION = 1;
5
+ /**
6
+ * The frame's own first paragraph (replaces the CC "will not update during the conversation" head,
7
+ * which would be FALSE under the frame protocol). Wording obligations: self-declares the update
8
+ * semantics (latest frame supersedes earlier ones), promises re-send on VISIBLE-view change only
9
+ * (truncation-bound honesty), and "observed while preparing" does not claim an atomic instant (the
10
+ * probe is two shell round-trips and a write can land between them).
11
+ */
12
+ export declare const GIT_STATUS_FRAME_PREAMBLE = "This is the git status observed while preparing this request. When the visible snapshot changes it will be re-sent in a later message \u2014 the most recent git status frame supersedes earlier ones.";
13
+ /** Tombstone body — the probes could not reach git at all while earlier frames announced a
14
+ * positive view (sent once, on the availability flip edge only). */
15
+ export declare const GIT_STATUS_UNAVAILABLE_BODY = "Git status is currently unavailable; the most recent git status frame above may be stale.";
16
+ /** Tombstone body — the directory stopped being a git repository while earlier frames announced a
17
+ * positive view (sent once, on the flip edge; the env block's static is-repo line flips the same leg). */
18
+ export declare const GIT_STATUS_NON_REPO_BODY = "The working directory is no longer a git repository; earlier git status frames no longer apply.";
19
+ /** The `steering_injected` echo previews for the frame — CONSTANT wordings on purpose: branch and
20
+ * status text are repo-controlled and must not enter the event telemetry plane through the echo. */
21
+ export declare const GIT_STATUS_ECHO_PREVIEW: Record<GitAnnouncementKind, string>;
22
+ /** The raw material of one probe cycle, as prepare-task resolved it (§4.3 unified ladder):
23
+ * - `full` — §E14 and the H4 snapshot both succeeded: `snapshot` = the pre-rendered
24
+ * {@link import("../../prompts/default.js").buildGitSnapshot} block (CC template,
25
+ * sanitized + bounded by its renderer);
26
+ * - `degraded` — §E14 yielded git facts but the H4 snapshot failed (exit 41/42, timeout, sentinel
27
+ * mis-split): branch + dirtiness are all that is honestly known;
28
+ * - `unavailable` — the §E14 probe itself failed (git facts unknowable this leg);
29
+ * - `non-repo` — the probe ran and the cwd is not a git repository. */
30
+ export type GitStatusProbeOutcome = {
31
+ kind: "full";
32
+ snapshot: string;
33
+ } | {
34
+ kind: "degraded";
35
+ branch?: string;
36
+ dirty?: boolean;
37
+ } | {
38
+ kind: "unavailable";
39
+ } | {
40
+ kind: "non-repo";
41
+ };
42
+ /** Render the frame BODY (the text inside the `<system-reminder>` shell) for one probe outcome. */
43
+ export declare function renderGitStatusFrameBody(outcome: GitStatusProbeOutcome): string;
44
+ /**
45
+ * Content hash of a rendered frame body — the hash half of the `(kind, hash)` comparison tuple.
46
+ * Digest domain = format version + canonical repo root + kind + body, NUL-separated (none of the
47
+ * inputs may contain NUL: the body is sanitized text, the root a canonical path). `canonicalRoot`
48
+ * is the ExecutionEnv-canonicalized worktree root (or cwd outside a repo) so identical bytes from
49
+ * two different checkouts never share an announcement.
50
+ */
51
+ export declare function hashGitStatusFrame(kind: GitAnnouncementKind, body: string, canonicalRoot: string): string;
52
+ /** One resolved frame — outcome rendered and hashed, ready for the (kind, hash) compare. */
53
+ export interface ResolvedGitStatusFrame {
54
+ kind: GitAnnouncementKind;
55
+ body: string;
56
+ hash: string;
57
+ /** Pre-rendered DEGRADED body + hash for the same probe cycle (present only when kind is
58
+ * `full`): the one-shot deterministic shrink target of the irreducible-core over-budget arc —
59
+ * computed at prepare time because the request-build path must not re-run probes. */
60
+ shrunk?: {
61
+ body: string;
62
+ hash: string;
63
+ };
64
+ }
65
+ /** Render + hash one probe outcome (and its deterministic degraded shrink target when full). */
66
+ export declare function resolveGitStatusFrame(outcome: GitStatusProbeOutcome, canonicalRoot: string, degradedMaterial?: {
67
+ branch?: string;
68
+ dirty?: boolean;
69
+ }): ResolvedGitStatusFrame;
70
+ /**
71
+ * The run-local git-status lane state (one per run, on `Prepared`) — the coordination surface
72
+ * between prepare (probe resolution), the run loop (delivery + receipt + compaction re-assertion)
73
+ * and the request-build context handler (trim protection). Deliberately a plain mutable ref, the
74
+ * `announcedListingsRef` discipline.
75
+ */
76
+ export interface GitStatusLaneRef {
77
+ /** This leg's resolved frame (undefined ⇒ hands-less leg: the lane is out of scope, announced
78
+ * state untouched — the env block equally carried no git facts for such a leg). */
79
+ frame?: ResolvedGitStatusFrame;
80
+ /** Canonical repo root the frame was hashed against. */
81
+ canonicalRoot?: string;
82
+ /** The announced state as THIS run knows it (seeded from the read ladder at leg start, advanced
83
+ * at each receipt) — mirrored onto the suspend checkpoint. `pending: true` ⇒ a re-announcement
84
+ * is owed (frame append failed / compaction restated pending); the next boundary retries. */
85
+ announced?: {
86
+ kind: GitAnnouncementKind;
87
+ hash: string;
88
+ entryId?: string;
89
+ pending?: true;
90
+ };
91
+ /** Exact WRAPPED text of the newest announced frame — the frame SEGMENT, on both carry forms
92
+ * (rescan doc-rot fix: r1 moved this off "the whole first-message text"; the contract here had
93
+ * kept the pre-r1 words). The context guard finds the carrier by CONTAINING this engine-held
94
+ * string (engine-region gated), protects that carrier as a replace-by-key slot — only the newest
95
+ * carrier is protected, older frames trim like ordinary history — and charges the irreducible
96
+ * core by THIS segment, never by the whole carrier (the objective riding the same message keeps
97
+ * its own overflow exit). */
98
+ protectedText?: string;
99
+ /** One-shot latch of the irreducible-core arc's DISCLOSURE: the request view substituted the
100
+ * degraded rendering at least once (the substitution itself is per-request deterministic; the
101
+ * disclosure + pending re-announcement fire only on the first). */
102
+ overBudgetShrunk?: boolean;
103
+ /** The exact WRAPPED frame segment of the protected carrier and its degraded substitution target
104
+ * (both `<system-reminder>`-shelled, exactly as delivered) — the irreducible-core shrink is a
105
+ * literal string replace of `find` with `replace` inside the carrier's text. Set at delivery
106
+ * alongside {@link GitStatusLaneRef.protectedText}; present only for a full-kind frame. */
107
+ wrappedShrink?: {
108
+ find: string;
109
+ replace: string;
110
+ };
111
+ /** Wired by the run loop once its queue exists: re-assert the current frame (compaction landing
112
+ * + boundary retry both call this). */
113
+ reassert?: () => Promise<void>;
114
+ /** F5 (falsification round 1) — the typed terminal's surface bridge: the loop converts a
115
+ * context-hook throw into a failure MESSAGE (text only), so the thrown `code` never reaches
116
+ * result assembly on its own. Set alongside the throw; the run loop lifts it into
117
+ * `TaskResult.errorCode`. */
118
+ terminalCode?: "irreducible_core_over_budget";
119
+ /** F4 (falsification round 1) — the receipt's mirror write, PARKED instead of fired: a
120
+ * fire-and-forget CAS append from inside the message_end walk can race the loop's own next
121
+ * transcript append and fail the CRITICAL write with a conflict. The run loop flushes this at
122
+ * serialization points only (turn boundary, prompt settle); an unflushed slot at suspend is
123
+ * covered by the checkpoint rung (the frame IS on the branch — visibility check passes). */
124
+ mirrorOwed?: {
125
+ kind: GitAnnouncementKind;
126
+ hash: string;
127
+ entryId: string;
128
+ };
129
+ /** The duplicate-tolerant receipt slot: set when a frame delivery is in flight, settled by the
130
+ * run loop's `message_end` walk when the CARRYING message commits (matched on the exact
131
+ * engine-held message text). Commit-on-receipt, never commit-before-append — the git lane's
132
+ * error asymmetry is the REVERSE of the listing lane's (a false "announced" = stale git facts
133
+ * forever; a duplicate frame = a few idempotent kilobytes). */
134
+ pendingReceipt?: {
135
+ text: string;
136
+ commit: (entryId: string) => void;
137
+ };
138
+ }
139
+ /**
140
+ * prepare-side probe half of the lane (H4, extracted from prepareTask under the design/238 D-7
141
+ * body ratchet): run the CC-shape snapshot round-trip through the SAME ExecutionEnv seam when the
142
+ * §E14 probe confirmed a repo, resolve the outcome through the §4.3 unified kind ladder, and hash
143
+ * the rendered frame with the canonical repo root bound in. `envFacts` carries the §E14 results
144
+ * (read-only here). Every degrade names its reason through `onDegrade` — including the sentinel
145
+ * MIS-SPLIT arm (parts !== 4), which previously dropped the snapshot with ZERO telemetry (the
146
+ * loud-bad-value fix). Returns the run's lane ref (empty on a hands-less leg).
147
+ *
148
+ * H4 anchor notes carried from the prepareTask body: four sections split by a sentinel line —
149
+ * main-branch inference (CC qP symref → [inferred, main, master] each show-ref → renderer falls
150
+ * back "main"), `git config user.name`, `git --no-optional-locks status --short`, and
151
+ * `git --no-optional-locks log --oneline -n 5` (CC-exact commands). 1.256 复审 MED-4: the two
152
+ * REQUIRED sections fail the WHOLE script with distinct exit codes (41 = status, 42 = log) so a
153
+ * failing status never half-renders as "(clean)"; both now DEGRADE the frame to the branch+dirty
154
+ * residual instead of silently skipping. The main-branch/user sections stay best-effort.
155
+ */
156
+ export declare function probeGitStatusLane(args: {
157
+ executionEnv: Pick<ExecutionEnv, "exec" | "canonicalPath">;
158
+ envFacts: {
159
+ isGitRepo?: boolean;
160
+ gitBranch?: string;
161
+ gitDirty?: boolean;
162
+ gitWorktreeRoot?: string;
163
+ cwd?: string;
164
+ };
165
+ handsEnabled: boolean;
166
+ taskRoot: string;
167
+ onDegrade: (reason: string) => void;
168
+ }): Promise<GitStatusLaneRef>;
169
+ /**
170
+ * Request-build half of the trim protection (extracted from prepareTask's context handler under the
171
+ * same D-7 ratchet): wraps {@link protectGitFrame} with the lane ref's state — the substitution
172
+ * pair, the one-shot disclosure latch and the pending re-announcement on the first shrink — and
173
+ * converts the over-budget verdict into the LOUD typed terminal (`irreducible_core_over_budget`),
174
+ * never a pretended trim success. Returns the (possibly re-inserted / shrunk) request view.
175
+ */
176
+ export declare function applyGitFrameGuard(args: {
177
+ before: AgentMessage[];
178
+ trimmed: AgentMessage[];
179
+ budgetTokens: number;
180
+ ref: GitStatusLaneRef;
181
+ charsPerToken?: number;
182
+ onDegrade: (message: string) => void;
183
+ }): AgentMessage[];
184
+ /**
185
+ * R2-1 + R3-1/R3-2 + r4-2 (falsification rounds 2-4) — the transcript-side classifier of engine
186
+ * git frames. The mirror plane is a CACHE of "which frame is newest"; the TRANSCRIPT is the truth,
187
+ * and the two diverge exactly when a mirror write was lost. This scan finds the NEWEST engine git
188
+ * frame on the branch, with TOP-LEVEL WRAPPED-UNIT matching:
189
+ * - positive frame: the wrapped unit's head (open tag + newline + the frame preamble);
190
+ * - tombstone: the exact wrapped unit (open tag + newline + tombstone body + newline + close tag);
191
+ * - the occurrence must sit inside the message's ENGINE region (engineMinted / enginePrefixChars
192
+ * / engineSegments — metadata gates, never user-text shape-guessing), AND at TOP LEVEL (r4-2:
193
+ * start-of-text or right after a close tag — a hook relay that QUOTES the exact frame unit
194
+ * nests it inside its own shell with prose before the open tag, and is rejected);
195
+ * - within one message the LAST top-level unit wins (textual order = issue order).
196
+ */
197
+ export declare function newestEngineGitFrame(branch: SessionTreeEntry[]): {
198
+ entryId: string;
199
+ positive: boolean;
200
+ } | undefined;
201
+ /**
202
+ * Remove POSITIVE git_status wrapped units from an engine-region text before a downstream parser
203
+ * scans it (rescan P3): the frame embeds REPO-CONTROLLED lines (branch names, commit subjects —
204
+ * tag-neutralized at wrap, but plain text rides verbatim), and the listing-replay parser reads
205
+ * engine-region text as trusted state — a commit subject spelling a listing header could reset the
206
+ * announced-set to empty (one spurious roster re-announcement). Tombstone units are constants
207
+ * (zero repo text) and need no stripping. The wrap sanitizer neutralizes the system-reminder CLOSE
208
+ * TAG inside the body (not `<` generally — refuter-precision note), so a unit's body cannot
209
+ * contain the close tag and the first close tag after a head is that unit's own on every
210
+ * contiguous region sema mints; the ownership check below is defense-in-depth for foreign text.
211
+ */
212
+ export declare function stripGitStatusUnits(text: string): string;
213
+ /**
214
+ * The readable-absence tombstone question (R2-1), answered by the classifier above: does the
215
+ * branch carry a still-context-visible POSITIVE engine frame as its newest git frame? A newest
216
+ * TOMBSTONE means the disowning already happened (no re-spam); no frame at all means a genuinely
217
+ * fresh lane (no tombstone out of nowhere).
218
+ */
219
+ export declare function branchCarriesVisiblePositiveGitFrame(branch: SessionTreeEntry[]): boolean;
@@ -0,0 +1,212 @@
1
+ import { createHash } from "node:crypto";
2
+ import { GIT_SNAPSHOT_CC_PREAMBLE, buildGitSnapshot } from "../../prompts/default.js";
3
+ import { inlineUntrusted } from "../untrusted-text.js";
4
+ import { engineRegionCovers, protectGitFrame } from "../context-guard.js";
5
+ import { gitFrameContextVisible } from "../../internal/harness.js";
6
+ export const GIT_STATUS_FRAME_FORMAT_VERSION = 1;
7
+ export const GIT_STATUS_FRAME_PREAMBLE = "This is the git status observed while preparing this request. When the visible snapshot changes it will be re-sent in a later message — the most recent git status frame supersedes earlier ones.";
8
+ export const GIT_STATUS_UNAVAILABLE_BODY = "Git status is currently unavailable; the most recent git status frame above may be stale.";
9
+ export const GIT_STATUS_NON_REPO_BODY = "The working directory is no longer a git repository; earlier git status frames no longer apply.";
10
+ export const GIT_STATUS_ECHO_PREVIEW = {
11
+ full: "git status updated",
12
+ degraded: "git status updated (degraded: branch and dirtiness only)",
13
+ unavailable: "git status unavailable",
14
+ "non-repo": "git status: no longer a git repository",
15
+ };
16
+ function toWellFormedText(s) {
17
+ const native = s.toWellFormed;
18
+ if (typeof native === "function")
19
+ return native.call(s);
20
+ return s.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, "�");
21
+ }
22
+ export function renderGitStatusFrameBody(outcome) {
23
+ switch (outcome.kind) {
24
+ case "full": {
25
+ const body = outcome.snapshot.startsWith(GIT_SNAPSHOT_CC_PREAMBLE)
26
+ ? GIT_STATUS_FRAME_PREAMBLE + outcome.snapshot.slice(GIT_SNAPSHOT_CC_PREAMBLE.length)
27
+ : `${GIT_STATUS_FRAME_PREAMBLE}\n\n${outcome.snapshot}`;
28
+ return toWellFormedText(body);
29
+ }
30
+ case "degraded": {
31
+ const lines = [GIT_STATUS_FRAME_PREAMBLE];
32
+ lines.push(`Current branch: ${inlineUntrusted(outcome.branch ?? "HEAD")}`);
33
+ if (outcome.dirty !== undefined) {
34
+ lines.push(`Git working tree: ${outcome.dirty ? "has uncommitted changes" : "clean"}`);
35
+ }
36
+ return toWellFormedText(lines.join("\n\n"));
37
+ }
38
+ case "unavailable":
39
+ return GIT_STATUS_UNAVAILABLE_BODY;
40
+ case "non-repo":
41
+ return GIT_STATUS_NON_REPO_BODY;
42
+ }
43
+ }
44
+ export function hashGitStatusFrame(kind, body, canonicalRoot) {
45
+ return `sha256:${createHash("sha256")
46
+ .update(`${GIT_STATUS_FRAME_FORMAT_VERSION}\u0000${canonicalRoot}\u0000${kind}\u0000${body}`)
47
+ .digest("hex")}`;
48
+ }
49
+ export function resolveGitStatusFrame(outcome, canonicalRoot, degradedMaterial) {
50
+ const body = renderGitStatusFrameBody(outcome);
51
+ const resolved = {
52
+ kind: outcome.kind,
53
+ body,
54
+ hash: hashGitStatusFrame(outcome.kind, body, canonicalRoot),
55
+ };
56
+ if (outcome.kind === "full") {
57
+ const shrunkBody = renderGitStatusFrameBody({ kind: "degraded", ...(degradedMaterial ?? {}) });
58
+ resolved.shrunk = { body: shrunkBody, hash: hashGitStatusFrame("degraded", shrunkBody, canonicalRoot) };
59
+ }
60
+ return resolved;
61
+ }
62
+ export async function probeGitStatusLane(args) {
63
+ const { executionEnv, envFacts, handsEnabled, taskRoot, onDegrade } = args;
64
+ const ref = {};
65
+ if (!handsEnabled)
66
+ return ref;
67
+ let outcome;
68
+ if (envFacts.isGitRepo === true) {
69
+ const SEP = "@@SEMA_ENV_GIT_SPLIT@@";
70
+ outcome = {
71
+ kind: "degraded",
72
+ ...(envFacts.gitBranch !== undefined ? { branch: envFacts.gitBranch } : {}),
73
+ ...(envFacts.gitDirty !== undefined ? { dirty: envFacts.gitDirty } : {}),
74
+ };
75
+ try {
76
+ const snap = await executionEnv.exec(`(m=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null); m=\${m#origin/}; for s in "$m" main master; do if [ -n "$s" ] && git show-ref --verify --quiet "refs/remotes/origin/$s"; then echo "$s"; break; fi; done) || true; echo "${SEP}"; (git config user.name 2>/dev/null || true); echo "${SEP}"; gs=$(git --no-optional-locks status --short) || exit 41; printf '%s\\n' "$gs"; echo "${SEP}"; gl=$(git --no-optional-locks log --oneline -n 5) || exit 42; printf '%s\\n' "$gl"`, { cwd: envFacts.cwd, timeout: 10 });
77
+ if (snap.ok && snap.value.exitCode === 0) {
78
+ const parts = snap.value.stdout.split(`${SEP}\n`);
79
+ if (parts.length === 4) {
80
+ outcome = {
81
+ kind: "full",
82
+ snapshot: buildGitSnapshot({
83
+ branch: envFacts.gitBranch ?? "HEAD",
84
+ mainBranch: parts[0].trim() || "main",
85
+ ...(parts[1].trim() ? { userName: parts[1].trim() } : {}),
86
+ status: parts[2],
87
+ log: parts[3],
88
+ }),
89
+ };
90
+ }
91
+ else {
92
+ onDegrade(`sentinel mis-split (${parts.length} sections, expected 4)`);
93
+ }
94
+ }
95
+ else {
96
+ onDegrade(snap.ok
97
+ ? snap.value.exitCode === 41
98
+ ? "git status failed (exit 41)"
99
+ : snap.value.exitCode === 42
100
+ ? "git log failed (exit 42)"
101
+ : `git exited ${snap.value.exitCode}`
102
+ : `exec failed: ${snap.error.message}`);
103
+ }
104
+ }
105
+ catch (err) {
106
+ onDegrade(err instanceof Error ? err.message : String(err));
107
+ }
108
+ }
109
+ else if (envFacts.isGitRepo === false) {
110
+ outcome = { kind: "non-repo" };
111
+ }
112
+ else {
113
+ outcome = { kind: "unavailable" };
114
+ }
115
+ const rootRaw = envFacts.gitWorktreeRoot ?? envFacts.cwd ?? taskRoot;
116
+ let canonicalRoot = rootRaw;
117
+ try {
118
+ const c = await executionEnv.canonicalPath(rootRaw);
119
+ if (c.ok)
120
+ canonicalRoot = c.value;
121
+ }
122
+ catch {
123
+ }
124
+ ref.canonicalRoot = canonicalRoot;
125
+ ref.frame = resolveGitStatusFrame(outcome, canonicalRoot, {
126
+ ...(envFacts.gitBranch !== undefined ? { branch: envFacts.gitBranch } : {}),
127
+ ...(envFacts.gitDirty !== undefined ? { dirty: envFacts.gitDirty } : {}),
128
+ });
129
+ return ref;
130
+ }
131
+ export function applyGitFrameGuard(args) {
132
+ const { before, trimmed, budgetTokens, ref, charsPerToken, onDegrade } = args;
133
+ if (ref.protectedText === undefined)
134
+ return trimmed;
135
+ const guarded = protectGitFrame(before, trimmed, budgetTokens, {
136
+ protectedText: ref.protectedText,
137
+ ...(ref.frame?.kind === "full" && ref.frame.shrunk !== undefined && ref.wrappedShrink !== undefined ? { substitute: ref.wrappedShrink } : {}),
138
+ }, charsPerToken);
139
+ if (guarded.action === "over_budget") {
140
+ ref.terminalCode = "irreducible_core_over_budget";
141
+ throw Object.assign(new Error("irreducible core over budget: the compaction summary plus the git status frame alone exceed the request budget — no trim can produce an honest request"), { code: "irreducible_core_over_budget" });
142
+ }
143
+ if (guarded.action === "shrunk" && !ref.overBudgetShrunk) {
144
+ ref.overBudgetShrunk = true;
145
+ if (ref.announced !== undefined)
146
+ ref.announced = { ...ref.announced, pending: true };
147
+ onDegrade("git status frame degraded under budget pressure: the full snapshot no longer fits the request budget — requests carry the branch+dirty residual; the degraded view will be re-announced");
148
+ }
149
+ return guarded.messages;
150
+ }
151
+ export function newestEngineGitFrame(branch) {
152
+ const positiveHead = `<system-reminder>\n${GIT_STATUS_FRAME_PREAMBLE}`;
153
+ const tombUnits = [`<system-reminder>\n${GIT_STATUS_UNAVAILABLE_BODY}\n</system-reminder>`, `<system-reminder>\n${GIT_STATUS_NON_REPO_BODY}\n</system-reminder>`];
154
+ const topLevel = (text, at) => {
155
+ if (at === 0)
156
+ return true;
157
+ const before = text.slice(0, at).replace(/\n+$/, "");
158
+ return before === "" || before.endsWith("</system-reminder>");
159
+ };
160
+ const allTopLevel = (text, needle, m) => {
161
+ const out = [];
162
+ for (let at = text.indexOf(needle); at !== -1; at = text.indexOf(needle, at + 1)) {
163
+ if (topLevel(text, at) && engineRegionCovers(m, at, needle.length))
164
+ out.push(at);
165
+ }
166
+ return out;
167
+ };
168
+ for (let i = branch.length - 1; i >= 0; i--) {
169
+ const entry = branch[i];
170
+ if (entry === undefined || entry.type !== "message")
171
+ continue;
172
+ const m = entry.message;
173
+ if (m.role !== "user")
174
+ continue;
175
+ const c = m.content;
176
+ const text = typeof c === "string" ? c : Array.isArray(c) && c.length >= 1 && c[0].type === "text" ? (c[0].text ?? "") : undefined;
177
+ if (text === undefined)
178
+ continue;
179
+ let best;
180
+ for (const at of allTopLevel(text, positiveHead, m)) {
181
+ if (best === undefined || at > best.at)
182
+ best = { at, positive: true };
183
+ }
184
+ for (const unit of tombUnits) {
185
+ for (const at of allTopLevel(text, unit, m)) {
186
+ if (best === undefined || at > best.at)
187
+ best = { at, positive: false };
188
+ }
189
+ }
190
+ if (best !== undefined)
191
+ return { entryId: entry.id, positive: best.positive };
192
+ }
193
+ return undefined;
194
+ }
195
+ export function stripGitStatusUnits(text) {
196
+ const head = `<system-reminder>\n${GIT_STATUS_FRAME_PREAMBLE}`;
197
+ const close = "</system-reminder>";
198
+ let out = text;
199
+ for (let at = out.indexOf(head); at !== -1; at = out.indexOf(head)) {
200
+ const end = out.indexOf(close, at);
201
+ if (end === -1)
202
+ break;
203
+ if (out.slice(at + head.length, end).includes("<system-reminder>"))
204
+ break;
205
+ out = out.slice(0, at) + out.slice(end + close.length);
206
+ }
207
+ return out;
208
+ }
209
+ export function branchCarriesVisiblePositiveGitFrame(branch) {
210
+ const newest = newestEngineGitFrame(branch);
211
+ return newest !== undefined && newest.positive && gitFrameContextVisible(branch, newest.entryId);
212
+ }
@@ -1,4 +1,4 @@
1
- import type { BeforeWriteHook, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
1
+ import type { BeforeWriteHook, EffectiveMemoryScopes, RunnerDeps, TaskSpec, ToolSpec } from "../types.js";
2
2
  import type { Prepared } from "./prepare-task.js";
3
3
  export interface PrepareMemoryInput {
4
4
  spec: TaskSpec;
@@ -106,5 +106,15 @@ export interface PrepareMemoryResult {
106
106
  /** The session's own frozen verdict to persist at suspend — present iff this leg ADJUDICATED an
107
107
  * org plane (including adjudicated-empty); `undefined` for an org-less task. */
108
108
  ownOrgVerdict: import("../memory-admission.js").OwnOrgAdmissionVerdict | undefined;
109
+ /**
110
+ * design/178 v2 §2.3 (件①) — the memory-visibility OBSERVATION this phase minted for
111
+ * `TaskResult.effectiveMemoryScopes` (see {@link EffectiveMemoryScopes} for the state law).
112
+ * ALWAYS present when the phase returns: the memory-less outcomes are their own values
113
+ * (`none` / `memoryless`), never an absent seat — absence on the RESULT means only "prepare
114
+ * never completed" (this phase threw). Minted AFTER the materialize outcome (the fail-open arm
115
+ * sits between the admission verdict and the mount — an earlier stamp would report a mount that
116
+ * never happened); the deliberate-refusal `config.memory_*` throws produce no value at all.
117
+ */
118
+ effectiveMemoryScopes: EffectiveMemoryScopes;
109
119
  }
110
120
  export declare function prepareMemory(input: PrepareMemoryInput): Promise<PrepareMemoryResult>;
@@ -12,12 +12,16 @@ export async function prepareMemory(input) {
12
12
  const memorySpec = normalizeMemorySpec(spec.memory);
13
13
  let admittedOrgScopes = [];
14
14
  let ownOrgVerdict;
15
+ const deploymentScopeSet = new Set(deps.deploymentMemoryScopes ?? []);
16
+ const scopeOriginsSnapshot = memorySpec?.scopeOrigins !== undefined ? { ...memorySpec.scopeOrigins } : undefined;
17
+ if (memorySpec && scopeOriginsSnapshot !== undefined)
18
+ memorySpec.scopeOrigins = scopeOriginsSnapshot;
15
19
  if (memorySpec && memorySpec.enabled && deps.memoryBackend) {
16
20
  const outcome = await admitMemoryScopes({
17
21
  memorySpec,
18
22
  principal: spec.principal,
19
23
  admission: deps.memoryScopeAdmission,
20
- deploymentScopes: new Set(deps.deploymentMemoryScopes ?? []),
24
+ deploymentScopes: deploymentScopeSet,
21
25
  orgMemoryDenied: admissionCtx.orgMemoryDenied,
22
26
  complianceDegraded: admissionCtx.complianceDegraded,
23
27
  parentAdmittedOrgScopes: admissionCtx.parentAdmittedOrgScopes,
@@ -38,6 +42,15 @@ export async function prepareMemory(input) {
38
42
  const useMemoryEngine = Boolean(memorySpec && memorySpec.enabled && deps.memoryBackend);
39
43
  let memoryEngineSession;
40
44
  let memoryTools;
45
+ let effectiveMemoryScopes;
46
+ const originOf = (scope) => scopeOriginsSnapshot !== undefined
47
+ ? scopeOriginsSnapshot[scope] === "deployment"
48
+ ? "deployment"
49
+ : "request"
50
+ : deploymentScopeSet.has(scope)
51
+ ? "deployment"
52
+ : "request";
53
+ const materializedResidue = [];
41
54
  if (useMemoryEngine && memorySpec) {
42
55
  try {
43
56
  const backend = deps.memoryBackend;
@@ -133,6 +146,12 @@ export async function prepareMemory(input) {
133
146
  b)
134
147
  : (b.retrievalView?.() ?? b);
135
148
  const planeScopes = (scopes, write) => [...new Set([...scopes, ...(write !== null ? [write] : [])])];
149
+ const mountedScopeRows = (dual
150
+ ? [
151
+ ...planeScopes(planes.project, planes.writePlane === "project" ? memorySpec.writeScope : null),
152
+ ...planeScopes(planes.personal, planes.writePlane === "personal" ? memorySpec.writeScope : null),
153
+ ]
154
+ : planeScopes(memorySpec.scopes, memorySpec.writeScope)).map((scope) => ({ scope, origin: originOf(scope) }));
136
155
  const pollutedOpts = (engine) => {
137
156
  const rec = engine.sessionPollution(sessionId);
138
157
  return rec !== undefined ? { polluted: { reason: rec.reason } } : {};
@@ -179,7 +198,9 @@ export async function prepareMemory(input) {
179
198
  const personalEngine = personal.engine;
180
199
  const p = planes;
181
200
  const projectHandle = await projectEngine.materialize(p.project, p.writePlane === "project" ? memorySpec.writeScope : null, { adoptionRestricted });
201
+ materializedResidue.push(...planeScopes(p.project, p.writePlane === "project" ? memorySpec.writeScope : null));
182
202
  const personalHandle = await personalEngine.materialize(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null, { adoptionRestricted });
203
+ materializedResidue.push(...planeScopes(p.personal, p.writePlane === "personal" ? memorySpec.writeScope : null));
183
204
  const writeIsPersonal = p.writePlane === "personal";
184
205
  writeEngine = writeIsPersonal ? personalEngine : projectEngine;
185
206
  writeHandle = writeIsPersonal ? personalHandle : projectHandle;
@@ -222,6 +243,7 @@ export async function prepareMemory(input) {
222
243
  const personal = createPersonalEngine(choosePersonalBackend());
223
244
  const personalEngine = personal.engine;
224
245
  const handle = await personalEngine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
246
+ materializedResidue.push(...planeScopes(memorySpec.scopes, memorySpec.writeScope));
225
247
  writeEngine = personalEngine;
226
248
  writeHandle = handle;
227
249
  injectFn = () => personalEngine.inject(handle, { writeToolMounted: input.writeToolsMounted });
@@ -243,6 +265,7 @@ export async function prepareMemory(input) {
243
265
  onIncident: onEngineIncident,
244
266
  });
245
267
  const handle = await engine.materialize(memorySpec.scopes, memorySpec.writeScope, { adoptionRestricted });
268
+ materializedResidue.push(...planeScopes(memorySpec.scopes, memorySpec.writeScope));
246
269
  writeEngine = engine;
247
270
  writeHandle = handle;
248
271
  injectFn = () => engine.inject(handle, { writeToolMounted: input.writeToolsMounted });
@@ -311,6 +334,12 @@ export async function prepareMemory(input) {
311
334
  execIsExternalContent: memorySpec.execIsExternalContent === true,
312
335
  },
313
336
  };
337
+ effectiveMemoryScopes = {
338
+ state: "mounted",
339
+ contract: memorySpec.scopeContract === "v2" ? "v2" : "legacy",
340
+ scopes: mountedScopeRows,
341
+ writeScope: memorySpec.writeScope,
342
+ };
314
343
  if (input.memorySearchToolsPlanned)
315
344
  memoryTools = createMemoryEngineTools({ planes: toolPlanes });
316
345
  }
@@ -322,6 +351,16 @@ export async function prepareMemory(input) {
322
351
  throw err;
323
352
  }
324
353
  deps.onError?.(err instanceof Error ? err : new Error(String(err)), { phase: "memory", sessionId });
354
+ if (effectiveMemoryScopes === undefined) {
355
+ effectiveMemoryScopes = {
356
+ state: "memoryless",
357
+ reason: "mount-failed",
358
+ contract: memorySpec.scopeContract === "v2" ? "v2" : "legacy",
359
+ scopes: [],
360
+ writeScope: null,
361
+ ...(materializedResidue.length > 0 ? { materializedResidue: [...new Set(materializedResidue)] } : {}),
362
+ };
363
+ }
325
364
  }
326
365
  }
327
366
  let memoryBlock;
@@ -359,5 +398,12 @@ export async function prepareMemory(input) {
359
398
  if (memoryBlock !== undefined && injection.indexSeed !== undefined && input.rosterCanPersist)
360
399
  seedFiles = [injection.indexSeed];
361
400
  }
362
- return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, ...(memoryTools !== undefined ? { memoryTools } : {}), ...(seedFiles !== undefined ? { seedFiles } : {}) };
401
+ if (effectiveMemoryScopes === undefined) {
402
+ effectiveMemoryScopes = memorySpec
403
+ ? memorySpec.enabled
404
+ ? { state: "memoryless", reason: "no-backend", contract: memorySpec.scopeContract === "v2" ? "v2" : "legacy", scopes: [], writeScope: null }
405
+ : { state: "none", reason: "disabled", scopes: [], writeScope: null }
406
+ : { state: "none", reason: "no-spec", scopes: [], writeScope: null };
407
+ }
408
+ return { memoryEngineSession, memoryBlock, admittedOrgScopes, ownOrgVerdict, effectiveMemoryScopes, ...(memoryTools !== undefined ? { memoryTools } : {}), ...(seedFiles !== undefined ? { seedFiles } : {}) };
363
409
  }
@@ -17,6 +17,7 @@ import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-de
17
17
  import { type BrainCallGuardrailRef } from "../../brain/timeout.js";
18
18
  import { type OutputRef, type BlockedRef, type SkillListingEntry } from "./synthetic-tools.js";
19
19
  import type { MemoryEngine } from "../memory-engine/engine.js";
20
+ import { type GitStatusLaneRef } from "./git-status-frame.js";
20
21
  import { type ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
21
22
  import type { ToolDisclosureManifest } from "../trace.js";
22
23
  import type { TaskNotificationPayload } from "../task-notification.js";
@@ -204,6 +205,11 @@ export interface Prepared {
204
205
  * excluded), echoed on `TaskResult.effectiveReadDenyPatterns`. Present iff non-empty; a defensive
205
206
  * copy (the wide-scope working array stays the engine's own). */
206
207
  effectiveReadDenyPatterns?: readonly import("../../tools/fs/read-deny.js").NormalizedReadDenyEntry[];
208
+ /** design/178 v2 §2.3 (件①) — the memory-visibility observation prepareMemory minted (echoed on
209
+ * `TaskResult.effectiveMemoryScopes`). Always present on a completed prepare (the memory-less
210
+ * states are their own values); the seat is optional only so a Prepared shape without the phase
211
+ * cannot fabricate one. */
212
+ effectiveMemoryScopes?: import("../types.js").EffectiveMemoryScopes;
207
213
  /** design/99 §E13 — the per-task logical cwd ref when a real shell is mounted (else undefined). The Runner
208
214
  * reads `cwdRef.current` after each tool to detect a `cd` move and emit `workspace_changed`. */
209
215
  cwdRef?: CwdRef;
@@ -747,6 +753,12 @@ export interface Prepared {
747
753
  skills?: readonly string[];
748
754
  models?: readonly string[];
749
755
  };
756
+ /** env-tail migration (#254 shape) — the git-status frame lane's run-local state: this leg's
757
+ * resolved frame (probe outcome rendered + hashed at prepare), the announced `(kind, hash)`
758
+ * mirror the checkpoint serializer reads, the trim-protection slot the request-build context
759
+ * handler matches on, and the re-assert closure the compaction landing + boundary retry call.
760
+ * Always present (empty object on a hands-less leg — the lane is then out of scope). */
761
+ gitStatusRef: GitStatusLaneRef;
750
762
  /** G1 通告层 — narrow post-compact getter over the process task registry: THIS run's visible
751
763
  * pending/running background tasks (same owner/scope/session identity the TaskOutput/TaskStop tools
752
764
  * use), as a bounded display projection (id/description/status — never handles/env/abort). Called by
@@ -1244,6 +1256,15 @@ export interface RunInternals {
1244
1256
  * stamping. TRUSTED run-scoped channel (never a {@link TaskSpec} field).
1245
1257
  */
1246
1258
  delegationTaskType?: import("../types.js").DelegationTaskType;
1259
+ /**
1260
+ * #258 — the registry row's stop-cycle generation this run executes as (fresh spawn = 1, a
1261
+ * revival's bumped counter), threaded by the BACKGROUND delegation lanes from the registry's own
1262
+ * `cycleSeq` so every `task_progress` tick the run mints carries it as `seq` (same axis as
1263
+ * `TaskNotificationPayload.seq` / `BackgroundChildEvent.seq`). Absent for runs with no `a*` row
1264
+ * (sync children, workflow agents, top-level) — same absence-is-a-fact posture as
1265
+ * {@link delegationTaskType} above. TRUSTED run-scoped channel (never a TaskSpec field).
1266
+ */
1267
+ cycleSeq?: number;
1247
1268
  /** δ 批 [1498]⑦/A-3 — the ROOT host session of the whole delegation tree (fixed point: the
1248
1269
  * spawner passes its own `ctx.rootSessionId ?? ctx.sessionId`, so depth 1 gets the host session
1249
1270
  * and every deeper level inherits it verbatim). `parentSessionId` is the IMMEDIATE spawner —