@sema-agent/server 7.126.0 → 7.127.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/MIGRATION.md +38 -0
- package/USAGE.md +86 -8
- package/deploy/sema-up/chart/templates/networkpolicy-sandbox.yaml +15 -5
- package/deploy/sema-up/chart/templates/sandbox-rbac.yaml +28 -1
- package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
- package/deploy/sema-up/chart/templates/server.yaml +1 -0
- package/deploy/sema-up/chart/values.yaml +16 -6
- package/deploy/sema-up/kube-up.sh +10 -4
- package/deploy/sema-up/lib/node-addrs.sh +42 -0
- package/deploy/sema-up/stack/sema-stack-cluster.yaml +20 -1
- package/dist/audit.js +1 -0
- package/dist/boot/execution-env.js +17 -4
- package/dist/boot/leader.d.ts +2 -1
- package/dist/boot/memory-consolidation.d.ts +37 -3
- package/dist/boot/memory-consolidation.js +18 -2
- package/dist/boot/runner-deps.d.ts +2 -1
- package/dist/boot/session-faces.d.ts +2 -1
- package/dist/boot/stage-06-runners.js +5 -2
- package/dist/config-catalog.js +3 -1
- package/dist/config-types.d.ts +5 -1
- package/dist/config.js +2 -0
- package/dist/execution-lane-caps.d.ts +12 -0
- package/dist/execution-lane-caps.js +16 -2
- package/dist/fleet/subagent-tail-bus.js +15 -5
- package/dist/http/admission.js +5 -1
- package/dist/http/durable-run-leg.js +1 -3
- package/dist/http/resume-legs.js +15 -6
- package/dist/http/route-ctx.d.ts +7 -1
- package/dist/http/routes/runs.js +23 -1
- package/dist/http/routes/tasks.js +17 -8
- package/dist/http/routes/workflows.js +8 -0
- package/dist/http/server.js +1 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lsp/manager.js +1 -1
- package/dist/observability/fail-open.d.ts +16 -0
- package/dist/observability/fail-open.js +16 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal.js +3 -0
- package/dist/plugins/caching-session-store.d.ts +39 -22
- package/dist/plugins/caching-session-store.js +16 -9
- package/dist/plugins/host-platform.d.ts +18 -3
- package/dist/plugins/host-platform.js +23 -10
- package/dist/plugins/k8s-exec-protocol.js +3 -0
- package/dist/plugins/one-time-migrations.d.ts +28 -4
- package/dist/plugins/one-time-migrations.js +22 -4
- package/dist/plugins/remote-env-host.d.ts +23 -6
- package/dist/plugins/remote-env-host.js +21 -12
- package/dist/plugins/remote-env-k8s.d.ts +40 -5
- package/dist/plugins/remote-env-k8s.js +119 -25
- package/dist/plugins/session-store.d.ts +4 -3
- package/dist/plugins/session-store.js +1 -1
- package/dist/plugins/store-backend.d.ts +12 -5
- package/dist/plugins/store-backend.js +12 -3
- package/dist/plugins/store-contracts.d.ts +3 -0
- package/dist/plugins/store-open-wait.js +3 -0
- package/dist/request-key-closure.d.ts +20 -7
- package/dist/request-key-closure.js +22 -6
- package/dist/run-acceptance.d.ts +51 -0
- package/dist/run-acceptance.js +27 -0
- package/dist/runs.d.ts +7 -11
- package/dist/runs.js +5 -16
- package/dist/sandbox-egress-policy.d.ts +61 -0
- package/dist/sandbox-egress-policy.js +235 -0
- package/dist/session-error-wire.d.ts +2 -2
- package/dist/session-titler.d.ts +5 -3
- package/dist/trace/core-keyset-guard.d.ts +3 -2
- package/dist/trace/ledger-events.d.ts +9 -3
- package/dist/trace/ledger-sink.d.ts +7 -0
- package/dist/trace/ledger-sink.js +14 -4
- package/dist/trace/project.d.ts +49 -8
- package/dist/trace/project.js +39 -7
- package/dist/trace/self-minted-wire.d.ts +19 -29
- package/dist/trace/self-minted-wire.js +2 -9
- package/dist/trace/tool-output-media.d.ts +26 -0
- package/dist/trace/tool-output-media.js +57 -0
- package/dist/trace/wire-projection-faces.d.ts +7 -1
- package/dist/trace/wire-projection-faces.js +2 -1
- package/package.json +3 -3
package/dist/http/routes/runs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { liveToolEndForBacklog } from "../../trace/tool-output-media.js";
|
|
1
2
|
import { redactHead, redactLedgerEventForRead } from "../../observability/run-terminal.js";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import { uuidv7, mintCheckpointToken, CheckpointError, policyCannotAnswer, validatePendingSteer, TASK_OUTPUT_REPLAY_KINDS, isSystemInjectionPriority } from "@sema-agent/core";
|
|
@@ -33,6 +34,7 @@ import { readResumeCtx } from "../resume-ctx-read.js";
|
|
|
33
34
|
import { storedBodyReplayAnswer, storedBodyReplayLogFields } from "../checkpoint-ctx.js";
|
|
34
35
|
import { serviceVersion } from "../../service-version.js";
|
|
35
36
|
import { storedBodyReplayRefusal } from "../stored-body-replay.js";
|
|
37
|
+
import { steerBodyKeyIssue } from "../../request-key-closure.js";
|
|
36
38
|
const STEER_PRIORITY_WORDS = INJECTION_TIERS.map((tier) => `'${tier}'`);
|
|
37
39
|
const STEER_PRIORITY_UNION = STEER_PRIORITY_WORDS.join(" | ");
|
|
38
40
|
const STEER_PRIORITY_CHOICES = STEER_PRIORITY_WORDS.length <= 2 ? STEER_PRIORITY_WORDS.join(" or ") : `${STEER_PRIORITY_WORDS.slice(0, -1).join(", ")}, or ${STEER_PRIORITY_WORDS[STEER_PRIORITY_WORDS.length - 1]}`;
|
|
@@ -413,6 +415,13 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
413
415
|
sendError(res, 400, "request.body_shape", `body must be { text: string (non-empty), mode?: 'all' | 'one-at-a-time', priority?: ${STEER_PRIORITY_UNION} }`);
|
|
414
416
|
return;
|
|
415
417
|
}
|
|
418
|
+
{
|
|
419
|
+
const keyIssue = steerBodyKeyIssue(body, "run");
|
|
420
|
+
if (keyIssue) {
|
|
421
|
+
sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
416
425
|
if (body.mode !== undefined && body.mode !== "all" && body.mode !== "one-at-a-time") {
|
|
417
426
|
sendError(res, 400, "request.field_invalid", "mode must be 'all' or 'one-at-a-time' when present");
|
|
418
427
|
return;
|
|
@@ -737,6 +746,11 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
737
746
|
return;
|
|
738
747
|
}
|
|
739
748
|
text = interruptBody.text;
|
|
749
|
+
const keyIssue = steerBodyKeyIssue(interruptBody, "interrupt");
|
|
750
|
+
if (keyIssue) {
|
|
751
|
+
sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
740
754
|
}
|
|
741
755
|
const interruptIdemRaw = headerStr(req.headers["idempotency-key"]);
|
|
742
756
|
const interruptInputId = interruptIdemRaw ? `idem-${idemKeyOf(interruptIdemRaw, idemCallerOf(req, ctx), "run").slice(0, 32)}` : uuidv7();
|
|
@@ -1040,7 +1054,8 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1040
1054
|
const n = await it.next();
|
|
1041
1055
|
if (n.done || closed || res.writableEnded)
|
|
1042
1056
|
break;
|
|
1043
|
-
|
|
1057
|
+
const frame = n.value.type === "tool_end" ? liveToolEndForBacklog(n.value, res.writableLength) : n.value;
|
|
1058
|
+
res.write(`event: forward\ndata: ${JSON.stringify(frame)}\n\n`);
|
|
1044
1059
|
if (n.value.type === "task_settled")
|
|
1045
1060
|
break;
|
|
1046
1061
|
}
|
|
@@ -1161,6 +1176,13 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1161
1176
|
sendError(res, 400, "request.body_shape", "body must be { content: string (non-empty) }");
|
|
1162
1177
|
return;
|
|
1163
1178
|
}
|
|
1179
|
+
{
|
|
1180
|
+
const keyIssue = steerBodyKeyIssue(body, "content");
|
|
1181
|
+
if (keyIssue) {
|
|
1182
|
+
sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1164
1186
|
if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
|
|
1165
1187
|
sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
|
|
1166
1188
|
return;
|
|
@@ -13,7 +13,8 @@ import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../..
|
|
|
13
13
|
import { buildCancelledLedgerRow, createRunAbort, mintCancelledResult } from "../../run-cancel-context.js";
|
|
14
14
|
import { recordResourceWindow } from "../../resource-window.js";
|
|
15
15
|
import { redactSecrets } from "../../trace/redact.js";
|
|
16
|
-
import {
|
|
16
|
+
import { liveToolEndForBacklog } from "../../trace/tool-output-media.js";
|
|
17
|
+
import { contextUsageEventData, turnEndEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskStartedEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestFaceEventData, toolRosterDeltaEventData, toolProgressEventData, toolDisclosureEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage, textEndEventData, liveIdentityFields } from "../../trace/project.js";
|
|
17
18
|
import { cascadeConfig, runMeta } from "../run-meta.js";
|
|
18
19
|
import { idemCallerOf, inflightKeyOf, submissionIdemOf } from "../idempotency.js";
|
|
19
20
|
import { replayOfExistingRun } from "../submit-replay.js";
|
|
@@ -37,7 +38,8 @@ const DISCONNECT_PARK_GRACE_MS = 10_000;
|
|
|
37
38
|
function sseData(res, payload) {
|
|
38
39
|
const anchor = payload.eventId;
|
|
39
40
|
const idLine = typeof anchor === "string" && anchor.length > 0 ? `id: ${anchor}\n` : "";
|
|
40
|
-
res.
|
|
41
|
+
const frame = payload.type === "tool_end" ? liveToolEndForBacklog(payload, res.writableLength) : payload;
|
|
42
|
+
res.write(`${idLine}data: ${JSON.stringify(frame)}\n\n`);
|
|
41
43
|
}
|
|
42
44
|
async function mintSubmitExitResult(result, checkpoints) {
|
|
43
45
|
return redactTerminalResult(sealResultForExit(await graftParkToolCallId(result, checkpoints)));
|
|
@@ -263,8 +265,6 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
263
265
|
clearTurnActivity(tid);
|
|
264
266
|
if (created.ok)
|
|
265
267
|
recordResourceWindow(tid, prepared.spec.resourceSuspend);
|
|
266
|
-
if (created.ok)
|
|
267
|
-
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
|
|
268
268
|
if (created.existing) {
|
|
269
269
|
const replay = await replayOfExistingRun(created.existing, { principal: submitter, idem }, "sync", deps, deps.runStore);
|
|
270
270
|
sseData(res, { type: "done", result: doneResultOfOutcome(replay), replay: true });
|
|
@@ -390,6 +390,9 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
390
390
|
if (t === "task_progress") {
|
|
391
391
|
sseData(res, { type: "task_progress", ...taskProgressEventData(e) });
|
|
392
392
|
}
|
|
393
|
+
else if (t === "task_started") {
|
|
394
|
+
sseData(res, { type: "task_started", ...taskStartedEventData(e) });
|
|
395
|
+
}
|
|
393
396
|
else if (t === "tool_start") {
|
|
394
397
|
const tsv = e;
|
|
395
398
|
sseData(res, { type: "tool_start", ...toolStartEventData(tsv) });
|
|
@@ -458,6 +461,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
458
461
|
userMsgEntryId = ev.entryId;
|
|
459
462
|
const causeAtDone = ev.type === "done" ? ac.cause() : undefined;
|
|
460
463
|
await deps.userInputLedger?.observeLegEvent(liveStream, ev);
|
|
464
|
+
prepared.acceptance.observe(ev);
|
|
461
465
|
if (ev.type === "done") {
|
|
462
466
|
finalResult = await mintSubmitExitResult(ev.result, deps.checkpointStore);
|
|
463
467
|
if (durableTaskId && finalResult)
|
|
@@ -465,7 +469,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
465
469
|
if (causeAtDone !== undefined && finalResult !== undefined) {
|
|
466
470
|
finalResult = mintCancelledResult({ taskId: durableTaskId ?? "", sessionId: prepared.spec.sessionId ?? "", startedAtMs: streamLegStartedAt, message: "cancelled by user", cause: causeAtDone, engineResult: finalResult });
|
|
467
471
|
}
|
|
468
|
-
finalizeTaskResult(ev.result, principal, prepared.spec.objective
|
|
472
|
+
finalizeTaskResult(ev.result, principal, prepared.spec.objective);
|
|
469
473
|
noteRunOutcome(finalResult);
|
|
470
474
|
putRewindAnchor();
|
|
471
475
|
settleFleet(outwardPlane(finalResult.terminal).status, fleetRunResiduals(finalResult));
|
|
@@ -517,6 +521,9 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
517
521
|
else if (ev.type === "task_progress") {
|
|
518
522
|
sseData(res, { type: "task_progress", ...taskProgressEventData(ev) });
|
|
519
523
|
}
|
|
524
|
+
else if (ev.type === "task_started") {
|
|
525
|
+
sseData(res, { type: "task_started", ...taskStartedEventData(ev) });
|
|
526
|
+
}
|
|
520
527
|
else if (ev.type === "compacted") {
|
|
521
528
|
sseData(res, { type: "compacted", ...compactedEventData(ev) });
|
|
522
529
|
}
|
|
@@ -843,7 +850,6 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
843
850
|
const placementReject = await commitPlacement(deps, deps.runStore, tid, prepared.bindingIntent);
|
|
844
851
|
if (placementReject)
|
|
845
852
|
return { status: placementReject.status, body: devicePlacementErrorBody(placementReject) };
|
|
846
|
-
deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
|
|
847
853
|
durableTaskId = tid;
|
|
848
854
|
}
|
|
849
855
|
commitSessionInheritEnv(deps, prepared.spec.sessionId, prepared.body);
|
|
@@ -886,8 +892,10 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
886
892
|
const cascadeLeg = () => runCascade(prepared.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd));
|
|
887
893
|
const plainLeg = async () => {
|
|
888
894
|
const stream = prepared.runner.runTaskStream(specWithSignal);
|
|
889
|
-
for await (const ev of stream)
|
|
895
|
+
for await (const ev of stream) {
|
|
890
896
|
observeSyncLegEvent(ev);
|
|
897
|
+
prepared.acceptance.observe(ev);
|
|
898
|
+
}
|
|
891
899
|
return stream.result();
|
|
892
900
|
};
|
|
893
901
|
result = await instructionLeg.run(prepared.spec.sessionId, () => withPrincipal(principal, () => prepared.verify ? verifyLeg(prepared.verify) : prepared.cascade ? cascadeLeg() : plainLeg()));
|
|
@@ -934,7 +942,8 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
934
942
|
}
|
|
935
943
|
if (durableTaskId && deps.runStore)
|
|
936
944
|
await deps.runStore.setTerminal(durableTaskId, syncPlane.status, exitResult, syncPlane.errorMessage ?? null);
|
|
937
|
-
|
|
945
|
+
prepared.acceptance.settle(result);
|
|
946
|
+
finalizeTaskResult(result, principal, prepared.spec.objective);
|
|
938
947
|
noteRunOutcome(result);
|
|
939
948
|
return { status: 200, body: withSyncSubmitKeys(buildSyncSubmitBody(exitResult)) };
|
|
940
949
|
};
|
|
@@ -13,6 +13,7 @@ import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
|
13
13
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
14
14
|
import { assertNever } from "../route-ctx.js";
|
|
15
15
|
import * as facts from "../../capability-facts.js";
|
|
16
|
+
import { steerBodyKeyIssue } from "../../request-key-closure.js";
|
|
16
17
|
export async function handleWorkflows(req, res, match, ctx) {
|
|
17
18
|
const { deps } = ctx;
|
|
18
19
|
const { runSessionAcceptOk } = ctx.helpers;
|
|
@@ -188,6 +189,13 @@ export async function handleWorkflowAgentSteer(req, res, match, ctx) {
|
|
|
188
189
|
sendError(res, 400, "request.body_shape", "body must be { content: string (non-empty) }");
|
|
189
190
|
return;
|
|
190
191
|
}
|
|
192
|
+
{
|
|
193
|
+
const keyIssue = steerBodyKeyIssue(body, "content");
|
|
194
|
+
if (keyIssue) {
|
|
195
|
+
sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
191
199
|
if (body.content.length > STEER_IN_MAX_REQUEST_CHARS) {
|
|
192
200
|
sendError(res, 413, "steer.content_too_large", `content exceeds ${STEER_IN_MAX_REQUEST_CHARS} characters (got ${body.content.length}); note only the first ${STEER_IN_MAX_CHARS} are delivered anyway`);
|
|
193
201
|
return;
|
package/dist/http/server.js
CHANGED
|
@@ -5,7 +5,6 @@ import { validatePendingSteer } from "@sema-agent/core";
|
|
|
5
5
|
import {} from "../web-admin-client/facade.js";
|
|
6
6
|
import { HttpError } from "../security.js";
|
|
7
7
|
import {} from "../memory-sync.js";
|
|
8
|
-
import { evictIfConflict } from "../runs.js";
|
|
9
8
|
import {} from "../orchestration/workflow-agent-steer.js";
|
|
10
9
|
import {} from "../orchestration/workflow-completion-inbox.js";
|
|
11
10
|
import {} from "../fleet/fleet-bus.js";
|
|
@@ -162,10 +161,9 @@ export function createHttpServer(rawDeps) {
|
|
|
162
161
|
deps.metrics?.inc("cascade_total", { outcome: cascadeOutcome });
|
|
163
162
|
deps.instrumentDegenerate?.(result);
|
|
164
163
|
}
|
|
165
|
-
function finalizeTaskResult(result, principal, objective
|
|
164
|
+
function finalizeTaskResult(result, principal, objective) {
|
|
166
165
|
recordTaskResult(result);
|
|
167
166
|
deps.planCacheProbe?.record(principal, objective, result.terminal.kind === "completed");
|
|
168
|
-
evictIfConflict(deps.runner, sessionId, result);
|
|
169
167
|
}
|
|
170
168
|
const isRunLiveOnThisReplica = (taskId) => inflightRuns.has(taskId) || steerableRuns.has(taskId);
|
|
171
169
|
return Object.assign(server, { denyExpiredApprovals, isRunLiveOnThisReplica, redeemParkedAsk });
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { composeExecutionLane, assertExecutionLaneInvariants, type ExecutionLane
|
|
|
3
3
|
export { createBrain } from "./brain.js";
|
|
4
4
|
export { createSessionStore } from "./plugins/session-store.js";
|
|
5
5
|
export { createStoreBackend, type StoreBackend, type SessionCaptureRecordSource } from "./plugins/store-backend.js";
|
|
6
|
+
export { RetiredSchemaPresentError } from "./plugins/one-time-migrations.js";
|
|
6
7
|
export { CachingSessionStore, type CachingOptions } from "./plugins/caching-session-store.js";
|
|
7
8
|
export type { RunRecord, RunEvent } from "./plugins/store-contracts.js";
|
|
8
9
|
export { runInBackground } from "./runs.js";
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { composeExecutionLane, assertExecutionLaneInvariants } from "./boot/exec
|
|
|
3
3
|
export { createBrain } from "./brain.js";
|
|
4
4
|
export { createSessionStore } from "./plugins/session-store.js";
|
|
5
5
|
export { createStoreBackend } from "./plugins/store-backend.js";
|
|
6
|
+
export { RetiredSchemaPresentError } from "./plugins/one-time-migrations.js";
|
|
6
7
|
export { CachingSessionStore } from "./plugins/caching-session-store.js";
|
|
7
8
|
export { runInBackground } from "./runs.js";
|
|
8
9
|
export { createHardenedVmRunner, runInHardenedVm } from "./orchestration/hardened-vm-runner.js";
|
package/dist/lsp/manager.js
CHANGED
|
@@ -79,7 +79,7 @@ export class E2bLspManager {
|
|
|
79
79
|
return undefined;
|
|
80
80
|
}
|
|
81
81
|
if (heal)
|
|
82
|
-
await session.warmOpen(heal.carry).catch(() => ({ opened: 0, failed: heal.carry.length }));
|
|
82
|
+
await session.warmOpen(heal.carry).catch(() => ({ opened: 0, failed: heal.carry.length, deferred: 0 }));
|
|
83
83
|
if (sessions.get(language) !== job) {
|
|
84
84
|
await session.close().catch(() => undefined);
|
|
85
85
|
return undefined;
|
|
@@ -35,6 +35,18 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
35
35
|
readonly cls: "F";
|
|
36
36
|
readonly note: "S-297(`http/routes/sessions.ts lastLegMcpFor`,`/mcp` 面板的可选键 `lastLegMcp`):按会话取最近 run 的索引读 / 那条 run 的账本读**抛错或超期** ⇒ 该键缺席,面板本体照常 200。放行的最坏后果=这一屏少一格「上一次真挂上了什么」(壳退回翻账本),不是门、不是权限、不是凭证 —— 而反方向(让附加键的读失败打掉整张面板)会给一口此前零 MCP 配置下根本不碰账本的**既有**读面新加一条挂掉的理由。缺席本就是本键 wire 上的一等状态(消费端必须会读),所以这条兜底不产生任何新的消费端形态;计数让「某台部署的账本一直读不出来」显形。";
|
|
37
37
|
};
|
|
38
|
+
readonly "server.ledger.forward-append-failed": {
|
|
39
|
+
readonly cls: "F";
|
|
40
|
+
readonly note: "durable 腿 forward 道的写失败(`trace/ledger-sink.ts` 的 `createForwardAppendFailedNote`:账本汇 `onForwardEvent` —— bg 腿、sync detach 车道、续跑腿 verify 臂 —— 与续跑腿自己的 forward 回调同一只出口)。forward 道是同步回调里的 fire-and-forget 写(序号同步分配,结构上不能 await),写失败 ⇒ 这一帧(子代的 `task_started` / `task_progress` / 工具卡)永远不在 `GET /v1/runs/:id/events` 的回放面,run 照跑。放行的最坏后果 = 回放面少一帧子代事实(起跑帧每周期一只,丢了就补不回 `agentType` / `rootSessionId` / `startedAt`),不是门、不是凭据、不改任何裁决;不重试 —— 晚到的重试行会落在读方游标之后(单写串行链存在的理由)。计数非零 = 这台部署的账本写口在出错(主流的 awaited 写多半同时在报错),去看库。detail = `type=<帧型> err=<原句,经记录器脱敏>`。";
|
|
41
|
+
};
|
|
42
|
+
readonly "server.tool-end-media.live-backlog-degraded": {
|
|
43
|
+
readonly cls: "F";
|
|
44
|
+
readonly note: "core 7.50.0 媒体块提货(`trace/tool-output-media.ts` 的 `liveToolEndForBacklog`,三处活面写口共用的那一只判据 —— 同步流出口 `sseData`、子代 tail 路由写环、tail 总线每订阅者队列):本帧媒体字节 + 这条连接 / 这个订阅者还没送出去的字节 > 上界(64 MiB)⇒ 本帧媒体块降成持久面那只描述符(`{ type, mimeType?, bytes, truncated: true }`,帧级 `truncated` + `totalChars`)。每降一帧记一次,只在这只函数里记(三处写口不各记)。放行的最坏后果 = 一个读得慢的观众在 SSE 上看不到这张图,只见「这里有一张多大的什么图、没送达」(账本面本就恒是描述符),不是门、不是凭据、不改任何裁决;反方向(不降)让一条停读的连接随帧数线性压住几百 MiB。帧上它与引擎自己的媒体预算降级同形不可分,所以计数是运维唯一能看出「多少观众丢了图」的位;计数持续非零 = 有消费端读流太慢(或网络瓶颈),去看那几条连接。detail = `backlog=<待发字节> media=<本帧媒体字节>`。";
|
|
45
|
+
};
|
|
46
|
+
readonly "server.memory-consolidation.operator-valve-unjudged": {
|
|
47
|
+
readonly cls: "P-DEBT";
|
|
48
|
+
readonly note: "core 7.50.0 #1653 提货(`boot/memory-consolidation.ts` `createOperatorValveWriteJudge`):运维手动整理阀门这条腿的写判官座答 `no_objection` —— 这条腿没有腿(无名册写脸、无引擎盖的豁免根、无人的持久规则车道),引擎包不导出它那只合成写判官的构造器,本仓不手搭第二只。每被问一次(早问库目录一次 + 每个产物一次)记一次。放行的最坏后果 = 运维手动整理时,一条点名记忆库路径的部署写策略或人的持久规则拦不住这一轮折叠(与引擎 7.49.x 逐字同:那时驱动根本不问);自动整理腿走引擎腿自己的判官,不在本 tag 射程。收口 = 引擎导出宿主可用的写判官构造器,换上即删本 tag。";
|
|
49
|
+
};
|
|
38
50
|
readonly "server.workflow-park-index.sync-failed": {
|
|
39
51
|
readonly cls: "P-DEBT";
|
|
40
52
|
readonly note: "S-185 车CM(codex r1-F2 [high],验真后登记):workflow 出身 park 的 join 索引(`workflow_agent_session` / local 账本)在**一次成功的 run 写之后**同步失败 ⇒ 该次同步被吞,重试要等这条 run 的**下一次**写。而 park 恰好常常是这条 run 的**最后**一次写(park = run 终局 `failed`)⇒ 那条 park 可能永久 join 不到。**多数情形**是诚实 miss:这张卡没有别的指针 ⇒ `/decide` 结构性退回本批之前的行为(409 `conflict.resume_context_unavailable`),不误配、不伪造、不放行,丢的是「人能不能把这条批准送出去」这个能力。**7.118.0 起不止于此**:路由把指针候选当持有者的全集读,并有分叉门(两条同代次持有者 ⇒ 409 `decide.workflow_park_forked`);同一张卡上一枚**竞争**指针没写上(本臂的写失败,或写记录与写指针之间进程崩溃 —— 后者不经本臂、不计数)⇒ 分叉被判成唯一持有者 ⇒ 决定投给幸存的那一支(它确实持有这张卡,但另一支同样站在上面,引擎把这种形交给消费方拒)。归 P 类的理由:缺指针决定**车道选择**(缺行 ⇒ 选中 legacy 腿),且能让分叉门失效;归 DEBT 的理由是真正的解要一条**按子代 checkpoint 键的耐久决议回执 + 重启期回填**(与 codex r1-F3 同一件欠账,另车),分叉门失效那一形另有设计票。计数非零 = 有 park 可能决不掉、或这张卡的分叉门可能失效,运维要按 detail 里的 runId 去查那条 run 的续跑链。";
|
|
@@ -431,6 +443,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
431
443
|
readonly cls: "F";
|
|
432
444
|
readonly note: "k8s 车道工作区空间结束时对**快照对象**的回收(`plugins/remote-env-k8s.ts` `createSpaceReclaimer` 的对象部分;与上一条同一只回收座、两条腿:会话删除协调器与托管留存 sweep):按空间的 `<前缀>/<化身>/space/` 分区(快照 + `latest`)和该会话停驻车道里名字带被结束住客归属记号的 `<前缀>/<会话 id>/*.tar.gz` 分别 LIST、逐个预签 DELETE;LIST 失败、DELETE 失败(HTTP 非 2xx / 非 404、网络)LIST 超过 1000 个对象被截断,或回收座一次调用的绝对截止(= 快照时限;卷回收也在内)到点〔切断在途请求、停掉余下的;这次调用里还没轮到的空间零请求、各记一次;没删的归生命周期规则〕,各记一次(detail `lane=space|park stage=list|delete|budget`;会话删除口把回收座脱离应答路径,座若抛了——契约禁止——记 `stage=seat_threw`)⇒ 这些对象留在桶里,会话照删(不挡删除)。放行的最坏后果 = 一份已无人能读的工作区快照留在桶里(会话已删,经 API 不可达;内容是用户工作区,所以不是「只占盘」—— 留到桶的生命周期规则把它过期掉为止);不碰任何别的空间、不是门不是凭据;反方向(清不掉对象就拒删会话)会让一个对象存储抖动挡住用户的删除权。故 F 类,但必须留痕:静默吞掉之后「这个会话删了、它的工作区内容还在不在桶里」在遥测里没有任何痕迹。同拍一行 `workspace_space_reclaim_failed`(`space` 前缀 / `sessionId` / `lane` / `stage` / `error`)。计数非零 = 有对象没回收掉,运维按行里的空间前缀 / 会话 id 去桶里手清,并核对象存储的可达性与 DELETE 权限。回收与同一空间的尚在途的 run 末提交不是互斥的:会话删除时没有活 run(409 挡),但上一条 run 的提交若恰在删除之后才落对象,这批对象不在本次回收射程内(同样归桶的生命周期规则)。停驻分区按会话 id 分区,所以只删名字带被结束住客归属记号的对象(不读任何钟)—— 会话 id 删后被别人重新登记,新住客停驻出的快照带它自己的记号、不碰;不带记号的旧形与 `none` 留给桶的生命周期规则。";
|
|
433
445
|
};
|
|
446
|
+
readonly "server.sandbox-egress.policy-gap": {
|
|
447
|
+
readonly cls: "P-DEBT";
|
|
448
|
+
readonly note: "k8s 车道沙箱出网的起服自检(`plugins/remote-env-k8s.ts` `checkSandboxEgressPolicy` 读节点表与沙箱 ns 的 NetworkPolicy,判法 `sandbox-egress-policy.ts` `sandboxEgressGaps`;起服一次、不挡起服)读出一个缺口 —— 某节点的 InternalIP / ExternalIP 落在一条放行 ipBlock 内(`kind=node_address_open`)、没有策略以 Egress 选中沙箱 pod(`kind=policy_absent`)、pod 保留 IPv6 而策略层担保不了 v6(`kind=ipv6_enabled`)、节点表或策略表读不到(`kind=unreadable what=nodes|networkpolicies`)⇒ 车道**照常起**,每个缺口一行 `sandbox_egress_policy_gap`(带后果句与去哪改)。归 P 类:出网策略是保护面,缺口意味着沙箱里的代码可能够到节点上的服务;归 DEBT 的理由:修法都在集群配置里(节点地址点名、kubelet 放行 v6 sysctl、CNI 的 v6 执行、RBAC),本服务改不了,而拒起车道会让缺省单节点装法(恒有 `ipv6_enabled`)升级即停摆沙箱执行 —— 车道自己的隔离(VM 边界、零 SA token)仍在。放行的最坏后果 = 读出缺口的那一面照旧可达,直到运维按行里的后果句改配置。计数非零 = 这台部署的沙箱出网面有读得出的缺口。读不出的面(宿主 DNAT 发布端口、「pod 所在节点恒放行」的实现差异、CNI 根本不执行策略)不在本 tag 的射程里。";
|
|
449
|
+
};
|
|
434
450
|
readonly "server.fleet.announce-reply-unreadable": {
|
|
435
451
|
readonly cls: "P-DEBT";
|
|
436
452
|
readonly note: "7.121.0 修复验证轮(`fleet-client.ts` 的公告环,fleet 成员每一拍心跳 2xx 答复;detail = `why=<body|not-an-object|invalid-capabilities> leaseState=<此刻的租约态>`):答复不是能力读数(体读不出 / 不是对象 / `capabilities` 键在场而形坏 —— settings-schema 9.0.0 `readCapabilityReading` 的 `unreadable`,外加本仓的 `body`)⇒ 当作没有这一拍答复,租约的状态与钟原样。**一律**计数(不论租约生效与否):租约还没生效(`leaseState=none`)时这只成员**一直不受围栏**,而修前这一形零计数(中心每拍答 `<html>` ⇒ 成员恒 none、零痕迹);租约已生效时计数说明中心的答复在坏、围栏靠的是上一次读数。归 P 类:围栏轴;归 DEBT:本进程判不出中心为什么答坏体,收口条件 = 中心契约 / 监控(中心侧对心跳答复形的执法)落地。每段连拍另有一行 warn `fleet_announce_reply_unreadable`(带 `why`)。";
|
|
@@ -16,6 +16,18 @@ export const FAIL_OPEN_TAGS = {
|
|
|
16
16
|
cls: "F",
|
|
17
17
|
note: "S-297(`http/routes/sessions.ts lastLegMcpFor`,`/mcp` 面板的可选键 `lastLegMcp`):按会话取最近 run 的索引读 / 那条 run 的账本读**抛错或超期** ⇒ 该键缺席,面板本体照常 200。放行的最坏后果=这一屏少一格「上一次真挂上了什么」(壳退回翻账本),不是门、不是权限、不是凭证 —— 而反方向(让附加键的读失败打掉整张面板)会给一口此前零 MCP 配置下根本不碰账本的**既有**读面新加一条挂掉的理由。缺席本就是本键 wire 上的一等状态(消费端必须会读),所以这条兜底不产生任何新的消费端形态;计数让「某台部署的账本一直读不出来」显形。",
|
|
18
18
|
},
|
|
19
|
+
"server.ledger.forward-append-failed": {
|
|
20
|
+
cls: "F",
|
|
21
|
+
note: "durable 腿 forward 道的写失败(`trace/ledger-sink.ts` 的 `createForwardAppendFailedNote`:账本汇 `onForwardEvent` —— bg 腿、sync detach 车道、续跑腿 verify 臂 —— 与续跑腿自己的 forward 回调同一只出口)。forward 道是同步回调里的 fire-and-forget 写(序号同步分配,结构上不能 await),写失败 ⇒ 这一帧(子代的 `task_started` / `task_progress` / 工具卡)永远不在 `GET /v1/runs/:id/events` 的回放面,run 照跑。放行的最坏后果 = 回放面少一帧子代事实(起跑帧每周期一只,丢了就补不回 `agentType` / `rootSessionId` / `startedAt`),不是门、不是凭据、不改任何裁决;不重试 —— 晚到的重试行会落在读方游标之后(单写串行链存在的理由)。计数非零 = 这台部署的账本写口在出错(主流的 awaited 写多半同时在报错),去看库。detail = `type=<帧型> err=<原句,经记录器脱敏>`。",
|
|
22
|
+
},
|
|
23
|
+
"server.tool-end-media.live-backlog-degraded": {
|
|
24
|
+
cls: "F",
|
|
25
|
+
note: "core 7.50.0 媒体块提货(`trace/tool-output-media.ts` 的 `liveToolEndForBacklog`,三处活面写口共用的那一只判据 —— 同步流出口 `sseData`、子代 tail 路由写环、tail 总线每订阅者队列):本帧媒体字节 + 这条连接 / 这个订阅者还没送出去的字节 > 上界(64 MiB)⇒ 本帧媒体块降成持久面那只描述符(`{ type, mimeType?, bytes, truncated: true }`,帧级 `truncated` + `totalChars`)。每降一帧记一次,只在这只函数里记(三处写口不各记)。放行的最坏后果 = 一个读得慢的观众在 SSE 上看不到这张图,只见「这里有一张多大的什么图、没送达」(账本面本就恒是描述符),不是门、不是凭据、不改任何裁决;反方向(不降)让一条停读的连接随帧数线性压住几百 MiB。帧上它与引擎自己的媒体预算降级同形不可分,所以计数是运维唯一能看出「多少观众丢了图」的位;计数持续非零 = 有消费端读流太慢(或网络瓶颈),去看那几条连接。detail = `backlog=<待发字节> media=<本帧媒体字节>`。",
|
|
26
|
+
},
|
|
27
|
+
"server.memory-consolidation.operator-valve-unjudged": {
|
|
28
|
+
cls: "P-DEBT",
|
|
29
|
+
note: "core 7.50.0 #1653 提货(`boot/memory-consolidation.ts` `createOperatorValveWriteJudge`):运维手动整理阀门这条腿的写判官座答 `no_objection` —— 这条腿没有腿(无名册写脸、无引擎盖的豁免根、无人的持久规则车道),引擎包不导出它那只合成写判官的构造器,本仓不手搭第二只。每被问一次(早问库目录一次 + 每个产物一次)记一次。放行的最坏后果 = 运维手动整理时,一条点名记忆库路径的部署写策略或人的持久规则拦不住这一轮折叠(与引擎 7.49.x 逐字同:那时驱动根本不问);自动整理腿走引擎腿自己的判官,不在本 tag 射程。收口 = 引擎导出宿主可用的写判官构造器,换上即删本 tag。",
|
|
30
|
+
},
|
|
19
31
|
"server.workflow-park-index.sync-failed": {
|
|
20
32
|
cls: "P-DEBT",
|
|
21
33
|
note: "S-185 车CM(codex r1-F2 [high],验真后登记):workflow 出身 park 的 join 索引(`workflow_agent_session` / local 账本)在**一次成功的 run 写之后**同步失败 ⇒ 该次同步被吞,重试要等这条 run 的**下一次**写。而 park 恰好常常是这条 run 的**最后**一次写(park = run 终局 `failed`)⇒ 那条 park 可能永久 join 不到。**多数情形**是诚实 miss:这张卡没有别的指针 ⇒ `/decide` 结构性退回本批之前的行为(409 `conflict.resume_context_unavailable`),不误配、不伪造、不放行,丢的是「人能不能把这条批准送出去」这个能力。**7.118.0 起不止于此**:路由把指针候选当持有者的全集读,并有分叉门(两条同代次持有者 ⇒ 409 `decide.workflow_park_forked`);同一张卡上一枚**竞争**指针没写上(本臂的写失败,或写记录与写指针之间进程崩溃 —— 后者不经本臂、不计数)⇒ 分叉被判成唯一持有者 ⇒ 决定投给幸存的那一支(它确实持有这张卡,但另一支同样站在上面,引擎把这种形交给消费方拒)。归 P 类的理由:缺指针决定**车道选择**(缺行 ⇒ 选中 legacy 腿),且能让分叉门失效;归 DEBT 的理由是真正的解要一条**按子代 checkpoint 键的耐久决议回执 + 重启期回填**(与 codex r1-F3 同一件欠账,另车),分叉门失效那一形另有设计票。计数非零 = 有 park 可能决不掉、或这张卡的分叉门可能失效,运维要按 detail 里的 runId 去查那条 run 的续跑链。",
|
|
@@ -391,6 +403,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
391
403
|
cls: "F",
|
|
392
404
|
note: "k8s 车道工作区空间结束时对**快照对象**的回收(`plugins/remote-env-k8s.ts` `createSpaceReclaimer` 的对象部分;与上一条同一只回收座、两条腿:会话删除协调器与托管留存 sweep):按空间的 `<前缀>/<化身>/space/` 分区(快照 + `latest`)和该会话停驻车道里名字带被结束住客归属记号的 `<前缀>/<会话 id>/*.tar.gz` 分别 LIST、逐个预签 DELETE;LIST 失败、DELETE 失败(HTTP 非 2xx / 非 404、网络)LIST 超过 1000 个对象被截断,或回收座一次调用的绝对截止(= 快照时限;卷回收也在内)到点〔切断在途请求、停掉余下的;这次调用里还没轮到的空间零请求、各记一次;没删的归生命周期规则〕,各记一次(detail `lane=space|park stage=list|delete|budget`;会话删除口把回收座脱离应答路径,座若抛了——契约禁止——记 `stage=seat_threw`)⇒ 这些对象留在桶里,会话照删(不挡删除)。放行的最坏后果 = 一份已无人能读的工作区快照留在桶里(会话已删,经 API 不可达;内容是用户工作区,所以不是「只占盘」—— 留到桶的生命周期规则把它过期掉为止);不碰任何别的空间、不是门不是凭据;反方向(清不掉对象就拒删会话)会让一个对象存储抖动挡住用户的删除权。故 F 类,但必须留痕:静默吞掉之后「这个会话删了、它的工作区内容还在不在桶里」在遥测里没有任何痕迹。同拍一行 `workspace_space_reclaim_failed`(`space` 前缀 / `sessionId` / `lane` / `stage` / `error`)。计数非零 = 有对象没回收掉,运维按行里的空间前缀 / 会话 id 去桶里手清,并核对象存储的可达性与 DELETE 权限。回收与同一空间的尚在途的 run 末提交不是互斥的:会话删除时没有活 run(409 挡),但上一条 run 的提交若恰在删除之后才落对象,这批对象不在本次回收射程内(同样归桶的生命周期规则)。停驻分区按会话 id 分区,所以只删名字带被结束住客归属记号的对象(不读任何钟)—— 会话 id 删后被别人重新登记,新住客停驻出的快照带它自己的记号、不碰;不带记号的旧形与 `none` 留给桶的生命周期规则。",
|
|
393
405
|
},
|
|
406
|
+
"server.sandbox-egress.policy-gap": {
|
|
407
|
+
cls: "P-DEBT",
|
|
408
|
+
note: "k8s 车道沙箱出网的起服自检(`plugins/remote-env-k8s.ts` `checkSandboxEgressPolicy` 读节点表与沙箱 ns 的 NetworkPolicy,判法 `sandbox-egress-policy.ts` `sandboxEgressGaps`;起服一次、不挡起服)读出一个缺口 —— 某节点的 InternalIP / ExternalIP 落在一条放行 ipBlock 内(`kind=node_address_open`)、没有策略以 Egress 选中沙箱 pod(`kind=policy_absent`)、pod 保留 IPv6 而策略层担保不了 v6(`kind=ipv6_enabled`)、节点表或策略表读不到(`kind=unreadable what=nodes|networkpolicies`)⇒ 车道**照常起**,每个缺口一行 `sandbox_egress_policy_gap`(带后果句与去哪改)。归 P 类:出网策略是保护面,缺口意味着沙箱里的代码可能够到节点上的服务;归 DEBT 的理由:修法都在集群配置里(节点地址点名、kubelet 放行 v6 sysctl、CNI 的 v6 执行、RBAC),本服务改不了,而拒起车道会让缺省单节点装法(恒有 `ipv6_enabled`)升级即停摆沙箱执行 —— 车道自己的隔离(VM 边界、零 SA token)仍在。放行的最坏后果 = 读出缺口的那一面照旧可达,直到运维按行里的后果句改配置。计数非零 = 这台部署的沙箱出网面有读得出的缺口。读不出的面(宿主 DNAT 发布端口、「pod 所在节点恒放行」的实现差异、CNI 根本不执行策略)不在本 tag 的射程里。",
|
|
409
|
+
},
|
|
394
410
|
"server.fleet.announce-reply-unreadable": {
|
|
395
411
|
cls: "P-DEBT",
|
|
396
412
|
note: "7.121.0 修复验证轮(`fleet-client.ts` 的公告环,fleet 成员每一拍心跳 2xx 答复;detail = `why=<body|not-an-object|invalid-capabilities> leaseState=<此刻的租约态>`):答复不是能力读数(体读不出 / 不是对象 / `capabilities` 键在场而形坏 —— settings-schema 9.0.0 `readCapabilityReading` 的 `unreadable`,外加本仓的 `body`)⇒ 当作没有这一拍答复,租约的状态与钟原样。**一律**计数(不论租约生效与否):租约还没生效(`leaseState=none`)时这只成员**一直不受围栏**,而修前这一形零计数(中心每拍答 `<html>` ⇒ 成员恒 none、零痕迹);租约已生效时计数说明中心的答复在坏、围栏靠的是上一次读数。归 P 类:围栏轴;归 DEBT:本进程判不出中心为什么答坏体,收口条件 = 中心契约 / 监控(中心侧对心跳答复形的执法)落地。每段连拍另有一行 warn `fleet_announce_reply_unreadable`(带 `why`)。",
|
|
@@ -280,7 +280,7 @@ export function createMetrics() {
|
|
|
280
280
|
m.counter("memory_dedup_folded_total", "Memory writes folded into an existing near-duplicate row (LOW)");
|
|
281
281
|
m.counter("memory_dedup_probe_failed_total", "Memory dedup KNN probes that failed and fell through to INSERT (LOW)");
|
|
282
282
|
m.counter("redactions_applied_total", "redactSecrets rewrites (S24), by pattern label and `arm` (S-88: full vs prefix-fragment arm — which RULE fired, NOT proof the text was upstream-truncated)");
|
|
283
|
-
m.counter("
|
|
283
|
+
m.counter("session_cache_stale_hit_total", "Warm session-cache hits dropped because the store had moved past the cached leaf (written by another replica, or gone); the session was re-woken from the store");
|
|
284
284
|
m.counter("breaker_writethrough_failed_total", "Circuit-breaker cross-replica write-through failures (LOW), by backend");
|
|
285
285
|
m.counter("web_search_bad_payload_total", "Web-search provider responses whose results field was not an array (LOW), by provider");
|
|
286
286
|
m.counter("auto_mode_classifier_deny_cause_total", "Auto-mode gate denials where the classifier did NOT rule, by the FORM of the deny (core CLASSIFIER_DENY_CAUSES on GateDisposition.cause: 'unavailable' = the classifier could not run at all (threw/rejected/ran past the cap — retryable), 'parse_error' = it ran but answered outside its verdict contract (blocked for safety); 'other' = this build cannot name the word). 🔴 BREAKING in server 7.72.0: this counter was named auto_mode_classifier_unavailable_total through 7.71.0 and is RENAMED with NO alias (a counter that also counts parse_error is not an 'unavailable' counter); its label vocabulary was 'error'/'timeout' (WHY it was unavailable) — those two collapsed into 'unavailable' (the WHY now rides the deny sentence and the auto_mode.classified trace frame), and 'parse_error' is a bucket the old counter could not see, so by-cause series do NOT splice across the upgrade. Distinct from permission_denied_total{deniedBy=\"classifier\"}, which also counts genuine safety blocks (the classifier's own <block> ruling carries NO cause) — a spike here means the classifier itself is not working (gateway/credential fault, too slow, or answering off-contract), not that the model kept asking for dangerous things");
|
|
@@ -6,6 +6,7 @@ import { isUnsuccessfulRunStatus } from "../plugins/store-contracts.js";
|
|
|
6
6
|
import { sealResultForExit } from "../terminal.js";
|
|
7
7
|
import { errorText } from "./err-text.js";
|
|
8
8
|
import { recordFailOpen } from "./fail-open.js";
|
|
9
|
+
import { toolEndMediaAsDescriptors } from "../trace/tool-output-media.js";
|
|
9
10
|
const MESSAGE_CAP = 1024;
|
|
10
11
|
export function runTerminalLogLevel(status, errorCode) {
|
|
11
12
|
if (!isUnsuccessfulRunStatus(status))
|
|
@@ -451,6 +452,8 @@ export function redactLedgerEventData(type, data) {
|
|
|
451
452
|
}
|
|
452
453
|
if (type === "turn_end")
|
|
453
454
|
return shapeEchoMembers(data, TURN_END_ECHO_TREATMENT, "turn_end");
|
|
455
|
+
if (type === "tool_end")
|
|
456
|
+
return toolEndMediaAsDescriptors(data);
|
|
454
457
|
if (type === "task_notification") {
|
|
455
458
|
const af = data.apiFailure;
|
|
456
459
|
if (af === undefined)
|
|
@@ -8,24 +8,36 @@ export interface CachingOptions {
|
|
|
8
8
|
/** Idle seconds a woken session is kept warm. 0 disables caching (pure passthrough). */
|
|
9
9
|
ttlSec: number;
|
|
10
10
|
sweepIntervalMs?: number;
|
|
11
|
-
/**
|
|
12
|
-
* the
|
|
13
|
-
|
|
11
|
+
/** Called once per warm hit that turned out stale (the store's head no longer names the cached leaf, or the session is
|
|
12
|
+
* gone): the entry was dropped and the session woken from the store again. Not an error — the operator signal for
|
|
13
|
+
* "requests for one session alternate between replicas within the TTL" (the hit rate affinity would buy back). */
|
|
14
|
+
onStaleHit?: (sessionId: string) => void;
|
|
14
15
|
}
|
|
16
|
+
/** The store's current head for one session — the one read every warm hit is checked against (`undefined` = no such
|
|
17
|
+
* session). The same single-row read `OwnerAwareSessionStore.getHead` names; this cache requires its inner store to have it. */
|
|
18
|
+
export type SessionHeadReader = Required<Pick<OwnerAwareSessionStore, "getHead">>;
|
|
15
19
|
/**
|
|
16
|
-
* Warm
|
|
20
|
+
* Warm cache in front of a durable {@link SessionStore}.
|
|
17
21
|
*
|
|
18
|
-
* Keeps a woken session in memory and **reuses the same storage instance across turns**, so
|
|
19
|
-
*
|
|
20
|
-
* active). Idle sessions expire after `ttlSec`; `release` evicts immediately.
|
|
22
|
+
* Keeps a woken session in memory and **reuses the same storage instance across turns**, so an active session is not
|
|
23
|
+
* woken from the store (meta row + event log) on every short turn. Idle sessions expire after `ttlSec`; `release` evicts.
|
|
21
24
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* **One rule: a warm entry is served only while the store's head for that session still names the leaf the entry
|
|
26
|
+
* holds.** Every hit reads the head first (`getHead` — one primary-key row read on the SQL twins); head gone or leaf
|
|
27
|
+
* moved ⇒ the entry is dropped and the session is woken from the store again ({@link CachingOptions.onStaleHit}). The
|
|
28
|
+
* leaf is the token because it is exactly what the stores' conditional writes compare (`expectedLeafId`): a handle this
|
|
29
|
+
* cache hands out is never behind the store's leaf, so those writes fail only on a genuinely concurrent write — two runs
|
|
30
|
+
* writing one session at the same time. Any number of replicas may share one store under any routing; session affinity
|
|
31
|
+
* only raises the hit rate.
|
|
32
|
+
*
|
|
33
|
+
* Open item (the same before this rule): a write that rewrites history while leaving the final leaf id where it was —
|
|
34
|
+
* a session-sync replace / `overwrite-dst` staged commit on ANOTHER replica that changes entry payloads under the same
|
|
35
|
+
* ids — moves no leaf, so a warm entry elsewhere keeps its old payloads until it expires or is evicted. Its conditional
|
|
36
|
+
* writes still succeed (the leaf matches); what is stale is the content of the entries under it. Closing it needs a
|
|
37
|
+
* store revision bumped by every write path (append, leaf move, replace, staged commit) and carried by the woken handle.
|
|
38
|
+
*
|
|
39
|
+
* Writes that go around the cached handle through THIS wrapper (import / replace / staged commit / delete) also drop
|
|
40
|
+
* the entry at the write site, so the next acquire wakes from the written state.
|
|
29
41
|
*
|
|
30
42
|
* Orthogonal to the F3 branch-floor change: floor bounds the *cold wake* size; this cache removes
|
|
31
43
|
* *repeated* re-reads of an active session.
|
|
@@ -36,6 +48,7 @@ export declare class CachingSessionStore implements SessionStore {
|
|
|
36
48
|
private readonly pending;
|
|
37
49
|
private readonly ttlMs;
|
|
38
50
|
private readonly timer?;
|
|
51
|
+
private readonly onStaleHit?;
|
|
39
52
|
/** Delegated tenant-ownership ops, present only if the inner store exposes them (the TiDB store does). */
|
|
40
53
|
readonly ownerOf?: (sessionId: string) => Promise<string | null | undefined>;
|
|
41
54
|
/** S-790:属主 + 化身一次读,纯转发(不经暖缓存 —— 化身判的就是「此刻坐在座位上的是谁」,缓存的旧答案正是要消灭的那一形)。
|
|
@@ -76,11 +89,8 @@ export declare class CachingSessionStore implements SessionStore {
|
|
|
76
89
|
/** E18 — delegate the leaf read straight to inner (a cache-bypassing single SELECT; the resume-at turn-capture
|
|
77
90
|
* reads the DURABLE leaf, not a possibly-stale cached view — same discipline as the §0.5 repo ops above). */
|
|
78
91
|
readonly getLeafId?: (sessionId: string) => Promise<string | null>;
|
|
79
|
-
/** [ref] 六轮复审:owner+leafId 原子快照透传(cache-bypassing,getLeafId 同姿势)。 */
|
|
80
|
-
readonly getHead
|
|
81
|
-
owner: string | null;
|
|
82
|
-
leafId: string | null;
|
|
83
|
-
} | undefined>;
|
|
92
|
+
/** [ref] 六轮复审:owner+leafId 原子快照透传(cache-bypassing,getLeafId 同姿势)。内层店必有(本类的暖命中按它核新鲜)。 */
|
|
93
|
+
readonly getHead: SessionHeadReader["getHead"];
|
|
84
94
|
/** 2c session-sync — delegated export/import seam, DURABLE (bypasses the warm cache exactly like
|
|
85
95
|
* list/fork: an export reads the full durable log, an import writes durable state). */
|
|
86
96
|
readonly exportEntries?: (sessionId: string) => Promise<SessionTreeEntry[] | null>;
|
|
@@ -99,7 +109,7 @@ export declare class CachingSessionStore implements SessionStore {
|
|
|
99
109
|
* 转发白名单里 ⇒ 默认部署(SESSION_CACHE_TTL_SEC=300 把真店包进本类)PUSH Phase A 恒 501
|
|
100
110
|
* `capability.session_store_required`,而 capabilities().sessionSync 谓词三项(backend/exportEntries/
|
|
101
111
|
* fileHistoryStore)都被转发了 ⇒ 照报 true(能力面谎)。转发形:handle 的 `commit` 是唯一写真 id 的
|
|
102
|
-
* 时刻,成功后逐出该 id 的暖 handle
|
|
112
|
+
* 时刻,成功后逐出该 id 的暖 handle(importEntries/replaceEntries 同纪律);`abort` 只丢
|
|
103
113
|
* staging 行、不碰真 id ⇒ 不逐出。 */
|
|
104
114
|
readonly beginImportStaging?: (realSessionId: string, token: string) => StagingHandle;
|
|
105
115
|
/** Staging 清扫面(kernel `ImportStagingStore.sweepStagingSessions`)。当下 reaper 走 `backend.session()`
|
|
@@ -136,11 +146,18 @@ export declare class CachingSessionStore implements SessionStore {
|
|
|
136
146
|
scope?: string;
|
|
137
147
|
}) => Promise<PlacedSessionRow[]>;
|
|
138
148
|
readonly placementOf?: (sessionId: string) => Promise<SessionPlacementRecord | undefined>;
|
|
139
|
-
constructor(inner: SessionStore, opts: CachingOptions);
|
|
149
|
+
constructor(inner: SessionStore & SessionHeadReader, opts: CachingOptions);
|
|
140
150
|
acquire(sessionId?: string, opts?: PlacementAcquireOpts): Promise<AcquiredSession>;
|
|
141
151
|
touch(sessionId: string): Promise<void>;
|
|
142
152
|
release(sessionId: string): Promise<void>;
|
|
143
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* The cache's one rule: the warm entry for `sessionId`, if the store's head still names the leaf it holds; otherwise
|
|
155
|
+
* the entry is dropped (counted once via `onStaleHit`) and `undefined` sends the caller to the store. The head is read
|
|
156
|
+
* first, the cached leaf second: a write this replica commits in between can only turn a fresh verdict into a stale
|
|
157
|
+
* one (an extra wake), never the reverse.
|
|
158
|
+
*/
|
|
159
|
+
private freshHit;
|
|
160
|
+
/** Drop a session from the warm cache. */
|
|
144
161
|
evict(sessionId: string): void;
|
|
145
162
|
get size(): number;
|
|
146
163
|
dispose(): void;
|
|
@@ -5,6 +5,7 @@ export class CachingSessionStore {
|
|
|
5
5
|
pending = new Map();
|
|
6
6
|
ttlMs;
|
|
7
7
|
timer;
|
|
8
|
+
onStaleHit;
|
|
8
9
|
ownerOf;
|
|
9
10
|
sessionFaceOf;
|
|
10
11
|
incarnationOf;
|
|
@@ -35,6 +36,8 @@ export class CachingSessionStore {
|
|
|
35
36
|
constructor(inner, opts) {
|
|
36
37
|
this.inner = inner;
|
|
37
38
|
this.ttlMs = Math.max(0, opts.ttlSec) * 1000;
|
|
39
|
+
this.onStaleHit = opts.onStaleHit;
|
|
40
|
+
this.getHead = inner.getHead.bind(inner);
|
|
38
41
|
const declared = inner.retention;
|
|
39
42
|
if (declared !== undefined)
|
|
40
43
|
this.retention = declared;
|
|
@@ -72,8 +75,6 @@ export class CachingSessionStore {
|
|
|
72
75
|
this.fork = ownerAware.fork.bind(inner);
|
|
73
76
|
if (ownerAware.getLeafId)
|
|
74
77
|
this.getLeafId = ownerAware.getLeafId.bind(inner);
|
|
75
|
-
if (ownerAware.getHead)
|
|
76
|
-
this.getHead = ownerAware.getHead.bind(inner);
|
|
77
78
|
if (ownerAware.exportEntries)
|
|
78
79
|
this.exportEntries = ownerAware.exportEntries.bind(inner);
|
|
79
80
|
if (ownerAware.listEntryIds)
|
|
@@ -88,8 +89,6 @@ export class CachingSessionStore {
|
|
|
88
89
|
const imp = ownerAware.importEntries.bind(inner);
|
|
89
90
|
this.importEntries = async (sessionId, owner, entries) => {
|
|
90
91
|
await imp(sessionId, owner, entries);
|
|
91
|
-
if (this.cache.has(sessionId))
|
|
92
|
-
opts.onConflictEvict?.(sessionId);
|
|
93
92
|
this.evict(sessionId);
|
|
94
93
|
};
|
|
95
94
|
}
|
|
@@ -97,8 +96,6 @@ export class CachingSessionStore {
|
|
|
97
96
|
const rep = ownerAware.replaceEntries.bind(inner);
|
|
98
97
|
this.replaceEntries = async (sessionId, owner, entries) => {
|
|
99
98
|
await rep(sessionId, owner, entries);
|
|
100
|
-
if (this.cache.has(sessionId))
|
|
101
|
-
opts.onConflictEvict?.(sessionId);
|
|
102
99
|
this.evict(sessionId);
|
|
103
100
|
};
|
|
104
101
|
}
|
|
@@ -112,8 +109,6 @@ export class CachingSessionStore {
|
|
|
112
109
|
appendBatch: (entries) => h.appendBatch(entries),
|
|
113
110
|
commit: async (owner, o) => {
|
|
114
111
|
const r = await h.commit(owner, o);
|
|
115
|
-
if (this.cache.has(realSessionId))
|
|
116
|
-
opts.onConflictEvict?.(realSessionId);
|
|
117
112
|
this.evict(realSessionId);
|
|
118
113
|
return r;
|
|
119
114
|
},
|
|
@@ -144,7 +139,7 @@ export class CachingSessionStore {
|
|
|
144
139
|
this.store(fresh);
|
|
145
140
|
return fresh;
|
|
146
141
|
}
|
|
147
|
-
const hit = this.
|
|
142
|
+
const hit = await this.freshHit(sessionId);
|
|
148
143
|
if (hit && opts?.requireExisting !== true && this.placements !== undefined && this.placementOf !== undefined) {
|
|
149
144
|
assertPlacementAdmission(sessionId, await this.placementOf(sessionId), opts);
|
|
150
145
|
}
|
|
@@ -185,6 +180,18 @@ export class CachingSessionStore {
|
|
|
185
180
|
this.cache.delete(sessionId);
|
|
186
181
|
await this.inner.release(sessionId);
|
|
187
182
|
}
|
|
183
|
+
async freshHit(sessionId) {
|
|
184
|
+
const hit = this.cache.get(sessionId);
|
|
185
|
+
if (hit === undefined)
|
|
186
|
+
return undefined;
|
|
187
|
+
const head = await this.inner.getHead(sessionId);
|
|
188
|
+
if (head !== undefined && head.leafId === (await hit.acquired.session.getLeafId()))
|
|
189
|
+
return hit;
|
|
190
|
+
if (this.cache.get(sessionId) === hit)
|
|
191
|
+
this.cache.delete(sessionId);
|
|
192
|
+
this.onStaleHit?.(sessionId);
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
188
195
|
evict(sessionId) {
|
|
189
196
|
this.cache.delete(sessionId);
|
|
190
197
|
}
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
* pair from case-sensitive Object.assign reaches CreateProcess as ONE undefined-which
|
|
19
19
|
* entry). Canonical casing = the first-seen key (process.env's native casing wins since the
|
|
20
20
|
* inherit base is spread first).
|
|
21
|
+
* - layering = env COMPOSITION is a pure function of config + process.env and never reads the resolved shell;
|
|
22
|
+
* the shell belongs to the SPAWN layer only ({@link pinHostShell}, applied by the spawn sites after
|
|
23
|
+
* `shellReady()`). A read surface (`cdPath`) therefore never depends on resolution having settled.
|
|
21
24
|
*
|
|
22
25
|
* ⚠️ The win32 branches are UNVERIFIED on a real machine until S5 (Windows CI runner) — design D5 discipline:
|
|
23
26
|
* structural tests only on mac/Linux; behavior-level bite happens on the first Windows-runner green.
|
|
@@ -39,10 +42,22 @@ export interface HostShell {
|
|
|
39
42
|
* read the cached value synchronously via {@link hostShell}.
|
|
40
43
|
*/
|
|
41
44
|
export declare function resolveHostShell(): Promise<HostShell>;
|
|
42
|
-
/** Synchronous read of the resolved shell — valid only AFTER {@link resolveHostShell} settled
|
|
43
|
-
* are all behind `shellReady()`, which awaits it).
|
|
44
|
-
*
|
|
45
|
+
/** Synchronous read of the resolved shell — a SPAWN-layer read, valid only AFTER {@link resolveHostShell} settled
|
|
46
|
+
* (the spawn sites are all behind `shellReady()`, which awaits it). It is never an env-composition or read-surface
|
|
47
|
+
* input (see {@link pinHostShell}). POSIX never needs the async step.
|
|
48
|
+
* win32, not settled: a resolve that FAILED throws that failure (the real cause + the way out, memoised above);
|
|
49
|
+
* only a resolve that never ran / is still in flight reaches the ordering sentence — on the spawn paths that arm
|
|
50
|
+
* is unreachable (they await the resolve first), so it stays as the loud guard against a future mis-ordered caller
|
|
51
|
+
* (a programming bug, not an operator condition — fail loud, not /bin/sh-on-windows). */
|
|
45
52
|
export declare function hostShell(): HostShell;
|
|
53
|
+
/**
|
|
54
|
+
* The SPAWN layer's one env edit: win32 pins `SHELL` to the resolved Git Bash (subshell scripts read `$SHELL`).
|
|
55
|
+
* Takes the {@link HostShell} the caller already read via {@link hostShell} — so a spawn site cannot reach the pin
|
|
56
|
+
* without a resolved shell (the type is the ordering guard), and env COMPOSITION never needs one. Mutates and
|
|
57
|
+
* returns `env` (call it BEFORE {@link collapseWin32EnvKeys} so an inherited case-variant `Shell` folds into one
|
|
58
|
+
* entry). POSIX: identity — `SHELL` is whatever the composed env already carried (byte-identical to pre-Windows).
|
|
59
|
+
*/
|
|
60
|
+
export declare function pinHostShell(env: NodeJS.ProcessEnv, sh: HostShell): NodeJS.ProcessEnv;
|
|
46
61
|
/** test-only: reset the shell caches (each test can re-drive resolution). */
|
|
47
62
|
export declare function resetHostShellCacheForTest(): void;
|
|
48
63
|
/** Per-platform spawn options for a group-killable child (design D3).
|