@sema-agent/core 7.49.5 → 7.50.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 +157 -0
- package/dist/agents/observer.js +3 -10
- package/dist/agents/subagent-steps.d.ts +6 -14
- package/dist/agents/subagent-steps.js +3 -17
- package/dist/brain/stream-shared.d.ts +1 -1
- package/dist/core/allow-entry.d.ts +1 -1
- package/dist/core/allow-entry.js +1 -1
- package/dist/core/ask-origin.d.ts +15 -11
- package/dist/core/ask-origin.js +6 -14
- package/dist/core/auto-mode-prompt.js +6 -2
- package/dist/core/checkpoint-store.d.ts +6 -4
- package/dist/core/checkpoint-store.js +1 -1
- package/dist/core/context-edit.d.ts +2 -2
- package/dist/core/decision-receipt.d.ts +2 -2
- package/dist/core/decision-receipt.js +30 -11
- package/dist/core/delegation-frames.d.ts +66 -18
- package/dist/core/delegation-frames.js +22 -0
- package/dist/core/engine-notice.d.ts +7 -5
- package/dist/core/frame-settlement.d.ts +6 -1
- package/dist/core/gate-fold.js +34 -19
- package/dist/core/gate-lanes.js +15 -13
- package/dist/core/gate-outcome.d.ts +2 -2
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +28 -20
- package/dist/core/human-input-projection.js +2 -0
- package/dist/core/lsp-diagnostics.d.ts +3 -0
- package/dist/core/lsp-diagnostics.js +6 -0
- package/dist/core/lsp-session.d.ts +39 -17
- package/dist/core/lsp-session.js +118 -58
- package/dist/core/lsp.d.ts +83 -3
- package/dist/core/lsp.js +146 -16
- package/dist/core/mcp.js +2 -1
- package/dist/core/media-byte-cap.d.ts +14 -0
- package/dist/core/media-byte-cap.js +15 -3
- package/dist/core/memory-engine/consolidation-driver.d.ts +23 -1
- package/dist/core/memory-engine/consolidation-driver.js +54 -6
- package/dist/core/memory-engine/consolidation-write-gate.d.ts +46 -0
- package/dist/core/memory-engine/consolidation-write-gate.js +82 -0
- package/dist/core/memory-engine/consolidation.d.ts +17 -0
- package/dist/core/memory-engine/consolidation.js +1 -0
- package/dist/core/memory-engine/distiller.d.ts +8 -2
- package/dist/core/memory-engine/distiller.js +8 -6
- package/dist/core/memory-engine/engine.d.ts +37 -4
- package/dist/core/memory-engine/engine.js +127 -38
- package/dist/core/memory-engine/file-backend.d.ts +8 -3
- package/dist/core/memory-engine/file-backend.js +44 -16
- package/dist/core/memory-engine/index.d.ts +2 -2
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/landing-admission.d.ts +65 -0
- package/dist/core/memory-engine/landing-admission.js +68 -0
- package/dist/core/memory-engine/types.d.ts +23 -0
- package/dist/core/mode-policy.d.ts +3 -2
- package/dist/core/mode-policy.js +2 -0
- package/dist/core/permission-rule-model.d.ts +19 -13
- package/dist/core/permission-rule-model.js +7 -4
- package/dist/core/permission-rule-provider.d.ts +11 -0
- package/dist/core/permission-rules.d.ts +1 -1
- package/dist/core/permission-rules.js +5 -2
- package/dist/core/persisted-rule-arms.d.ts +15 -7
- package/dist/core/persisted-rule-arms.js +13 -6
- package/dist/core/reminder-mint.d.ts +5 -0
- package/dist/core/reminder-mint.js +1 -0
- package/dist/core/retired-keys.d.ts +8 -0
- package/dist/core/retired-keys.js +4 -0
- package/dist/core/rule-lane-reads.d.ts +4 -4
- package/dist/core/rule-lane-reads.js +10 -1
- package/dist/core/runner/construction-doors.d.ts +10 -4
- package/dist/core/runner/construction-doors.js +25 -2
- package/dist/core/runner/contracts.d.ts +20 -3
- package/dist/core/runner/env-capability-census.js +1 -1
- package/dist/core/runner/gate-exit.js +2 -1
- package/dist/core/runner/memory-consolidation-driver.d.ts +6 -4
- package/dist/core/runner/memory-consolidation-driver.js +5 -2
- package/dist/core/runner/memory-consolidation-write-judge.d.ts +17 -0
- package/dist/core/runner/memory-consolidation-write-judge.js +14 -0
- package/dist/core/runner/permission-rule-lanes.d.ts +16 -22
- package/dist/core/runner/permission-rule-lanes.js +13 -43
- package/dist/core/runner/person-path-rows.d.ts +244 -0
- package/dist/core/runner/person-path-rows.js +387 -0
- package/dist/core/runner/policy-throw.d.ts +34 -0
- package/dist/core/runner/policy-throw.js +25 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +8 -1
- package/dist/core/runner/prepare-artifact.d.ts +5 -3
- package/dist/core/runner/prepare-artifact.js +2 -8
- package/dist/core/runner/prepare-ask-lane.d.ts +2 -1
- package/dist/core/runner/prepare-ask-lane.js +14 -16
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
- package/dist/core/runner/prepare-context-lane.js +2 -2
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -3
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +19 -10
- package/dist/core/runner/prepare-hands-readface.js +57 -43
- package/dist/core/runner/prepare-lsp.d.ts +15 -13
- package/dist/core/runner/prepare-lsp.js +11 -11
- package/dist/core/runner/prepare-policy-chain.d.ts +8 -4
- package/dist/core/runner/prepare-policy-chain.js +11 -10
- package/dist/core/runner/prepare-run-refs.js +1 -1
- package/dist/core/runner/prepare-task.js +24 -16
- package/dist/core/runner/prepare-turn-wiring.d.ts +13 -6
- package/dist/core/runner/prepare-turn-wiring.js +19 -8
- package/dist/core/runner/read-root-grant.d.ts +1 -25
- package/dist/core/runner/read-root-grant.js +1 -17
- package/dist/core/runner/retired-runner-deps.d.ts +2 -2
- package/dist/core/runner/run-brain-sinks.d.ts +1 -5
- package/dist/core/runner/run-brain-sinks.js +2 -4
- package/dist/core/runner/run-harness-handlers.js +5 -4
- package/dist/core/runner/run-identity-wiring.d.ts +4 -4
- package/dist/core/runner/run-identity-wiring.js +10 -1
- package/dist/core/runner/run-leg.d.ts +1 -1
- package/dist/core/runner/run-leg.js +22 -9
- package/dist/core/runner/run-settle-and-teardown.d.ts +0 -4
- package/dist/core/runner/run-settle-and-teardown.js +7 -4
- package/dist/core/runner/run-terminal-adoption.d.ts +1 -1
- package/dist/core/runner/run-terminal-adoption.js +1 -1
- package/dist/core/runner/run-turn-boundary.js +2 -2
- package/dist/core/runner/runtask.js +8 -8
- package/dist/core/runner/session-file-state-replay.d.ts +14 -4
- package/dist/core/runner/session-file-state-replay.js +8 -0
- package/dist/core/runner/stream-steer-verb.js +3 -0
- package/dist/core/runner/tool-end-body.d.ts +5 -4
- package/dist/core/runner/tool-end-body.js +4 -4
- package/dist/core/runner/tool-output-projection.d.ts +6 -4
- package/dist/core/runner/tool-output-projection.js +66 -47
- package/dist/core/runner-deps.d.ts +6 -3
- package/dist/core/shared-memory/types.d.ts +2 -2
- package/dist/core/shell-read-silence.d.ts +23 -0
- package/dist/core/shell-read-silence.js +18 -0
- package/dist/core/synthetic-read-verdict.d.ts +80 -21
- package/dist/core/synthetic-read-verdict.js +89 -18
- package/dist/core/task-event.d.ts +113 -25
- package/dist/core/task-spec.d.ts +44 -0
- package/dist/core/task-tool-shape.d.ts +1 -1
- package/dist/core/tool-catalog-entries.js +16 -2
- package/dist/core/tool-policy.d.ts +25 -15
- package/dist/core/tool-policy.js +9 -13
- package/dist/core/trace.d.ts +3 -2
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/core/untrusted-text.d.ts +2 -2
- package/dist/core/user-attachment.d.ts +164 -0
- package/dist/core/user-attachment.js +437 -0
- package/dist/engine/compaction/compaction.js +10 -3
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +19 -1
- package/dist/engine/execution-env/node-execution-env.js +125 -0
- package/dist/engine/harness/agent-harness.d.ts +12 -2
- package/dist/engine/harness/agent-harness.js +15 -3
- package/dist/engine/harness/types.d.ts +21 -0
- package/dist/engine/llm/content-block-render.d.ts +15 -0
- package/dist/engine/llm/content-block-render.js +10 -0
- package/dist/engine/llm/index.d.ts +1 -0
- package/dist/engine/llm/index.js +1 -0
- package/dist/engine/llm/types.d.ts +80 -4
- package/dist/engine/llm/types.js +43 -1
- package/dist/engine/session/import-validate.js +2 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +2 -1
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/internal/llm.d.ts +2 -2
- package/dist/internal/llm.js +1 -1
- package/dist/orchestration/workflow.d.ts +4 -5
- package/dist/orchestration/workflow.js +2 -2
- package/dist/prompts/default.d.ts +1 -1
- package/dist/tools/artifact/artifact-tool.js +1 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/dir-entries.d.ts +4 -10
- package/dist/tools/fs/dir-entries.js +4 -11
- package/dist/tools/fs/file-read-check.js +3 -3
- package/dist/tools/fs/fs-pdf.js +2 -4
- package/dist/tools/fs/fs-read.d.ts +47 -4
- package/dist/tools/fs/fs-read.js +63 -19
- package/dist/tools/fs/fs-search-tools.d.ts +3 -3
- package/dist/tools/fs/fs-search-tools.js +7 -2
- package/dist/tools/fs/fs-write.d.ts +1 -1
- package/dist/tools/fs/fs-write.js +7 -6
- package/dist/tools/fs/index.d.ts +7 -1
- package/dist/tools/fs/index.js +56 -34
- package/dist/tools/fs/read-deny-home.d.ts +19 -1
- package/dist/tools/fs/read-deny-home.js +32 -22
- package/dist/tools/fs/read-deny.d.ts +14 -0
- package/dist/tools/fs/read-deny.js +8 -0
- package/dist/tools/fs/repo-map.d.ts +3 -2
- package/dist/tools/fs/repo-map.js +11 -3
- package/dist/tools/fs/safety.d.ts +41 -2
- package/dist/tools/fs/safety.js +12 -3
- package/dist/tools/fs/shell-read-check.d.ts +12 -3
- package/dist/tools/fs/shell-read-check.js +44 -24
- package/dist/tools/fs/shell-read-device.d.ts +1 -1
- package/dist/tools/fs/shell-read-face.d.ts +15 -0
- package/dist/tools/fs/shell-read-face.js +5 -0
- package/dist/tools/fs/shell-read-judge.d.ts +9 -4
- package/dist/tools/fs/shell-read-judge.js +1 -1
- package/dist/tools/loop-tick.d.ts +2 -2
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +39 -1
- package/wire-changes/core@7.50.0.json +1699 -0
- package/wire-changes/core@7.50.1.json +65 -0
- package/dist/core/runner/persisted-path-rows.d.ts +0 -88
- package/dist/core/runner/persisted-path-rows.js +0 -108
package/dist/core/lsp.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type RootsSeat } from "../tools/fs/safety.js";
|
|
2
|
+
import { type ReadDenyMatcher } from "../tools/fs/read-deny.js";
|
|
3
|
+
import type { HomeViewsCarrier } from "../tools/fs/read-deny-home.js";
|
|
4
|
+
import type { ContainmentFace } from "../tools/fs/containment-face.js";
|
|
5
|
+
import type { AgentTool, BoundFileObject, ExecutionEnv } from "../internal/harness-types.js";
|
|
2
6
|
/**
|
|
3
7
|
* LSP code-intelligence tool (design/64 §13.1) — the app-dev命门 (go-to-def / find-refs / hover across
|
|
4
8
|
* Dart/Kotlin/TS). A single `lsp` tool with a 9-operation enum, built over a pluggable {@link LspServerManager}.
|
|
@@ -147,8 +151,71 @@ export interface LspRequestParams {
|
|
|
147
151
|
}
|
|
148
152
|
/** An active language-server session for one language, scoped to a workspace. */
|
|
149
153
|
export interface LspSession {
|
|
150
|
-
|
|
154
|
+
/** `gate` = the caller's read face at the session's file reads ({@link LspReadGate}); absent ⇒ a caller with none. */
|
|
155
|
+
request(op: LspOperation, params: LspRequestParams, signal?: AbortSignal, gate?: LspReadGate): Promise<LspResult>;
|
|
151
156
|
}
|
|
157
|
+
/** What a session's read gate answered for one file: the object its judgment named, opened and HELD (the environment's
|
|
158
|
+
* `openBound`) under the canonical key it was judged as — the session reads that object, never the name again, and closes
|
|
159
|
+
* it — or a refusal with its reason. */
|
|
160
|
+
export type LspReadAdmission = {
|
|
161
|
+
readonly ok: true;
|
|
162
|
+
readonly key: string;
|
|
163
|
+
readonly object: BoundFileObject;
|
|
164
|
+
} | {
|
|
165
|
+
readonly ok: false;
|
|
166
|
+
readonly reason: string;
|
|
167
|
+
};
|
|
168
|
+
/** May a session read `filePath` (a path {@link resolveLspPath} minted) for this caller, now — and if so, WHAT: the gate
|
|
169
|
+
* admits an object, not a name ({@link LspReadAdmission}). A session reads a file only from the object its caller's gate
|
|
170
|
+
* admitted, and closes an opened document the gate does not admit. */
|
|
171
|
+
export type LspReadGate = (filePath: string, signal?: AbortSignal) => Promise<LspReadAdmission>;
|
|
172
|
+
/** The read face one LSP mount reads under: the seats the fs read tools take, so the gate and the tool resolve and judge
|
|
173
|
+
* a `filePath` alike (see {@link LspToolOptions.readFace}). */
|
|
174
|
+
export interface LspReadFace {
|
|
175
|
+
/** The canonical task root: the primary containment root, and the base when no working directory is tracked. */
|
|
176
|
+
readonly rootCanonical: string;
|
|
177
|
+
/** The extra read roots (write-capable and read-only), read per call. */
|
|
178
|
+
readonly roots?: RootsSeat;
|
|
179
|
+
/** The tracked working directory; `current` is read per call as the base of a relative `filePath`. */
|
|
180
|
+
readonly cwdRef?: {
|
|
181
|
+
readonly current: string;
|
|
182
|
+
};
|
|
183
|
+
/** The read-deny table in force, with its placement's home views. */
|
|
184
|
+
readonly readDeny?: ReadDenyMatcher & HomeViewsCarrier;
|
|
185
|
+
/** The read containment face. Absent ⇒ `roots`. */
|
|
186
|
+
readonly face?: ContainmentFace;
|
|
187
|
+
/** The file read station's admission: an out-of-root target the gate asked about and let through, read once. */
|
|
188
|
+
readonly admitted?: (toolCallId: string | undefined, canonicalKey: string) => boolean;
|
|
189
|
+
/** Settles once the read-deny table and read roots a judgment takes are ready (a Runner compiles them per read of the
|
|
190
|
+
* person's rules): a call's own (`toolCallId`), or — with none — the run's newest; at `signal` too; never rejects;
|
|
191
|
+
* absent ⇒ ready. The tool waits for it before it takes its call's; the session's own reads before they judge. */
|
|
192
|
+
readonly ready?: (toolCallId: string | undefined, signal?: AbortSignal) => Promise<void>;
|
|
193
|
+
}
|
|
194
|
+
/** The base a relative `filePath` resolves against: the catalog declaration through the one selector the fences read it
|
|
195
|
+
* with; with no read face the env's working directory is the only base there is. */
|
|
196
|
+
export declare function lspBaseOf(readFace: LspReadFace | undefined, env?: ExecutionEnv): string | undefined;
|
|
197
|
+
/** The session gate over a read face — the bound read a user attachment makes, at the session's read: the name is judged
|
|
198
|
+
* as `Read` judges its target (a judgment that throws refuses), the object the judgment named is opened and held
|
|
199
|
+
* (`openBound`; an environment without it cannot bind and refuses), and the name, judged again, must still land on the
|
|
200
|
+
* held key. The admission carries the held object; the session reads only it. `authorizedKey` = the canonical key this
|
|
201
|
+
* call's target was authorized as (the station's one-read admission already spent on it): a spelling that still lands
|
|
202
|
+
* there passes outside the roots, nothing else does. */
|
|
203
|
+
export declare function lspReadGateOf(env: ExecutionEnv, face: LspReadFace, authorizedKey?: string): LspReadGate;
|
|
204
|
+
/** Why a file the language server named is withheld: a read-deny row reaches one of its views, or its views could not be
|
|
205
|
+
* read (`unreadable` — the judgment's absence word). */
|
|
206
|
+
export type LspListedWithholding = {
|
|
207
|
+
readonly pattern: string;
|
|
208
|
+
} | {
|
|
209
|
+
readonly unreadable: true;
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* The files a language server named (result locations, symbols, drained diagnostics), judged as ONE bounded batch against
|
|
213
|
+
* the read face's deny table on every view of each file — the spelling, the canonical target, the home view and the link
|
|
214
|
+
* hops, through the same `resolveKey` a read target meets (containment is not this question: the face is judged open).
|
|
215
|
+
* Fail-closed: a file whose views cannot be read, and every file when the batch outlives the gate's deadline, is withheld.
|
|
216
|
+
* Kept files are absent from the result.
|
|
217
|
+
*/
|
|
218
|
+
export declare function lspListedWithheld(env: ExecutionEnv, face: LspReadFace, uris: readonly string[], signal?: AbortSignal): Promise<ReadonlyMap<string, LspListedWithholding>>;
|
|
152
219
|
/**
|
|
153
220
|
* A JSON-RPC transport to ONE language server, POST-`initialize`. The DEPLOYMENT provides the concrete
|
|
154
221
|
* transport; the protocol logic ({@link buildRequest}/`parseResult`, `TransportLspSession`) drives the LSP
|
|
@@ -175,7 +242,8 @@ export interface LspTransport {
|
|
|
175
242
|
* a test mock stays minimal. */
|
|
176
243
|
readonly closed?: boolean;
|
|
177
244
|
}
|
|
178
|
-
/** Reads a workspace file's CURRENT text for the session's didOpen/didChange re-sync
|
|
245
|
+
/** Reads a workspace file's CURRENT text for the session's didOpen/didChange re-sync BY NAME — the read of a caller with no
|
|
246
|
+
* gate (no read face) only; a gated read is the admitted held object's ({@link LspReadAdmission}). TOC = local fs; TOB =
|
|
179
247
|
* the sandbox ExecutionEnv. */
|
|
180
248
|
export type LspReadText = (filePath: string, signal?: AbortSignal) => Promise<string>;
|
|
181
249
|
/** CC 2.1.220's LSP file cap (10MB — `File too large for LSP analysis`, (pretty220.js:474745, historical; not relocated in 250 — see cc-250 anchors)). Single-sourced
|
|
@@ -216,6 +284,18 @@ export interface LspToolOptions {
|
|
|
216
284
|
/** The task's ExecutionEnv, passed through to {@link LspServerManager.sessionFor} (the agent's sandbox —
|
|
217
285
|
* where an in-env language server must run). The Runner wires this from the per-task env. */
|
|
218
286
|
env?: ExecutionEnv;
|
|
287
|
+
/**
|
|
288
|
+
* The read face this mount reads under (needs `env`). PRESENT ⇒ every read the tool causes passes it: the `filePath`
|
|
289
|
+
* target is resolved against the declared base (the tracked working directory, else the root) and fenced as `Read`
|
|
290
|
+
* fences its own — containment with the station's one-read admission, the read-deny table, blocked devices; the file
|
|
291
|
+
* preconditions (exists / a regular file / the size cap) are read from the object that judgment named, held at its key
|
|
292
|
+
* (the environment's `openBound`), never from the spelling again; the session reads an opened file only from the
|
|
293
|
+
* object the face admitted and held (an environment without `openBound` reads nothing) and closes one it does not
|
|
294
|
+
* admit; every location or symbol in a file the deny table reaches is withheld, and the result says how many. ABSENT ⇒
|
|
295
|
+
* no read face judged this mount (a library-direct tool with no gate): a relative `filePath` resolves against
|
|
296
|
+
* `env.cwd`, the preconditions and the session's reads go by name, nothing is fenced and nothing is withheld.
|
|
297
|
+
*/
|
|
298
|
+
readFace?: LspReadFace;
|
|
219
299
|
}
|
|
220
300
|
/** Build the `lsp` tool over a {@link LspServerManager}. Mounted only when a manager is wired (opt-in). */
|
|
221
301
|
export declare function createLspTool(manager: LspServerManager, opts?: LspToolOptions): AgentTool;
|
package/dist/core/lsp.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { uriToPath } from "./lsp-protocol.js";
|
|
2
|
-
import { anchorSpelling, foldRedundantPathSegments, pathFamilyOf } from "../tools/fs/safety.js";
|
|
2
|
+
import { anchorSpelling, foldRedundantPathSegments, pathFamilyOf, resolveKey, violationDetails, violationText } from "../tools/fs/safety.js";
|
|
3
|
+
import { seatForCall, sensitivePathDenyList } from "../tools/fs/read-deny.js";
|
|
4
|
+
import { pathTargetBaseOf } from "./effective-path-target.js";
|
|
5
|
+
import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, withGateTimeout } from "./gate-bounded-await.js";
|
|
6
|
+
import { pathTargetOf } from "./tool-registry.js";
|
|
3
7
|
import { Type } from "typebox";
|
|
4
8
|
import { defineTool, errorResult } from "./tools.js";
|
|
5
9
|
import { toolFace } from "./tool-catalog-entries.js";
|
|
10
|
+
import { thrownText } from "./thrown-value.js";
|
|
6
11
|
export const LSP_OPERATIONS = [
|
|
7
12
|
"goToDefinition", "findReferences", "hover", "documentSymbol", "workspaceSymbol",
|
|
8
13
|
"goToImplementation", "prepareCallHierarchy", "incomingCalls", "outgoingCalls",
|
|
@@ -73,6 +78,63 @@ export function resolveLspPath(filePath, cwd) {
|
|
|
73
78
|
return foldRedundantPathSegments(filePath);
|
|
74
79
|
return foldRedundantPathSegments(anchorSpelling(pathFamilyOf({ root: cwd }), cwd, filePath));
|
|
75
80
|
}
|
|
81
|
+
const LSP_PATH_TARGET = pathTargetOf("LSP");
|
|
82
|
+
export function lspBaseOf(readFace, env) {
|
|
83
|
+
return pathTargetBaseOf(LSP_PATH_TARGET, readFace !== undefined ? { root: readFace.rootCanonical, cwd: readFace.cwdRef?.current } : { cwd: env?.cwd });
|
|
84
|
+
}
|
|
85
|
+
function judgeLspRead(env, face, spelling, signal, admitted) {
|
|
86
|
+
return resolveKey(env, face.rootCanonical, spelling, signal, lspBaseOf(face), face.roots, admitted, face.readDeny, face.face);
|
|
87
|
+
}
|
|
88
|
+
const LSP_UNBINDABLE = "this environment cannot bind a read to the file that was checked";
|
|
89
|
+
export function lspReadGateOf(env, face, authorizedKey) {
|
|
90
|
+
const exempt = authorizedKey === undefined ? undefined : (key) => key === authorizedKey;
|
|
91
|
+
const keyOf = async (filePath, signal) => {
|
|
92
|
+
const r = await judgeLspRead(env, face, filePath, signal, exempt).catch(() => undefined);
|
|
93
|
+
return r?.ok === true ? r.key : undefined;
|
|
94
|
+
};
|
|
95
|
+
return async (filePath, signal) => {
|
|
96
|
+
await face.ready?.(undefined, signal);
|
|
97
|
+
const key = await keyOf(filePath, signal);
|
|
98
|
+
if (key === undefined)
|
|
99
|
+
return { ok: false, reason: "the read boundary refuses this file" };
|
|
100
|
+
if (env.openBound === undefined)
|
|
101
|
+
return { ok: false, reason: LSP_UNBINDABLE };
|
|
102
|
+
let held;
|
|
103
|
+
try {
|
|
104
|
+
held = await env.openBound(key, signal);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
return { ok: false, reason: thrownText(e) };
|
|
108
|
+
}
|
|
109
|
+
if (!held.ok)
|
|
110
|
+
return { ok: false, reason: held.error.message };
|
|
111
|
+
if ((await keyOf(filePath, signal)) !== key) {
|
|
112
|
+
await held.value.close();
|
|
113
|
+
return { ok: false, reason: "it resolved to a different file while it was being checked" };
|
|
114
|
+
}
|
|
115
|
+
return { ok: true, key, object: held.value };
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export async function lspListedWithheld(env, face, uris, signal) {
|
|
119
|
+
const out = new Map();
|
|
120
|
+
if (face.readDeny === undefined || uris.length === 0)
|
|
121
|
+
return out;
|
|
122
|
+
await face.ready?.(undefined, signal);
|
|
123
|
+
const distinct = [...new Set(uris)];
|
|
124
|
+
const unreadable = { unreadable: true };
|
|
125
|
+
const judgeOne = async (uri) => {
|
|
126
|
+
const r = await resolveKey(env, face.rootCanonical, uriToPath(uri), signal, undefined, face.roots, undefined, face.readDeny, "open");
|
|
127
|
+
if (r.ok)
|
|
128
|
+
return undefined;
|
|
129
|
+
return r.violation.code === "read_path_denied" && r.violation.pattern !== undefined ? { pattern: r.violation.pattern } : unreadable;
|
|
130
|
+
};
|
|
131
|
+
const batch = Promise.all(distinct.map(async (uri) => [uri, await judgeOne(uri).catch(() => unreadable)]));
|
|
132
|
+
const judged = await withGateTimeout(batch, DEFAULT_GATE_AWAIT_TIMEOUT_MS, signal, "lspListedPaths").catch(() => undefined);
|
|
133
|
+
for (const [uri, withholding] of judged ?? distinct.map((uri) => [uri, unreadable]))
|
|
134
|
+
if (withholding !== undefined)
|
|
135
|
+
out.set(uri, withholding);
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
76
138
|
export const MAX_LSP_FILE_BYTES = 10 * 1000 * 1000;
|
|
77
139
|
function fmt(result, ctx) {
|
|
78
140
|
switch (result.kind) {
|
|
@@ -139,10 +201,67 @@ async function filterIgnored(result, isIgnored) {
|
|
|
139
201
|
}
|
|
140
202
|
return result;
|
|
141
203
|
}
|
|
204
|
+
async function withholdDenied(result, withheldOf) {
|
|
205
|
+
if (result.kind !== "locations" && result.kind !== "symbols")
|
|
206
|
+
return { result, note: "" };
|
|
207
|
+
const entries = result.kind === "locations" ? result.locations : result.symbols;
|
|
208
|
+
const withheld = await withheldOf(entries.map((e) => e.uri));
|
|
209
|
+
const patterns = [];
|
|
210
|
+
let denied = 0;
|
|
211
|
+
let unreadable = 0;
|
|
212
|
+
const keeps = (uri) => {
|
|
213
|
+
const w = withheld.get(uri);
|
|
214
|
+
if (w === undefined)
|
|
215
|
+
return true;
|
|
216
|
+
if ("pattern" in w) {
|
|
217
|
+
denied += 1;
|
|
218
|
+
if (!patterns.includes(w.pattern))
|
|
219
|
+
patterns.push(w.pattern);
|
|
220
|
+
}
|
|
221
|
+
else
|
|
222
|
+
unreadable += 1;
|
|
223
|
+
return false;
|
|
224
|
+
};
|
|
225
|
+
const kept = result.kind === "locations" ? { kind: "locations", locations: result.locations.filter((l) => keeps(l.uri)) } : { kind: "symbols", symbols: result.symbols.filter((s) => keeps(s.uri)) };
|
|
226
|
+
const n = (k) => `${k} result${k === 1 ? "" : "s"}`;
|
|
227
|
+
const note = (denied > 0 ? `\n[note: ${n(denied)} in files matching ${sensitivePathDenyList(patterns)} ${denied === 1 ? "was" : "were"} withheld]` : "") +
|
|
228
|
+
(unreadable > 0 ? `\n[note: ${n(unreadable)} withheld: the file${unreadable === 1 ? "" : "s"} could not be resolved to be judged against the read deny list]` : "");
|
|
229
|
+
return { result: kept, note };
|
|
230
|
+
}
|
|
231
|
+
const factsOfFileError = (code) => (code === "not_found" ? "missing" : code === "invalid" ? "not_a_file" : undefined);
|
|
232
|
+
async function heldTargetFacts(env, key, signal) {
|
|
233
|
+
if (env.openBound === undefined)
|
|
234
|
+
return undefined;
|
|
235
|
+
const held = await env.openBound(key, signal).catch(() => undefined);
|
|
236
|
+
if (held === undefined)
|
|
237
|
+
return undefined;
|
|
238
|
+
if (!held.ok)
|
|
239
|
+
return factsOfFileError(held.error.code);
|
|
240
|
+
try {
|
|
241
|
+
if (held.value.kind === "directory")
|
|
242
|
+
return "not_a_file";
|
|
243
|
+
const size = await held.value.size();
|
|
244
|
+
return size.ok ? { size: size.value } : undefined;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
await held.value.close();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function namedTargetFacts(env, target, signal) {
|
|
251
|
+
const info = await env.fileInfo(target, signal);
|
|
252
|
+
if (!info.ok)
|
|
253
|
+
return factsOfFileError(info.error.code);
|
|
254
|
+
if (info.value.kind === "directory")
|
|
255
|
+
return "not_a_file";
|
|
256
|
+
return info.value.kind === "file" ? { size: info.value.size } : undefined;
|
|
257
|
+
}
|
|
142
258
|
const POSITION_OPS = new Set([
|
|
143
259
|
"goToDefinition", "findReferences", "hover", "goToImplementation", "prepareCallHierarchy", "incomingCalls", "outgoingCalls",
|
|
144
260
|
]);
|
|
145
261
|
export function createLspTool(manager, opts = {}) {
|
|
262
|
+
const faceSeat = opts.readFace;
|
|
263
|
+
if (faceSeat !== undefined && opts.env === undefined)
|
|
264
|
+
throw new TypeError("createLspTool: LspToolOptions.readFace needs the env it reads through (LspToolOptions.env)");
|
|
146
265
|
return defineTool({
|
|
147
266
|
name: "LSP",
|
|
148
267
|
...toolFace("lsp"),
|
|
@@ -173,6 +292,8 @@ export function createLspTool(manager, opts = {}) {
|
|
|
173
292
|
query: Type.Optional(Type.String({ description: "Symbol query for workspaceSymbol (may be empty to browse broadly)." })),
|
|
174
293
|
}),
|
|
175
294
|
execute: async (args, ctx) => {
|
|
295
|
+
await faceSeat?.ready?.(ctx.toolCallId, ctx.signal);
|
|
296
|
+
const readFace = faceSeat === undefined ? undefined : { ...faceSeat, readDeny: seatForCall(faceSeat.readDeny, ctx.toolCallId), roots: seatForCall(faceSeat.roots, ctx.toolCallId), ready: undefined };
|
|
176
297
|
const { operation, filePath, line, character, query } = args;
|
|
177
298
|
if (POSITION_OPS.has(operation) && (line === undefined || character === undefined)) {
|
|
178
299
|
return errorResult(`Error (LSP): the "${operation}" operation needs both line and character.`);
|
|
@@ -184,18 +305,24 @@ export function createLspTool(manager, opts = {}) {
|
|
|
184
305
|
if (operation === "workspaceSymbol" && wsQuery === undefined) {
|
|
185
306
|
return errorResult(`Error (LSP): the "workspaceSymbol" operation needs a query.`);
|
|
186
307
|
}
|
|
187
|
-
const target = resolveLspPath(filePath, opts.env
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
308
|
+
const target = resolveLspPath(filePath, lspBaseOf(readFace, opts.env));
|
|
309
|
+
let gate;
|
|
310
|
+
let judgedKey;
|
|
311
|
+
if (readFace !== undefined && opts.env !== undefined) {
|
|
312
|
+
const admitted = readFace.admitted;
|
|
313
|
+
const r = await judgeLspRead(opts.env, readFace, filePath, ctx.signal, admitted && ((key) => admitted(ctx.toolCallId, key)));
|
|
314
|
+
if (!r.ok)
|
|
315
|
+
return errorResult(violationText("LSP", r.violation), violationDetails(r.violation));
|
|
316
|
+
judgedKey = r.key;
|
|
317
|
+
gate = lspReadGateOf(opts.env, readFace, r.key);
|
|
318
|
+
}
|
|
319
|
+
const facts = opts.env === undefined ? undefined : judgedKey !== undefined ? await heldTargetFacts(opts.env, judgedKey, ctx.signal) : await namedTargetFacts(opts.env, target, ctx.signal);
|
|
320
|
+
if (facts === "missing")
|
|
321
|
+
return errorResult(`Error (LSP): File does not exist: ${filePath}`);
|
|
322
|
+
if (facts === "not_a_file")
|
|
323
|
+
return errorResult(`Error (LSP): Path is not a file: ${filePath}`);
|
|
324
|
+
if (facts !== undefined && facts.size > MAX_LSP_FILE_BYTES) {
|
|
325
|
+
return `File too large for LSP analysis (${Math.ceil(facts.size / 1e6)}MB exceeds 10MB limit)`;
|
|
199
326
|
}
|
|
200
327
|
const session = await manager.sessionFor(target, ctx.signal, opts.env);
|
|
201
328
|
if (!session) {
|
|
@@ -203,9 +330,12 @@ export function createLspTool(manager, opts = {}) {
|
|
|
203
330
|
return `LSP is unavailable for "${filePath}" (${why}). Fall back to Grep/Read for this lookup.`;
|
|
204
331
|
}
|
|
205
332
|
const seamCharacter = character !== undefined ? character - 1 : undefined;
|
|
206
|
-
const result = await session.request(operation, { filePath: target, line, character: seamCharacter, symbol: wsQuery }, ctx.signal);
|
|
207
|
-
const
|
|
208
|
-
const
|
|
333
|
+
const result = await session.request(operation, { filePath: target, line, character: seamCharacter, symbol: wsQuery }, ctx.signal, gate);
|
|
334
|
+
const ignored = await filterIgnored(result, opts.isPathIgnored);
|
|
335
|
+
const env = opts.env;
|
|
336
|
+
const kept = readFace !== undefined && env !== undefined ? await withholdDenied(ignored, (uris) => lspListedWithheld(env, readFace, uris, ctx.signal)) : { result: ignored, note: "" };
|
|
337
|
+
const final = toModelBase(kept.result);
|
|
338
|
+
const text = fmt(final, { operation, filePath }) + kept.note;
|
|
209
339
|
return final.kind === "none" && final.reason !== undefined && LSP_ERROR_REASONS.has(final.reason) ? errorResult(text) : text;
|
|
210
340
|
},
|
|
211
341
|
});
|
package/dist/core/mcp.js
CHANGED
|
@@ -19,6 +19,7 @@ import { sliceHeadSafe, sliceTailSafe } from "./surrogate-safe-slice.js";
|
|
|
19
19
|
import { truncateError } from "./tool-errors.js";
|
|
20
20
|
import { delimitUntrusted, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
|
|
21
21
|
import { discloseReminderShaped, observeReminderMarkEcho } from "./reminder-disclosure.js";
|
|
22
|
+
import { MARKED_BLOCK_AFTER_DATA } from "./reminder-mint.js";
|
|
22
23
|
import { resolveMcpDeclaredResultSize, resolveMcpRequiresUserInteraction } from "./mcp-tool-meta.js";
|
|
23
24
|
export { resolveMcpDeclaredResultSize } from "./mcp-tool-meta.js";
|
|
24
25
|
import { withContentOrigin } from "./memory-engine/content-origin.js";
|
|
@@ -1797,7 +1798,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1797
1798
|
if (d.defused)
|
|
1798
1799
|
textIdx.forEach((ci, si) => (content[ci] = { type: "text", text: d.segments[si] }));
|
|
1799
1800
|
if (d.trailer !== undefined)
|
|
1800
|
-
content.push({ type: "text", text: d.trailer });
|
|
1801
|
+
content.push({ type: "text", text: `${MARKED_BLOCK_AFTER_DATA}${d.trailer}` });
|
|
1801
1802
|
}
|
|
1802
1803
|
if (sc !== undefined)
|
|
1803
1804
|
return { content, details: { type: "mcp", structuredContent: sc }, terminate: false };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentMessage } from "../internal/harness-types.js";
|
|
2
|
+
import type { DocumentContent, ImageContent } from "../internal/llm.js";
|
|
2
3
|
/**
|
|
3
4
|
* Aggregate MEDIA (image) byte budget — the multimodal sibling of {@link AGGREGATE_TOOL_RESULT_BUDGET_CHARS}
|
|
4
5
|
* (which only counts TEXT, design/64 §17.2). batch-C added image reads (`Read` → `ImageContent`); each is bounded
|
|
@@ -14,6 +15,19 @@ import type { AgentMessage } from "../internal/harness-types.js";
|
|
|
14
15
|
/** Default aggregate inline-media budget (bytes). Generous — only the pathological "N parallel huge image reads"
|
|
15
16
|
* case strips. Bounds the request while leaving normal multimodal use untouched. Overridable per-deployment. */
|
|
16
17
|
export declare const AGGREGATE_MEDIA_BUDGET_BYTES: number;
|
|
18
|
+
/** The one reading of a deployment's media budget (`RunnerDeps.mediaByteCapBytes`, screened by {@link checkedMediaByteCap}
|
|
19
|
+
* at `new Runner`), for both of its readers — the request strip below and the `tool_end.output` projection: absent ⇒
|
|
20
|
+
* {@link AGGREGATE_MEDIA_BUDGET_BYTES}; a finite positive number ⇒ itself; `0` or `Infinity` ⇒ no budget, `Infinity`
|
|
21
|
+
* (a direct caller of {@link capAggregateMediaBytes} that passes a negative or `NaN` meets the same no-op). */
|
|
22
|
+
export declare function mediaBudgetBytes(cap: number | undefined): number;
|
|
23
|
+
/** The deployment's media budget, screened once at `new Runner`: absent, a non-negative number of bytes, or `Infinity`
|
|
24
|
+
* (`0` and `Infinity` both mean "no budget"). A non-number, `NaN` or a negative is refused (`config.invalid_argument`),
|
|
25
|
+
* never read as "no budget". */
|
|
26
|
+
export declare function checkedMediaByteCap(value: unknown, seat: string): void;
|
|
27
|
+
/** Decoded byte size of a base64 media payload (4 base64 chars ≈ 3 bytes). Approximate — fine for a budget.
|
|
28
|
+
* PDF read: `document` blocks (base64 PDFs) count against the SAME aggregate budget — same class of inline
|
|
29
|
+
* media bytes, same unbounded-accumulation failure mode. */
|
|
30
|
+
export declare function imageBytes(b: ImageContent | DocumentContent): number;
|
|
17
31
|
/** Telemetry for one strip pass (mirrors CC's `tengu_media_byte_cap_stripped` fields). */
|
|
18
32
|
export interface MediaStripInfo {
|
|
19
33
|
totalBytes: number;
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import { describeRejectedValue } from "./tool-search-setting.js";
|
|
1
2
|
export const AGGREGATE_MEDIA_BUDGET_BYTES = 30 * 1024 * 1024;
|
|
3
|
+
export function mediaBudgetBytes(cap) {
|
|
4
|
+
const limit = cap ?? AGGREGATE_MEDIA_BUDGET_BYTES;
|
|
5
|
+
return Number.isFinite(limit) && limit > 0 ? limit : Infinity;
|
|
6
|
+
}
|
|
7
|
+
export function checkedMediaByteCap(value, seat) {
|
|
8
|
+
if (value === undefined || (typeof value === "number" && value >= 0))
|
|
9
|
+
return;
|
|
10
|
+
const e = new Error(`${seat} ${describeRejectedValue(value)} is not a media byte budget — expected a non-negative number of bytes (0 or Infinity: no budget), or omit it for the default ${AGGREGATE_MEDIA_BUDGET_BYTES}`);
|
|
11
|
+
e.code = "config.invalid_argument";
|
|
12
|
+
throw e;
|
|
13
|
+
}
|
|
2
14
|
const MEDIA_STRIPPED_MARKER = "[media removed to fit the request media budget — re-read the file if you still need to see it]";
|
|
3
|
-
function imageBytes(b) {
|
|
15
|
+
export function imageBytes(b) {
|
|
4
16
|
return typeof b.data === "string" ? Math.floor((b.data.length * 3) / 4) : 0;
|
|
5
17
|
}
|
|
6
18
|
function blocksOf(m) {
|
|
@@ -8,8 +20,8 @@ function blocksOf(m) {
|
|
|
8
20
|
return Array.isArray(content) ? content : undefined;
|
|
9
21
|
}
|
|
10
22
|
export function capAggregateMediaBytes(messages, opts) {
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
23
|
+
const limitBytes = mediaBudgetBytes(opts.limitBytes);
|
|
24
|
+
if (limitBytes === Infinity)
|
|
13
25
|
return messages;
|
|
14
26
|
let total = 0;
|
|
15
27
|
for (const m of messages) {
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
* auto lane's task-boundary tick), never on a background schedule of core's own.
|
|
22
22
|
*/
|
|
23
23
|
import type { EngineNotice } from "../types.js";
|
|
24
|
-
import { type ConsolidationGateRow, type ConsolidationRunStopReason, type MemoryConsolidationOptions } from "./consolidation.js";
|
|
24
|
+
import { type ConsolidationGateRow, CONSOLIDATION_WRITE_REFUSED_CODE, type ConsolidationRunStopReason, type MemoryConsolidationOptions } from "./consolidation.js";
|
|
25
25
|
import { type ConsolidationPlanFoldEvidence, type ConsolidationPlanSummary } from "./engine.js";
|
|
26
|
+
import type { SyntheticWriteVerdict } from "../synthetic-read-verdict.js";
|
|
26
27
|
import { mintLlmConsolidationPlan, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationFoldState, type DistillerChatFn, type LlmConsolidationPlan, type LlmConsolidationPlanProduct, type LlmDistillerContract } from "./distiller.js";
|
|
27
28
|
export declare const CONSOLIDATION_DRIVER_RUNS_FILE = "distiller-runs.json";
|
|
28
29
|
/** §1.6-4 — the mint archive: what was shown to the model and what it answered, verbatim, per run
|
|
@@ -142,14 +143,35 @@ export interface ConsolidationDriverEngine extends ConsolidationDriveEngine {
|
|
|
142
143
|
listConsolidationPlans(): ConsolidationPlanSummary[];
|
|
143
144
|
readConsolidationPlanFoldEvidence(planId: string): ConsolidationPlanFoldEvidence;
|
|
144
145
|
readConsolidationFoldState(scope: string): Promise<ConsolidationFoldState>;
|
|
146
|
+
/** The canonical directory a commit lands the scope's products in (the write judge's early question). */
|
|
147
|
+
consolidationWriteHome(scope: string): string;
|
|
145
148
|
readonly controlPlaneRoot: string;
|
|
146
149
|
}
|
|
150
|
+
/** One write the judge is asked about: `path` is a file (or the library directory) of `scope`'s library, absolute and canonical. */
|
|
151
|
+
export interface ConsolidationWriteTarget {
|
|
152
|
+
scope: string;
|
|
153
|
+
path: string;
|
|
154
|
+
}
|
|
155
|
+
/** The write judge's seat: one target in, the synthetic-write judge's verdict out (only `no_objection` permits the write). */
|
|
156
|
+
export type ConsolidationWriteJudge = (target: ConsolidationWriteTarget) => Promise<SyntheticWriteVerdict> | SyntheticWriteVerdict;
|
|
157
|
+
export { CONSOLIDATION_WRITE_REFUSED_CODE };
|
|
147
158
|
export interface RunMemoryConsolidationOptions {
|
|
148
159
|
/** The explicit model seat (design/339 §1.1: the ENGINE holds none). Build one with
|
|
149
160
|
* `openAiCompatChatSeat` or hand any JSON-only completion function. */
|
|
150
161
|
chat: DistillerChatFn;
|
|
151
162
|
/** The explicit model id, recorded into the run and the archive (alias spellings refuse). */
|
|
152
163
|
model: string;
|
|
164
|
+
/** The write judge — REQUIRED. A run writes the library with nobody watching, so it asks the judgment a foreground write
|
|
165
|
+
* into the same library meets: the person's standing path rules and the deployment's policy, read through the ONE judge
|
|
166
|
+
* (`createSyntheticWriteJudge`; the Runner's auto lane passes the leg's own). It is asked in two places over the run, with the same
|
|
167
|
+
* seat and the same refusal: once for the library directory before any claim, seat or model call (an early refusal that
|
|
168
|
+
* spends nothing), and — the authority — where a plan is applied (a commit, a replay of a suspended plan, the drive's
|
|
169
|
+
* recovery), at the backend's final spelling of every product before any is written: a refusal there writes nothing and
|
|
170
|
+
* discards the plan.
|
|
171
|
+
* Anything but `no_objection` — a deny, an ask (nobody is present to answer it), an unreadable rule source, a
|
|
172
|
+
* throw — refuses the run with `memory.consolidation_write_refused`, and nothing is written. A host that holds no rules
|
|
173
|
+
* says so by passing a judge that answers `no_objection`; an absent seat refuses, it never reads as "no rules". */
|
|
174
|
+
writeJudge: ConsolidationWriteJudge;
|
|
153
175
|
/** Audit locator recorded into the archive when the seat is endpoint-shaped. */
|
|
154
176
|
baseUrl?: string;
|
|
155
177
|
/** The frozen prompt contract; default = the product v1 (dl-3 byte-identical). A different
|
|
@@ -3,7 +3,7 @@ import { uuidv7 } from "../../internal/harness.js";
|
|
|
3
3
|
import { atomicWriteFileSync, ensureDirExists, lockedStrictUpdate, readStrictSidecar, enqueueMemoryAnnouncement, ControlPlaneCorruptError } from "./layout.js";
|
|
4
4
|
import { inlineUntrusted } from "../untrusted-text.js";
|
|
5
5
|
import { deliverEngineNotice } from "../engine-notice.js";
|
|
6
|
-
import { CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, screenConsolidationOptions, supersessionFuseCeiling, updateConsolidationGate, recommendationUnconsumed, } from "./consolidation.js";
|
|
6
|
+
import { CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS, screenConsolidationOptions, supersessionFuseCeiling, updateConsolidationGate, recommendationUnconsumed, CONSOLIDATION_WRITE_REFUSED_CODE, ConsolidationRefusedError, } from "./consolidation.js";
|
|
7
7
|
import { memoryConsolidationIncompleteNotice, memoryConsolidationWithheldNotice } from "./engine.js";
|
|
8
8
|
import { asThrownError, thrownText } from "../thrown-value.js";
|
|
9
9
|
import { CONSOLIDATION_DRIVE_ABORT, CONSOLIDATION_SNAPSHOT_PARKED, MEMORY_DISTILLER_CONTRACT_V1, detectCleanArmVerbatimLeak, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, normalizedCandidateRev, } from "./distiller.js";
|
|
@@ -61,6 +61,7 @@ export function archiveDistillerPlan(controlDir, runId, plan, attempt = 1) {
|
|
|
61
61
|
atomicWriteFileSync(join(controlDir, CONSOLIDATION_DRIVER_PLANS_DIR, name), `${JSON.stringify(plan, null, 2)}\n`);
|
|
62
62
|
return name;
|
|
63
63
|
}
|
|
64
|
+
export { CONSOLIDATION_WRITE_REFUSED_CODE };
|
|
64
65
|
function driverConfigRefusal(message) {
|
|
65
66
|
const e = new Error(message);
|
|
66
67
|
e.code = "config.memory_consolidation_driver";
|
|
@@ -93,6 +94,33 @@ const codeOf = (err) => {
|
|
|
93
94
|
const c = err?.code;
|
|
94
95
|
return typeof c === "string" ? c : undefined;
|
|
95
96
|
};
|
|
97
|
+
async function refuseUnlessWriteAllowed(judge, target, consequence) {
|
|
98
|
+
let verdict;
|
|
99
|
+
let threw;
|
|
100
|
+
try {
|
|
101
|
+
verdict = await judge(target);
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
threw = thrownText(err);
|
|
105
|
+
}
|
|
106
|
+
const answer = typeof verdict === "object" && verdict !== null ? verdict : undefined;
|
|
107
|
+
if (threw === undefined && answer?.verdict === "no_objection")
|
|
108
|
+
return;
|
|
109
|
+
const text = (v) => (typeof v === "string" && v !== "" ? inlineUntrusted(v, 200) : undefined);
|
|
110
|
+
const where = text(answer?.layer);
|
|
111
|
+
let why;
|
|
112
|
+
if (threw !== undefined)
|
|
113
|
+
why = `the write judge threw (${inlineUntrusted(threw, 200)}), so whether a standing rule forbids this write is unknown`;
|
|
114
|
+
else if (answer?.verdict === "deny")
|
|
115
|
+
why = `a standing rule layer${where !== undefined ? ` (${where})` : ""} refuses writes into this library${text(answer.message) !== undefined ? `: ${text(answer.message)}` : ""}`;
|
|
116
|
+
else if (answer?.verdict === "ask")
|
|
117
|
+
why = `a standing rule layer${where !== undefined ? ` (${where})` : ""} would ask a person before a write into this library, and a background run has nobody to answer${text(answer.message) !== undefined ? `: ${text(answer.message)}` : ""}`;
|
|
118
|
+
else if (answer?.verdict === "unanswerable")
|
|
119
|
+
why = `the write judge could not answer${where !== undefined ? ` at ${where}` : ""}${text(answer.cause) !== undefined ? ` (${text(answer.cause)})` : ""}${text(answer.reason) !== undefined ? `: ${text(answer.reason)}` : ""}, so whether a standing rule forbids this write is unknown`;
|
|
120
|
+
else
|
|
121
|
+
why = "the write judge answered something outside its closed set of verdicts";
|
|
122
|
+
throw new ConsolidationRefusedError(CONSOLIDATION_WRITE_REFUSED_CODE, `memory consolidation refused for scope ${inlineUntrusted(target.scope, 80)} (${inlineUntrusted(target.path, 200)}): ${why}${consequence !== undefined ? ` — ${consequence}` : ""}`);
|
|
123
|
+
}
|
|
96
124
|
const MAX_THROTTLE_WAITS_PER_SNAPSHOT = 4;
|
|
97
125
|
const MAX_BUSY_WAITS = 2;
|
|
98
126
|
export async function runMemoryConsolidationDriver(engine, scope, opts) {
|
|
@@ -127,7 +155,24 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
|
|
|
127
155
|
};
|
|
128
156
|
const throttleWindowMs = Math.min(screened.minRunIntervalMs, CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS);
|
|
129
157
|
const controlDir = engine.controlPlaneRoot;
|
|
130
|
-
|
|
158
|
+
if (typeof opts.writeJudge !== "function")
|
|
159
|
+
throw driverConfigRefusal(`runMemoryConsolidationDriver needs an explicit write judge (opts.writeJudge), got ${typeof opts.writeJudge} — a run writes the library with nobody watching, so it asks the judgment a foreground write meets before it starts; an absent seat is refused, never read as "no rules apply" (a host that holds no rules passes a judge that answers no_objection).`);
|
|
160
|
+
let libraryHome;
|
|
161
|
+
try {
|
|
162
|
+
libraryHome = engine.consolidationWriteHome(scope);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
throw new ConsolidationRefusedError(CONSOLIDATION_WRITE_REFUSED_CODE, `memory consolidation refused for scope ${inlineUntrusted(scope, 80)}: where this run would write could not be derived (${inlineUntrusted(thrownText(err), 200)}), so whether a standing rule forbids it is unknown — no run was started and nothing was written.`);
|
|
166
|
+
}
|
|
167
|
+
await refuseUnlessWriteAllowed(opts.writeJudge, { scope, path: libraryHome }, "no run was started and nothing was written.");
|
|
168
|
+
const writeGate = (target) => refuseUnlessWriteAllowed(opts.writeJudge, target);
|
|
169
|
+
let refusedSeat = undefined;
|
|
170
|
+
const parkedSeat = await recoverSeat(engine, scope, { sleep, windowMs: throttleWindowMs, writeGate }).catch((err) => {
|
|
171
|
+
if (!(err instanceof ConsolidationRefusedError) || err.code !== CONSOLIDATION_WRITE_REFUSED_CODE)
|
|
172
|
+
throw err;
|
|
173
|
+
refusedSeat = err;
|
|
174
|
+
return undefined;
|
|
175
|
+
});
|
|
131
176
|
const openFold = await engine.readConsolidationFoldState(scope);
|
|
132
177
|
const { row: run, resumed } = claimDriverRun(controlDir, scope, () => ({
|
|
133
178
|
runId: uuidv7(),
|
|
@@ -258,6 +303,8 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
|
|
|
258
303
|
if (parkedSeat !== undefined) {
|
|
259
304
|
return settle("parked", undefined, parkedSeat);
|
|
260
305
|
}
|
|
306
|
+
if (refusedSeat !== undefined)
|
|
307
|
+
return settle("refused", undefined, `reconcileConsolidation refused the plan an earlier run left seated: ${refusedSeat.code}: ${refusedSeat.message}`);
|
|
261
308
|
let staleResidue = [];
|
|
262
309
|
let staleResidueMemberIds = new Set();
|
|
263
310
|
const distill = async (candidates, snapshot, s) => {
|
|
@@ -425,7 +472,7 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
|
|
|
425
472
|
if (busyWaits >= MAX_BUSY_WAITS)
|
|
426
473
|
throw err;
|
|
427
474
|
busyWaits += 1;
|
|
428
|
-
const parked = await recoverSeat(engine, scope, { sleep, windowMs: throttleWindowMs });
|
|
475
|
+
const parked = await recoverSeat(engine, scope, { sleep, windowMs: throttleWindowMs, writeGate });
|
|
429
476
|
if (parked !== undefined) {
|
|
430
477
|
const e = new Error(parked);
|
|
431
478
|
e[CONSOLIDATION_SNAPSHOT_PARKED] = true;
|
|
@@ -448,6 +495,7 @@ export async function runMemoryConsolidationDriver(engine, scope, opts) {
|
|
|
448
495
|
requestIdPrefix: run.requestId,
|
|
449
496
|
maxCycles: opts.maxCycles ?? 64,
|
|
450
497
|
takeSnapshot,
|
|
498
|
+
writeGate,
|
|
451
499
|
onCycle: (row) => {
|
|
452
500
|
cyclesSeen.push(row);
|
|
453
501
|
if (row.committed.products > 0 || row.committed.superseded > 0)
|
|
@@ -564,8 +612,8 @@ function collectRunFoldEvidence(engine, scope, requestId) {
|
|
|
564
612
|
}
|
|
565
613
|
return { targets, skipped };
|
|
566
614
|
}
|
|
567
|
-
async function recoverSeat(engine, scope, { sleep, windowMs }) {
|
|
568
|
-
await engine.reconcileConsolidation(scope);
|
|
615
|
+
async function recoverSeat(engine, scope, { sleep, windowMs, writeGate }) {
|
|
616
|
+
await engine.reconcileConsolidation(scope, { writeGate });
|
|
569
617
|
let seat = engine.readConsolidationGate(scope)?.openPlanId;
|
|
570
618
|
if (seat === undefined)
|
|
571
619
|
return undefined;
|
|
@@ -574,7 +622,7 @@ async function recoverSeat(engine, scope, { sleep, windowMs }) {
|
|
|
574
622
|
return `plan ${seat} holds the ${JSON.stringify(scope)} seat in a ${summary.corrupt === true ? "corrupt" : "conflict"} state — resolveConsolidationPlan(planId, "retry" | "discard") is the host valve; the run parks rather than stacking a second plan on the seat`;
|
|
575
623
|
}
|
|
576
624
|
await sleep(windowMs);
|
|
577
|
-
await engine.reconcileConsolidation(scope);
|
|
625
|
+
await engine.reconcileConsolidation(scope, { writeGate });
|
|
578
626
|
seat = engine.readConsolidationGate(scope)?.openPlanId;
|
|
579
627
|
if (seat === undefined)
|
|
580
628
|
return undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write gate, asked at the apply for every entry file a plan writes or removes: the backend's FINAL spelling of each (as its
|
|
3
|
+
* `admit`), or the one the apply knows for a backend that cannot admit. Kept beside the engine rather than in it; everything about
|
|
4
|
+
* how a refusal is shaped lives here — including what it says was written, which only the apply knows ({@link refusalNamingWritten}).
|
|
5
|
+
*/
|
|
6
|
+
import { ConsolidationRefusedError, type ConsolidationDirectedPatch, type ConsolidationWriteGate } from "./consolidation.js";
|
|
7
|
+
import type { AdmitLandings, PatchLanding } from "./landing-admission.js";
|
|
8
|
+
import type { MemoryBackend, MemoryEntry } from "./types.js";
|
|
9
|
+
/** The refusal of a verb that may apply a plan and was handed no write gate — thrown before the verb claims or changes anything. */
|
|
10
|
+
export declare const CONSOLIDATION_WRITE_GATE_REQUIRED_CODE = "config.memory_consolidation_write_gate";
|
|
11
|
+
/** A verb that may apply a plan carries the write gate; absent, it refuses before it claims or changes anything (a host that holds
|
|
12
|
+
* no rules passes a gate that resolves, and so says it). */
|
|
13
|
+
export declare function requireWriteGate(gate: unknown, verb: string): asserts gate is ConsolidationWriteGate;
|
|
14
|
+
/**
|
|
15
|
+
* Where a plan's pending directed patches write or remove files, as the store holds the entries now: an update's target and, when
|
|
16
|
+
* it moves the entry, the file it leaves; the file a delete removes. An id the store no longer holds is no write of a delete.
|
|
17
|
+
*/
|
|
18
|
+
export declare function directedLandings(directed: readonly ConsolidationDirectedPatch[], getByIds: (ids: readonly string[]) => Promise<MemoryEntry[]>): Promise<PatchLanding[]>;
|
|
19
|
+
/** What of a plan the store already holds: products it carries (the ids are the plan's own, minted at the freeze, so the store
|
|
20
|
+
* holding one means the plan wrote it) and directed patches whose planned state stands. */
|
|
21
|
+
export interface PlanWritten {
|
|
22
|
+
readonly products: readonly PatchLanding[];
|
|
23
|
+
readonly directed: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* {@link PlanWritten} for `plan`, read through `committed` once `backend` has replayed every commit it holds (a locked entry that
|
|
27
|
+
* writes nothing runs its recovery first) — what an earlier pass, which a crash or a failure cut short before its book was written,
|
|
28
|
+
* or this pass before a refusal, put in the store. Undefined when the store could not be read (a refusal then says so).
|
|
29
|
+
*/
|
|
30
|
+
export declare function planWritten(plan: {
|
|
31
|
+
readonly products: readonly {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
}[];
|
|
34
|
+
readonly directed: readonly ConsolidationDirectedPatch[];
|
|
35
|
+
}, backend: Pick<MemoryBackend, "applyPatches">, committed: Pick<MemoryBackend, "getByIds">): Promise<PlanWritten | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* The write gate's refusal at the apply, completed with what the plan has written: the gate knows why it refused; the apply knows
|
|
38
|
+
* that nothing further is written and what the store already holds of the plan (`written`; undefined = could not be read).
|
|
39
|
+
*/
|
|
40
|
+
export declare function refusalNamingWritten(err: ConsolidationRefusedError, written: PlanWritten | undefined): ConsolidationRefusedError;
|
|
41
|
+
/**
|
|
42
|
+
* The write gate as one apply's `admit`: every landing (`<slug>.md` in its scope's library directory) is put to the gate, all of
|
|
43
|
+
* them before any is written; the first refusal rejects with the gate's own error. A landing admitted once in the apply is not
|
|
44
|
+
* asked again (the apply asks its directed targets up front, and the backend names them again when it plans their batch).
|
|
45
|
+
*/
|
|
46
|
+
export declare function admitLandingsThrough(gate: ConsolidationWriteGate, homeOf: (scope: string) => string): AdmitLandings;
|