@sema-agent/core 7.29.0 → 7.30.1
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 +393 -0
- package/dist/agents/child-model-seat.d.ts +59 -7
- package/dist/agents/child-model-seat.js +33 -7
- package/dist/agents/cross-session-judge.d.ts +27 -5
- package/dist/agents/cross-session-judge.js +23 -12
- package/dist/agents/list-agents-tool.d.ts +31 -6
- package/dist/agents/list-agents-tool.js +66 -14
- package/dist/agents/peer-admission.d.ts +20 -8
- package/dist/agents/peer-admission.js +21 -12
- package/dist/agents/peer-directory.d.ts +29 -0
- package/dist/agents/peer-directory.js +20 -0
- package/dist/agents/peer-held-queue.d.ts +31 -4
- package/dist/agents/peer-held-queue.js +32 -1
- package/dist/agents/peer-session-drain.d.ts +91 -13
- package/dist/agents/peer-session-drain.js +409 -102
- package/dist/agents/read-inbox-tool.d.ts +22 -0
- package/dist/agents/read-inbox-tool.js +39 -0
- package/dist/agents/roster-store.d.ts +2 -4
- package/dist/agents/roster-store.js +1 -1
- package/dist/agents/send-message-tool.d.ts +4 -4
- package/dist/agents/send-message-tool.js +22 -2
- package/dist/agents/subagent.js +62 -44
- package/dist/agents/team-role.d.ts +34 -0
- package/dist/agents/team-role.js +17 -0
- package/dist/agents/verify.js +2 -2
- package/dist/core/auto-compaction.d.ts +42 -0
- package/dist/core/auto-compaction.js +35 -2
- package/dist/core/child-clamp-seats.d.ts +11 -0
- package/dist/core/child-clamp-seats.js +3 -0
- package/dist/core/context-edit.d.ts +3 -2
- package/dist/core/context-edit.js +6 -5
- package/dist/core/context-guard.d.ts +2 -1
- package/dist/core/context-guard.js +3 -2
- package/dist/core/context-settings.d.ts +103 -0
- package/dist/core/context-settings.js +60 -0
- package/dist/core/engine-notice.d.ts +11 -1
- package/dist/core/frame-settlement.d.ts +97 -0
- package/dist/core/frame-settlement.js +36 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +10 -10
- package/dist/core/mailbox-store.d.ts +41 -7
- package/dist/core/mailbox-store.js +9 -3
- package/dist/core/parent-spec-seat.d.ts +14 -1
- package/dist/core/parent-spec-seat.js +6 -0
- package/dist/core/permission-rule-model.js +1 -1
- package/dist/core/roles.d.ts +20 -0
- package/dist/core/roles.js +35 -16
- package/dist/core/runner/assemble-result.d.ts +2 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/compaction-call-options.d.ts +26 -2
- package/dist/core/runner/compaction-call-options.js +9 -0
- package/dist/core/runner/compaction-seams.d.ts +4 -3
- package/dist/core/runner/compaction-seams.js +13 -6
- package/dist/core/runner/contracts.d.ts +56 -7
- package/dist/core/runner/decide-continuation.d.ts +2 -1
- package/dist/core/runner/decide-continuation.js +11 -10
- package/dist/core/runner/initial-run-state.js +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
- package/dist/core/runner/prepare-config-doors.js +4 -1
- package/dist/core/runner/prepare-context-lane.d.ts +9 -2
- package/dist/core/runner/prepare-context-lane.js +15 -10
- package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
- package/dist/core/runner/prepare-delegation-surface.js +9 -3
- package/dist/core/runner/prepare-file-history.js +10 -8
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
- package/dist/core/runner/prepare-gate-stations.js +3 -1
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
- package/dist/core/runner/prepare-prompt-inputs.js +2 -1
- package/dist/core/runner/prepare-task.js +22 -9
- package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
- package/dist/core/runner/prepare-wiring-manifest.js +6 -1
- package/dist/core/runner/retired-runner-deps.d.ts +3 -1
- package/dist/core/runner/retired-runner-deps.js +1 -0
- package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
- package/dist/core/runner/run-compaction-machinery.js +5 -3
- package/dist/core/runner/run-harness-handlers.js +1 -1
- package/dist/core/runner/run-identity-wiring.js +19 -1
- package/dist/core/runner/run-leg.js +1 -1
- package/dist/core/runner/run-notification-lane.js +8 -2
- package/dist/core/runner/run-recovery-lanes.js +9 -5
- package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
- package/dist/core/runner/run-settle-and-teardown.js +6 -10
- package/dist/core/runner/run-stop-and-final-verify.js +1 -0
- package/dist/core/runner/run-terminal-adoption.js +5 -5
- package/dist/core/runner/run-turn-boundary.js +32 -5
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +32 -1
- package/dist/core/runner-deps.d.ts +44 -13
- package/dist/core/secret-env.js +2 -1
- package/dist/core/swappable-deps.d.ts +20 -4
- package/dist/core/swappable-deps.js +6 -1
- package/dist/core/task-event.d.ts +5 -0
- package/dist/core/task-notification.d.ts +144 -2
- package/dist/core/task-notification.js +154 -5
- package/dist/core/task-result.d.ts +14 -0
- package/dist/core/task-spec.d.ts +7 -0
- package/dist/core/tool-catalog-entries.js +2 -1
- package/dist/core/tool-conformance.js +1 -0
- package/dist/core/tool-policy.d.ts +9 -4
- package/dist/core/tool-spec.d.ts +14 -0
- package/dist/core/wiring-manifest.d.ts +12 -0
- package/dist/core/wiring-manifest.js +5 -1
- package/dist/engine/harness/agent-harness.d.ts +14 -1
- package/dist/engine/harness/agent-harness.js +28 -3
- package/dist/engine/harness/types.d.ts +3 -1
- package/dist/engine/llm/types.d.ts +8 -7
- package/dist/engine/llm/types.js +1 -1
- package/dist/engine/lsp/node-lsp-manager.d.ts +3 -0
- package/dist/engine/lsp/node-lsp-manager.js +5 -3
- package/dist/engine/session/import-validate.js +4 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.js +7 -5
- package/dist/orchestration/run-workflow-tool.d.ts +4 -4
- package/dist/orchestration/workflow.d.ts +1 -1
- package/dist/orchestration/workflow.js +8 -5
- package/dist/shell/lex.d.ts +38 -54
- package/dist/shell/lex.js +239 -286
- package/dist/stores/cc/mailbox-store.js +3 -2
- package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
- package/dist/stores/file/file-history-store.d.ts +2 -4
- package/dist/stores/file/file-history-store.js +1 -1
- package/dist/stores/file/fs-atomic.d.ts +16 -11
- package/dist/stores/file/fs-atomic.js +17 -6
- package/dist/stores/file/index.d.ts +5 -6
- package/dist/stores/file/index.js +1 -1
- package/dist/stores/file/mailbox-store.d.ts +4 -9
- package/dist/stores/file/mailbox-store.js +29 -30
- package/dist/stores/file/session-policy-store.d.ts +2 -0
- package/dist/stores/file/session-policy-store.js +1 -1
- package/dist/stores/file/session-store.d.ts +11 -7
- package/dist/stores/file/session-store.js +23 -16
- package/dist/stores/file/shared-ledger.js +3 -14
- package/dist/stores/file/tool-result-store.d.ts +2 -0
- package/dist/stores/file/tool-result-store.js +5 -2
- package/dist/stores/file/workflow-journal-store.js +14 -9
- package/dist/tools/fs/bash-program-position.d.ts +15 -24
- package/dist/tools/fs/bash-program-position.js +11 -29
- package/dist/tools/fs/bash-readonly-classifier.d.ts +131 -117
- package/dist/tools/fs/bash-readonly-classifier.js +404 -272
- package/dist/tools/fs/shell-read-check.js +3 -3
- package/dist/tools/fs/shell-read-establish.d.ts +1 -1
- package/dist/tools/fs/shell-read-establish.js +5 -33
- package/dist/tools/fs/shell-read-identity.d.ts +13 -0
- package/dist/tools/fs/shell-read-identity.js +39 -0
- package/dist/tools/fs/shell-read-judge.d.ts +8 -0
- package/dist/tools/fs/shell-read-judge.js +7 -0
- package/dist/tools/fs/shell-read-plan.d.ts +15 -6
- package/dist/tools/fs/shell-read-plan.js +171 -195
- package/dist/tools/fs/shell-read-word.d.ts +64 -0
- package/dist/tools/fs/shell-read-word.js +94 -0
- package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
- package/dist/tools/fs/shell-reentry-text.js +73 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +58 -8
- package/wire-changes/core@7.30.0.json +1821 -0
- package/wire-changes/core@7.30.1.json +154 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Model } from "../internal/llm.js";
|
|
8
8
|
import type { AgentDefinition } from "./agent-definition.js";
|
|
9
|
+
import type { ContextSettings } from "./context-settings.js";
|
|
9
10
|
import type { BackgroundChildEvent, DelegationLifecycleEvent } from "./delegation-frames.js";
|
|
10
11
|
import type { EngineNotice } from "./engine-notice.js";
|
|
11
12
|
import type { HandsBandOptions } from "./hands-band.js";
|
|
@@ -113,6 +114,22 @@ export interface RunnerDeps {
|
|
|
113
114
|
brain: Brain;
|
|
114
115
|
/** Catalog used to resolve string ModelRefs to Model objects. */
|
|
115
116
|
models?: Record<string, Model>;
|
|
117
|
+
/**
|
|
118
|
+
* The deployment's per-model CONTEXT-WINDOW overlay, keyed by CATALOG WORD ({@link ContextSettings}):
|
|
119
|
+
* `contextTokens` / `autoCompactTokens` / `enabled` over what the model card declares.
|
|
120
|
+
*
|
|
121
|
+
* It is the ONE hot-swappable seat an IN-FLIGHT leg re-reads. Every other seat follows the snapshot law
|
|
122
|
+
* (a running task keeps the deps object it prepared under) because those seats are the routing card —
|
|
123
|
+
* endpoint, credentials, price, fence — and a request in flight must reach what it was priced for. The
|
|
124
|
+
* usable window is not the card: it is a setting, and an operator who lowers it is asking the legs
|
|
125
|
+
* running RIGHT NOW to compact sooner, which is exactly what they were unable to do while the number
|
|
126
|
+
* lived on the model object. Read at every turn boundary, every recovery pass, the end-of-task summary
|
|
127
|
+
* and every request build; clamped to the physical window by the one resolver.
|
|
128
|
+
*
|
|
129
|
+
* A leg that was handed a `Model` OBJECT (or resolved one through a `select` criteria) has no catalog
|
|
130
|
+
* word and therefore no overlay — the pin is the caller saying "this exact model, as declared".
|
|
131
|
+
*/
|
|
132
|
+
contextSettings?: ContextSettings;
|
|
116
133
|
/**
|
|
117
134
|
* design/338 (mid-turn MCP revocation) — the HOST's revocation ledger, probed synchronously at
|
|
118
135
|
* every MCP dispatch (tool call + the three resource tools) BEFORE the transport. The engine
|
|
@@ -512,9 +529,11 @@ export interface RunnerDeps {
|
|
|
512
529
|
* {@link InMemoryFileHistoryStore}; a deployment supplies a durable backend behind the same seam
|
|
513
530
|
* (backends MUST reuse the exported env-half helpers — see the contract JSDoc). Works against ANY
|
|
514
531
|
* ExecutionEnv (local or remote): bytes are read/written through the env's FileSystem face and
|
|
515
|
-
* stored host-side, so an env rebuild does not lose history.
|
|
532
|
+
* stored host-side, so an env rebuild does not lose history. `"disabled"` = the deployment DECIDED
|
|
533
|
+
* not to capture: the omitted-key semantics minus the once-per-Runner `onError` advisory an omitted
|
|
534
|
+
* key earns on a filesystem-capable run (`classification: "file-history-no-store"`).
|
|
516
535
|
*/
|
|
517
|
-
fileHistoryStore?: import("./file-history-store.js").FileHistoryStore;
|
|
536
|
+
fileHistoryStore?: import("./file-history-store.js").FileHistoryStore | "disabled";
|
|
518
537
|
/**
|
|
519
538
|
* design/381 DV-14 (终审落锤) — what an Edit/Write/NotebookEdit does when its FIRST-TOUCH history
|
|
520
539
|
* record cannot be persisted (`FileHistoryStore.trackEdit` answered `{ok:false}` = NO durable
|
|
@@ -877,12 +896,12 @@ export interface RunnerDeps {
|
|
|
877
896
|
maxConcurrent?: number;
|
|
878
897
|
maxCumulativePerSession?: number;
|
|
879
898
|
};
|
|
880
|
-
/**
|
|
881
|
-
*
|
|
882
|
-
*
|
|
883
|
-
* absent means the default table
|
|
884
|
-
* `SendMessageToolOptions.
|
|
885
|
-
|
|
899
|
+
/** The peer lane's ONE knob table (`PeerLaneConfig`): the always-on admission gate (rate/dedup/hop-chain/
|
|
900
|
+
* queue bounds), the wake and per-pass delivery budgets, and the `ListAgents` freshness window. Per-field
|
|
901
|
+
* range-validated with out-of-range values falling back to that field's default, announced once; there
|
|
902
|
+
* is NO off switch — absent means the default table. Threaded into the auto-mounted SendMessage as
|
|
903
|
+
* `SendMessageToolOptions.lane` and into the session-box drain; read per call / per pass. */
|
|
904
|
+
peerLane?: Partial<import("../agents/peer-admission.js").PeerLaneConfig>;
|
|
886
905
|
/**
|
|
887
906
|
* design/385 §2.1 / §5.1 — the peer-session DIRECTORY: the discovery truth of the cross-session
|
|
888
907
|
* lane (other sessions of this engine for the same user). A HOST implements the contract (cli: the
|
|
@@ -917,6 +936,16 @@ export interface RunnerDeps {
|
|
|
917
936
|
* {@link peerDirectory}.
|
|
918
937
|
*/
|
|
919
938
|
crossSessionDialogExpiry?: import("../agents/peer-notices.js").CrossSessionDialogExpiry | (() => import("../agents/peer-notices.js").CrossSessionDialogExpiry | undefined);
|
|
939
|
+
/**
|
|
940
|
+
* The deployment's CAPABILITY FLOOR for durable engine frames: `true` declares that EVERY process
|
|
941
|
+
* that drains this deployment's session boxes reads `engine_frame` records (this engine's codec or
|
|
942
|
+
* newer). With it, an engine-minted task-notification frame addressed to an idle session parks in
|
|
943
|
+
* the session's durable box (`MailboxPeerMeta.kind: "engine_frame"`) and survives a process restart;
|
|
944
|
+
* without it (absent / `false`) such a frame parks in process exactly as before. A deployment flips
|
|
945
|
+
* it only after every drainer is upgraded — a pre-codec drainer reads the record as a peer's words.
|
|
946
|
+
* Inert without a cross-process-safe {@link mailboxStore}.
|
|
947
|
+
*/
|
|
948
|
+
sessionBoxEngineFrames?: boolean;
|
|
920
949
|
/**
|
|
921
950
|
* design/164 件五 — DEPLOYMENT-level usage governance: allowances that span TASKS, evaluated per
|
|
922
951
|
* principal (or once for the whole deployment when a task declares none). A different axis from
|
|
@@ -1266,11 +1295,13 @@ export interface RunnerDeps {
|
|
|
1266
1295
|
phase: "compaction" | "prompt-cache" | "prompt-constitution" | "degraded" | "config" | "memory" | "mcp" | "a2a" | "interrupt-reconcile" | "suggestions" | "rewind" | "hook";
|
|
1267
1296
|
sessionId: string;
|
|
1268
1297
|
/**
|
|
1269
|
-
* Machine-readable sub-classification when the
|
|
1270
|
-
* cache-break detector's root cause (`"model-switch" | "tool-schema" |
|
|
1271
|
-
* "system-prefix" | "server-or-ttl"`)
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1298
|
+
* Machine-readable sub-classification when the site has one (a kebab-case word naming the arm) —
|
|
1299
|
+
* e.g. the design/31 cache-break detector's root cause (`"model-switch" | "tool-schema" |
|
|
1300
|
+
* "tool-set" | "system-prefix" | "server-or-ttl"`), or the file-history liveness arms
|
|
1301
|
+
* (`"file-history-no-store"` / `"file-history-no-env"`). Lets the host pick its own log level:
|
|
1302
|
+
* `"server-or-ttl"` is usually benign in agentic tasks (slow tools ⇒ 5min+ request gaps expire
|
|
1303
|
+
* provider caches — service[64] flag②) and warrants info/warn, while the prefix-bug causes
|
|
1304
|
+
* warrant attention.
|
|
1274
1305
|
*/
|
|
1275
1306
|
classification?: string;
|
|
1276
1307
|
}) => void;
|
package/dist/core/secret-env.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const
|
|
1
|
+
const TOKEN_COUNT_WORDS = ["MAX", "OUTPUT", "INPUT", "THINKING", "NUM", "TOTAL"];
|
|
2
|
+
const SECRET_ENV_RE = new RegExp(`(^|_)(KEY|KEYS|TOKEN|(?<!(?:^|_)(?:${TOKEN_COUNT_WORDS.join("|")})_)TOKENS|SECRET|SECRETS|PASSWORD|PASSWORDS|PASSWD|CREDENTIAL|CREDENTIALS|PRIVATEKEY|APIKEY|ACCESS_KEY|SESSION_TOKEN)(?:_(?:\\d+|ID))?$`, "i");
|
|
2
3
|
const SECRET_ENV_EXACT_NAMES = new Set([
|
|
3
4
|
"DATABASE_URL", "CONNECTION_STRING", "DSN", "NPM_AUTH",
|
|
4
5
|
"PGPASSWORD", "MYSQL_PWD", "SSHPASS", "GPG_PASSPHRASE", "GH_PAT", "GITHUB_PAT",
|
|
@@ -12,11 +12,23 @@
|
|
|
12
12
|
* the door refuses a key outside the set at run time (never a silent ignore — a hot-config driver
|
|
13
13
|
* handing `{ brain }` must learn that the brain is not a swappable seat, not watch nothing happen).
|
|
14
14
|
*
|
|
15
|
-
* WHAT A SWAP MEANS — the natural snapshot, one sentence for every seat: a running task reads the
|
|
16
|
-
* object it was handed at its prepare and holds that reference; a swap replaces the Runner's object, so
|
|
15
|
+
* WHAT A SWAP MEANS — the natural snapshot, one sentence for every ROUTING seat: a running task reads the
|
|
16
|
+
* deps object it was handed at its prepare and holds that reference; a swap replaces the Runner's object, so
|
|
17
17
|
* every LATER prepare reads the new seat and every in-flight leg keeps the value it prepared under.
|
|
18
18
|
* Nothing is re-routed, re-priced or re-fenced mid-run; the divergence window is exactly the in-flight
|
|
19
|
-
* legs' lifetime, by design (the `swapModels` precedent
|
|
19
|
+
* legs' lifetime, by design (the `swapModels` precedent).
|
|
20
|
+
*
|
|
21
|
+
* …WITH ONE SEAT OUTSIDE IT, and the boundary is the point rather than the exception: `contextSettings` is
|
|
22
|
+
* read LIVE by in-flight legs. The snapshot law protects the routing CARD — a request already in flight must
|
|
23
|
+
* reach the endpoint it was priced, credentialed and fenced for, and re-reading any of that mid-run buys
|
|
24
|
+
* nothing and breaks the request. A leg's usable WINDOW is not on that card: it is a setting about how much
|
|
25
|
+
* conversation may accumulate before the leg summarizes, the reference implementation keeps it off the model
|
|
26
|
+
* object for exactly this reason and re-reads it on every request (clamped to the model's physical window),
|
|
27
|
+
* and an operator who lowers it is asking the legs running RIGHT NOW to compact sooner — which is the whole
|
|
28
|
+
* request. So the window declarations live on their own seat (`context-settings.ts`), keyed by catalog word,
|
|
29
|
+
* and the legs read them at every boundary, every recovery pass, the end-of-task summary and every request
|
|
30
|
+
* build. Routing stays snapshotted; geometry follows the operator. The two are not the same question, and
|
|
31
|
+
* one law over both could only be wrong about one of them.
|
|
20
32
|
*
|
|
21
33
|
* KEY PRESENCE IS THE INSTRUCTION: a key present on the candidate is REPLACED (an explicit `undefined`
|
|
22
34
|
* CLEARS an optional seat back to its engine default — `readFace: undefined` ⇒ the deployment declares
|
|
@@ -44,8 +56,11 @@ import type { RunnerDeps } from "./runner-deps.js";
|
|
|
44
56
|
* deployment seat that resolution reads.
|
|
45
57
|
* - `writeFace` — the deployment's write-face declaration ({@link RunnerDeps.writeFace}); the read seat's
|
|
46
58
|
* twin (same value gate, same in-flight snapshot rule, its own notice) and independent of it.
|
|
59
|
+
* - `contextSettings` — the per-model context-window overlay ({@link RunnerDeps.contextSettings}); screened
|
|
60
|
+
* by the same value gate construction applies (`assertContextSettingsValue`). THE ONE SEAT
|
|
61
|
+
* IN-FLIGHT LEGS RE-READ — see the header: it carries geometry, not route.
|
|
47
62
|
*/
|
|
48
|
-
export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace", "writeFace"];
|
|
63
|
+
export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace", "writeFace", "contextSettings"];
|
|
49
64
|
export type SwappableDepSeat = (typeof SWAPPABLE_DEP_SEATS)[number];
|
|
50
65
|
/** The candidate a swap takes: the swappable seats of {@link RunnerDeps}, each optional (presence = instruction). */
|
|
51
66
|
export type SwappableDeps = Pick<RunnerDeps, SwappableDepSeat>;
|
|
@@ -61,6 +76,7 @@ export declare const SWAP_SEAT_NOTICE: {
|
|
|
61
76
|
readonly tiers: "config.models_swapped";
|
|
62
77
|
readonly readFace: "config.read_face_swapped";
|
|
63
78
|
readonly writeFace: "config.write_face_swapped";
|
|
79
|
+
readonly contextSettings: "config.context_settings_swapped";
|
|
64
80
|
};
|
|
65
81
|
/** The fence over the notice table: `never` while every seat has a row. */
|
|
66
82
|
export type SwapNoticeTableCoversEverySeat = AssertAllKeysHandled<Exclude<SwappableDepSeat, keyof typeof SWAP_SEAT_NOTICE>>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assertContainmentFaceValue } from "../tools/fs/containment-face.js";
|
|
2
|
-
|
|
2
|
+
import { assertContextSettingsValue } from "./context-settings.js";
|
|
3
|
+
export const SWAPPABLE_DEP_SEATS = ["models", "tiers", "readFace", "writeFace", "contextSettings"];
|
|
3
4
|
const SWAPPABLE_DEP_SEAT_SET = new Set(SWAPPABLE_DEP_SEATS);
|
|
4
5
|
export function isSwappableDepSeat(k) {
|
|
5
6
|
return SWAPPABLE_DEP_SEAT_SET.has(k);
|
|
@@ -9,6 +10,7 @@ export const SWAP_SEAT_NOTICE = {
|
|
|
9
10
|
tiers: "config.models_swapped",
|
|
10
11
|
readFace: "config.read_face_swapped",
|
|
11
12
|
writeFace: "config.write_face_swapped",
|
|
13
|
+
contextSettings: "config.context_settings_swapped",
|
|
12
14
|
};
|
|
13
15
|
export function screenSwappableDeps(next) {
|
|
14
16
|
if (next === null || typeof next !== "object" || Array.isArray(next)) {
|
|
@@ -49,6 +51,9 @@ export function screenSwappableDeps(next) {
|
|
|
49
51
|
case "writeFace":
|
|
50
52
|
candidate.writeFace = assertContainmentFaceValue(v, "write", "deps");
|
|
51
53
|
break;
|
|
54
|
+
case "contextSettings":
|
|
55
|
+
candidate.contextSettings = assertContextSettingsValue(v, "swapDeps");
|
|
56
|
+
break;
|
|
52
57
|
default: {
|
|
53
58
|
const _exhaustive = seat;
|
|
54
59
|
throw new Error(`swapDeps: unhandled seat ${String(_exhaustive)}`);
|
|
@@ -629,6 +629,11 @@ export type TaskEvent = ({
|
|
|
629
629
|
preserved_segment?: {
|
|
630
630
|
firstKeptEntryId: string;
|
|
631
631
|
};
|
|
632
|
+
/** Cross-session message frames that sat in the SUMMARIZED prefix of this pass — delivered
|
|
633
|
+
* earlier and visible to the model for the turns since. A record still in the session's box is
|
|
634
|
+
* handed over again by the engine (once; a second loss is disclosed `resupply_exhausted`), so
|
|
635
|
+
* this counts what the summary swallowed, not what is gone. Present iff at least one. */
|
|
636
|
+
summarizedCrossSessionMessages?: number;
|
|
632
637
|
/** CC post-compact file-restore parity (2026-07-03): the working files the engine
|
|
633
638
|
* re-read into the summary (attachWorkingFiles), attachment order. A shell renders CC's
|
|
634
639
|
* `Read {path} (…)` cards under the compaction divider from this. Absent when the feature is
|
|
@@ -107,6 +107,54 @@ export interface TaskNotificationPayload {
|
|
|
107
107
|
* semantics as the poll face's `partial_result` flag (one meaning across both surfaces). Absent on
|
|
108
108
|
* every other notification (a completed result is never flagged). */
|
|
109
109
|
partial?: boolean;
|
|
110
|
+
/** Delegation lanes, terminal frames: the child's EXPLICIT `Agent({name})` spawn key — the same word
|
|
111
|
+
* its `task_progress.spawnName`, roster row and hook identity carry, rendered as `<agent-name>` so
|
|
112
|
+
* the parent can `SendMessage({to})` it by name. Minted from the identity the frame's producer holds
|
|
113
|
+
* (never from the description); ABSENT = an unnamed delegation. */
|
|
114
|
+
spawnName?: string;
|
|
115
|
+
/** Delegation lanes, terminal frames — how the child's report reached this frame under the
|
|
116
|
+
* hand-back contract: `send` = a report was delivered; `flagged` = delivered under the hand-back
|
|
117
|
+
* screening's SECURITY WARNING; `withheld` = nothing was delivered (no result text, no partial text).
|
|
118
|
+
* Absent on non-terminal frames and on the non-delegation lanes. The model face renders a
|
|
119
|
+
* `<handback>` line only for `flagged` / `withheld` — the parent is told when a report did not
|
|
120
|
+
* arrive plainly, and nothing for the ordinary case. */
|
|
121
|
+
handback?: HandbackState;
|
|
122
|
+
/** Delegation lanes, terminal frames, present iff the child's leg compacted its context at least once
|
|
123
|
+
* or opened its compaction breaker: `passes` = landed compactions, `breakerOpen` = compaction was
|
|
124
|
+
* disabled for the rest of the leg (later context was trimmed, not summarized), `kind` = the last
|
|
125
|
+
* landed pass's trigger. The parent otherwise cannot tell that a child forgot. */
|
|
126
|
+
compaction?: {
|
|
127
|
+
passes: number;
|
|
128
|
+
breakerOpen: boolean;
|
|
129
|
+
kind?: CompactionKind;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* The INBOX NOTICE carrier: one drain pass of this session's box stopped with records still waiting
|
|
133
|
+
* (the pass budget, an admission stop, a lane refusal). `pending` = records still leased and not yet
|
|
134
|
+
* injected when the pass ended; `delivered` = records injected this pass; `urgent` = how many of the
|
|
135
|
+
* waiting ones their sender marked urgent (absent = 0); `box` = the session box. Its PRESENCE is the
|
|
136
|
+
* render discriminator: the model reads ONE engine line (`[Inbox notice] …`), never a shell. Minted
|
|
137
|
+
* ONLY by the engine's session-box drain, keyed `task_id: "inbox:<box>"` with the pass counter as
|
|
138
|
+
* `seq` so it never collides with a message frame's key; absent when the pass emptied the box.
|
|
139
|
+
*/
|
|
140
|
+
inboxNotice?: {
|
|
141
|
+
pending: number;
|
|
142
|
+
delivered: number;
|
|
143
|
+
urgent?: number;
|
|
144
|
+
resupplied?: number;
|
|
145
|
+
box: string;
|
|
146
|
+
};
|
|
147
|
+
/** #1036 — THIS occurrence is a RE-SUPPLY of a record the model was already shown once and
|
|
148
|
+
* whose frame a compaction then summarized away. Same record, same `task_id`, same `seq`, a new
|
|
149
|
+
* occurrence: `attempt` says which one. ABSENT = not a re-supply (a first delivery); there is no
|
|
150
|
+
* `false` form. Only the session-box drain mints it. */
|
|
151
|
+
resupplied?: true;
|
|
152
|
+
/** #1036 — which DELIVERY ATTEMPT of this record's frame this occurrence is: 1 = the first
|
|
153
|
+
* delivery, 2 = the one re-supply a summarized record gets. ABSENT = 1 (there is no `0`, and a
|
|
154
|
+
* consumer must not read absence as "unknown"). It folds into the injection queue's dedup key
|
|
155
|
+
* ({@link taskNotificationDedupKey}), which is what keeps a re-supply from collapsing into the
|
|
156
|
+
* first occurrence's entry. Only the session-box drain mints it. */
|
|
157
|
+
attempt?: number;
|
|
110
158
|
output_file?: string;
|
|
111
159
|
/** Whatever the settling lane knows about its own spend, JSON-rendered verbatim into one `<usage>`
|
|
112
160
|
* tag. Deliberately untyped — the lanes report different quantities and nothing here should force
|
|
@@ -223,7 +271,7 @@ export interface SemaProvenance {
|
|
|
223
271
|
/** `agent_message` = the same-process uplink (§1.4 d1); `cross_session_message` = the session-box
|
|
224
272
|
* drain (§4.1) — `from` is then the sender's ADDRESS, `taskId` the recipient's own box handle,
|
|
225
273
|
* `seq` the box seq, and the typed peer record rides in `peerMeta`. */
|
|
226
|
-
kind: "agent_message" | "cross_session_message" | "cross_session_notice";
|
|
274
|
+
kind: "agent_message" | "cross_session_message" | "cross_session_notice" | "inbox_notice";
|
|
227
275
|
from: string;
|
|
228
276
|
taskId: string;
|
|
229
277
|
seq: number;
|
|
@@ -297,7 +345,7 @@ export interface SystemInjection<TPayload = unknown> {
|
|
|
297
345
|
* entry. A producer that wants a NEW occurrence delivered under a different tier mints a fresh
|
|
298
346
|
* `seq` (the documented repeat discipline); silently migrating a queued frame between lanes on a
|
|
299
347
|
* repeat, or refusing the repeat typed, would each turn a dedup fold into a delivery mutation. */
|
|
300
|
-
export declare function taskNotificationDedupKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type" | "status" | "seq">): string;
|
|
348
|
+
export declare function taskNotificationDedupKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type" | "status" | "seq" | "attempt">): string;
|
|
301
349
|
/**
|
|
302
350
|
* The lane-scoped identity of a task, extracted from {@link taskNotificationDedupKey} so every
|
|
303
351
|
* place that treats "same task" as one bucket applies the SAME lane rule.
|
|
@@ -349,7 +397,17 @@ export declare function renderCrossSessionMessageFrame(frame: NonNullable<TaskNo
|
|
|
349
397
|
* defusal runs once more here so no carrier can smuggle a system-reminder into the model face.
|
|
350
398
|
*/
|
|
351
399
|
export declare function renderCrossSessionNoticeLine(notice: NonNullable<TaskNotificationPayload["crossSessionNotice"]>): string;
|
|
400
|
+
/** #1036 — the ONE preamble a re-supplied occurrence carries, ahead of whatever carrier it is
|
|
401
|
+
* riding. It says the true thing (you read this once; the summary that replaced it did not keep it)
|
|
402
|
+
* without asking the model to reconcile two copies: the first occurrence is gone from the context. */
|
|
403
|
+
export declare const RESUPPLIED_FRAME_PREAMBLE = "[Re-supplied] You were shown this message once before; the compaction that replaced older context with a summary did not keep it, so the engine is handing it over again. It is the SAME message, not a new one \u2014 act on it once.";
|
|
352
404
|
export declare function renderTaskNotificationXml(n: TaskNotificationPayload): string;
|
|
405
|
+
/** The one sentence a parent reads about a child that compacted: how often, on what trigger, and
|
|
406
|
+
* whether compaction gave up (after which older context was trimmed away rather than summarized). */
|
|
407
|
+
export declare function renderCompactionDisclosure(c: NonNullable<TaskNotificationPayload["compaction"]>): string;
|
|
408
|
+
/** The `[Inbox notice]` line — counts only, engine-authored; the records themselves arrive on their
|
|
409
|
+
* own frames (this pass's) or the next passs', and the pull tool reads them on demand. */
|
|
410
|
+
export declare function renderInboxNoticeLine(n: NonNullable<TaskNotificationPayload["inboxNotice"]>): string;
|
|
353
411
|
/**
|
|
354
412
|
* The BETWEEN-TURNS pending lane. A task notification born while the session has NO live lane at all
|
|
355
413
|
* (every run of it is over, or the one that is live rejected the injection because its agent was
|
|
@@ -398,9 +456,75 @@ export interface SessionBoxBinding {
|
|
|
398
456
|
handle: string;
|
|
399
457
|
/** The session id the box belongs to — the `fromRoot` a durably parked child frame records. */
|
|
400
458
|
sessionId: string;
|
|
459
|
+
/** The deployment's engine-frame floor (`RunnerDeps.sessionBoxEngineFrames`): `true` ⇒ an engine
|
|
460
|
+
* frame parks in this box as an `engine_frame` record; otherwise it parks in process. */
|
|
461
|
+
engineFrames?: boolean;
|
|
401
462
|
/** Where an append fault is disclosed (the frame then takes the in-process park — never lost). */
|
|
402
463
|
onError?: (e: unknown) => void;
|
|
403
464
|
}
|
|
465
|
+
/** The hand-back contract's three words (see {@link TaskNotificationPayload.handback}). */
|
|
466
|
+
export declare const HANDBACK_STATES: readonly ["send", "flagged", "withheld"];
|
|
467
|
+
export type HandbackState = (typeof HANDBACK_STATES)[number];
|
|
468
|
+
/** THE hand-back projection, one rule every terminal frame producer calls: a warning ⇒ `flagged`; any
|
|
469
|
+
* report or partial text ⇒ `send`; neither ⇒ `withheld`. */
|
|
470
|
+
export declare function handbackStateOf(facts: {
|
|
471
|
+
result?: string | undefined;
|
|
472
|
+
partialText?: string | undefined;
|
|
473
|
+
warning?: string | undefined;
|
|
474
|
+
}): HandbackState;
|
|
475
|
+
/** The three-way compaction trigger word a terminal frame carries (the wire's `forced` is `reactive` here). */
|
|
476
|
+
export declare const COMPACTION_KINDS: readonly ["manual", "auto", "reactive"];
|
|
477
|
+
export type CompactionKind = (typeof COMPACTION_KINDS)[number];
|
|
478
|
+
/** The wire trigger word → the frame's kind word (`forced` = a reactive pass the run could not skip). */
|
|
479
|
+
export declare function compactionKindOf(trigger: "auto" | "manual" | "forced"): CompactionKind;
|
|
480
|
+
/**
|
|
481
|
+
* THE projection every delegation terminal frame calls for its identity + hand-back + context facets:
|
|
482
|
+
* the spawn key from the identity the producer holds (never the description), the hand-back state from
|
|
483
|
+
* the delivered text and the screening's warning, and the child's compaction record verbatim. Absent
|
|
484
|
+
* inputs stay absent; `handback` is always minted (a terminal always says how the report reached it).
|
|
485
|
+
*/
|
|
486
|
+
export declare function terminalFrameFacets(facts: {
|
|
487
|
+
spawnName?: string | undefined;
|
|
488
|
+
result?: string | undefined;
|
|
489
|
+
partialText?: string | undefined;
|
|
490
|
+
warning?: string | undefined;
|
|
491
|
+
compaction?: TaskNotificationPayload["compaction"] | undefined;
|
|
492
|
+
}): Pick<TaskNotificationPayload, "spawnName" | "handback" | "compaction">;
|
|
493
|
+
/** The codec version this engine writes into an `engine_frame` record and the highest it reads. */
|
|
494
|
+
export declare const ENGINE_FRAME_CODEC_VERSION = 1;
|
|
495
|
+
/** The engine's own lanes — the only frames the durable park may encode (an `external` event never is). */
|
|
496
|
+
export declare const ENGINE_FRAME_TASK_TYPES: readonly ["background_agent", "monitor", "workflow"];
|
|
497
|
+
/** May this frame take the durable engine-frame park? Its lane must be one of the engine's own and it
|
|
498
|
+
* must wear no shell-less carrier. */
|
|
499
|
+
export declare function isEngineFrameParkable(payload: TaskNotificationPayload): boolean;
|
|
500
|
+
/** The record text of a durably parked engine frame: the codec version, the injection tier it was
|
|
501
|
+
* submitted at, and the frame itself (its own `seq` is the lane's cycle count — a different domain
|
|
502
|
+
* from the box seq, carried inside untouched). */
|
|
503
|
+
export declare function encodeEngineFrame(payload: TaskNotificationPayload, priority: SystemInjectionPriority): string;
|
|
504
|
+
export type EngineFrameDecode = {
|
|
505
|
+
ok: true;
|
|
506
|
+
payload: TaskNotificationPayload;
|
|
507
|
+
priority: SystemInjectionPriority;
|
|
508
|
+
}
|
|
509
|
+
/** A version this engine does not read: the record is a NEWER engine's; leave it for one. */
|
|
510
|
+
| {
|
|
511
|
+
ok: false;
|
|
512
|
+
reason: "newer";
|
|
513
|
+
codecVersion: number;
|
|
514
|
+
}
|
|
515
|
+
/** Not this codec at all (or a frame outside the engine's lanes): settle it without delivery. */
|
|
516
|
+
| {
|
|
517
|
+
ok: false;
|
|
518
|
+
reason: "malformed";
|
|
519
|
+
detail: string;
|
|
520
|
+
};
|
|
521
|
+
/** Read an `engine_frame` record back. Every field the lane reads is re-typed here; a carrier the
|
|
522
|
+
* writer must never have encoded is dropped rather than trusted. */
|
|
523
|
+
export declare function decodeEngineFrame(content: string): EngineFrameDecode;
|
|
524
|
+
/** The durable form of an engine frame: the codec text under `kind: "engine_frame"`, stamped with
|
|
525
|
+
* the box owner as `fromRoot` (the park seat's own fact, as for a peer-words record); no hop chain —
|
|
526
|
+
* it is not a peer message and is never admitted as one. */
|
|
527
|
+
export declare function engineFrameRecordOf(payload: TaskNotificationPayload, priority: SystemInjectionPriority, box: Pick<SessionBoxBinding, "sessionId">): MailboxAppendMessage;
|
|
404
528
|
/**
|
|
405
529
|
* RB-375 A2 (ruled 2026-08-03) — is this frame the TERMINAL of a delegated background agent?
|
|
406
530
|
*
|
|
@@ -616,6 +740,24 @@ export declare class PendingSessionNotifications {
|
|
|
616
740
|
markDelivered(sessionId: string, dedupKey: string): void;
|
|
617
741
|
get size(): number;
|
|
618
742
|
}
|
|
743
|
+
/**
|
|
744
|
+
* The durable box appends ONE run leg caused — its obligations. Attribution is by execution: an append
|
|
745
|
+
* belongs to the leg whose execution (its tools, its producers, its teardown reap, a child terminal it
|
|
746
|
+
* hands up the delegation chain) reached the park seat, whichever lane's routing carried it there. A
|
|
747
|
+
* leg's teardown awaits its own obligations and nobody else's — another session's pending append is
|
|
748
|
+
* never a condition of this leg's completion. A park reached outside any leg is nobody's obligation.
|
|
749
|
+
*/
|
|
750
|
+
export declare class DurableAppendObligations {
|
|
751
|
+
private static readonly scope;
|
|
752
|
+
private readonly inFlight;
|
|
753
|
+
/** The obligations of the leg whose execution is running now; undefined outside every leg. */
|
|
754
|
+
static current(): DurableAppendObligations | undefined;
|
|
755
|
+
/** Run `body` as this leg (synchronous: the body's own return value, no added tick). */
|
|
756
|
+
run<R>(body: () => R): R;
|
|
757
|
+
track(p: Promise<unknown>): void;
|
|
758
|
+
/** Resolves when every append this leg caused has settled — the fault arm already re-parked in process. */
|
|
759
|
+
settle(): Promise<void>;
|
|
760
|
+
}
|
|
619
761
|
/**
|
|
620
762
|
* WHERE A FRAME ADDRESSED TO ONE SESSION GOES — the single decision, so the two addresses that ask it
|
|
621
763
|
* (a run's own session, and the delegation tree's root anchor) cannot drift apart. Three outcomes:
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { escapeEnvelopeTag, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
2
3
|
import { PEER_MESSAGE_NOTICE } from "../agents/peer-admission.js";
|
|
3
4
|
import { buildCrossSessionEnvelope, CROSS_SESSION_MESSAGE_NOTICE, neutralizePeerBody } from "../agents/cross-session-envelope.js";
|
|
@@ -24,6 +25,9 @@ const TASK_NOTIFICATION_TAGS = [
|
|
|
24
25
|
"recent-steps",
|
|
25
26
|
"edited-files",
|
|
26
27
|
"resumable",
|
|
28
|
+
"agent-name",
|
|
29
|
+
"handback",
|
|
30
|
+
"context",
|
|
27
31
|
];
|
|
28
32
|
function renderResidual(n) {
|
|
29
33
|
const lines = [];
|
|
@@ -36,14 +40,22 @@ function renderResidual(n) {
|
|
|
36
40
|
return lines.length > 0 ? lines.join("\n") : undefined;
|
|
37
41
|
}
|
|
38
42
|
function tag(name, value) {
|
|
39
|
-
|
|
43
|
+
let shown;
|
|
44
|
+
try {
|
|
45
|
+
shown = typeof value === "string" ? value : String(value);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
shown = `[unrenderable ${typeof value}]`;
|
|
49
|
+
}
|
|
50
|
+
const text = sanitizeUntrustedText(shown, TASK_NOTIFICATION_TAGS)
|
|
40
51
|
.replace(/&/g, "&")
|
|
41
52
|
.replace(/</g, "<")
|
|
42
53
|
.replace(/>/g, ">");
|
|
43
54
|
return `<${name}>${text}</${name}>`;
|
|
44
55
|
}
|
|
45
56
|
export function taskNotificationDedupKey(n) {
|
|
46
|
-
|
|
57
|
+
const attempt = n.attempt !== undefined && Number.isInteger(n.attempt) && n.attempt > 1 ? `:a${n.attempt}` : "";
|
|
58
|
+
return `task_notification:${taskNotificationLaneKey(n)}:${n.status}${n.seq !== undefined ? `:${n.seq}` : ""}${attempt}`;
|
|
47
59
|
}
|
|
48
60
|
export function taskNotificationLaneKey(n) {
|
|
49
61
|
return n.task_type === "external" ? `external:${n.task_id}` : n.task_id;
|
|
@@ -64,13 +76,20 @@ export function renderCrossSessionMessageFrame(frame) {
|
|
|
64
76
|
export function renderCrossSessionNoticeLine(notice) {
|
|
65
77
|
return sanitizeUntrustedText(notice.text);
|
|
66
78
|
}
|
|
79
|
+
export const RESUPPLIED_FRAME_PREAMBLE = "[Re-supplied] You were shown this message once before; the compaction that replaced older context with a summary did not keep it, so the engine is handing it over again. It is the SAME message, not a new one \u2014 act on it once.";
|
|
67
80
|
export function renderTaskNotificationXml(n) {
|
|
81
|
+
const body = renderTaskNotificationCarrier(n);
|
|
82
|
+
return n.resupplied === true ? `${RESUPPLIED_FRAME_PREAMBLE}\n\n${body}` : body;
|
|
83
|
+
}
|
|
84
|
+
function renderTaskNotificationCarrier(n) {
|
|
68
85
|
if (n.agentMessage !== undefined)
|
|
69
86
|
return renderAgentMessageFrame(n.agentMessage);
|
|
70
87
|
if (n.crossSessionMessage !== undefined)
|
|
71
88
|
return renderCrossSessionMessageFrame(n.crossSessionMessage);
|
|
72
89
|
if (n.crossSessionNotice !== undefined)
|
|
73
90
|
return renderCrossSessionNoticeLine(n.crossSessionNotice);
|
|
91
|
+
if (n.inboxNotice !== undefined)
|
|
92
|
+
return renderInboxNoticeLine(n.inboxNotice);
|
|
74
93
|
const usage = n.usage === undefined
|
|
75
94
|
? undefined
|
|
76
95
|
: (() => {
|
|
@@ -92,6 +111,8 @@ export function renderTaskNotificationXml(n) {
|
|
|
92
111
|
tag("task-type", n.task_type),
|
|
93
112
|
...(n.toolUseId !== undefined ? [tag("tool-use-id", n.toolUseId)] : []),
|
|
94
113
|
tag("status", n.status),
|
|
114
|
+
...(n.spawnName !== undefined ? [tag("agent-name", n.spawnName)] : []),
|
|
115
|
+
...(n.handback !== undefined && n.handback !== "send" ? [tag("handback", n.handback)] : []),
|
|
95
116
|
tag("summary", n.summary),
|
|
96
117
|
...(n.result !== undefined ? [tag("result", n.result)] : []),
|
|
97
118
|
...(n.output_file !== undefined ? [tag("output-file", n.output_file)] : []),
|
|
@@ -103,15 +124,123 @@ export function renderTaskNotificationXml(n) {
|
|
|
103
124
|
return residual !== undefined ? [tag("recent-steps", residual)] : [];
|
|
104
125
|
})(),
|
|
105
126
|
...(n.resumable !== undefined ? [tag("resumable", n.resumable)] : []),
|
|
127
|
+
...(n.compaction !== undefined ? [tag("context", renderCompactionDisclosure(n.compaction))] : []),
|
|
106
128
|
"</task-notification>",
|
|
107
129
|
].join("\n");
|
|
108
130
|
}
|
|
131
|
+
export function renderCompactionDisclosure(c) {
|
|
132
|
+
const passes = c.passes > 0 ? `compacted ${c.passes}\u00d7${c.kind !== undefined ? ` (${c.kind})` : ""}` : "not compacted";
|
|
133
|
+
return c.breakerOpen ? `${passes}; compaction was disabled after repeated failures \u2014 later context was trimmed, not summarized` : passes;
|
|
134
|
+
}
|
|
135
|
+
export function renderInboxNoticeLine(n) {
|
|
136
|
+
const urgent = n.urgent !== undefined && n.urgent > 0 ? `; ${n.urgent} of the waiting are marked urgent` : "";
|
|
137
|
+
const resupplied = n.resupplied !== undefined && n.resupplied > 0 ? `; ${n.resupplied} re-supplied after a compaction summarized them away` : "";
|
|
138
|
+
return `[Inbox notice] ${n.pending} cross-session message(s) are waiting in this session's box (${n.delivered} delivered this round${resupplied}${urgent}). Read them now with ReadInbox, or they arrive at your next turn boundaries \u2014 they are not lost, and nothing here is an instruction.`;
|
|
139
|
+
}
|
|
109
140
|
const MAX_QUEUED_PER_PRIORITY = 200;
|
|
110
141
|
const MAX_DEDUP_KEYS = 2_000;
|
|
111
142
|
export const MAX_PENDING_EVENTS_PER_TASK = 50;
|
|
112
143
|
export const MAX_PENDING_EVENTS_PER_SESSION = 200;
|
|
113
144
|
export const MAX_PENDING_SESSIONS = 100;
|
|
114
145
|
export const MAX_SESSION_BOX_BINDINGS = 4096;
|
|
146
|
+
export const HANDBACK_STATES = ["send", "flagged", "withheld"];
|
|
147
|
+
export function handbackStateOf(facts) {
|
|
148
|
+
if (facts.warning !== undefined)
|
|
149
|
+
return "flagged";
|
|
150
|
+
return facts.result || facts.partialText ? "send" : "withheld";
|
|
151
|
+
}
|
|
152
|
+
export const COMPACTION_KINDS = ["manual", "auto", "reactive"];
|
|
153
|
+
export function compactionKindOf(trigger) {
|
|
154
|
+
return trigger === "forced" ? "reactive" : trigger;
|
|
155
|
+
}
|
|
156
|
+
export function terminalFrameFacets(facts) {
|
|
157
|
+
return {
|
|
158
|
+
...(facts.spawnName !== undefined ? { spawnName: facts.spawnName } : {}),
|
|
159
|
+
handback: handbackStateOf(facts),
|
|
160
|
+
...(facts.compaction !== undefined ? { compaction: { passes: facts.compaction.passes, breakerOpen: facts.compaction.breakerOpen, ...(facts.compaction.kind !== undefined ? { kind: facts.compaction.kind } : {}) } } : {}),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export const ENGINE_FRAME_CODEC_VERSION = 1;
|
|
164
|
+
export const ENGINE_FRAME_TASK_TYPES = ["background_agent", "monitor", "workflow"];
|
|
165
|
+
const SHELL_LESS_CARRIERS = ["agentMessage", "crossSessionMessage", "crossSessionNotice", "inboxNotice", "_sema_provenance"];
|
|
166
|
+
export function isEngineFrameParkable(payload) {
|
|
167
|
+
return ENGINE_FRAME_TASK_TYPES.includes(payload.task_type) && SHELL_LESS_CARRIERS.every((k) => payload[k] === undefined);
|
|
168
|
+
}
|
|
169
|
+
export function encodeEngineFrame(payload, priority) {
|
|
170
|
+
return JSON.stringify({ codecVersion: ENGINE_FRAME_CODEC_VERSION, priority, payload });
|
|
171
|
+
}
|
|
172
|
+
export function decodeEngineFrame(content) {
|
|
173
|
+
let raw;
|
|
174
|
+
try {
|
|
175
|
+
raw = JSON.parse(content);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return { ok: false, reason: "malformed", detail: "not JSON" };
|
|
179
|
+
}
|
|
180
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
181
|
+
return { ok: false, reason: "malformed", detail: "not an object" };
|
|
182
|
+
const r = raw;
|
|
183
|
+
const version = r["codecVersion"];
|
|
184
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version < 1)
|
|
185
|
+
return { ok: false, reason: "malformed", detail: "codecVersion missing" };
|
|
186
|
+
if (version > ENGINE_FRAME_CODEC_VERSION)
|
|
187
|
+
return { ok: false, reason: "newer", codecVersion: version };
|
|
188
|
+
const priority = r["priority"];
|
|
189
|
+
if (!isSystemInjectionPriority(priority))
|
|
190
|
+
return { ok: false, reason: "malformed", detail: "priority outside the ladder" };
|
|
191
|
+
const p = r["payload"];
|
|
192
|
+
if (p === null || typeof p !== "object" || Array.isArray(p))
|
|
193
|
+
return { ok: false, reason: "malformed", detail: "payload missing" };
|
|
194
|
+
const f = p;
|
|
195
|
+
if (typeof f["task_id"] !== "string" || f["task_id"] === "")
|
|
196
|
+
return { ok: false, reason: "malformed", detail: "task_id missing" };
|
|
197
|
+
if (!ENGINE_FRAME_TASK_TYPES.includes(f["task_type"]))
|
|
198
|
+
return { ok: false, reason: "malformed", detail: "task_type outside the engine's lanes" };
|
|
199
|
+
if (typeof f["status"] !== "string" || typeof f["summary"] !== "string")
|
|
200
|
+
return { ok: false, reason: "malformed", detail: "status/summary missing" };
|
|
201
|
+
const facet = malformedFacet(f);
|
|
202
|
+
if (facet !== undefined)
|
|
203
|
+
return { ok: false, reason: "malformed", detail: facet };
|
|
204
|
+
const payload = { ...p };
|
|
205
|
+
for (const k of SHELL_LESS_CARRIERS)
|
|
206
|
+
delete payload[k];
|
|
207
|
+
return { ok: true, payload, priority };
|
|
208
|
+
}
|
|
209
|
+
const SPAWN_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
210
|
+
function malformedFacet(f) {
|
|
211
|
+
if (f["spawnName"] !== undefined && (typeof f["spawnName"] !== "string" || !SPAWN_NAME_RE.test(f["spawnName"])))
|
|
212
|
+
return "spawnName outside the spawn-name grammar";
|
|
213
|
+
if (f["handback"] !== undefined && !(typeof f["handback"] === "string" && HANDBACK_STATES.includes(f["handback"])))
|
|
214
|
+
return "handback outside its closed set";
|
|
215
|
+
const steps = f["recentSteps"];
|
|
216
|
+
if (steps !== undefined && !(Array.isArray(steps) && steps.every((s) => s !== null && typeof s === "object" && typeof s.tool === "string" && typeof s.target === "string" && typeof s.outcome === "string")))
|
|
217
|
+
return "recentSteps is not a list of steps";
|
|
218
|
+
const edits = f["editedFiles"];
|
|
219
|
+
if (edits !== undefined && !(Array.isArray(edits) && edits.every((e) => e !== null && typeof e === "object" && typeof e.path === "string" && typeof e.edits === "number")))
|
|
220
|
+
return "editedFiles is not a list of files";
|
|
221
|
+
if (f["lines"] !== undefined && !(Array.isArray(f["lines"]) && f["lines"].every((l) => typeof l === "string")))
|
|
222
|
+
return "lines is not a list of strings";
|
|
223
|
+
const c = f["compaction"];
|
|
224
|
+
if (c !== undefined) {
|
|
225
|
+
if (c === null || typeof c !== "object" || Array.isArray(c))
|
|
226
|
+
return "compaction is not an object";
|
|
227
|
+
const { passes, breakerOpen, kind } = c;
|
|
228
|
+
if (typeof passes !== "number" || !Number.isInteger(passes) || passes < 0)
|
|
229
|
+
return "compaction.passes is not a count";
|
|
230
|
+
if (typeof breakerOpen !== "boolean")
|
|
231
|
+
return "compaction.breakerOpen is not a boolean";
|
|
232
|
+
if (kind !== undefined && !(typeof kind === "string" && COMPACTION_KINDS.includes(kind)))
|
|
233
|
+
return "compaction.kind outside its closed set";
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
export function engineFrameRecordOf(payload, priority, box) {
|
|
238
|
+
return {
|
|
239
|
+
content: encodeEngineFrame(payload, priority),
|
|
240
|
+
sentAt: Date.now(),
|
|
241
|
+
peerMeta: { fromRoot: box.sessionId, kind: "engine_frame" },
|
|
242
|
+
};
|
|
243
|
+
}
|
|
115
244
|
const MAX_TOMBSTONES = 5_000;
|
|
116
245
|
const TERMINAL_STATUSES = new Set(["completed", "failed", "killed", "cancelled"]);
|
|
117
246
|
export function isDelegatedAgentTerminal(n) {
|
|
@@ -327,6 +456,24 @@ export class PendingSessionNotifications {
|
|
|
327
456
|
return this.sessions.size;
|
|
328
457
|
}
|
|
329
458
|
}
|
|
459
|
+
export class DurableAppendObligations {
|
|
460
|
+
static scope = new AsyncLocalStorage();
|
|
461
|
+
inFlight = new Set();
|
|
462
|
+
static current() {
|
|
463
|
+
return DurableAppendObligations.scope.getStore();
|
|
464
|
+
}
|
|
465
|
+
run(body) {
|
|
466
|
+
return DurableAppendObligations.scope.run(this, body);
|
|
467
|
+
}
|
|
468
|
+
track(p) {
|
|
469
|
+
this.inFlight.add(p);
|
|
470
|
+
void p.finally(() => this.inFlight.delete(p));
|
|
471
|
+
}
|
|
472
|
+
async settle() {
|
|
473
|
+
while (this.inFlight.size > 0)
|
|
474
|
+
await Promise.allSettled([...this.inFlight]);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
330
477
|
export function routeOrParkNotification(store, sessionId, payload, priority, onDisposition) {
|
|
331
478
|
const dedupKey = taskNotificationDedupKey(payload);
|
|
332
479
|
if (store.holdsKey(sessionId, dedupKey)) {
|
|
@@ -337,9 +484,10 @@ export function routeOrParkNotification(store, sessionId, payload, priority, onD
|
|
|
337
484
|
if (routed === "unavailable") {
|
|
338
485
|
const box = store.sessionBoxFor(sessionId);
|
|
339
486
|
const carrier = payload.agentMessage;
|
|
340
|
-
|
|
487
|
+
const durable = box !== undefined && (carrier !== undefined || (box.engineFrames === true && isEngineFrameParkable(payload)));
|
|
488
|
+
if (box !== undefined && durable) {
|
|
341
489
|
store.noteDurablePark(sessionId, dedupKey);
|
|
342
|
-
const record = sessionBoxRecordOf({ ...payload, agentMessage: carrier }, box);
|
|
490
|
+
const record = carrier !== undefined ? sessionBoxRecordOf({ ...payload, agentMessage: carrier }, box) : engineFrameRecordOf(payload, priority, box);
|
|
343
491
|
let appended;
|
|
344
492
|
try {
|
|
345
493
|
appended = Promise.resolve(box.mailbox.append(box.scope, box.handle, record));
|
|
@@ -347,7 +495,7 @@ export function routeOrParkNotification(store, sessionId, payload, priority, onD
|
|
|
347
495
|
catch (e) {
|
|
348
496
|
appended = Promise.reject(e);
|
|
349
497
|
}
|
|
350
|
-
appended.then(() => onDisposition?.("parked_durably"), (e) => {
|
|
498
|
+
const settled = appended.then(() => onDisposition?.("parked_durably"), (e) => {
|
|
351
499
|
store.forgetDurablePark(sessionId, dedupKey);
|
|
352
500
|
try {
|
|
353
501
|
box.onError?.(e);
|
|
@@ -357,6 +505,7 @@ export function routeOrParkNotification(store, sessionId, payload, priority, onD
|
|
|
357
505
|
store.pend(sessionId, payload, priority);
|
|
358
506
|
onDisposition?.("parked");
|
|
359
507
|
});
|
|
508
|
+
DurableAppendObligations.current()?.track(settled);
|
|
360
509
|
return;
|
|
361
510
|
}
|
|
362
511
|
store.pend(sessionId, payload, priority);
|