@sema-agent/server 7.140.0 → 7.141.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +290 -0
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +75 -13
- package/dist/approval-batch-census.d.ts +27 -0
- package/dist/approval-batch-census.js +27 -0
- package/dist/approval-card.d.ts +31 -16
- package/dist/approval-card.js +6 -3
- package/dist/approval.d.ts +15 -0
- package/dist/approval.js +10 -0
- package/dist/audit.d.ts +4 -2
- package/dist/audit.js +2 -2
- package/dist/boot/device-lane.d.ts +11 -0
- package/dist/boot/device-lane.js +16 -0
- package/dist/boot/engine-lease.d.ts +24 -3
- package/dist/boot/engine-lease.js +9 -2
- package/dist/boot/execution-env.js +8 -4
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/memory-consolidation.d.ts +1 -1
- package/dist/boot/memory-consolidation.js +1 -1
- package/dist/boot/reapers.d.ts +2 -2
- package/dist/boot/resolve-spec.js +7 -4
- package/dist/boot/session-faces.d.ts +1 -1
- package/dist/boot/session-faces.js +4 -3
- package/dist/boot/shutdown.js +5 -0
- package/dist/boot/stage-05-execution-env.js +3 -1
- package/dist/boot/stage-06-runners.d.ts +0 -2
- package/dist/boot/stage-06-runners.js +1 -1
- package/dist/boot/stage-07-capability-layer.d.ts +0 -1
- package/dist/boot/stage-07-capability-layer.js +8 -10
- package/dist/boot/stage-08-reapers.d.ts +0 -1
- package/dist/boot/stage-09-leader.d.ts +0 -1
- package/dist/boot/stage-10-http-server.d.ts +0 -1
- package/dist/boot/stage-10-http-server.js +1 -0
- package/dist/boot/stores.js +10 -4
- package/dist/boot/web-admin.js +1 -0
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +9 -0
- package/dist/capability-facts.js +9 -0
- package/dist/card-void.d.ts +38 -0
- package/dist/card-void.js +23 -0
- package/dist/config-catalog.js +11 -8
- package/dist/config-provider.d.ts +13 -13
- package/dist/config-provider.js +19 -33
- package/dist/config-types.d.ts +26 -8
- package/dist/config.js +55 -46
- package/dist/delegated-session-owner.d.ts +21 -19
- package/dist/delegated-session-owner.js +1 -1
- package/dist/device-store.d.ts +7 -0
- package/dist/device-store.js +3 -0
- package/dist/execution-config-root.d.ts +22 -0
- package/dist/execution-config-root.js +35 -0
- package/dist/execution-lane-caps.d.ts +49 -2
- package/dist/execution-lane-caps.js +90 -13
- package/dist/execution-placement.d.ts +3 -0
- package/dist/execution-placement.js +7 -0
- package/dist/fleet/drain-entry.d.ts +3 -0
- package/dist/hosted-posture.d.ts +8 -0
- package/dist/hosted-posture.js +4 -0
- package/dist/http/admission.js +3 -3
- package/dist/http/caller-trust.d.ts +3 -2
- package/dist/http/caller-trust.js +2 -1
- package/dist/http/dispatch.js +4 -0
- package/dist/http/live-frame.d.ts +24 -0
- package/dist/http/live-frame.js +16 -0
- package/dist/http/resume-legs.js +37 -20
- package/dist/http/route-table.js +2 -0
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/capabilities.js +5 -4
- package/dist/http/routes/diagnostics.d.ts +0 -19
- package/dist/http/routes/diagnostics.js +16 -2
- package/dist/http/routes/memory-bundle.d.ts +2 -2
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-policy.js +2 -2
- package/dist/http/routes/run-permission-mode.d.ts +75 -0
- package/dist/http/routes/run-permission-mode.js +170 -0
- package/dist/http/routes/session-sync.d.ts +3 -6
- package/dist/http/routes/session-sync.js +7 -2
- package/dist/http/routes/sessions.js +5 -4
- package/dist/http/routes/tasks.js +63 -66
- package/dist/http/run-read-face.js +3 -3
- package/dist/http/send.d.ts +1 -1
- package/dist/http/send.js +2 -2
- package/dist/http/server.d.ts +9 -2
- package/dist/http/sse-log.d.ts +3 -8
- package/dist/http/stored-body-replay.d.ts +3 -3
- package/dist/http/stored-body-replay.js +3 -1
- package/dist/instruction-source-notice.d.ts +7 -0
- package/dist/instruction-source-notice.js +6 -6
- package/dist/mcp-assertion-header.d.ts +4 -0
- package/dist/mcp-assertion-header.js +5 -0
- package/dist/memory-posture.d.ts +1 -1
- package/dist/memory-scope.d.ts +4 -4
- package/dist/memory-scope.js +1 -1
- package/dist/memory-sync-client.d.ts +14 -2
- package/dist/memory-sync-client.js +80 -18
- package/dist/observability/fail-open.d.ts +30 -2
- package/dist/observability/fail-open.js +30 -2
- package/dist/observability/metrics.js +0 -1
- package/dist/observability/run-terminal.d.ts +6 -2
- package/dist/observability/run-terminal.js +25 -26
- package/dist/observability/secret-env-scrub.d.ts +20 -1
- package/dist/observability/secret-env-scrub.js +14 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -2
- package/dist/orchestration/workflow-completion-inbox.js +13 -5
- package/dist/orchestration/workflow-notify-journal.d.ts +5 -3
- package/dist/orchestration/workflow-notify-journal.js +2 -1
- package/dist/permission-mode-admission.d.ts +68 -0
- package/dist/permission-mode-admission.js +23 -0
- package/dist/permission-rule-list.d.ts +55 -6
- package/dist/permission-rule-list.js +52 -5
- package/dist/plugins/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +3 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +65 -17
- package/dist/plugins/checkpoint-store-sql.js +74 -3
- package/dist/plugins/device-store-sql.d.ts +5 -2
- package/dist/plugins/device-store-sql.js +4 -1
- package/dist/plugins/execution-placement-sql.d.ts +11 -1
- package/dist/plugins/execution-placement-sql.js +12 -1
- package/dist/plugins/file-run-store.d.ts +2 -2
- package/dist/plugins/file-run-store.js +1 -1
- package/dist/plugins/fork-routing-session-store.d.ts +10 -32
- package/dist/plugins/fork-routing-session-store.js +2 -61
- package/dist/plugins/k8s-exec-protocol.d.ts +4 -1
- package/dist/plugins/k8s-exec-protocol.js +6 -1
- package/dist/plugins/local-checkpoint-store.d.ts +10 -1
- package/dist/plugins/local-checkpoint-store.js +22 -0
- package/dist/plugins/local-session-store.d.ts +227 -35
- package/dist/plugins/local-session-store.js +518 -37
- package/dist/plugins/local-session-window.d.ts +17 -3
- package/dist/plugins/local-session-window.js +7 -13
- package/dist/plugins/memory-run-store.d.ts +2 -2
- package/dist/plugins/memory-run-store.js +1 -1
- package/dist/plugins/one-time-migrations.d.ts +42 -2
- package/dist/plugins/one-time-migrations.js +65 -3
- package/dist/plugins/pg-pool.js +3 -2
- package/dist/plugins/pg-session-storage.d.ts +2 -0
- package/dist/plugins/pg-session-storage.js +3 -2
- package/dist/plugins/remote-env-e2b.d.ts +24 -3
- package/dist/plugins/remote-env-e2b.js +131 -18
- package/dist/plugins/remote-env-k8s.d.ts +20 -1
- package/dist/plugins/remote-env-k8s.js +148 -25
- package/dist/plugins/remote-env-local-docker.d.ts +25 -11
- package/dist/plugins/remote-env-local-docker.js +87 -21
- package/dist/plugins/run-store-sql.d.ts +3 -3
- package/dist/plugins/run-store-sql.js +4 -3
- package/dist/plugins/sandbox-read-floor.d.ts +273 -0
- package/dist/plugins/sandbox-read-floor.js +608 -0
- package/dist/plugins/session-ownership.d.ts +2 -1
- package/dist/plugins/session-placement.d.ts +17 -4
- package/dist/plugins/session-placement.js +2 -1
- package/dist/plugins/session-store.d.ts +18 -20
- package/dist/plugins/session-store.js +4 -12
- package/dist/plugins/store-backend.d.ts +31 -0
- package/dist/plugins/store-backend.js +21 -4
- package/dist/plugins/store-contracts.d.ts +9 -2
- package/dist/plugins/tidb-pool.js +2 -1
- package/dist/plugins/tidb-session-store.d.ts +4 -1
- package/dist/plugins/tidb-session-store.js +3 -2
- package/dist/run-cancel-context.d.ts +34 -0
- package/dist/run-cancel-context.js +7 -0
- package/dist/runs.d.ts +39 -12
- package/dist/runs.js +68 -53
- package/dist/security.d.ts +2 -2
- package/dist/server-secret-env.d.ts +2 -1
- package/dist/server-secret-env.js +2 -4
- package/dist/session-error-wire.d.ts +22 -4
- package/dist/session-error-wire.js +16 -0
- package/dist/session-incarnation.d.ts +9 -3
- package/dist/session-sync-kernel.d.ts +42 -1
- package/dist/session-sync-kernel.js +17 -0
- package/dist/session-sync.js +16 -4
- package/dist/sql-engine-coordinates.d.ts +49 -0
- package/dist/sql-engine-coordinates.js +27 -0
- package/dist/standing-answer-grant.d.ts +31 -2
- package/dist/standing-answer-grant.js +14 -2
- package/dist/store-posture.d.ts +1 -1
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-mcp.d.ts +1 -1
- package/dist/task-settings.d.ts +52 -25
- package/dist/task-settings.js +91 -28
- package/dist/terminal.d.ts +77 -20
- package/dist/terminal.js +66 -20
- package/dist/trace/core-keyset-guard.d.ts +79 -2
- package/dist/trace/exit-seat.d.ts +7 -0
- package/dist/trace/exit-seat.js +1 -0
- package/dist/trace/model-segment.d.ts +2 -2
- package/dist/trace/redact.js +323 -90
- package/dist/trace/self-minted-wire.d.ts +168 -2
- package/dist/trace/self-minted-wire.js +63 -0
- package/dist/verifier-pause-card.d.ts +8 -0
- package/dist/verifier-pause-card.js +19 -0
- package/dist/web-admin-client/apply-effective.d.ts +5 -7
- package/dist/web-admin-client/apply-effective.js +2 -2
- package/dist/web-admin-client/candidate-gate.d.ts +1 -1
- package/dist/web-admin-client/read-warnings.d.ts +2 -2
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -2
- package/package.json +2 -2
- package/wire-changes/server@7.141.0.json +367 -0
package/dist/runs.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { runWithVerification, runCascade, defaultTaskRegistry, taskNotificationDedupKey, DURABLE_AGENT_HANDLE_RE } from "@sema-agent/core";
|
|
2
2
|
import { atHostBoundary, failureCodeOf, sealResultForExit, outwardPlane } from "./terminal.js";
|
|
3
|
+
import { voidVerifierPauseCard } from "./verifier-pause-card.js";
|
|
4
|
+
import { voidPendingCard } from "./card-void.js";
|
|
3
5
|
import { NOT_IN_ROSTER, entryProblemsFacts } from "./entry-problems.js";
|
|
4
6
|
import { GOVERNANCE_HTTP_STATUS } from "./governance-http-status.js";
|
|
5
7
|
import { TOOL_DECLARATION_HTTP_STATUS } from "./tool-declaration-http-status.js";
|
|
@@ -21,6 +23,7 @@ import { defaultSubagentTailBus } from "./fleet/subagent-tail-bus.js";
|
|
|
21
23
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "./orchestration/workflow-completion-inbox.js";
|
|
22
24
|
import { isParkLedgerEventType } from "./trace/ledger-events.js";
|
|
23
25
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
26
|
+
import { BASELINE_IO_DEADLINE_MS, IoDeadlineExceeded, withinIoDeadline } from "./instruction-source-notice.js";
|
|
24
27
|
import { errorText as errText } from "./observability/err-text.js";
|
|
25
28
|
import { LIVENESS_HEARTBEAT_MS } from "./config-invariants.js";
|
|
26
29
|
export async function backgroundAgentOutput(registry, handle, access, agentStore) {
|
|
@@ -198,16 +201,25 @@ export function createPromptCommitReader() {
|
|
|
198
201
|
return closed.commits[closed.commits.length - 1 - closed.frames];
|
|
199
202
|
};
|
|
200
203
|
}
|
|
204
|
+
export async function settleResumeAnchor(write, where) {
|
|
205
|
+
try {
|
|
206
|
+
await withinIoDeadline(write);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
const why = err instanceof IoDeadlineExceeded ? `reason=timeout boundMs=${BASELINE_IO_DEADLINE_MS}` : `reason=error error=${err instanceof Error ? err.message : String(err)}`;
|
|
210
|
+
recordFailOpen("server.resume-anchor.capture-failed", `leg=${where.leg} kind=${where.kind} ${why}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
201
213
|
export class TurnAnchorCapture {
|
|
202
214
|
capture;
|
|
203
|
-
|
|
215
|
+
leg;
|
|
204
216
|
captureUser;
|
|
205
217
|
firstTextEventId;
|
|
206
218
|
turnHadTool = false;
|
|
207
219
|
promptCommit = createPromptCommitReader();
|
|
208
|
-
constructor(capture,
|
|
220
|
+
constructor(capture, leg, captureUser) {
|
|
209
221
|
this.capture = capture;
|
|
210
|
-
this.
|
|
222
|
+
this.leg = leg;
|
|
211
223
|
this.captureUser = captureUser;
|
|
212
224
|
}
|
|
213
225
|
onHumanInput(ev) {
|
|
@@ -216,9 +228,9 @@ export class TurnAnchorCapture {
|
|
|
216
228
|
async onMessageCommitted(role, entryId) {
|
|
217
229
|
const committed = { type: "message_committed", role, entryId };
|
|
218
230
|
const prompt = this.promptCommit(committed);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
231
|
+
const captureUser = this.captureUser;
|
|
232
|
+
if (prompt !== undefined && captureUser)
|
|
233
|
+
await settleResumeAnchor(() => captureUser(prompt), { leg: this.leg, kind: "prompt" });
|
|
222
234
|
}
|
|
223
235
|
onText(eventId) {
|
|
224
236
|
if (this.firstTextEventId === undefined) {
|
|
@@ -231,9 +243,10 @@ export class TurnAnchorCapture {
|
|
|
231
243
|
}
|
|
232
244
|
async onTurnEnd() {
|
|
233
245
|
this.promptCommit({ type: "turn_end" });
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
246
|
+
const capture = this.capture;
|
|
247
|
+
const handle = this.firstTextEventId;
|
|
248
|
+
if (handle && !this.turnHadTool && capture)
|
|
249
|
+
await settleResumeAnchor(() => capture(handle), { leg: this.leg, kind: "turn" });
|
|
237
250
|
this.firstTextEventId = undefined;
|
|
238
251
|
this.turnHadTool = false;
|
|
239
252
|
}
|
|
@@ -259,7 +272,12 @@ export async function graftParkToolCallId(result, cs) {
|
|
|
259
272
|
const id = await parkToolCallId(result, cs);
|
|
260
273
|
return id === undefined ? result : { ...result, toolCallId: id };
|
|
261
274
|
}
|
|
262
|
-
export async function
|
|
275
|
+
export async function voidHeldParkCard(held, hostSessionId, cards, leg) {
|
|
276
|
+
if (held?.terminal.kind !== "paused")
|
|
277
|
+
return;
|
|
278
|
+
await voidPendingCard({ token: held.terminal.token, sessionId: hostSessionId }, cards, "server.park.held-card-void-failed", `leg=${leg}`);
|
|
279
|
+
}
|
|
280
|
+
export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, checkpointCards, pendingEngineNotices, trustedInternals, userInputLedger, acceptance, workspaceRecorder) {
|
|
263
281
|
const startedAt = Date.now();
|
|
264
282
|
metrics?.addGauge("runs_active", 1);
|
|
265
283
|
fleetPublisher?.onStart();
|
|
@@ -274,7 +292,6 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
274
292
|
? mintCancelledResult({ taskId, sessionId: spec.sessionId ?? "", startedAtMs: startedAt, message: "cancelled by user", cause, engineResult })
|
|
275
293
|
: mintCancelledResult({ taskId, sessionId: spec.sessionId ?? "", startedAtMs: startedAt, message: "cancelled by user", cause });
|
|
276
294
|
const recordDone = (result, status) => {
|
|
277
|
-
metrics?.addGauge("runs_active", -1);
|
|
278
295
|
metrics?.inc("tasks_total", { status });
|
|
279
296
|
metrics?.observe("task_duration_seconds", (Date.now() - startedAt) / 1000);
|
|
280
297
|
if (result?.stats.tokens)
|
|
@@ -319,7 +336,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
319
336
|
}
|
|
320
337
|
}, HEARTBEAT_MS);
|
|
321
338
|
heartbeat.unref?.();
|
|
322
|
-
const anchor = new TurnAnchorCapture(captureTurnAnchor,
|
|
339
|
+
const anchor = new TurnAnchorCapture(captureTurnAnchor, "bg", captureUserMessageAnchor);
|
|
323
340
|
let reached;
|
|
324
341
|
let observedDone;
|
|
325
342
|
let causeAtDone;
|
|
@@ -333,15 +350,13 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
333
350
|
needs_review: (sealed) => runStore.setNeedsReview(taskId, sealed),
|
|
334
351
|
};
|
|
335
352
|
const parkLeg = async (word, res) => {
|
|
336
|
-
const grafted = await graftParkToolCallId(res,
|
|
353
|
+
const grafted = await graftParkToolCallId(res, checkpointCards);
|
|
337
354
|
await sink.appendParked(word, grafted);
|
|
338
355
|
await flushModelUsage();
|
|
339
356
|
const sealed = sealResultForExit(grafted);
|
|
340
357
|
reached = { kind: word, result: sealed };
|
|
341
358
|
await parkRowWrite[word](sealed);
|
|
342
359
|
acceptance?.settle(sealed);
|
|
343
|
-
metrics?.addGauge("runs_active", -1);
|
|
344
|
-
metrics?.inc("tasks_total", { status: word });
|
|
345
360
|
};
|
|
346
361
|
const settleLegResult = async (engineResult) => {
|
|
347
362
|
const result = atHostBoundary(engineResult, spec.sessionId);
|
|
@@ -359,7 +374,6 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
359
374
|
await runStore.setTerminal(taskId, plane.status, safe, plane.errorMessage ?? null);
|
|
360
375
|
await releaseKeyIfZeroWorkRefusal(runStore, taskId, safe);
|
|
361
376
|
acceptance?.settle(safe);
|
|
362
|
-
recordDone(safe, settledAsCancel ? CANCELLED_CODE : plane.status);
|
|
363
377
|
};
|
|
364
378
|
const instructionLeg = createInstructionSelectionLeg();
|
|
365
379
|
const sink = createLedgerSink({ appendEvent: (seq, type, data) => runStore.appendEvent(taskId, seq, type, data), persistThinking, notifiedKeys, onActivity: (kind, statusData) => recordTurnActivity(taskId, kind, statusData), legSections: () => ({ instructions: instructionLeg.segment(), skills: runSkillsOf(spec) }) });
|
|
@@ -454,6 +468,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
454
468
|
const result = await compositeLeg();
|
|
455
469
|
observedDone = result;
|
|
456
470
|
causeAtDone = cancelCtrl.cause();
|
|
471
|
+
await voidVerifierPauseCard(result, checkpointCards);
|
|
457
472
|
await flush();
|
|
458
473
|
await flushPromptManifest();
|
|
459
474
|
await settleLegResult(result);
|
|
@@ -547,7 +562,6 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
547
562
|
}
|
|
548
563
|
reached = { kind: "done", result: c };
|
|
549
564
|
await runStore.setTerminal(taskId, "failed", c, outwardPlane(c.terminal).errorMessage ?? null);
|
|
550
|
-
recordDone(c, CANCELLED_CODE);
|
|
551
565
|
}
|
|
552
566
|
}
|
|
553
567
|
catch (err) {
|
|
@@ -585,6 +599,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
585
599
|
else {
|
|
586
600
|
settledAsCancel = false;
|
|
587
601
|
const safeMsg = redactErrorMessage(message) ?? message;
|
|
602
|
+
await voidHeldParkCard(observedDone === undefined ? undefined : atHostBoundary(observedDone, spec.sessionId), spec.sessionId, checkpointCards, "bg");
|
|
588
603
|
try {
|
|
589
604
|
await append("failed", { errorMessage: safeMsg });
|
|
590
605
|
}
|
|
@@ -596,48 +611,48 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
596
611
|
}
|
|
597
612
|
catch {
|
|
598
613
|
}
|
|
599
|
-
if (reached?.kind === "done")
|
|
600
|
-
recordDone(reached.result, failureCodeOf(reached.result.terminal) === CANCELLED_CODE ? CANCELLED_CODE : outwardPlane(reached.result.terminal).status);
|
|
601
|
-
else if (reached !== undefined) {
|
|
602
|
-
metrics?.addGauge("runs_active", -1);
|
|
603
|
-
metrics?.inc("tasks_total", { status: reached.kind });
|
|
604
|
-
}
|
|
605
|
-
else if (settledAsCancel) {
|
|
606
|
-
recordDone(null, CANCELLED_CODE);
|
|
607
|
-
}
|
|
608
|
-
else {
|
|
609
|
-
recordDone(null, "failed");
|
|
610
|
-
}
|
|
611
614
|
}
|
|
612
615
|
finally {
|
|
613
616
|
legLive = false;
|
|
614
617
|
unregisterEngineNotice();
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
618
|
+
const verdict = reached?.kind === "done"
|
|
619
|
+
? { kind: "terminal", label: failureCodeOf(reached.result.terminal) === CANCELLED_CODE ? CANCELLED_CODE : outwardPlane(reached.result.terminal).status, result: reached.result }
|
|
620
|
+
: reached !== undefined
|
|
621
|
+
? { kind: "park", word: reached.kind }
|
|
622
|
+
: { kind: "terminal", label: settledAsCancel ? CANCELLED_CODE : "failed", result: null };
|
|
623
|
+
try {
|
|
624
|
+
metrics?.addGauge("runs_active", -1);
|
|
625
|
+
if (verdict.kind === "park")
|
|
626
|
+
metrics?.inc("tasks_total", { status: verdict.word });
|
|
622
627
|
else
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
628
|
+
recordDone(verdict.result, verdict.label);
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
if (fleetPublisher) {
|
|
632
|
+
if (verdict.kind === "park")
|
|
633
|
+
fleetPublisher.onTerminal(verdict.word);
|
|
634
|
+
else if (verdict.result !== null)
|
|
635
|
+
fleetPublisher.onTerminal(verdict.label, fleetRunResiduals(verdict.result));
|
|
636
|
+
else
|
|
637
|
+
fleetPublisher.onTerminal(verdict.label, {
|
|
638
|
+
...(verdict.label === CANCELLED_CODE ? { stoppedBy: "user" } : {}),
|
|
639
|
+
...(spec.sessionId ? { transcriptId: spec.sessionId } : {}),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
if (steerableStreamRef !== undefined)
|
|
643
|
+
await userInputLedger?.settleLeg(steerableStreamRef);
|
|
644
|
+
if (inflight?.get(taskId) === cancelCtrl)
|
|
645
|
+
inflight.delete(taskId);
|
|
646
|
+
if (preemptable?.get(taskId) === preemptCtrl)
|
|
647
|
+
preemptable.delete(taskId);
|
|
648
|
+
if (steerableStreamRef !== undefined && steerable?.get(taskId) === steerableStreamRef)
|
|
649
|
+
steerable.delete(taskId);
|
|
650
|
+
for (const evict of subagentHandleEvictions)
|
|
651
|
+
evict();
|
|
652
|
+
modelUsage?.clear(taskId);
|
|
653
|
+
promptManifests?.clear(taskId, manifestLease);
|
|
654
|
+
clearInterval(heartbeat);
|
|
627
655
|
}
|
|
628
|
-
if (steerableStreamRef !== undefined)
|
|
629
|
-
await userInputLedger?.settleLeg(steerableStreamRef);
|
|
630
|
-
if (inflight?.get(taskId) === cancelCtrl)
|
|
631
|
-
inflight.delete(taskId);
|
|
632
|
-
if (preemptable?.get(taskId) === preemptCtrl)
|
|
633
|
-
preemptable.delete(taskId);
|
|
634
|
-
if (steerableStreamRef !== undefined && steerable?.get(taskId) === steerableStreamRef)
|
|
635
|
-
steerable.delete(taskId);
|
|
636
|
-
for (const evict of subagentHandleEvictions)
|
|
637
|
-
evict();
|
|
638
|
-
modelUsage?.clear(taskId);
|
|
639
|
-
promptManifests?.clear(taskId, manifestLease);
|
|
640
|
-
clearInterval(heartbeat);
|
|
641
656
|
}
|
|
642
657
|
}
|
|
643
658
|
export const HEARTBEAT_MS = LIVENESS_HEARTBEAT_MS;
|
package/dist/security.d.ts
CHANGED
|
@@ -176,10 +176,10 @@ export declare function legacyUnownedAmnestyOpen(deployment: DeploymentShapeInpu
|
|
|
176
176
|
export type OwnerAwareSessionStore = SessionStore & {
|
|
177
177
|
/** 属主登记(core `SessionStore.register` 的契约:对没有行的 id 建行并记下属主,首写赢;对已有的行什么都不做)。读回走 `headOf`。 */
|
|
178
178
|
register?: (sessionId: string, owner: string | null) => Promise<void>;
|
|
179
|
-
/** [ref] 委派继承律的**认领口** —— 只在记录委派树的店上(
|
|
179
|
+
/** [ref] 委派继承律的**认领口** —— 只在记录委派树的店上(三只持久会话店,只命中 placed 行;不记委派树的店没有):判断时读到的那一位住客仍是无主的 placed 行 ⇒ 写 `owner`,
|
|
180
180
|
* 返回是否写成(只 CAS 子行自己)。唯一调用方 = 停驻挂点的 `settleDelegatedOwner`(`delegated-session-owner.ts`;M23 走同一句 SQL);规则写在那个模块。 */
|
|
181
181
|
claimOwnerless?: OwnerlessClaim;
|
|
182
|
-
/** [ref] placed 分区的元组读口(店的既有读口,一行 PK 读;`undefined` = 普通会话或无此行)。店不交它 = 不实现 placement = 没有 placed 行(
|
|
182
|
+
/** [ref] placed 分区的元组读口(店的既有读口,一行 PK 读;`undefined` = 普通会话或无此行)。店不交它 = 不实现 placement = 没有 placed 行(无 repo 的内存店)。两个读者:
|
|
183
183
|
* ① 会话面的属主门只在属主为 NULL 时读它一次(无主的 placed 行不是公共行,`http/principal-gate.ts` 的 `sessionOwnerCellOf`);
|
|
184
184
|
* ② 会话视图读它投影体顶层 `agent` 键(`audit.ts` 的 `sessionAgentOf`,只在视图读上付):店没有这一面 ⇒ 键缺席,读面不凭别处另铸子代身份。 */
|
|
185
185
|
placementOf?: (sessionId: string) => Promise<SessionPlacementRecord | undefined>;
|
|
@@ -73,7 +73,8 @@ export declare function serverCredentialValueSources(modelApiKeyEnv?: ModelApiKe
|
|
|
73
73
|
* 所以这一次抛只可能是名的问题。
|
|
74
74
|
* · **值臂**:{@link serverCredentialValueSources} 的每一个变量此刻在 env 里的值,逐个交给引擎**同一只**判(不另写「什么算凭据值」的第二份判据):引擎收下 ⇒ 记进值臂;
|
|
75
75
|
* 引擎拒(不是凭据值:空白 / 占位词 / 一个字符重复 / 不足 8 字符;或它是每个子进程都会拿到的变量的值)⇒ 这一个值不扣、名照扣、**不拒启**(与引擎自己的收值口
|
|
76
|
-
* `holdOwnCredentialValues` 同一姿势:不收 + 说一次)
|
|
76
|
+
* `holdOwnCredentialValues` 同一姿势:不收 + 说一次),经留痕座结构化点名那个变量一次 —— `warn` 行 `own_credential_value_not_withheld { name, reason }`
|
|
77
|
+
* ({@link recordOwnCredentialValueNotWithheld};`reason` 是引擎拒句原文,从不回显值;起服第一步还没有 logger 时当拍经进程默认的 JSON 行日志器写 stderr,不缓冲),能力位 `inherit_env.own_credential_values_withheld`
|
|
77
78
|
* 因此答 false(位真 ⇔ 值臂真扣)。引擎拒一个值时声明保持上一份,所以逐个交表的中间态都是上一份的超集。
|
|
78
79
|
* 返回声明的名单(只名、不值)。
|
|
79
80
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { declareWithheldEnv, hostChildEnv, inheritedHostEnv } from "@sema-agent/core";
|
|
2
2
|
import { CONFIG_CATALOG } from "./config-catalog.js";
|
|
3
3
|
import { ENV_NAME_ALLOWLIST_KNOB_NAMES } from "./env-name-allowlist-knobs.js";
|
|
4
|
-
import { recordSecretEnvScrub } from "./observability/secret-env-scrub.js";
|
|
4
|
+
import { recordOwnCredentialValueNotWithheld, recordSecretEnvScrub } from "./observability/secret-env-scrub.js";
|
|
5
5
|
export const CATALOG_SECRET_ENV_NAMES = new Set(CONFIG_CATALOG.filter((spec) => spec.type === "secret").map((spec) => spec.name));
|
|
6
6
|
const NO_MODEL_ENTRIES = Object.freeze({});
|
|
7
7
|
export function serverWithheldEnvNames(modelApiKeyEnv = NO_MODEL_ENTRIES) {
|
|
@@ -31,9 +31,7 @@ export function declareServerWithheldEnv(modelApiKeyEnv = NO_MODEL_ENTRIES) {
|
|
|
31
31
|
catch (e) {
|
|
32
32
|
if (!valueRefusalAnnounced.has(source)) {
|
|
33
33
|
valueRefusalAnnounced.add(source);
|
|
34
|
-
|
|
35
|
-
"the variable name is still withheld under every inheritEnv policy, but a variable holding the same string under another name is not, and GET /v1/capabilities answers inherit_env.own_credential_values_withheld: false. " +
|
|
36
|
-
"Give it a real credential value (at least 8 characters, not a placeholder) to withhold copies of it too.");
|
|
34
|
+
recordOwnCredentialValueNotWithheld({ name: source, reason: e instanceof Error ? e.message : String(e) });
|
|
37
35
|
}
|
|
38
36
|
}
|
|
39
37
|
}
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
* 列表里的缺席),码位各不相同,不由本表统一。
|
|
38
38
|
* · `placement_refused` —— `import` 面 **409 `conflict.session_placed`**(机读位 `detail.placement_kind`,固定句);`open` 面留白。
|
|
39
39
|
* 同一个词在两面上说的不是同一件事:import 面只有「会话同步往一只**分区会话**(委派子代的转录)上写」一个来源
|
|
40
|
-
* (
|
|
40
|
+
* (三只持久会话店的整段换装 / 暂存提交门:目标 id 的 `placement_kind` 在场 ⇒ 不适用),调用方没做错格式、是**目标**不收这种写;
|
|
41
41
|
* open 面上同一个词还含「声明了分区的创建撞上了分区外 / 异组的行」两形(core `TtlSessionStore.assertPlacementAdmission` 的后两条),
|
|
42
42
|
* 那不是「目标是分区会话」。而 run 终局上它是引擎词汇(`terminal.code`),原样随 failed 体走 —— 若在这里分诊成 409,
|
|
43
43
|
* 体上的 `errorCode` 仍是引擎词 `placement_refused`,一个状态两个名字。所以 open 面留白,`invalid_session` 是同形先例。
|
|
@@ -47,16 +47,18 @@
|
|
|
47
47
|
* (请求 core 把种类放进错误,而不是在这里解析句子)。
|
|
48
48
|
* · `invalid_entry` / `invalid_fork_target` —— 留白:本仓今天没有一条 wire 口把它们作为自己的
|
|
49
49
|
* 应答(它们在 core 的追加 / 分叉面上抛,落进各站点既有的处置或通用 500)。给它们起名是另一件事、另一张票。
|
|
50
|
-
* · `conflict` —— `import` 面 **409 `conflict`**(固定句 {@link STAGED_IMPORT_OWNER_CHANGED_SENTENCE});`open`
|
|
50
|
+
* · `conflict` —— `import` 面 **409 `conflict`**(固定句 {@link STAGED_IMPORT_OWNER_CHANGED_SENTENCE});`open` 面留白。按码映射时 import 面上这个词只有一个来源:
|
|
51
51
|
* 暂存提交的属主 CAS(`session-sync-kernel.ts` 的 `StagedCommitOwnerChangedError`,三家店在提交事务里比「此刻的属主格 = 这次导入的门判过的那一格」,
|
|
52
52
|
* 不等 ⇒ 回滚)—— 码位与同族的提交期同步冲突(`SyncConflictError` ⇒ 409 `conflict`「sync conflict」)同一个,出路也相同:从第一阶段重来(重新过门)。
|
|
53
|
+
* 本服务自铸的同码具名拒 {@link StampedSessionArchiveError}(local 店:存档仍带更早版本写下的属主戳)不按码映射 —— 出路不同(重来照样撞),
|
|
54
|
+
* 由 {@link sessionErrorWireOf} 按**型**先认:两面同答 409 `conflict` + 固定句 {@link STAMPED_SESSION_ARCHIVE_SENTENCE}。
|
|
53
55
|
* open 面上它是 run 的写撞上真并发写,落成那条 run 的 `failed` 终局(`terminal.code: "conflict"`),提交腿不另作处置。
|
|
54
56
|
* · `storage` / `unknown` —— 留白:故障类,通用 500 就是它们今天的应答。
|
|
55
57
|
*
|
|
56
58
|
* core 给闭集加一个词 ⇒ 下面的 `switch` 编译红,由人裁定它在两面上的答复,而不是让它静默落进某个恰好存在的臂
|
|
57
59
|
* ([ref] 词表纪律;同形先例 `session-policy-wire.ts` 的 `sessionPolicyErrorHttp`)。
|
|
58
60
|
*/
|
|
59
|
-
import
|
|
61
|
+
import { SessionError } from "@sema-agent/core";
|
|
60
62
|
/** core `SessionError` 的码闭集(core 不从包根导出这个型名 ⇒ 从导出类的 `code` 键派生,不手抄)。 */
|
|
61
63
|
export type SessionErrorCode = SessionError["code"];
|
|
62
64
|
/** 映射的面(见文件头注)。 */
|
|
@@ -76,7 +78,7 @@ export interface SessionErrorWire {
|
|
|
76
78
|
/** import 面 `invalid_session` 的固定句(修前 session-sync 暂存导入腿那一行的原文)。 */
|
|
77
79
|
export declare const STAGED_IMPORT_INVALID_SENTENCE = "staged session log failed validation";
|
|
78
80
|
/**
|
|
79
|
-
* import 面 `placement_refused` 的固定句:wire 上不发引擎原句 ——
|
|
81
|
+
* import 面 `placement_refused` 的固定句:wire 上不发引擎原句 —— 三只持久会话店的原句各不相同(措辞各自铸),都点名会话 id 与引擎内部词
|
|
80
82
|
* (「placement partition」「delegation transcript」);固定句只说调用方能做什么(冻结在 `test/api-error-text-freeze.test.ts` 的常量锚格)。
|
|
81
83
|
*/
|
|
82
84
|
export declare const PLACED_SESSION_SENTENCE = "this session holds a delegated agent's transcript, so session sync cannot overwrite it; sync into a different session id";
|
|
@@ -85,6 +87,22 @@ export declare const PLACED_SESSION_SENTENCE = "this session holds a delegated a
|
|
|
85
87
|
* 不发店的原句(它点名会话 id);固定句只说发生了什么与出路(冻结在 `test/api-error-text-freeze.test.ts` 的常量锚格)。
|
|
86
88
|
*/
|
|
87
89
|
export declare const STAGED_IMPORT_OWNER_CHANGED_SENTENCE = "session owner changed while the import streamed; nothing was imported \u2014 start the sync again";
|
|
90
|
+
/**
|
|
91
|
+
* local 店的具名拒(唯一铸点 `plugins/local-session-store.ts`):这只会话的引擎存档仍带更早版本写下的属主戳(一次性迁移 M33 改写不了它,或数据根跳过了带这次迁移的版本)
|
|
92
|
+
* ⇒ 不在它上面造新存档(fork),也不原地换它的日志(改日志的推送);零写。码是 core 闭集的 `conflict`(码不另铸);句子(给运维日志,点名会话 id)归本错,
|
|
93
|
+
* wire 上发固定句 {@link STAMPED_SESSION_ARCHIVE_SENTENCE}(两面同一句;映射 {@link sessionErrorWireOf})。
|
|
94
|
+
*/
|
|
95
|
+
export declare class StampedSessionArchiveError extends SessionError {
|
|
96
|
+
readonly sessionId: string;
|
|
97
|
+
constructor(sessionId: string);
|
|
98
|
+
}
|
|
99
|
+
/** {@link StampedSessionArchiveError} 的 wire 固定句(fork 与改日志的推送同一句;不点名会话 id;出路在句里 —— 从头再同步照样撞)。 */
|
|
100
|
+
export declare const STAMPED_SESSION_ARCHIVE_SENTENCE = "this session's stored archive still carries an owner stamp written by an earlier server version, so it cannot be forked or have its log replaced; nothing was written \u2014 delete the session and push it again";
|
|
101
|
+
/**
|
|
102
|
+
* 一只 `SessionError` 在一面上的 wire 答复 —— 下发点({@link sendSessionError} 的唯一读法)经这里:本服务自铸的同码具名拒先按型认(它的出路与同码的
|
|
103
|
+
* 按码答复不同),其余按码({@link sessionErrorHttp})。
|
|
104
|
+
*/
|
|
105
|
+
export declare function sessionErrorWireOf(e: SessionError, face: SessionErrorFace): SessionErrorWire | undefined;
|
|
88
106
|
export declare function sessionErrorHttp(code: SessionErrorCode, face: SessionErrorFace): SessionErrorWire | undefined;
|
|
89
107
|
export declare function isSessionErrorCode(code: string): code is SessionErrorCode;
|
|
90
108
|
//# sourceMappingURL=session-error-wire.d.ts.map
|
|
@@ -1,12 +1,28 @@
|
|
|
1
|
+
import { SessionError } from "@sema-agent/core";
|
|
1
2
|
export const STAGED_IMPORT_INVALID_SENTENCE = "staged session log failed validation";
|
|
2
3
|
export const PLACED_SESSION_SENTENCE = "this session holds a delegated agent's transcript, so session sync cannot overwrite it; sync into a different session id";
|
|
3
4
|
const PLACEMENT_KIND = "subagent";
|
|
4
5
|
export const STAGED_IMPORT_OWNER_CHANGED_SENTENCE = "session owner changed while the import streamed; nothing was imported — start the sync again";
|
|
6
|
+
export class StampedSessionArchiveError extends SessionError {
|
|
7
|
+
sessionId;
|
|
8
|
+
constructor(sessionId) {
|
|
9
|
+
super("conflict", `session ${sessionId}'s stored archive still carries an owner stamp written by an earlier version (the one-time migration M33 could not rewrite it as anonymous, ` +
|
|
10
|
+
"or this data root skipped the versions that carry that migration) — no new session is made from it and its log is not replaced, so this write was refused and nothing was written. " +
|
|
11
|
+
"Reading, resuming and pulling the session are unaffected. Way out: delete the session and push it again (a push onto an absent session creates it anew).");
|
|
12
|
+
this.sessionId = sessionId;
|
|
13
|
+
this.name = "StampedSessionArchiveError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export const STAMPED_SESSION_ARCHIVE_SENTENCE = "this session's stored archive still carries an owner stamp written by an earlier server version, so it cannot be forked or have its log replaced; nothing was written — delete the session and push it again";
|
|
5
17
|
const CORRUPT_SESSION = { status: 500, errorCode: "corrupt_session" };
|
|
6
18
|
const STORED_LOG_INVALID = { status: 500, errorCode: "invalid_session" };
|
|
7
19
|
const INVALID_ENTRIES = { status: 422, errorCode: "invalid_entries", sentence: STAGED_IMPORT_INVALID_SENTENCE };
|
|
8
20
|
const SESSION_PLACED = { status: 409, errorCode: "conflict.session_placed", sentence: PLACED_SESSION_SENTENCE, detail: { placement_kind: PLACEMENT_KIND } };
|
|
9
21
|
const STAGED_IMPORT_OWNER_CHANGED = { status: 409, errorCode: "conflict", sentence: STAGED_IMPORT_OWNER_CHANGED_SENTENCE };
|
|
22
|
+
const STAMPED_SESSION_ARCHIVE = { status: 409, errorCode: "conflict", sentence: STAMPED_SESSION_ARCHIVE_SENTENCE };
|
|
23
|
+
export function sessionErrorWireOf(e, face) {
|
|
24
|
+
return e instanceof StampedSessionArchiveError ? STAMPED_SESSION_ARCHIVE : sessionErrorHttp(e.code, face);
|
|
25
|
+
}
|
|
10
26
|
export function sessionErrorHttp(code, face) {
|
|
11
27
|
switch (code) {
|
|
12
28
|
case "corrupt_session":
|
|
@@ -6,9 +6,15 @@
|
|
|
6
6
|
* 一个可比的答案 —— 今天只能比属主,同一个人删了重建就比不出来。
|
|
7
7
|
*
|
|
8
8
|
* ## 规则(全仓唯一处写)
|
|
9
|
-
* · **铸点只在新建行**:会话店的 `register` / 无 id 创建 / fork / session-sync
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* · **铸点只在新建行**:会话店的 `register` / 无 id 创建 / fork / session-sync 的导入新行。已存在的行化身一个字节都不动;
|
|
10
|
+
* session-sync 的整段换装(replace / staged commit)换的是**同一位住客**的日志,化身沿用 —— 属主却可以被换装重戳为导入者(local 的 M10 认领同理),
|
|
11
|
+
* 所以化身与属主不是同一条律(读法分三种,见下「两读」)。删除随行消失,同 id 重新登记铸新的。
|
|
12
|
+
* · **两读(住客的三种问法,各读各的格,不混用)**:
|
|
13
|
+
* K1 **住客自己的记录**(工作区、记忆、空间分区……按「这一位住客」落的东西)只按化身绑 —— 同一位住客的属主换代(推送重戳 / 认领)不换住客,记录照落;
|
|
14
|
+
* K2 **以「这一行的这一代」为条件的写**(执行放置的换绑 / 收回、执行上下文的复核)比属主与化身整对,判官 = core 的行期望判官 `sessionRowMatches`
|
|
15
|
+
* (`execution-placement.ts` 的 `sameOccupant`,SQL `incarnation = ? AND owner <=> ?` 的对位)—— 读与写之间属主换了一代 ⇒ 不是判过的那一行;
|
|
16
|
+
* K0 **只问属主的门**(授权门、暂存提交的属主 CAS)只读属主那一格。三种都读会话**行**;local 店的行 = 会话元数据边档那一格(引擎仓里的存档是匿名内容仓,
|
|
17
|
+
* 存档头上的属主不是行事实 —— `plugins/local-session-store.ts` 头注)。
|
|
12
18
|
* · **值是不透明随机串**(`randomUUID()`,v4:没有时间戳、没有机器信息 —— 刻意不用 uuidv7,那一形带铸造时刻)。只拿来比较。
|
|
13
19
|
* · **一格、一个解码**:住客令牌是会话**行**上的一格 —— SQL `session_meta.incarnation`(两方言)、local 后端的会话元数据边档那一格
|
|
14
20
|
* (`plugins/local-session-store.ts` 的 `session-incarnations.json`)。行上的值只经 {@link incarnationFromCell} 解码。
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* (security ↔ session-sync; lens2 §B/§E). This file imports nothing from the repo but TYPES from leaves (core types, and
|
|
9
9
|
* the session-face type from `session-incarnation.ts`, itself a leaf) — a leaf, same shape as session-sync-content.ts. Its one VALUE import is
|
|
10
10
|
* core's `SessionError` class (the closed-set store error the three adapters already throw). session-sync.ts re-exports everything here, so existing import sites
|
|
11
|
-
* keep working unchanged.
|
|
11
|
+
* keep working unchanged. The import door's anchor judge ({@link readImportAnchors}) was written here, not moved: it is pure too, and the route and the
|
|
12
|
+
* tests read it from the same leaf.
|
|
12
13
|
*/
|
|
13
14
|
import { SessionError, type SessionTreeEntry } from "@sema-agent/core";
|
|
14
15
|
import type { SessionFace } from "./session-incarnation.js";
|
|
@@ -180,4 +181,44 @@ export declare class SyncConflictError extends Error {
|
|
|
180
181
|
relation: "fork" | "stale";
|
|
181
182
|
}>);
|
|
182
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* §9 — one anchor of a Phase-A import body as the import door reads it: the live `eventId` the shell holds → the persisted `entryId` it resumes at.
|
|
186
|
+
* The wire's `owner` is not carried: every replay re-keys the anchor to the IMPORTING principal (the importer, never the exporter, owns the row),
|
|
187
|
+
* so nothing downstream reads the sender's value and nothing here judges it.
|
|
188
|
+
*/
|
|
189
|
+
export interface ImportAnchor {
|
|
190
|
+
readonly eventId: string;
|
|
191
|
+
readonly entryId: string;
|
|
192
|
+
}
|
|
193
|
+
/** The import door's verdict on `anchors[]`: the judged list, or ONE sentence for the first violation (index + the offending `entryId`, nothing else). */
|
|
194
|
+
export type ImportAnchorsRead = {
|
|
195
|
+
readonly anchors: ImportAnchor[];
|
|
196
|
+
} | {
|
|
197
|
+
readonly problem: string;
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* §9 — THE predicate of the anchor law, and its only spelling: an anchor is sound iff the entry it resumes at is one of the entries declared next to it
|
|
201
|
+
* (an import body's `entryIds`; a manifest's `entryIds`). The import door ({@link readImportAnchors}) refuses an anchor that fails it, and the manifest
|
|
202
|
+
* export (`exportSessionManifest`) does not hand one out — one fact, one reading, on both sides of the wire.
|
|
203
|
+
*/
|
|
204
|
+
export declare function anchorEntryDeclared(declared: ReadonlySet<string>, anchorEntryId: string): boolean;
|
|
205
|
+
/**
|
|
206
|
+
* §9 — the ONE judge of a Phase-A body's `anchors[]`, run at the door before anything is classified, mirrored, staged or written (a refusal
|
|
207
|
+
* writes no anchor, no log, no session row, opens no staging, takes no lease, and leaves the execution cell alone).
|
|
208
|
+
*
|
|
209
|
+
* The law (single source): an anchor may only point at an entry the session HOLDS once the import has landed, and what an import lands is the log
|
|
210
|
+
* its body declares (`entryIds`): a commit REPLACES the destination's log with the staged entries (SQL: purge + row rename in one transaction; local:
|
|
211
|
+
* an in-place swap), `fresh` / `fast_forward` / `identical` leave no destination entry the declaration lacks, and an `overwrite-dst` `fork` / `stale`
|
|
212
|
+
* removes the destination-only entries (and wipes the anchors that pointed at them). So "the entries the import declares" is the whole question: an
|
|
213
|
+
* `entryId` outside it names a row that is not there once the import lands (an anchor to a destination-only entry is exactly the dangling anchor the
|
|
214
|
+
* purge exists to prevent). An entry a concurrent writer appends after the body's ids were read is outside the declaration too, and an anchor to it is
|
|
215
|
+
* refused the same way — the conservative direction: the sender re-reads its manifest and pushes again (`exportSessionManifest` reads the anchors BEFORE
|
|
216
|
+
* the entry ids, so a manifest never carries an anchor newer than its own id list, and drops — loudly, by the same {@link anchorEntryDeclared} — an
|
|
217
|
+
* anchor an earlier version stored for an entry the log does not hold). The replays downstream (the `identical` arm, the post-commit leg)
|
|
218
|
+
* only iterate the list this function returned — they do not judge again.
|
|
219
|
+
*
|
|
220
|
+
* Shape is judged by the same reader — an element that is not an object with a string `eventId` and a string `entryId` is refused by index —
|
|
221
|
+
* so a `null` element is a 400 at the door, not a TypeError in a replay leg.
|
|
222
|
+
*/
|
|
223
|
+
export declare function readImportAnchors(raw: readonly unknown[], declaredEntryIds: readonly string[]): ImportAnchorsRead;
|
|
183
224
|
//# sourceMappingURL=session-sync-kernel.d.ts.map
|
|
@@ -55,4 +55,21 @@ export class SyncConflictError extends Error {
|
|
|
55
55
|
this.name = "SyncConflictError";
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
export function anchorEntryDeclared(declared, anchorEntryId) {
|
|
59
|
+
return declared.has(anchorEntryId);
|
|
60
|
+
}
|
|
61
|
+
export function readImportAnchors(raw, declaredEntryIds) {
|
|
62
|
+
const declared = new Set(declaredEntryIds);
|
|
63
|
+
const anchors = [];
|
|
64
|
+
for (let i = 0; i < raw.length; i++) {
|
|
65
|
+
const a = raw[i];
|
|
66
|
+
const { eventId, entryId } = (typeof a === "object" && a !== null && !Array.isArray(a) ? a : {});
|
|
67
|
+
if (typeof eventId !== "string" || typeof entryId !== "string")
|
|
68
|
+
return { problem: `anchors: [${i}] must be an object with a string eventId and a string entryId` };
|
|
69
|
+
if (!anchorEntryDeclared(declared, entryId))
|
|
70
|
+
return { problem: `anchors: [${i}].entryId ${JSON.stringify(entryId)} is not among the entryIds this import declares` };
|
|
71
|
+
anchors.push({ eventId, entryId });
|
|
72
|
+
}
|
|
73
|
+
return { anchors };
|
|
74
|
+
}
|
|
58
75
|
//# sourceMappingURL=session-sync-kernel.js.map
|
package/dist/session-sync.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { classifySyncRelationship } from "./session-sync-kernel.js";
|
|
1
|
+
import { anchorEntryDeclared, classifySyncRelationship } from "./session-sync-kernel.js";
|
|
2
|
+
import { recordFailOpen } from "./observability/fail-open.js";
|
|
2
3
|
import { loosenReasons, normalizeRules } from "@sema-agent/core";
|
|
3
4
|
import { headReadOf } from "./session-incarnation.js";
|
|
4
5
|
import { judgeSessionRulesWrite } from "@sema-agent/core";
|
|
@@ -60,8 +61,14 @@ export async function replayFileHistory(store, scope, envelope, srcGetBlob, opts
|
|
|
60
61
|
function ownerAware(backend) {
|
|
61
62
|
return backend.session();
|
|
62
63
|
}
|
|
64
|
+
const DIAG_VALUE_MAX = 96;
|
|
65
|
+
function brief(value) {
|
|
66
|
+
return JSON.stringify(value.length > DIAG_VALUE_MAX ? `${value.slice(0, DIAG_VALUE_MAX)}…(${value.length} chars)` : value);
|
|
67
|
+
}
|
|
63
68
|
export async function exportSessionManifest(sessionId, srcBackend, historyStore) {
|
|
64
69
|
const session = ownerAware(srcBackend);
|
|
70
|
+
const anchorStore = srcBackend.resumeAnchor();
|
|
71
|
+
const anchors = await anchorStore.listBySession(sessionId);
|
|
65
72
|
const listEntryIds = session.listEntryIds?.bind(session);
|
|
66
73
|
if (!listEntryIds)
|
|
67
74
|
return null;
|
|
@@ -76,15 +83,20 @@ export async function exportSessionManifest(sessionId, srcBackend, historyStore)
|
|
|
76
83
|
const policy = typeof policyStore.listBySession === "function"
|
|
77
84
|
? await policyStore.listBySession(sessionId)
|
|
78
85
|
: [];
|
|
79
|
-
const anchorStore = srcBackend.resumeAnchor();
|
|
80
|
-
const anchors = await anchorStore.listBySession(sessionId);
|
|
81
86
|
const summaryOf = session.sessionSummaryOf?.bind(session);
|
|
82
87
|
if (summaryOf === undefined)
|
|
83
88
|
throw new Error("exportSessionManifest: the session store has no row read face (sessionSummaryOf) — the sessionSync fact should have refused this deployment");
|
|
84
89
|
const row = await summaryOf(sessionId);
|
|
85
90
|
const getBlob = blobGetterOf(fs);
|
|
91
|
+
const declared = new Set(entryIds);
|
|
92
|
+
const soundAnchors = anchors.filter((a) => {
|
|
93
|
+
if (anchorEntryDeclared(declared, a.entryId))
|
|
94
|
+
return true;
|
|
95
|
+
recordFailOpen("server.session-sync.dangling-anchor-export", `sessionId=${brief(sessionId)} eventId=${brief(a.eventId)} entryId=${brief(a.entryId)}`);
|
|
96
|
+
return false;
|
|
97
|
+
});
|
|
86
98
|
return {
|
|
87
|
-
manifest: { sessionId, entryIds, entryCount: entryIds.length, leafId, fileHistory, policy, anchors, ...(row !== undefined ? { title: row.title, pinned: row.pinned } : {}) },
|
|
99
|
+
manifest: { sessionId, entryIds, entryCount: entryIds.length, leafId, fileHistory, policy, anchors: soundAnchors, ...(row !== undefined ? { title: row.title, pinned: row.pinned } : {}) },
|
|
88
100
|
getBlob,
|
|
89
101
|
};
|
|
90
102
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一只 SQL 引擎「开店所需的坐标」—— 单一来源(纯叶,零依赖)。
|
|
3
|
+
*
|
|
4
|
+
* ## 病
|
|
5
|
+
* 显式 `DB_BACKEND=pg|mysql` 是运维在说「我要这只持久引擎」。可是坐标缺席时,起服在三种 `SESSION_BACKEND` 写法下走三条路:
|
|
6
|
+
* 未设 / `mysql` ⇒ 读坐标抛 `missing required env`(响亮);`auto` ⇒ 「坐标不在就不探测」,**静默落内存**,姿态位却照报 `declared: "pg"` ——
|
|
7
|
+
* 一个自以为在持久化的部署跑在内存里,`/health` 照样 200。`REQUIRE_PRINCIPAL=true` 同形。同一个声明,拒不拒取决于第二个旋钮怎么写。
|
|
8
|
+
*
|
|
9
|
+
* ## 药
|
|
10
|
+
* 声明 ∧ 坐标缺席 ⇒ 配置期拒启,**不看** `SESSION_BACKEND`(判官在 `config.ts` 的 store 域,一只):三种写法同一句拒句,
|
|
11
|
+
* 拒句点名缺的坐标**键名**(不回显值,也读不到值)与两条出路。本模块只出两样东西:各引擎的必需坐标键表,与按这张表铸的两句话
|
|
12
|
+
* ({@link buildDeclaredEngineWithoutCoordinatesRefusal} 给配置判官、{@link buildSqlPoolMissingRefusal} 给 `boot/stores.ts` 的记忆引擎同池门)。
|
|
13
|
+
*
|
|
14
|
+
* ## 为什么键表在这里、`config.ts` 的 `env("MYSQL_HOST")` 读法不动
|
|
15
|
+
* 配置目录的对账门按名字面扫 `env("NAME")` 与 `process.env.NAME`,把读法改成按表取名会让它看不见这些键;所以两块 `env()` 读仍是字面读,
|
|
16
|
+
* 本表与它们的一致由 `test/sql-engine-coordinates.test.ts` 的源码钉守(表里每个键在 `config.ts` 里恰有一处 `env("KEY")` 必需读)。
|
|
17
|
+
*/
|
|
18
|
+
/** SQL 引擎词(`DB_BACKEND` 闭集里的两只持久引擎;`local` / `memory` 没有坐标)。 */
|
|
19
|
+
export type SqlEngine = "mysql" | "pg";
|
|
20
|
+
/**
|
|
21
|
+
* 每只引擎开店**必需**的坐标键(缺一 `config.ts` 的对应块抛 `missing required env`)。可选键(`*_PORT` / `*_PASSWORD` / `*_POOL_SIZE`)不在表里 ——
|
|
22
|
+
* 它们缺席有缺省,不是「没有坐标」。主机键是首位:它是「SQL 信号」与 `auto` 探测的唯一判据(`config.ts` `dbHostSet`)。
|
|
23
|
+
*/
|
|
24
|
+
export declare const SQL_ENGINE_COORDINATE_KEYS: {
|
|
25
|
+
readonly mysql: readonly ["MYSQL_HOST", "MYSQL_USER", "MYSQL_DATABASE"];
|
|
26
|
+
readonly pg: readonly ["PG_HOST", "PG_USER", "PG_DATABASE"];
|
|
27
|
+
};
|
|
28
|
+
/** 该引擎的主机键(`SQL_ENGINE_COORDINATE_KEYS` 的首位)。 */
|
|
29
|
+
export declare function sqlEngineHostKey(engine: SqlEngine): string;
|
|
30
|
+
/** 必需坐标键里此刻缺席(未设或空串 —— 空串在本仓一律等同未设)的那几个,表序。只读键名,不读值。 */
|
|
31
|
+
export declare function missingSqlCoordinateKeys(engine: SqlEngine, env: NodeJS.ProcessEnv): readonly string[];
|
|
32
|
+
/**
|
|
33
|
+
* 显式 `DB_BACKEND=<engine>` 而该引擎的坐标缺席 ⇒ 配置期拒启句(唯一铸点;**与 `SESSION_BACKEND` 无关**,所以句子里没有它)。
|
|
34
|
+
* `missing` 是 {@link missingSqlCoordinateKeys} 的读数(此时至少含主机键)。
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildDeclaredEngineWithoutCoordinatesRefusal(engine: SqlEngine, missing: readonly string[]): string;
|
|
37
|
+
/**
|
|
38
|
+
* 记忆引擎要绑 `<engine>` 的池而店后端没给 ⇒ `boot/stores.ts` 的拒句。记忆持久面的 SQL 方言词与店的引擎词是**同一个字**(`MEMORY_ENGINE_BACKEND=pg|mysql`
|
|
39
|
+
* ↔ `DB_BACKEND=pg|mysql`),所以拒句只收引擎词。分两形(`DB_BACKEND` 是否已经点名该引擎):
|
|
40
|
+
* · 没点名 ⇒ 缺的是声明:`MEMORY_ENGINE_BACKEND=<engine>` 要 `DB_BACKEND=<engine>`;
|
|
41
|
+
* · 已点名 ⇒ 声明没缺,「要 DB_BACKEND=<engine>」指错了修法 —— 点名真正开店的两个条件(持久会话后端、必需坐标)与此刻缺的坐标键。
|
|
42
|
+
* 坐标缺席本身在配置期已被 {@link buildDeclaredEngineWithoutCoordinatesRefusal} 拒掉,走到这里的已点名形 = 坐标齐(或只缺非主机键)而会话后端是 `memory`。
|
|
43
|
+
*/
|
|
44
|
+
export declare function buildSqlPoolMissingRefusal(args: {
|
|
45
|
+
readonly engine: SqlEngine;
|
|
46
|
+
readonly declared: boolean;
|
|
47
|
+
readonly missing: readonly string[];
|
|
48
|
+
}): string;
|
|
49
|
+
//# sourceMappingURL=sql-engine-coordinates.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const SQL_ENGINE_COORDINATE_KEYS = {
|
|
2
|
+
mysql: ["MYSQL_HOST", "MYSQL_USER", "MYSQL_DATABASE"],
|
|
3
|
+
pg: ["PG_HOST", "PG_USER", "PG_DATABASE"],
|
|
4
|
+
};
|
|
5
|
+
export function sqlEngineHostKey(engine) {
|
|
6
|
+
return SQL_ENGINE_COORDINATE_KEYS[engine][0];
|
|
7
|
+
}
|
|
8
|
+
export function missingSqlCoordinateKeys(engine, env) {
|
|
9
|
+
return SQL_ENGINE_COORDINATE_KEYS[engine].filter((key) => env[key] === undefined || env[key] === "");
|
|
10
|
+
}
|
|
11
|
+
const ENGINE_LABEL = { mysql: "MySQL-protocol", pg: "PostgreSQL" };
|
|
12
|
+
export function buildDeclaredEngineWithoutCoordinatesRefusal(engine, missing) {
|
|
13
|
+
const required = SQL_ENGINE_COORDINATE_KEYS[engine];
|
|
14
|
+
return (`DB_BACKEND=${engine} declares the ${ENGINE_LABEL[engine]} engine, but its connection coordinates are not set (missing: ${missing.join(", ")}) — ` +
|
|
15
|
+
`no session store can open, so the deployment would run IN MEMORY while declaring a durable engine. ` +
|
|
16
|
+
`Refusing to boot rather than starting non-durable under that declaration (whatever SESSION_BACKEND says). ` +
|
|
17
|
+
`To persist: set ${required.join(", ")} (optional: ${engine === "pg" ? "PG_PORT, PG_PASSWORD" : "MYSQL_PORT, MYSQL_PASSWORD"}); ` +
|
|
18
|
+
`to run without SQL: DB_BACKEND=local (single-user file store) or DB_BACKEND=memory.`);
|
|
19
|
+
}
|
|
20
|
+
export function buildSqlPoolMissingRefusal(args) {
|
|
21
|
+
const { engine, declared, missing } = args;
|
|
22
|
+
if (!declared)
|
|
23
|
+
return `MEMORY_ENGINE_BACKEND=${engine} requires DB_BACKEND=${engine} (the memory engine binds the same ${ENGINE_LABEL[engine]} pool) — refusing to start half-configured`;
|
|
24
|
+
return (`MEMORY_ENGINE_BACKEND=${engine} binds the ${ENGINE_LABEL[engine]} pool of the deployment's store, but none is open although the effective DB_BACKEND is ${engine}: the SQL store opens only under a durable session backend (SESSION_BACKEND unset, mysql or auto — not memory) ` +
|
|
25
|
+
`with ${SQL_ENGINE_COORDINATE_KEYS[engine].join(", ")} set${missing.length > 0 ? ` (not set now: ${missing.join(", ")})` : ""} — refusing to start half-configured`);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=sql-engine-coordinates.js.map
|