@sema-agent/core 5.35.0 → 5.36.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 (34) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/agents/subagent.js +29 -2
  3. package/dist/core/auto-compaction.d.ts +23 -0
  4. package/dist/core/auto-compaction.js +8 -0
  5. package/dist/core/checkpoint-store.d.ts +16 -0
  6. package/dist/core/context-guard.d.ts +41 -0
  7. package/dist/core/context-guard.js +76 -0
  8. package/dist/core/memory-engine/engine.js +1 -1
  9. package/dist/core/park-selfcheck.d.ts +5 -0
  10. package/dist/core/runner/assemble-result.d.ts +3 -0
  11. package/dist/core/runner/assemble-result.js +3 -0
  12. package/dist/core/runner/git-status-frame.d.ts +219 -0
  13. package/dist/core/runner/git-status-frame.js +212 -0
  14. package/dist/core/runner/prepare-task.d.ts +16 -0
  15. package/dist/core/runner/prepare-task.js +27 -34
  16. package/dist/core/runner/runtask.js +266 -5
  17. package/dist/core/task-registry-agent.d.ts +15 -0
  18. package/dist/core/task-registry-agent.js +9 -0
  19. package/dist/core/task-registry.d.ts +3 -0
  20. package/dist/core/task-registry.js +4 -1
  21. package/dist/core/types.d.ts +27 -7
  22. package/dist/engine/harness/types.d.ts +65 -1
  23. package/dist/engine/harness/types.js +20 -0
  24. package/dist/engine/session/import-validate.js +10 -1
  25. package/dist/engine/session/session.d.ts +37 -1
  26. package/dist/engine/session/session.js +56 -1
  27. package/dist/internal/harness-types.d.ts +1 -0
  28. package/dist/internal/harness.d.ts +2 -0
  29. package/dist/internal/harness.js +2 -0
  30. package/dist/prompt-assembly/epoch.js +1 -1
  31. package/dist/prompt-assembly/event-registry.js +1 -0
  32. package/dist/prompts/default.d.ts +20 -7
  33. package/dist/prompts/default.js +2 -7
  34. package/package.json +1 -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
+ }
@@ -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";
@@ -747,6 +748,12 @@ export interface Prepared {
747
748
  skills?: readonly string[];
748
749
  models?: readonly string[];
749
750
  };
751
+ /** env-tail migration (#254 shape) — the git-status frame lane's run-local state: this leg's
752
+ * resolved frame (probe outcome rendered + hashed at prepare), the announced `(kind, hash)`
753
+ * mirror the checkpoint serializer reads, the trim-protection slot the request-build context
754
+ * handler matches on, and the re-assert closure the compaction landing + boundary retry call.
755
+ * Always present (empty object on a hands-less leg — the lane is then out of scope). */
756
+ gitStatusRef: GitStatusLaneRef;
750
757
  /** G1 通告层 — narrow post-compact getter over the process task registry: THIS run's visible
751
758
  * pending/running background tasks (same owner/scope/session identity the TaskOutput/TaskStop tools
752
759
  * use), as a bounded display projection (id/description/status — never handles/env/abort). Called by
@@ -1244,6 +1251,15 @@ export interface RunInternals {
1244
1251
  * stamping. TRUSTED run-scoped channel (never a {@link TaskSpec} field).
1245
1252
  */
1246
1253
  delegationTaskType?: import("../types.js").DelegationTaskType;
1254
+ /**
1255
+ * #258 — the registry row's stop-cycle generation this run executes as (fresh spawn = 1, a
1256
+ * revival's bumped counter), threaded by the BACKGROUND delegation lanes from the registry's own
1257
+ * `cycleSeq` so every `task_progress` tick the run mints carries it as `seq` (same axis as
1258
+ * `TaskNotificationPayload.seq` / `BackgroundChildEvent.seq`). Absent for runs with no `a*` row
1259
+ * (sync children, workflow agents, top-level) — same absence-is-a-fact posture as
1260
+ * {@link delegationTaskType} above. TRUSTED run-scoped channel (never a TaskSpec field).
1261
+ */
1262
+ cycleSeq?: number;
1247
1263
  /** δ 批 [1498]⑦/A-3 — the ROOT host session of the whole delegation tree (fixed point: the
1248
1264
  * spawner passes its own `ctx.rootSessionId ?? ctx.sessionId`, so depth 1 gets the host session
1249
1265
  * and every deeper level inherits it verbatim). `parentSessionId` is the IMMEDIATE spawner —
@@ -56,7 +56,8 @@ import { limitConfigError, prepareConfigDoors } from "./prepare-config-doors.js"
56
56
  import { NAMESPACED_NAME_SHAPES, prepareSafetyScan } from "./prepare-safety-scan.js";
57
57
  import { prepareAcquireReconcile } from "./prepare-acquire-reconcile.js";
58
58
  import { prepareWorkspaceRestore, rebaseWorkspacePath, remoteEnvFailureNote, restoreWorkspaceWithRetry } from "./prepare-workspace-restore.js";
59
- import { defaultPromptProvider, buildEnvironmentContext, buildGitSnapshot, formatLocalDate, isValidTimeZone, PROJECT_CONTEXT_FRAMING } from "../../prompts/default.js";
59
+ import { defaultPromptProvider, buildEnvironmentContext, formatLocalDate, isValidTimeZone, PROJECT_CONTEXT_FRAMING } from "../../prompts/default.js";
60
+ import { applyGitFrameGuard, probeGitStatusLane } from "./git-status-frame.js";
60
61
  import { assemblePrompt } from "../../prompt-assembly/assemble.js";
61
62
  import { auditToolCollisions, getToolContract, projectToolManifest } from "../../prompt-assembly/tool-catalog.js";
62
63
  import { resolveEpochAgainstBundled } from "../../prompt-assembly/epoch.js";
@@ -1875,38 +1876,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1875
1876
  }
1876
1877
  catch {
1877
1878
  }
1878
- if (envFacts.isGitRepo === true) {
1879
- const SEP = "@@SEMA_ENV_GIT_SPLIT@@";
1880
- try {
1881
- 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 });
1882
- if (snap.ok && snap.value.exitCode === 0) {
1883
- const parts = snap.value.stdout.split(`${SEP}\n`);
1884
- if (parts.length === 4) {
1885
- envFacts.gitSnapshot = buildGitSnapshot({
1886
- branch: envFacts.gitBranch ?? "HEAD",
1887
- mainBranch: parts[0].trim() || "main",
1888
- ...(parts[1].trim() ? { userName: parts[1].trim() } : {}),
1889
- status: parts[2],
1890
- log: parts[3],
1891
- });
1892
- }
1893
- }
1894
- else {
1895
- const reason = snap.ok
1896
- ? snap.value.exitCode === 41
1897
- ? "git status failed (exit 41)"
1898
- : snap.value.exitCode === 42
1899
- ? "git log failed (exit 42)"
1900
- : `git exited ${snap.value.exitCode}`
1901
- : `exec failed: ${snap.error.message}`;
1902
- deps.onError?.(new Error(`env git snapshot skipped — ${reason}`), { phase: "degraded", sessionId, classification: "env-git-snapshot" });
1903
- }
1904
- }
1905
- catch (err) {
1906
- deps.onError?.(new Error(`env git snapshot skipped — ${err instanceof Error ? err.message : String(err)}`), { phase: "degraded", sessionId, classification: "env-git-snapshot" });
1907
- }
1908
- }
1909
1879
  }
1880
+ const gitStatusRef = await probeGitStatusLane({
1881
+ executionEnv,
1882
+ envFacts,
1883
+ handsEnabled,
1884
+ taskRoot: taskRootFinal,
1885
+ onDegrade: (reason) => deps.onError?.(new Error(`env git snapshot degraded — ${reason}`), { phase: "degraded", sessionId, classification: "env-git-snapshot" }),
1886
+ });
1910
1887
  if (toolFaceSnapshot.exclude !== undefined && toolFaceSnapshot.exclude.length > 0) {
1911
1888
  const excluded = new Set(toolFaceSnapshot.exclude);
1912
1889
  for (let i = tools.length - 1; i >= 0; i--)
@@ -3577,6 +3554,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3577
3554
  ...(announcedListingsRef.models !== undefined ? { models: [...announcedListingsRef.models] } : {}),
3578
3555
  }
3579
3556
  : undefined,
3557
+ gitAnnouncement: gitStatusRef.announced !== undefined ? { ...gitStatusRef.announced } : undefined,
3580
3558
  delegationProvenance: internals?.delegationProvenance !== undefined ? { ...internals.delegationProvenance.ref.current } : undefined,
3581
3559
  });
3582
3560
  const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle) => {
@@ -4388,6 +4366,20 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4388
4366
  ts: Date.now(),
4389
4367
  }));
4390
4368
  }
4369
+ trimmed = applyGitFrameGuard({
4370
+ before: edited,
4371
+ trimmed,
4372
+ budgetTokens: guardAt,
4373
+ ref: gitStatusRef,
4374
+ charsPerToken,
4375
+ onDegrade: (message) => {
4376
+ try {
4377
+ deps.onError?.(new Error(message), { phase: "degraded", sessionId, classification: "env-git-snapshot" });
4378
+ }
4379
+ catch {
4380
+ }
4381
+ },
4382
+ });
4391
4383
  const swept = dropOrphanToolResults(trimmed);
4392
4384
  if (swept.dropped.length > 0) {
4393
4385
  try {
@@ -4404,7 +4396,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4404
4396
  mediaCapped !== capped ||
4405
4397
  edited !== mediaCapped ||
4406
4398
  trimDroppedMessages ||
4407
- swept.dropped.length > 0;
4399
+ swept.dropped.length > 0 ||
4400
+ gitStatusRef.overBudgetShrunk === true;
4408
4401
  return { messages: swept.messages };
4409
4402
  });
4410
4403
  const cacheBreakDetector = deps.cacheBreakDetection === false ? undefined : new CacheBreakDetector();
@@ -4593,7 +4586,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4593
4586
  const effectiveReadFaceObserved = carrierReadFace();
4594
4587
  const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
4595
4588
  const preparedHolder = {};
4596
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4589
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4597
4590
  const prepared = buildPrepared();
4598
4591
  preparedHolder.current = prepared;
4599
4592
  return prepared;