@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
|
@@ -10,7 +10,7 @@ import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task
|
|
|
10
10
|
import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, renderSpoolBody, spoolDropNote, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
|
|
11
11
|
export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
|
|
12
12
|
import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
|
|
13
|
-
import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, recordBackgroundAgentOrgAdmissionLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
|
|
13
|
+
import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, noteBackgroundAgentActivityLane, reapStaleSessionBackgroundAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, recordBackgroundAgentOrgAdmissionLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, backgroundAgentCycleSeqLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
|
|
14
14
|
export { canAccessWorkflowRun, clipTaskOutput, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE };
|
|
15
15
|
const BLOCK_DEFAULT_TIMEOUT_MS = 30_000;
|
|
16
16
|
const BLOCK_MAX_TIMEOUT_MS = 600_000;
|
|
@@ -191,6 +191,9 @@ export class TaskRegistry {
|
|
|
191
191
|
markRetainedContinuation(id) {
|
|
192
192
|
return markRetainedContinuationLane(this.core, id);
|
|
193
193
|
}
|
|
194
|
+
backgroundAgentCycleSeq(id) {
|
|
195
|
+
return backgroundAgentCycleSeqLane(this.core, id);
|
|
196
|
+
}
|
|
194
197
|
reviveBackgroundAgent(id, access, abort) {
|
|
195
198
|
return reviveBackgroundAgentLane(this.core, id, access, abort);
|
|
196
199
|
}
|
|
@@ -117,8 +117,8 @@ export type WorkerErrorClass = "budget" | "limit" | "output" | "suspend" | "revi
|
|
|
117
117
|
* are NOT all terminal-errorCode-shaped — see {@link EXACT_CODE_CLASS}'s comment), plus the flat 1.36 codes.
|
|
118
118
|
* The `limits.` namespace splits into the `budget` and `limit` classes by exact code — see
|
|
119
119
|
* {@link EXACT_CODE_CLASS}. Operation-level dotted codes that are surfaced to a *caller* and never become a
|
|
120
|
-
* task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content
|
|
121
|
-
* rejected to the `steer()` caller) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
|
|
120
|
+
* task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content`/
|
|
121
|
+
* `steering.duplicate_input_id`, rejected to the `steer()` caller) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
|
|
122
122
|
* reaches `TaskResult.errorCode`, so a caller will never pass them here. A genuinely unmapped code → `"unknown"` (which therefore means
|
|
123
123
|
* "known-but-foreign or no code", e.g. a leaked fs/Node code, NOT "an OUR terminal class we forgot to add").
|
|
124
124
|
*/
|
|
@@ -118,6 +118,34 @@ export type ApprovalSettledBy = "human" | "timeout" | "aborted";
|
|
|
118
118
|
export declare const APPROVAL_SETTLED_BY_VALUES: readonly ApprovalSettledBy[];
|
|
119
119
|
/** True iff `v` is one of the three {@link ApprovalSettledBy} words. */
|
|
120
120
|
export declare function isApprovalSettledBy(v: unknown): v is ApprovalSettledBy;
|
|
121
|
+
/**
|
|
122
|
+
* design/252 G-7 — how long an approver-attribution identifier may be.
|
|
123
|
+
*
|
|
124
|
+
* Sized for the identifiers approval channels actually carry (a login, an email, an opaque account id,
|
|
125
|
+
* a queue name) with room to spare, and bounded at all because this value lands on an operator-plane
|
|
126
|
+
* frame that a deployment may persist: an unbounded field on a record channel is its own denial of a
|
|
127
|
+
* readable record. An over-long value is REFUSED, never truncated — a cut identifier names a different
|
|
128
|
+
* party, or nobody, and either is worse than the honest refusal.
|
|
129
|
+
*/
|
|
130
|
+
export declare const APPROVER_ATTRIBUTION_MAX_CHARS = 256;
|
|
131
|
+
/**
|
|
132
|
+
* design/252 G-7 — screen a deployment-supplied approver identifier before it becomes an observation.
|
|
133
|
+
*
|
|
134
|
+
* THE POSTURE, stated so it is not mistaken for something stronger: core does not authenticate this
|
|
135
|
+
* value, does not compare it to anything, and never reads it back to decide anything. It is a
|
|
136
|
+
* TRANSCRIPTION of what the approval channel said about its own settlement — the channel (a server's
|
|
137
|
+
* approval card, an HMAC-verified callback, an operator console) is where identity is established, and
|
|
138
|
+
* a library that holds no identity surface cannot second-guess it. What core owns is that the value is
|
|
139
|
+
* a value: a string, bounded, free of control bytes, or else loudly refused.
|
|
140
|
+
*
|
|
141
|
+
* Returns `{}` for "nothing supplied" (absent, and an empty string — an id of no characters is the
|
|
142
|
+
* absence of an id, the same truthiness fold the deny-note seat uses), `{ approver }` for a value to
|
|
143
|
+
* carry, or `{ defect }` with the sentence a caller puts in its own refusal.
|
|
144
|
+
*/
|
|
145
|
+
export declare function screenApproverAttribution(v: unknown): {
|
|
146
|
+
approver?: string;
|
|
147
|
+
defect?: string;
|
|
148
|
+
};
|
|
121
149
|
/**
|
|
122
150
|
* A three-state permission decision for a tool call (design/37). Upgrades the old two-state
|
|
123
151
|
* `{allow|deny}`:
|
|
@@ -162,6 +190,7 @@ export type PermissionResult = {
|
|
|
162
190
|
message?: string;
|
|
163
191
|
decisionReason?: DecisionReason;
|
|
164
192
|
settledBy?: Extract<ApprovalSettledBy, "human">;
|
|
193
|
+
approver?: string;
|
|
165
194
|
} | {
|
|
166
195
|
action: "ask";
|
|
167
196
|
updatedInput?: unknown;
|
|
@@ -199,13 +228,86 @@ export type PermissionResult = {
|
|
|
199
228
|
* when ANY folded-away concurrent ask bore it (monotone, tighten-only); it is consumed inside the
|
|
200
229
|
* gate and deliberately NOT copied onto the park/approval-card request. */
|
|
201
230
|
matchedAskRule?: string;
|
|
231
|
+
/** design/252 G-2 (additive): the RULE-PROVENANCE evidence behind this ask — see
|
|
232
|
+
* {@link AskRuleEvidence}. ENGINE-STAMPED inside the gate, once, after the org layer and the
|
|
233
|
+
* persisted-rule lane have both spoken; a policy that self-declares it is overwritten there
|
|
234
|
+
* (the member is a record of what the ENGINE's own governance layers did, so a layer's claim
|
|
235
|
+
* about itself is not evidence). Carried onto the approval request by the gate's own ask mint
|
|
236
|
+
* site. Display/reconciliation metadata, never adjudication input. */
|
|
237
|
+
ruleEvidence?: AskRuleEvidence;
|
|
202
238
|
} | {
|
|
203
239
|
action: "deny";
|
|
204
240
|
updatedInput?: unknown;
|
|
205
241
|
message?: string;
|
|
206
242
|
decisionReason?: DecisionReason;
|
|
207
243
|
settledBy?: ApprovalSettledBy;
|
|
244
|
+
approver?: string;
|
|
208
245
|
};
|
|
246
|
+
/**
|
|
247
|
+
* design/252 G-2 — WHY a piece of rule-provenance evidence is not on an ask.
|
|
248
|
+
*
|
|
249
|
+
* The vocabulary exists because a bare `undefined` reads the same for facts that are opposite: "no
|
|
250
|
+
* governance layer is wired here, so there is nothing to name" and "the layer ran and we could not
|
|
251
|
+
* read what it said" are not the same audit answer, and collapsing them is how an evidence chain
|
|
252
|
+
* comes to be reconstructed as "nothing governed this call". Every member of {@link AskRuleEvidence}
|
|
253
|
+
* therefore ships as a value OR a named absence, never as silence.
|
|
254
|
+
*
|
|
255
|
+
* - `"not_wired"` — the lane does not exist on this leg (an ungoverned deployment, no rule store).
|
|
256
|
+
* The field cannot apply; nothing was lost.
|
|
257
|
+
* - `"not_adjudicated"` — the lane exists but this call never reached it (an exempt tool, a call
|
|
258
|
+
* the rule grammar cannot describe). Applicable in principle, skipped in fact.
|
|
259
|
+
* - `"unavailable"` — the lane was consulted and could not read its source. The evidence is LOST,
|
|
260
|
+
* not absent, and this is the one member of the vocabulary that means an auditor should treat the
|
|
261
|
+
* chain as broken rather than empty.
|
|
262
|
+
* - `"no_match"` — consulted, readable, and nothing spoke for this call. A real negative answer.
|
|
263
|
+
* - `"not_reported"` — the supplying seam answered without the identity (a foreign overlay/store
|
|
264
|
+
* implementation, or one written before the identity was projected). Evidence lost at the seam.
|
|
265
|
+
*/
|
|
266
|
+
export type AskEvidenceAbsence = "not_wired" | "not_adjudicated" | "unavailable" | "no_match" | "not_reported";
|
|
267
|
+
/** The closed set above, for runtime domain checks at the seams that accept a caller-supplied value. */
|
|
268
|
+
export declare const ASK_EVIDENCE_ABSENCE_VALUES: readonly AskEvidenceAbsence[];
|
|
269
|
+
/**
|
|
270
|
+
* design/252 G-2 — the machine-reconcilable provenance of the governance decision behind ONE ask.
|
|
271
|
+
*
|
|
272
|
+
* WHAT THIS IS: a PROJECTION of identity keys the engine's governance layers already hold — the org
|
|
273
|
+
* snapshot's `revision`, the personal rule's add `dot`s — onto the surface a consumer can actually
|
|
274
|
+
* read. It performs no new judgment and changes no verdict; removing it would leave every decision
|
|
275
|
+
* byte-identical. The prose channels ({@link PermissionResult}'s ask `message`,
|
|
276
|
+
* {@link AskRequest.persistedRuleShadowed}) say the same things to a PERSON; those are sanitized,
|
|
277
|
+
* capped, display-shaped strings, and reconciling a decision against a published policy revision by
|
|
278
|
+
* regexing them is not an audit trail. This member is the machine's copy.
|
|
279
|
+
*
|
|
280
|
+
* WHAT THIS IS NOT: an authority channel. Nothing in the engine reads it back to decide anything, and
|
|
281
|
+
* a host that ignores it entirely is governed exactly as before.
|
|
282
|
+
*
|
|
283
|
+
* ABSENCE DISCIPLINE (the reason each member has a `…Absent` twin): see {@link AskEvidenceAbsence}.
|
|
284
|
+
* Exactly one of each pair is present — a member and its absence reason are never both set, and never
|
|
285
|
+
* both missing, on evidence the engine stamped.
|
|
286
|
+
*/
|
|
287
|
+
export interface AskRuleEvidence {
|
|
288
|
+
/** The org snapshot `revision` this call was adjudicated against — the published-policy version an
|
|
289
|
+
* auditor reconciles the decision against. Absent ⇒ {@link orgRevisionAbsent} names why. */
|
|
290
|
+
readonly orgRevision?: number;
|
|
291
|
+
/** Present iff {@link orgRevision} is not. `"unavailable"` here is the load-bearing one: the org
|
|
292
|
+
* layer spoke, its answer was "this deployment cannot see the organization's rules", and the ask
|
|
293
|
+
* in hand is the fail-closed tighten that followed — not an ask any published rule asked for. */
|
|
294
|
+
readonly orgRevisionAbsent?: AskEvidenceAbsence;
|
|
295
|
+
/** The org rule that spoke for this call, verbatim as published (org rules are administrator
|
|
296
|
+
* authored and are relayed unmodified — the prose channel's copy is the same text). Absent ⇒
|
|
297
|
+
* {@link orgRuleAbsent} names why. */
|
|
298
|
+
readonly orgRule?: string;
|
|
299
|
+
/** Present iff {@link orgRule} is not. */
|
|
300
|
+
readonly orgRuleAbsent?: AskEvidenceAbsence;
|
|
301
|
+
/** The add dots of the PERSONAL allow rule that matched this call but did not clear the ask (the
|
|
302
|
+
* #144 shadowed arm). The dots are the rule's durable identity — unlike
|
|
303
|
+
* {@link AskRequest.persistedRuleShadowed}, which is a sanitized, length-capped DISPLAY value and
|
|
304
|
+
* deliberately not an identity channel. A rule is a set of adds (concurrent approvals on one text
|
|
305
|
+
* each redeem their own dot), so this is an array by construction: render/reconcile the entries as
|
|
306
|
+
* data, never re-derive one scalar id by joining them. Absent ⇒ {@link personalRuleDotsAbsent}. */
|
|
307
|
+
readonly personalRuleDots?: readonly import("./permission-rule-model.js").RuleDot[];
|
|
308
|
+
/** Present iff {@link personalRuleDots} is not. */
|
|
309
|
+
readonly personalRuleDotsAbsent?: AskEvidenceAbsence;
|
|
310
|
+
}
|
|
209
311
|
/** The human/model-readable text of a decision. */
|
|
210
312
|
export declare function decisionText(d: PermissionResult): string | undefined;
|
|
211
313
|
/**
|
|
@@ -682,6 +784,15 @@ export interface AskRequest {
|
|
|
682
784
|
* structure: render the entries as data, never re-derive structure by splitting or joining them.
|
|
683
785
|
* The durable park route carries the same value as `RiskDescriptor.probeCause`. */
|
|
684
786
|
readonly probeCause?: import("./checkpoint-store.js").ProbeCause;
|
|
787
|
+
/** design/252 G-2: the RULE-PROVENANCE evidence behind this ask — the org snapshot revision and the
|
|
788
|
+
* matched rules' own identity keys, each present as a value or as a named absence (see
|
|
789
|
+
* {@link AskRuleEvidence}). Present on EVERY ask the engine mints, so a consumer never has to tell a
|
|
790
|
+
* missing field from a field that means something: the gate's own mint site fills it from what its
|
|
791
|
+
* layers did, and the three INHERITED-lane sites — which present an ANCESTOR policy's decision, from
|
|
792
|
+
* upstream of both lanes — fill it with an all-`"not_adjudicated"` record rather than a fabricated
|
|
793
|
+
* revision. Optional on the TYPE only because a deployment may invoke its approver directly.
|
|
794
|
+
* RECONCILIATION metadata, never adjudication input. */
|
|
795
|
+
readonly ruleEvidence?: AskRuleEvidence;
|
|
685
796
|
toolCallId: string;
|
|
686
797
|
/** The (post-rewrite) args the tool would run with. */
|
|
687
798
|
args: unknown;
|
|
@@ -859,12 +970,26 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
859
970
|
* domain the durable leg enforces pre-CAS for the same field), a THROWING read is a fail-closed
|
|
860
971
|
* deny naming the true cause (never a raw rejection out of the gate), and the empty string reads
|
|
861
972
|
* as absent (truthiness, the durable consumer's own read).
|
|
973
|
+
*
|
|
974
|
+
* `approver` (design/252 G-7) — the ATTRIBUTION seat: the identifier the approval channel reports for
|
|
975
|
+
* the party that ended this wait. Core does not authenticate it, compare it, or read it back to decide
|
|
976
|
+
* anything; it transcribes it onto the settlement observation next to `settledBy`, so an audit that can
|
|
977
|
+
* already say "a person ended this wait" can also say which one, without core growing an identity
|
|
978
|
+
* surface it deliberately does not have. Read the two together: `settledBy:"timeout"` with an
|
|
979
|
+
* `approver` names the queue whose window elapsed, NOT someone who refused.
|
|
980
|
+
* Screened, not trusted (see {@link screenApproverAttribution}): a non-string, an over-long value or one
|
|
981
|
+
* carrying control characters is a loud fail-closed refusal at {@link resolveAsk}, the same posture this
|
|
982
|
+
* seam takes for every other out-of-contract value — and the same reason, since a defective attribution
|
|
983
|
+
* that were quietly dropped would leave a settlement looking unattributed rather than misreported. It is
|
|
984
|
+
* carried on the arms where the CALLER settled something (its allow, its human/timeout deny) and never
|
|
985
|
+
* on an end the engine produced (an abort, a throw, an unclonable edit): nobody approved those.
|
|
862
986
|
*/
|
|
863
987
|
export type AskOutcome = boolean | "unavailable" | {
|
|
864
988
|
allow: boolean;
|
|
865
989
|
updatedInput?: unknown;
|
|
866
990
|
settledBy?: Extract<ApprovalSettledBy, "human" | "timeout">;
|
|
867
991
|
reason?: string;
|
|
992
|
+
approver?: string;
|
|
868
993
|
};
|
|
869
994
|
/**
|
|
870
995
|
* ruled 2026-08-04 — forward an approver into a delegated child, stamping every ask it raises with the
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -13,6 +13,26 @@ export const APPROVAL_SETTLED_BY_VALUES = ["human", "timeout", "aborted"];
|
|
|
13
13
|
export function isApprovalSettledBy(v) {
|
|
14
14
|
return typeof v === "string" && APPROVAL_SETTLED_BY_VALUES.includes(v);
|
|
15
15
|
}
|
|
16
|
+
export const APPROVER_ATTRIBUTION_MAX_CHARS = 256;
|
|
17
|
+
const APPROVER_REFUSED_CHARS_RE = /[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/u;
|
|
18
|
+
export function screenApproverAttribution(v) {
|
|
19
|
+
if (v === undefined || v === "")
|
|
20
|
+
return {};
|
|
21
|
+
if (typeof v !== "string") {
|
|
22
|
+
return { defect: `an approver attribution must be a plain string (got ${v === null ? "null" : typeof v})` };
|
|
23
|
+
}
|
|
24
|
+
if (v.length > 2 * APPROVER_ATTRIBUTION_MAX_CHARS || [...v].length > APPROVER_ATTRIBUTION_MAX_CHARS) {
|
|
25
|
+
return { defect: `an approver attribution is capped at ${APPROVER_ATTRIBUTION_MAX_CHARS} characters and this one is longer; it is refused rather than truncated, because a cut identifier names someone else` };
|
|
26
|
+
}
|
|
27
|
+
if (/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(v)) {
|
|
28
|
+
return { defect: "an approver attribution contains an unpaired surrogate; it is not well-formed text and would collapse to a replacement character in a UTF-8 sink, so two distinct approvers could converge into one record" };
|
|
29
|
+
}
|
|
30
|
+
if (APPROVER_REFUSED_CHARS_RE.test(v)) {
|
|
31
|
+
return { defect: "an approver attribution carries control characters, line separators or bidirectional overrides; an identifier whose bytes can forge a line — or reorder what a reader sees — in whatever renders it is refused" };
|
|
32
|
+
}
|
|
33
|
+
return { approver: v };
|
|
34
|
+
}
|
|
35
|
+
export const ASK_EVIDENCE_ABSENCE_VALUES = ["not_wired", "not_adjudicated", "unavailable", "no_match", "not_reported"];
|
|
16
36
|
export function decisionText(d) {
|
|
17
37
|
return d.message;
|
|
18
38
|
}
|
|
@@ -899,10 +919,12 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
899
919
|
let supplied;
|
|
900
920
|
let allowed;
|
|
901
921
|
let suppliedEdit;
|
|
922
|
+
let suppliedApprover;
|
|
902
923
|
try {
|
|
903
924
|
supplied = ok.settledBy;
|
|
904
925
|
allowed = ok.allow;
|
|
905
926
|
suppliedEdit = ok.updatedInput;
|
|
927
|
+
suppliedApprover = ok.approver;
|
|
906
928
|
}
|
|
907
929
|
catch (err) {
|
|
908
930
|
return {
|
|
@@ -930,6 +952,16 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
930
952
|
settledBy: "aborted",
|
|
931
953
|
};
|
|
932
954
|
}
|
|
955
|
+
const attribution = screenApproverAttribution(suppliedApprover);
|
|
956
|
+
if (attribution.defect !== undefined) {
|
|
957
|
+
return {
|
|
958
|
+
action: "deny",
|
|
959
|
+
message: `the approver for "${req.toolName}" reported an attribution this seam refuses: ${attribution.defect}; denied fail-closed`,
|
|
960
|
+
decisionReason: "mode",
|
|
961
|
+
settledBy: "aborted",
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
const attributionCell = attribution.approver !== undefined ? { approver: attribution.approver } : {};
|
|
933
965
|
if (supplied === "timeout" && allowed === true) {
|
|
934
966
|
return {
|
|
935
967
|
action: "deny",
|
|
@@ -969,10 +1001,11 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
969
1001
|
: humanRefusalMessage(req, reasonText),
|
|
970
1002
|
decisionReason: "mode",
|
|
971
1003
|
settledBy: supplied === "timeout" ? "timeout" : "human",
|
|
1004
|
+
...attributionCell,
|
|
972
1005
|
};
|
|
973
1006
|
}
|
|
974
1007
|
if (suppliedEdit === undefined)
|
|
975
|
-
return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settledBy: "human" };
|
|
1008
|
+
return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settledBy: "human", ...attributionCell };
|
|
976
1009
|
const edit = tryCloneArgs(suppliedEdit);
|
|
977
1010
|
if (!edit.ok) {
|
|
978
1011
|
return {
|
|
@@ -982,7 +1015,7 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
982
1015
|
settledBy: "aborted",
|
|
983
1016
|
};
|
|
984
1017
|
}
|
|
985
|
-
return { action: "allow", updatedInput: edit.value, decisionReason: "mode", settledBy: "human" };
|
|
1018
|
+
return { action: "allow", updatedInput: edit.value, decisionReason: "mode", settledBy: "human", ...attributionCell };
|
|
986
1019
|
}
|
|
987
1020
|
const okRaw = ok;
|
|
988
1021
|
if (okRaw === true)
|
package/dist/core/types.d.ts
CHANGED
|
@@ -3437,6 +3437,26 @@ export type TaskEvent = ({
|
|
|
3437
3437
|
* must not treat "absent" as "a human decided".
|
|
3438
3438
|
*/
|
|
3439
3439
|
settledBy?: import("./tool-policy.js").ApprovalSettledBy;
|
|
3440
|
+
/**
|
|
3441
|
+
* design/252 G-7 — WHOSE settlement that was: the identifier the approval channel reported for
|
|
3442
|
+
* the party that ended this wait, beside the {@link settledBy} word that says what KIND of end
|
|
3443
|
+
* it was. The two are read together and neither substitutes for the other:
|
|
3444
|
+
* `settledBy:"timeout"` with an `approver` names the queue whose window elapsed, NOT someone
|
|
3445
|
+
* who refused.
|
|
3446
|
+
*
|
|
3447
|
+
* WHAT CORE PROMISES ABOUT IT — exactly one thing: it is what the settling caller said, screened
|
|
3448
|
+
* for shape (a plain string, bounded, no control characters) and otherwise untouched. Core does
|
|
3449
|
+
* NOT authenticate it, does not compare it to a principal, and never reads it back to decide
|
|
3450
|
+
* anything. Identity is established by the approval channel a deployment integrates (its card,
|
|
3451
|
+
* its signature, its console); this is the transcription that lets an audit which already knows
|
|
3452
|
+
* a person ended a wait also say which person, without the engine growing an identity surface it
|
|
3453
|
+
* deliberately does not have. Treat it accordingly: it is a RECORD of a claim, and its
|
|
3454
|
+
* trustworthiness is exactly the trustworthiness of the channel that made it.
|
|
3455
|
+
*
|
|
3456
|
+
* ABSENT on every frame that settled no approval, and on a settled approval whose channel named
|
|
3457
|
+
* nobody. Absence means nobody SAID — never "nobody approved this", and never "a human did".
|
|
3458
|
+
*/
|
|
3459
|
+
approver?: string;
|
|
3440
3460
|
/**
|
|
3441
3461
|
* design/99 §E1 — `true` when {@link output} was SIZE-bounded by core (the full body exceeded the cap and
|
|
3442
3462
|
* was degraded to a truncated string). Lets a consumer detect truncation programmatically instead of
|
|
@@ -3627,9 +3647,15 @@ export type TaskEvent = ({
|
|
|
3627
3647
|
* (one event per attachment; a multi-attachment boundary still coalesces into ONE steer message
|
|
3628
3648
|
* on the model lane). [c209-C]: the listing family's FIRST-FRAME deliveries (initial roster /
|
|
3629
3649
|
* `<skills>` block riding the first user message, not a steer) emit the same echo frames.
|
|
3650
|
+
*
|
|
3651
|
+
* `git_status` (env-tail migration, additive member): the git-status frame — the turn-dynamic
|
|
3652
|
+
* git facts' carrier since they left the system prompt. Emitted at the frame's append RECEIPT
|
|
3653
|
+
* (first-frame / resume-continuation / compaction re-assert deliveries alike). Its `preview`
|
|
3654
|
+
* is a CONSTANT wording on purpose (never frame bytes): branch names and status text are
|
|
3655
|
+
* repo-controlled and must not enter the event telemetry plane through this echo.
|
|
3630
3656
|
*/
|
|
3631
3657
|
type: "steering_injected";
|
|
3632
|
-
source: "limit_approach" | "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools" | "final_verification";
|
|
3658
|
+
source: "limit_approach" | "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools" | "final_verification" | "git_status";
|
|
3633
3659
|
preview: string;
|
|
3634
3660
|
} & TaskEventIdentity) | ({
|
|
3635
3661
|
/**
|
|
@@ -3770,6 +3796,18 @@ export type TaskEvent = ({
|
|
|
3770
3796
|
* read absence as "not an agent".
|
|
3771
3797
|
*/
|
|
3772
3798
|
taskType?: DelegationTaskType;
|
|
3799
|
+
/**
|
|
3800
|
+
* #258 — the registry row's STOP-CYCLE generation this tick reports from (fresh
|
|
3801
|
+
* spawn = 1, every launched revival bumps it), the same counter `TaskNotificationPayload.seq`
|
|
3802
|
+
* and `BackgroundChildEvent.seq` already speak — one axis, not a third spelling. It answers
|
|
3803
|
+
* the one question a fleet consumer cannot otherwise decide when a frame arrives late: "late
|
|
3804
|
+
* first frame of the cycle I know (same value), or a revived run I have not folded yet
|
|
3805
|
+
* (higher value)?" Advisory observation, stamped at spawn/revive from the registry's own
|
|
3806
|
+
* counter; the settle-time ledger stays the authority. ABSENT is a fact, not a gap: a run
|
|
3807
|
+
* with no `a*` registry row (a SYNCHRONOUS delegated child, a workflow `wa*` agent, a
|
|
3808
|
+
* top-level run) has no generation concept, and absence must never be read as "cycle 1".
|
|
3809
|
+
*/
|
|
3810
|
+
seq?: number;
|
|
3773
3811
|
/** [1611] workflow-lane self-identification (server field-proof: the SSE-forwarded tick of a
|
|
3774
3812
|
* WORKFLOW child previously carried a bare uuid with no workflow identity — indistinguishable
|
|
3775
3813
|
* from an unknown nested subagent; the fleet lane had `wa*`+workflowRunId but this lane had
|
|
@@ -3876,8 +3914,12 @@ export interface TaskStream extends AsyncIterable<TaskEvent> {
|
|
|
3876
3914
|
/**
|
|
3877
3915
|
* Inject a mid-task **steering** message (design/47) that the running task sees at the start of its
|
|
3878
3916
|
* next turn (delivered via the harness steering queue). Resolves once queued; **throws** (code
|
|
3879
|
-
* `steering.not_running`)
|
|
3880
|
-
*
|
|
3917
|
+
* `steering.not_running`) once the task has finished (teardown included) — never silently dropped. A
|
|
3918
|
+
* steer issued BEFORE the run has ISSUED ITS FIRST PROMPT is not refused: it is HELD for the birth
|
|
3919
|
+
* window and enters the queue as soon as the loop goes live, so the model sees it at the next turn
|
|
3920
|
+
* boundary like any other steer — it is refused if the loop ends first, or if that BOUNDED wait runs
|
|
3921
|
+
* out while the run still has not started (ruled 2026-08-05; a retry under the same `inputId` is then
|
|
3922
|
+
* clean — a refusal reserves nothing). By default the text enters as a normal **user** message (in an orchestration the caller IS
|
|
3881
3923
|
* the task's user). Pass `trusted: true` ONLY for operator/system-level guidance: it is wrapped as a
|
|
3882
3924
|
* `<system-reminder>` (elevated authority) — do NOT use it for caller/third-party content that could
|
|
3883
3925
|
* carry a prompt injection. The seam gives the channel; the caller owns the judgement (design/43 §6).
|
|
@@ -3885,10 +3927,55 @@ export interface TaskStream extends AsyncIterable<TaskEvent> {
|
|
|
3885
3927
|
* design/171 §5.2 — `actor` attributes WHO steered (a shared session's second voice): the text gets
|
|
3886
3928
|
* the speaker envelope from the single projection point and the queued message carries the
|
|
3887
3929
|
* metadata seat. Attribution only, never authority; absent = anonymous (bytes unchanged).
|
|
3930
|
+
*
|
|
3931
|
+
* design/171 §6.3 (additive) — `inputId` is the caller's correlation/idempotency key, the SAME key
|
|
3932
|
+
* space as the parked queue's `PendingSteerEntry.inputId` and the `human_input` event's `inputId`
|
|
3933
|
+
* (an ingress that already minted a message id passes it here, and the emitted frame carries it
|
|
3934
|
+
* VERBATIM instead of a fresh uuidv7). It exists because the two legs of one steering ingress must be
|
|
3935
|
+
* equally replay-safe: the parked leg has taken this key since the queue landed, so a retried request
|
|
3936
|
+
* that arrives while the task is LIVE was the only one that injected twice.
|
|
3937
|
+
* - **Absent ⇒ nothing changes**: a uuidv7 is minted for the event, nothing is recorded for the call,
|
|
3938
|
+
* and the delivered bytes are what they always were. Every pre-existing caller is on this arm.
|
|
3939
|
+
* - **Replay ⇒ idempotent no-op**: re-steering an id this stream already ACCEPTED, with an identical
|
|
3940
|
+
* payload (same text, same `trusted`, same `actor`), injects nothing and emits no second
|
|
3941
|
+
* `human_input` frame. What is remembered is exactly what QUEUED: a delivery that was refused
|
|
3942
|
+
* (not running) can be retried under its own id, and a call whose DELIVERED payload is
|
|
3943
|
+
* whitespace-only — which the harness discards without minting a frame; note a `trusted` steer is
|
|
3944
|
+
* wrapped first, so it queues even for blank text — reserves nothing, leaving that id usable.
|
|
3945
|
+
* - **Same id, DIFFERENT instruction ⇒ typed throw** `steering.duplicate_input_id`: a key is not
|
|
3946
|
+
* evidence of a replay, and two callers colliding on one id must not silently lose the second
|
|
3947
|
+
* (the parked leg's `appendPendingSteer` refuses it identically). Re-issue under a fresh id.
|
|
3948
|
+
* - **Bad value ⇒ typed throw** `steering.invalid_content`, never a silent fallback to "no id": the
|
|
3949
|
+
* value domain is the parked leg's (a non-empty string of at most `MAX_STEER_INPUT_ID_CHARS`
|
|
3950
|
+
* characters, and never the reserved `LEGACY_PENDING_STEER_INPUT_ID`), so one key is accepted or
|
|
3951
|
+
* refused the same way on both legs. Validated before the liveness check, like the parked leg's.
|
|
3952
|
+
* - **Liveness outranks the key**: once the run's loop-liveness latch has flipped (the same signal
|
|
3953
|
+
* the injection path stops polling on) a replay is refused `steering.not_running` like any other
|
|
3954
|
+
* steer, never answered "already accepted" — the parked leg's row CAS answers `false` for a
|
|
3955
|
+
* resolved checkpoint on a replayed id for the same reason. The one asymmetry, stated rather than
|
|
3956
|
+
* papered over: in the sub-window where the harness has gone idle but the latch has not yet
|
|
3957
|
+
* flipped, a FRESH steer polls (and is refused when the latch flips) while a replay answers
|
|
3958
|
+
* immediately with the SAME outcome its original call reported — a key whose answer depended on
|
|
3959
|
+
* microsecond timing would defeat its own purpose. Note what that outcome has always meant on this
|
|
3960
|
+
* verb: ACCEPTED INTO THE QUEUE, not consumed by the model. A steer accepted in the last moments of
|
|
3961
|
+
* a run can be stranded by the run ending before the next boundary drains it (true of every steer,
|
|
3962
|
+
* keyed or not); a caller that needs delivery evidence reads the run's own events, not this receipt.
|
|
3963
|
+
* - **The receipt does NOT distinguish the two**: a fresh accept and a replay both resolve `void`,
|
|
3964
|
+
* exactly as `setPendingSteer` answers `true` for both. The observable difference is on the event
|
|
3965
|
+
* stream (a fresh accept emits the `human_input` frame; a replay emits none), which is also where
|
|
3966
|
+
* the parked leg's difference shows (a replay adds no queue entry, so the resume drains one frame).
|
|
3967
|
+
* - **Honest window** (weaker than the parked leg's, deliberately stated): the live dedup domain is
|
|
3968
|
+
* THIS stream object — one run leg, in this process. It is not persisted, so it does not span a
|
|
3969
|
+
* restart, a replica, or a second `runTaskStream`/`resumeStream` call on the same session; and it
|
|
3970
|
+
* holds only what the LIVE verb accepted, so an id already delivered by the parked leg (drained
|
|
3971
|
+
* into this run's resume prompt) is NOT in it and WOULD inject again. A deployment that needs
|
|
3972
|
+
* cross-leg or cross-process idempotency owns that half (its own key ledger), the same division
|
|
3973
|
+
* of labor `notify`'s park-window dedup states.
|
|
3888
3974
|
*/
|
|
3889
3975
|
steer(text: string, options?: {
|
|
3890
3976
|
trusted?: boolean;
|
|
3891
3977
|
actor?: ActorAssertion;
|
|
3978
|
+
inputId?: string;
|
|
3892
3979
|
}): Promise<void>;
|
|
3893
3980
|
/**
|
|
3894
3981
|
* design/144 §2 — inject an EXTERNAL structured event into this run's task-notification lane, as a
|
|
@@ -4225,12 +4312,14 @@ export interface BackgroundChildEvent {
|
|
|
4225
4312
|
resumable?: boolean;
|
|
4226
4313
|
/** terminal: the settled status. */
|
|
4227
4314
|
status?: "completed" | "killed" | "failed";
|
|
4228
|
-
/** terminal (
|
|
4229
|
-
* task_notification's `TaskNotificationPayload.seq` (same settle, same X3
|
|
4230
|
-
*
|
|
4231
|
-
*
|
|
4232
|
-
*
|
|
4233
|
-
*
|
|
4315
|
+
/** terminal + spawn/tick (#258 widened the carriers): the stop-cycle number — on a TERMINAL frame
|
|
4316
|
+
* a MIRROR of the sibling task_notification's `TaskNotificationPayload.seq` (same settle, same X3
|
|
4317
|
+
* snapshot); on SPAWN and TICK frames the registry row's generation at emit (fresh spawn = 1, a
|
|
4318
|
+
* revived cycle's bumped counter), so a fleet consumer can tell a LATE first frame from a revived
|
|
4319
|
+
* cycle's frame without waiting for the terminal. One axis with `task_progress`'s `seq` — the tick
|
|
4320
|
+
* mirrors the frame's own stamp. Same honest downgrade as ever: present whenever a cycle number is
|
|
4321
|
+
* knowable (the registry handle / retain ledger's `cycleSeq`, the durable row's `seq` on a tier-3
|
|
4322
|
+
* revival / parked resume), absent when no carrier exists (forging a period would lie). */
|
|
4234
4323
|
seq?: number;
|
|
4235
4324
|
/** terminal, P1-3(黑板 [1920]/[1921]/[1924]/[1925]): the cross-channel completion correlation id —
|
|
4236
4325
|
* MIRROR of the sibling task_notification's `TaskNotificationPayload.completionId` (same settle,
|
|
@@ -498,6 +498,53 @@ export interface WorkspaceState {
|
|
|
498
498
|
export interface WorkspaceStateEntry extends SessionTreeEntryBase, WorkspaceState {
|
|
499
499
|
type: "workspace_state";
|
|
500
500
|
}
|
|
501
|
+
/** The closed kind set of a git-status announcement (the availability half of the `(kind, hash)`
|
|
502
|
+
* comparison tuple): `full` = five-segment snapshot frame; `degraded` = branch+dirty two-line frame
|
|
503
|
+
* (the snapshot round-trip failed while the basic probe succeeded); `unavailable` / `non-repo` =
|
|
504
|
+
* tombstone frames announcing that earlier git frames no longer describe the tree. */
|
|
505
|
+
export type GitAnnouncementKind = "full" | "degraded" | "unavailable" | "non-repo";
|
|
506
|
+
/**
|
|
507
|
+
* The git-status frame ANNOUNCED STATE — which rendered git view the model has last been shown on
|
|
508
|
+
* this branch, as a `(kind, hash)` tuple (hash = content hash of the rendered frame body with the
|
|
509
|
+
* frame format version and the canonical repo root bound into the digest domain). Two-phase receipt
|
|
510
|
+
* protocol: `pending: true` = a re-announcement is OWED but its frame append has not produced a
|
|
511
|
+
* receipt (a compaction restates the tuple as pending in the same CAS that lands the summary; the
|
|
512
|
+
* announced form is written only once the frame's own append returned an entry id) — any reader
|
|
513
|
+
* that finds pending nearest MUST conservatively re-announce. `entryId` = the session entry of the
|
|
514
|
+
* message CARRYING the announced frame; an announced state whose entryId is not on the active
|
|
515
|
+
* branch (rewind cut it) is treated as pending by the read walk (branch-authority read ladder).
|
|
516
|
+
*/
|
|
517
|
+
export interface GitAnnouncementState {
|
|
518
|
+
kind: GitAnnouncementKind;
|
|
519
|
+
hash: string;
|
|
520
|
+
/** Session entry id of the message carrying the announced frame (absent on a pending restatement). */
|
|
521
|
+
entryId?: string;
|
|
522
|
+
/** Two-phase receipt: the tuple is owed but its frame append has no receipt yet. */
|
|
523
|
+
pending?: true;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* The git-status announced state as a FIRST-CLASS typed entry (same snapshot doctrine as
|
|
527
|
+
* {@link AnnouncedListingEntry}): the nearest carrier on the branch IS the announced state.
|
|
528
|
+
* Written at every frame COMMIT point (first-frame receipt / resume-continuation receipt /
|
|
529
|
+
* compaction re-assertion receipt); a compaction additionally restates the tuple as `pending`
|
|
530
|
+
* inside its own `details.gitAnnouncement` (same-CAS with the new baseline) until the re-asserted
|
|
531
|
+
* frame's append receipt lands. The checkpoint mirror (`CheckpointState.gitAnnouncement`) is the
|
|
532
|
+
* lower seed rung, consulted only when the branch carries no mirror at all.
|
|
533
|
+
*/
|
|
534
|
+
export interface GitAnnouncementEntry extends SessionTreeEntryBase, GitAnnouncementState {
|
|
535
|
+
type: "git_announcement";
|
|
536
|
+
}
|
|
537
|
+
/** Bounded cap for {@link normalizeGitAnnouncement} entry ids (self-generated ids are short; an
|
|
538
|
+
* oversize value is a forgery signal, same posture as the sibling caps). */
|
|
539
|
+
export declare const GIT_ANNOUNCEMENT_MAX_ENTRY_ID_CHARS = 256;
|
|
540
|
+
/**
|
|
541
|
+
* Strict shape gate for the git announced state — the SINGLE normalization the read walk, the
|
|
542
|
+
* public append, and the import-validate door all use (the {@link normalizeAnnouncedListing}
|
|
543
|
+
* posture). Returns a shaped copy carrying ONLY the known keys, or undefined when the value is not
|
|
544
|
+
* structurally valid: closed kind set, full `sha256:<hex64>` hash grammar (a prefix-only check
|
|
545
|
+
* would accept junk), bounded entryId, `pending` only as literal `true`.
|
|
546
|
+
*/
|
|
547
|
+
export declare function normalizeGitAnnouncement(v: unknown): GitAnnouncementState | undefined;
|
|
501
548
|
/** Bounded cap for {@link normalizeWorkspaceState} paths (a sha is capped by its hex shape check). */
|
|
502
549
|
export declare const WORKSPACE_STATE_MAX_PATH_CHARS = 4096;
|
|
503
550
|
/**
|
|
@@ -544,7 +591,7 @@ export declare function normalizeAnnouncedListing(v: unknown): {
|
|
|
544
591
|
models?: string[];
|
|
545
592
|
} | undefined;
|
|
546
593
|
/** All persisted session tree entry variants. */
|
|
547
|
-
export type SessionTreeEntry = MessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | CompactionEntry | CustomEntry | CustomMessageEntry | LabelEntry | SessionInfoEntry | LeafEntry | PromptEpochEntry | AnnouncedListingEntry | WorkspaceStateEntry;
|
|
594
|
+
export type SessionTreeEntry = MessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | CompactionEntry | CustomEntry | CustomMessageEntry | LabelEntry | SessionInfoEntry | LeafEntry | PromptEpochEntry | AnnouncedListingEntry | GitAnnouncementEntry | WorkspaceStateEntry;
|
|
548
595
|
export interface SessionContext {
|
|
549
596
|
messages: AgentMessage[];
|
|
550
597
|
thinkingLevel: string;
|
|
@@ -684,6 +731,23 @@ export interface Session<TMetadata extends SessionMetadata = SessionMetadata> {
|
|
|
684
731
|
* epoch restatement) calls it with `?.()` — an implementer without it degrades to the pre-epoch
|
|
685
732
|
* path (no restatement), never a crash. */
|
|
686
733
|
getPromptEpoch?(): Promise<import("../../prompt-assembly/epoch.js").PromptEpochDescriptor | undefined>;
|
|
734
|
+
/** Persist the git-status announced state as a first-class {@link GitAnnouncementEntry}
|
|
735
|
+
* (snapshot semantics; see `Session.appendGitAnnouncement`). OPTIONAL on the interface — same
|
|
736
|
+
* external-implementer posture as `getPromptEpoch`; consumers call it with `?.()` and an
|
|
737
|
+
* implementer without it degrades to conservative re-announcement every leg (duplicate-tolerant
|
|
738
|
+
* by design), never a crash. */
|
|
739
|
+
appendGitAnnouncement?(state: GitAnnouncementState): Promise<string>;
|
|
740
|
+
/** Branch-authority read of the git announced state (see `Session.getGitAnnouncement`): nearest
|
|
741
|
+
* carrier on the active branch, with a pending restatement — or an announced state whose frame
|
|
742
|
+
* entryId is NOT on the active branch — surfaced as `status:"pending"` (the reader must
|
|
743
|
+
* conservatively re-announce). Undefined ⇒ no carrier visible on this branch. OPTIONAL, same
|
|
744
|
+
* posture as `appendGitAnnouncement`. */
|
|
745
|
+
getGitAnnouncement?(): Promise<{
|
|
746
|
+
kind: GitAnnouncementKind;
|
|
747
|
+
hash: string;
|
|
748
|
+
status: "announced" | "pending";
|
|
749
|
+
entryId?: string;
|
|
750
|
+
} | undefined>;
|
|
687
751
|
}
|
|
688
752
|
export interface SessionCreateOptions {
|
|
689
753
|
id?: string;
|
|
@@ -66,6 +66,26 @@ export class AgentHarnessError extends Error {
|
|
|
66
66
|
this.code = code;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
export const GIT_ANNOUNCEMENT_MAX_ENTRY_ID_CHARS = 256;
|
|
70
|
+
export function normalizeGitAnnouncement(v) {
|
|
71
|
+
if (typeof v !== "object" || v === null)
|
|
72
|
+
return undefined;
|
|
73
|
+
const src = v;
|
|
74
|
+
if (src.kind !== "full" && src.kind !== "degraded" && src.kind !== "unavailable" && src.kind !== "non-repo")
|
|
75
|
+
return undefined;
|
|
76
|
+
if (typeof src.hash !== "string" || !/^sha256:[0-9a-f]{64}$/.test(src.hash))
|
|
77
|
+
return undefined;
|
|
78
|
+
if (src.entryId !== undefined && (typeof src.entryId !== "string" || src.entryId.length === 0 || src.entryId.length > GIT_ANNOUNCEMENT_MAX_ENTRY_ID_CHARS))
|
|
79
|
+
return undefined;
|
|
80
|
+
if (src.pending !== undefined && src.pending !== true)
|
|
81
|
+
return undefined;
|
|
82
|
+
return {
|
|
83
|
+
kind: src.kind,
|
|
84
|
+
hash: src.hash,
|
|
85
|
+
...(src.entryId !== undefined ? { entryId: src.entryId } : {}),
|
|
86
|
+
...(src.pending === true ? { pending: true } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
69
89
|
export const WORKSPACE_STATE_MAX_PATH_CHARS = 4096;
|
|
70
90
|
export function normalizeWorkspaceState(value) {
|
|
71
91
|
if (value === null || typeof value !== "object")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SessionError, normalizeAnnouncedListing, normalizeWorkspaceState, normalizeCompactionStateCarrier, isValidThinkingLevelChange, isValidModelChange, isOptionalDisplayString, } from "../harness/types.js";
|
|
1
|
+
import { SessionError, normalizeAnnouncedListing, normalizeGitAnnouncement, normalizeWorkspaceState, normalizeCompactionStateCarrier, isValidThinkingLevelChange, isValidModelChange, isOptionalDisplayString, } from "../harness/types.js";
|
|
2
2
|
import { leafIdAfterEntry } from "./storage-base.js";
|
|
3
3
|
import { parseSessionTimestampMs } from "./timestamps.js";
|
|
4
4
|
import { flattenableUserText, normalizeEngineSegments } from "../../core/untrusted-text.js";
|
|
@@ -87,6 +87,11 @@ export class StreamingImportValidator {
|
|
|
87
87
|
throw new SessionError("invalid_session", `workspace_state entry "${e.id}" is structurally invalid`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
else if (e.type === "git_announcement") {
|
|
91
|
+
if (!normalizeGitAnnouncement(e)) {
|
|
92
|
+
throw new SessionError("invalid_session", `git_announcement entry "${e.id}" is structurally invalid`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
90
95
|
if (e.type === "thinking_level_change" && !isValidThinkingLevelChange(e)) {
|
|
91
96
|
throw new SessionError("invalid_session", `thinking_level_change entry "${e.id}" carries an invalid thinkingLevel`);
|
|
92
97
|
}
|
|
@@ -119,6 +124,10 @@ export class StreamingImportValidator {
|
|
|
119
124
|
if (restatedListings !== undefined && !normalizeAnnouncedListing(restatedListings)) {
|
|
120
125
|
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid announcedListings restatement`);
|
|
121
126
|
}
|
|
127
|
+
const restatedGit = e.details?.gitAnnouncement;
|
|
128
|
+
if (restatedGit !== undefined && !normalizeGitAnnouncement(restatedGit)) {
|
|
129
|
+
throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid gitAnnouncement restatement`);
|
|
130
|
+
}
|
|
122
131
|
const carrier = e.details;
|
|
123
132
|
const shaped = normalizeCompactionStateCarrier(e.details);
|
|
124
133
|
if (carrier?.thinkingLevel !== undefined && shaped.thinkingLevel === undefined) {
|