@sema-agent/core 5.34.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.
- package/CHANGELOG.md +104 -0
- package/dist/agents/subagent.js +29 -2
- package/dist/core/auto-compaction.d.ts +23 -0
- package/dist/core/auto-compaction.js +8 -0
- package/dist/core/checkpoint-store.d.ts +49 -4
- package/dist/core/context-guard.d.ts +41 -0
- package/dist/core/context-guard.js +76 -0
- package/dist/core/hooks.d.ts +98 -3
- package/dist/core/hooks.js +146 -8
- package/dist/core/memory-engine/engine.js +1 -1
- package/dist/core/park-selfcheck.d.ts +161 -0
- package/dist/core/park-selfcheck.js +251 -0
- package/dist/core/runner/assemble-result.d.ts +3 -0
- package/dist/core/runner/assemble-result.js +3 -0
- package/dist/core/runner/git-status-frame.d.ts +219 -0
- package/dist/core/runner/git-status-frame.js +212 -0
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +2 -1
- package/dist/core/runner/prepare-task.d.ts +28 -4
- package/dist/core/runner/prepare-task.js +86 -52
- package/dist/core/runner/runtask.d.ts +6 -1
- package/dist/core/runner/runtask.js +330 -19
- package/dist/core/task-registry-agent.d.ts +15 -0
- package/dist/core/task-registry-agent.js +9 -0
- package/dist/core/task-registry.d.ts +3 -0
- package/dist/core/task-registry.js +4 -1
- package/dist/core/tool-errors.d.ts +2 -2
- package/dist/core/tool-policy.d.ts +125 -0
- package/dist/core/tool-policy.js +35 -2
- package/dist/core/types.d.ts +98 -9
- package/dist/engine/harness/types.d.ts +65 -1
- package/dist/engine/harness/types.js +20 -0
- package/dist/engine/session/import-validate.js +10 -1
- package/dist/engine/session/session.d.ts +37 -1
- package/dist/engine/session/session.js +56 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/internal/harness-types.d.ts +1 -0
- package/dist/internal/harness.d.ts +2 -0
- package/dist/internal/harness.js +2 -0
- package/dist/orchestration/workflow.d.ts +1 -1
- package/dist/prompt-assembly/epoch.js +1 -1
- package/dist/prompt-assembly/event-registry.js +1 -0
- package/dist/prompts/default.d.ts +20 -7
- package/dist/prompts/default.js +2 -7
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +17 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { boundInputHashOf, canonicalize } from "./canonical-json.js";
|
|
2
|
+
import { MAX_SUPPORTED_CHECKPOINT_VERSION, mintCheckpointToken, resolveCheckpointStore } from "./checkpoint-store.js";
|
|
3
|
+
import { resolveDeclaredDurability } from "./wiring-manifest.js";
|
|
4
|
+
export const PARK_SELFCHECK_SCOPE_PREFIX = "sema:park-selfcheck:";
|
|
5
|
+
export const PARK_SELFCHECK_STEP_TIMEOUT_MS = 10_000;
|
|
6
|
+
export const PARK_SELFCHECK_ROW_TTL_MS = 60_000;
|
|
7
|
+
async function within(op, step) {
|
|
8
|
+
let timer;
|
|
9
|
+
try {
|
|
10
|
+
return await Promise.race([
|
|
11
|
+
Promise.resolve()
|
|
12
|
+
.then(op)
|
|
13
|
+
.then((ok) => ({ ok }))
|
|
14
|
+
.catch((threw) => ({ threw })),
|
|
15
|
+
new Promise((resolve) => {
|
|
16
|
+
timer = setTimeout(() => resolve({ timedOut: `${step} did not answer within ${PARK_SELFCHECK_STEP_TIMEOUT_MS}ms — a store that neither answers nor errors is the failure this probe exists to surface, not one it may wait on` }), PARK_SELFCHECK_STEP_TIMEOUT_MS);
|
|
17
|
+
}),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
if (timer !== undefined)
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function declaresDurable(store, name) {
|
|
26
|
+
try {
|
|
27
|
+
return resolveDeclaredDurability(store, name) === "durable";
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function durableParkGapOf(halves) {
|
|
34
|
+
if (!halves.checkpointWired)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (halves.checkpointDurable && halves.sessionDurable)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (halves.checkpointDurable && !halves.sessionDurable) {
|
|
39
|
+
return ("this deployment wires a checkpoint store that declares durability but a session store that does not " +
|
|
40
|
+
"(RunnerDeps.sessionStore — absent means the built-in in-memory store): a durable park needs BOTH halves, " +
|
|
41
|
+
"so a parked approval survives the restart while the conversation it resumes into does not, and a resume " +
|
|
42
|
+
"arriving afterwards finds the checkpoint and not the session");
|
|
43
|
+
}
|
|
44
|
+
if (!halves.checkpointDurable && halves.sessionDurable) {
|
|
45
|
+
return ("this deployment wires a session store that declares durability but a checkpoint store that does not " +
|
|
46
|
+
"(RunnerDeps.checkpointStore.durability): a durable park needs BOTH halves, so the conversation survives " +
|
|
47
|
+
"the restart while the parked approval that was to resume it does not");
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
export function durableParkGapFor(deps, spec = {}) {
|
|
52
|
+
const checkpointStore = resolveCheckpointStore(spec, deps);
|
|
53
|
+
return durableParkGapOf({
|
|
54
|
+
checkpointWired: checkpointStore !== undefined,
|
|
55
|
+
checkpointDurable: declaresDurable(checkpointStore, "checkpointStore"),
|
|
56
|
+
sessionDurable: declaresDurable(deps.sessionStore, "sessionStore"),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function syntheticCheckpoint(scope) {
|
|
60
|
+
return {
|
|
61
|
+
token: mintCheckpointToken(),
|
|
62
|
+
scope,
|
|
63
|
+
sessionId: `${scope}:session`,
|
|
64
|
+
leafId: `${scope}:leaf`,
|
|
65
|
+
gate: { kind: "human", reason: "park wiring self-check (design/252 G-6) — no person is being asked", toolName: "SelfCheck" },
|
|
66
|
+
pendingAction: {
|
|
67
|
+
kind: "tool_approval",
|
|
68
|
+
toolCallId: `${scope}:call`,
|
|
69
|
+
toolName: "SelfCheck",
|
|
70
|
+
args: { probe: true, nested: { depth: 2, list: [1, 2, 3] } },
|
|
71
|
+
boundInputHash: boundInputHashOf({ probe: true, nested: { depth: 2, list: [1, 2, 3] } }),
|
|
72
|
+
batchToolCallIds: [`${scope}:call`],
|
|
73
|
+
completedCallIds: [],
|
|
74
|
+
},
|
|
75
|
+
state: { activeTools: ["SelfCheck"], nestedStats: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0, anyUnpriced: false } },
|
|
76
|
+
status: "pending",
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
version: MAX_SUPPORTED_CHECKPOINT_VERSION,
|
|
79
|
+
suspendCount: 1,
|
|
80
|
+
suspendedAt: Date.now(),
|
|
81
|
+
resourceLedger: { spentMicroUsd: 0, spentTokens: 0, spentTurns: 0, sliceCount: 1 },
|
|
82
|
+
humanReview: { count: 1, totalWaitMs: 0, gates: [{ kind: "human", waitMs: 0, decision: "approve" }] },
|
|
83
|
+
sourceTaskId: `${scope}:task`,
|
|
84
|
+
principal: `${scope}:principal`,
|
|
85
|
+
durableApproval: { scope, ttlMs: PARK_SELFCHECK_ROW_TTL_MS },
|
|
86
|
+
deadline: Date.now() + PARK_SELFCHECK_ROW_TTL_MS,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function faithfulnessDelta(sent, got) {
|
|
90
|
+
const project = (cp) => ({
|
|
91
|
+
token: cp.token,
|
|
92
|
+
scope: cp.scope,
|
|
93
|
+
sessionId: cp.sessionId,
|
|
94
|
+
leafId: cp.leafId,
|
|
95
|
+
gate: cp.gate,
|
|
96
|
+
pendingAction: cp.pendingAction,
|
|
97
|
+
state: cp.state,
|
|
98
|
+
deadline: cp.deadline,
|
|
99
|
+
createdAt: cp.createdAt,
|
|
100
|
+
version: cp.version,
|
|
101
|
+
suspendCount: cp.suspendCount,
|
|
102
|
+
suspendedAt: cp.suspendedAt,
|
|
103
|
+
resourceLedger: cp.resourceLedger,
|
|
104
|
+
humanReview: cp.humanReview,
|
|
105
|
+
sourceTaskId: cp.sourceTaskId,
|
|
106
|
+
principal: cp.principal,
|
|
107
|
+
durableApproval: cp.durableApproval,
|
|
108
|
+
});
|
|
109
|
+
const a = canonicalize(project(sent));
|
|
110
|
+
const b = canonicalize(project(got));
|
|
111
|
+
return a === b ? undefined : `the row came back different from the row that was filed (sent ${a}; read ${b})`;
|
|
112
|
+
}
|
|
113
|
+
export async function probeParkRoundTrip(deps, spec = {}) {
|
|
114
|
+
const gap = durableParkGapFor(deps, spec);
|
|
115
|
+
const gapCell = gap !== undefined ? { durableTopologyGap: gap } : {};
|
|
116
|
+
const store = resolveCheckpointStore(spec, deps);
|
|
117
|
+
if (store === undefined) {
|
|
118
|
+
return {
|
|
119
|
+
verdict: "not_probed",
|
|
120
|
+
findings: [],
|
|
121
|
+
summary: "no checkpoint store is wired on this assembly — there is no park lane to probe (the manifest reports the same fact as parkLane.capable: false)",
|
|
122
|
+
...gapCell,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const scope = `${PARK_SELFCHECK_SCOPE_PREFIX}${mintCheckpointToken()}`;
|
|
126
|
+
const cp = syntheticCheckpoint(scope);
|
|
127
|
+
const said = (err) => {
|
|
128
|
+
try {
|
|
129
|
+
const m = err instanceof Error ? err.message : undefined;
|
|
130
|
+
return typeof m === "string" ? m : `a ${typeof err} the store threw`;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return "a value the store threw that could not be described";
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const shown = (v) => {
|
|
137
|
+
try {
|
|
138
|
+
return typeof v === "string" ? JSON.stringify(v) : `a ${typeof v}`;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return "an undescribable value";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const fenceQuietly = async () => {
|
|
145
|
+
await within(async () => {
|
|
146
|
+
try {
|
|
147
|
+
await store.expire(cp.token, scope);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
}
|
|
151
|
+
}, "the cleanup fence");
|
|
152
|
+
};
|
|
153
|
+
const failed = async (code, detail) => {
|
|
154
|
+
await fenceQuietly();
|
|
155
|
+
return { verdict: "failed", findings: [{ code, detail }], scope, summary: `the park lane failed its round-trip self-check at "${code}": ${detail}`, ...gapCell };
|
|
156
|
+
};
|
|
157
|
+
let putStarted;
|
|
158
|
+
const put = await within(() => {
|
|
159
|
+
const p = Promise.resolve().then(() => store.put(cp.token, cp));
|
|
160
|
+
putStarted = p;
|
|
161
|
+
return p;
|
|
162
|
+
}, "put");
|
|
163
|
+
if ("timedOut" in put) {
|
|
164
|
+
void putStarted?.catch(() => { }).then(() => store.expire(cp.token, scope)).catch(() => { });
|
|
165
|
+
return await failed("put_failed", put.timedOut);
|
|
166
|
+
}
|
|
167
|
+
if ("threw" in put)
|
|
168
|
+
return await failed("put_failed", `the checkpoint store refused to file a synthetic park: ${said(put.threw)}`);
|
|
169
|
+
const first = await within(() => store.get(cp.token), "get");
|
|
170
|
+
if ("timedOut" in first)
|
|
171
|
+
return await failed("row_not_readable", first.timedOut);
|
|
172
|
+
if ("threw" in first)
|
|
173
|
+
return await failed("row_not_readable", `the checkpoint store threw reading back the row it had just accepted: ${said(first.threw)}`);
|
|
174
|
+
const read = first.ok;
|
|
175
|
+
if (read === null)
|
|
176
|
+
return await failed("row_not_readable", "the checkpoint store accepted the row and then answered null for its own token — a park filed here would be unresumable");
|
|
177
|
+
if (typeof read !== "object")
|
|
178
|
+
return await failed("row_not_readable", `the checkpoint store answered ${read === undefined ? "undefined" : typeof read} for its own token — neither a checkpoint nor the absence of one`);
|
|
179
|
+
let delta;
|
|
180
|
+
let status;
|
|
181
|
+
try {
|
|
182
|
+
delta = faithfulnessDelta(cp, read);
|
|
183
|
+
status = read.status;
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
return await failed("row_not_faithful", `the row the store returned could not be read for comparison: ${said(err)}`);
|
|
187
|
+
}
|
|
188
|
+
if (delta !== undefined)
|
|
189
|
+
return await failed("row_not_faithful", delta);
|
|
190
|
+
if (status !== "pending")
|
|
191
|
+
return await failed("row_not_pending", `the filed row came back with status ${shown(status)} — a park in any other status is one nobody can resolve`);
|
|
192
|
+
const enumerable = store.listByScope !== undefined;
|
|
193
|
+
if (store.listByScope !== undefined) {
|
|
194
|
+
const listByScope = store.listByScope.bind(store);
|
|
195
|
+
const listed = await within(() => listByScope(scope), "listByScope");
|
|
196
|
+
if ("timedOut" in listed)
|
|
197
|
+
return await failed("row_not_enumerable", listed.timedOut);
|
|
198
|
+
if ("threw" in listed)
|
|
199
|
+
return await failed("row_not_enumerable", `listByScope threw for the probe's own scope: ${said(listed.threw)}`);
|
|
200
|
+
let present;
|
|
201
|
+
try {
|
|
202
|
+
present = Array.isArray(listed.ok) && listed.ok.some((row) => row?.token === cp.token);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
return await failed("row_not_enumerable", `the listByScope result could not be read: ${said(err)}`);
|
|
206
|
+
}
|
|
207
|
+
if (!present)
|
|
208
|
+
return await failed("row_not_enumerable", `listByScope("${scope}") did not include the pending row it holds — an inbox enumerating this scope would show no approvals`);
|
|
209
|
+
}
|
|
210
|
+
const fence = await within(() => store.expire(cp.token, scope), "expire");
|
|
211
|
+
if ("timedOut" in fence)
|
|
212
|
+
return await failed("fence_failed", fence.timedOut);
|
|
213
|
+
if ("threw" in fence)
|
|
214
|
+
return await failed("fence_failed", `expire threw on the probe's own pending row: ${said(fence.threw)}`);
|
|
215
|
+
const fenced = fence.ok === true;
|
|
216
|
+
const second = await within(() => store.get(cp.token), "get (after the fence)");
|
|
217
|
+
if ("timedOut" in second)
|
|
218
|
+
return await failed("fence_not_durable", second.timedOut);
|
|
219
|
+
if ("threw" in second)
|
|
220
|
+
return await failed("fence_not_durable", `the store threw re-reading the fenced row: ${said(second.threw)}`);
|
|
221
|
+
const after = second.ok;
|
|
222
|
+
let afterStatus;
|
|
223
|
+
if (after !== null) {
|
|
224
|
+
if (typeof after !== "object")
|
|
225
|
+
return await failed("fence_not_durable", `the store answered ${after === undefined ? "undefined" : typeof after} re-reading the fenced row — neither a checkpoint nor the absence of one`);
|
|
226
|
+
try {
|
|
227
|
+
afterStatus = after.status;
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
return await failed("fence_not_durable", `the fenced row's status could not be read: ${said(err)}`);
|
|
231
|
+
}
|
|
232
|
+
if (afterStatus !== "expired") {
|
|
233
|
+
return await failed(fenced ? "fence_not_durable" : "fence_failed", fenced
|
|
234
|
+
? `expire reported a win and the row reads ${shown(afterStatus)} — the contract's transition is pending → expired, so the CAS did not land`
|
|
235
|
+
: `expire lost an UNCONTENDED CAS and the row reads ${shown(afterStatus)} — the fence that keeps a checkpoint from being both reaped and resumed does not close here`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const steps = `filed, read back${enumerable ? ", enumerated" : ""} and fenced`;
|
|
239
|
+
const notExercised = enumerable ? "" : " (this store implements no listByScope, so the inbox enumeration was NOT exercised)";
|
|
240
|
+
const casNote = fenced ? "" : " (the expire CAS reported a loss while the row closed — consistent with store housekeeping, but a backend that mis-reports CAS wins is not distinguished here)";
|
|
241
|
+
return {
|
|
242
|
+
verdict: "round_trip_ok",
|
|
243
|
+
findings: [],
|
|
244
|
+
...(fenced ? {} : { casLossObserved: true }),
|
|
245
|
+
scope,
|
|
246
|
+
summary: after === null
|
|
247
|
+
? `the park lane ${steps} a synthetic checkpoint${notExercised} — the row is no longer present (a deleting backend or its own housekeeping removed it), so nothing resumable remains${casNote}`
|
|
248
|
+
: `the park lane ${steps} a synthetic checkpoint${notExercised} — residual expired row in scope "${scope}"${casNote}`,
|
|
249
|
+
...gapCell,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
@@ -149,6 +149,9 @@ export interface ResultFlags {
|
|
|
149
149
|
budgetAxis?: BudgetAxis;
|
|
150
150
|
blockedReason?: string;
|
|
151
151
|
conflict?: boolean;
|
|
152
|
+
/** env-tail migration F5: the request-build guard threw the irreducible-core terminal this run —
|
|
153
|
+
* the loop folded it into a text-only failure message, so the typed code rides this flag. */
|
|
154
|
+
gitCoreOverBudget?: boolean;
|
|
152
155
|
outputInvalid?: boolean;
|
|
153
156
|
/** design/72 §2.2 (B): a re-suspend was refused because the task already suspended `maxSuspends` times
|
|
154
157
|
* (a resume/restart loop). It aborted the run (no `threw`) but must read as `failed`/`suspend.loop`,
|
|
@@ -141,6 +141,9 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
|
|
|
141
141
|
errorCode = lifted;
|
|
142
142
|
errorMessage = stripErrorCodePrefix(errorMessage);
|
|
143
143
|
}
|
|
144
|
+
else if (flags.gitCoreOverBudget) {
|
|
145
|
+
errorCode = "irreducible_core_over_budget";
|
|
146
|
+
}
|
|
144
147
|
else if (flags.conflict) {
|
|
145
148
|
errorCode = "conflict";
|
|
146
149
|
}
|
|
@@ -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;
|