@sema-agent/server 7.142.0 → 7.143.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 +442 -0
- package/USAGE.md +41 -24
- package/dist/adoption/plan.js +22 -0
- package/dist/approval-ask-audit-store.d.ts +7 -5
- package/dist/approval-ask-audit-store.js +12 -6
- package/dist/approval-ask-machine.d.ts +15 -7
- package/dist/approval-ask-machine.js +10 -4
- package/dist/approval-card.d.ts +124 -25
- package/dist/approval-card.js +103 -23
- package/dist/approval-queue-kind.d.ts +27 -0
- package/dist/approval-queue-kind.js +6 -0
- package/dist/approval-reconciler.d.ts +8 -8
- package/dist/approval-reconciler.js +3 -3
- package/dist/audit.js +4 -2
- package/dist/boot/budget-tracing.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +26 -41
- package/dist/boot/governance-seams.js +61 -28
- package/dist/boot/late-binds.d.ts +7 -4
- package/dist/boot/late-binds.js +3 -1
- package/dist/boot/leader.js +2 -4
- package/dist/boot/memory-consolidation.d.ts +0 -8
- package/dist/boot/memory-consolidation.js +3 -3
- package/dist/boot/reapers.js +5 -4
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +8 -8
- package/dist/boot/runner-deps.d.ts +10 -7
- package/dist/boot/runner-deps.js +5 -17
- package/dist/boot/shutdown.d.ts +9 -0
- package/dist/boot/shutdown.js +16 -2
- package/dist/boot/stage-02-web-admin-pull.js +3 -0
- package/dist/boot/stage-03-store-backend.d.ts +4 -1
- package/dist/boot/stage-03-store-backend.js +1 -2
- package/dist/boot/stage-04-stores.d.ts +5 -2
- package/dist/boot/stage-04-stores.js +3 -5
- package/dist/boot/stage-05-execution-env.d.ts +5 -2
- package/dist/boot/stage-06-runners.d.ts +5 -2
- package/dist/boot/stage-06-runners.js +1 -3
- package/dist/boot/stage-07-capability-layer.d.ts +5 -2
- package/dist/boot/stage-07-capability-layer.js +5 -1
- package/dist/boot/stage-08-reapers.d.ts +5 -2
- package/dist/boot/stage-09-leader.d.ts +5 -2
- package/dist/boot/stage-10-http-server.d.ts +5 -2
- package/dist/boot/stage-10-http-server.js +9 -1
- package/dist/boot/stores.d.ts +5 -2
- package/dist/boot/stores.js +7 -7
- package/dist/boot/web-admin.d.ts +5 -1
- package/dist/boot/web-admin.js +31 -19
- package/dist/capabilities/hands-lane.d.ts +9 -39
- package/dist/capabilities/hands-lane.js +8 -13
- package/dist/capabilities/mcp-probe.d.ts +3 -2
- package/dist/capabilities/scenarios.d.ts +11 -1
- package/dist/capabilities/scenarios.js +1 -0
- package/dist/capabilities/skills.d.ts +1 -17
- package/dist/capabilities/skills.js +0 -8
- package/dist/capability-facts.d.ts +8 -8
- package/dist/capability-facts.js +4 -4
- package/dist/card-void.d.ts +6 -9
- package/dist/config-catalog.js +1 -1
- package/dist/config-dir-trust.js +58 -29
- package/dist/config-invariants.d.ts +7 -1
- package/dist/config-invariants.js +7 -0
- package/dist/config-provider.d.ts +10 -0
- package/dist/config-provider.js +15 -4
- package/dist/config-root-migration.d.ts +13 -0
- package/dist/config-root-migration.js +99 -0
- package/dist/config-types.d.ts +5 -5
- package/dist/config.d.ts +4 -3
- package/dist/config.js +1 -1
- package/dist/delegated-session-owner.d.ts +5 -3
- package/dist/delegated-session-owner.js +1 -1
- package/dist/device-ws-protocol.d.ts +1 -0
- package/dist/device-ws-protocol.js +2 -1
- package/dist/env-model-table-gate.d.ts +53 -0
- package/dist/env-model-table-gate.js +53 -0
- package/dist/fleet/fleet-bus.d.ts +28 -52
- package/dist/fleet/fleet-bus.js +35 -100
- package/dist/fleet/fleet-reconciler.d.ts +4 -4
- package/dist/fleet/fleet-reconciler.js +1 -1
- package/dist/fleet/subagent-tail-bus.d.ts +14 -11
- package/dist/fleet/subagent-tail-bus.js +15 -28
- package/dist/hooks/hook-runner.js +28 -7
- package/dist/host-decision.d.ts +54 -20
- package/dist/host-decision.js +42 -14
- package/dist/http/admission.d.ts +1 -1
- package/dist/http/admission.js +7 -3
- package/dist/http/approval-sweeps.js +11 -8
- package/dist/http/caller-trust.d.ts +1 -1
- package/dist/http/durable-run-leg.js +2 -2
- package/dist/http/engine-refusal.d.ts +16 -0
- package/dist/http/engine-refusal.js +17 -0
- package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
- package/dist/http/memory-compliance-failure-reply.js +4 -6
- package/dist/http/owner-access.d.ts +11 -0
- package/dist/http/owner-access.js +13 -0
- package/dist/http/resume-legs.d.ts +21 -7
- package/dist/http/resume-legs.js +71 -49
- package/dist/http/resume-restatement.d.ts +39 -0
- package/dist/http/resume-restatement.js +25 -0
- package/dist/http/route-ctx.d.ts +35 -10
- package/dist/http/route-ctx.js +1 -1
- package/dist/http/routes/approvals-assistant.d.ts +9 -1
- package/dist/http/routes/approvals-assistant.js +142 -31
- package/dist/http/routes/capabilities.js +11 -2
- package/dist/http/routes/memory-policy.d.ts +2 -2
- package/dist/http/routes/memory-policy.js +1 -1
- package/dist/http/routes/notify-wake.js +23 -25
- package/dist/http/routes/rules.js +48 -13
- package/dist/http/routes/runs.js +9 -8
- package/dist/http/routes/session-memory-sync.d.ts +34 -6
- package/dist/http/routes/session-memory-sync.js +27 -10
- package/dist/http/routes/session-sync.js +13 -20
- package/dist/http/routes/sessions.d.ts +2 -2
- package/dist/http/routes/sessions.js +22 -23
- package/dist/http/routes/tasks.js +27 -49
- package/dist/http/server.d.ts +13 -7
- package/dist/http/server.js +2 -2
- package/dist/http/stored-body-replay.js +15 -4
- package/dist/http/wire-types.d.ts +17 -7
- package/dist/leader/leader.js +5 -2
- package/dist/leader/wire.d.ts +7 -1
- package/dist/leader/wire.js +6 -5
- package/dist/leg-facts.d.ts +37 -0
- package/dist/leg-facts.js +15 -0
- package/dist/memory-bundle-engine.d.ts +1 -4
- package/dist/memory-bundle-engine.js +2 -2
- package/dist/memory-operator-faces.d.ts +30 -66
- package/dist/memory-operator-faces.js +17 -88
- package/dist/memory-scope-stores.d.ts +38 -40
- package/dist/memory-scope-stores.js +51 -36
- package/dist/memory-scope.d.ts +0 -9
- package/dist/memory-scope.js +0 -9
- package/dist/memory-sync-client.js +14 -3
- package/dist/memory-sync.d.ts +20 -4
- package/dist/memory-sync.js +23 -4
- package/dist/model-plane-face.d.ts +13 -7
- package/dist/model-plane-face.js +2 -1
- package/dist/observability/fail-open.d.ts +20 -4
- package/dist/observability/fail-open.js +17 -4
- package/dist/observability/run-terminal.js +4 -0
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
- package/dist/orchestration/workflow-completion-inbox.js +0 -12
- package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
- package/dist/orchestration/workflow-park-decision.d.ts +8 -3
- package/dist/orchestration/workflow-park-decision.js +5 -3
- package/dist/org-delivery-gate.d.ts +11 -0
- package/dist/org-delivery-gate.js +19 -1
- package/dist/parked-decide.d.ts +13 -9
- package/dist/parked-decide.js +7 -7
- package/dist/parked-revive-surface.js +1 -1
- package/dist/peer-inbound-face.d.ts +127 -0
- package/dist/peer-inbound-face.js +158 -0
- package/dist/permission-rule-list.d.ts +13 -9
- package/dist/permission-rule-list.js +7 -3
- package/dist/plugins/approval-ask-store-file.js +18 -6
- package/dist/plugins/approval-ask-store-memory.js +8 -6
- package/dist/plugins/approval-ask-store-sql.d.ts +28 -13
- package/dist/plugins/approval-ask-store-sql.js +30 -11
- package/dist/plugins/checkpoint-store-sql.d.ts +4 -4
- package/dist/plugins/checkpoint-store-sql.js +9 -7
- package/dist/plugins/file-history-store-sql.d.ts +24 -18
- package/dist/plugins/file-history-store-sql.js +24 -19
- package/dist/plugins/file-run-store.d.ts +29 -0
- package/dist/plugins/file-run-store.js +97 -8
- package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
- package/dist/plugins/imported-layer-root-retirement.js +96 -0
- package/dist/plugins/local-checkpoint-store.d.ts +1 -1
- package/dist/plugins/local-checkpoint-store.js +2 -2
- package/dist/plugins/local-session-store.d.ts +6 -2
- package/dist/plugins/local-session-store.js +8 -2
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
- package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
- package/dist/plugins/mailbox-store-sql.d.ts +12 -8
- package/dist/plugins/mailbox-store-sql.js +76 -7
- package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
- package/dist/plugins/memory-sync-store-pg.js +21 -0
- package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
- package/dist/plugins/memory-sync-store-tidb.js +13 -1
- package/dist/plugins/one-time-migrations.d.ts +32 -2
- package/dist/plugins/one-time-migrations.js +106 -3
- package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
- package/dist/plugins/org-delivery-quota-store.js +2 -12
- package/dist/plugins/park-commit-watch.d.ts +1 -1
- package/dist/plugins/permission-rule-store-file.d.ts +10 -1
- package/dist/plugins/permission-rule-store-file.js +15 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
- package/dist/plugins/permission-rule-store-sql.js +35 -2
- package/dist/plugins/remote-env-device.d.ts +8 -11
- package/dist/plugins/remote-env-device.js +6 -3
- package/dist/plugins/remote-env-k8s.d.ts +37 -24
- package/dist/plugins/remote-env-k8s.js +50 -17
- package/dist/plugins/retention-store-sql.d.ts +2 -2
- package/dist/plugins/sandbox-read-floor.d.ts +5 -7
- package/dist/plugins/sandbox-read-floor.js +2 -7
- package/dist/plugins/sql-errors.d.ts +10 -1
- package/dist/plugins/sql-errors.js +13 -0
- package/dist/plugins/store-backend.d.ts +23 -28
- package/dist/plugins/store-backend.js +9 -1
- package/dist/plugins/store-contracts.d.ts +29 -5
- package/dist/plugins/store-contracts.js +4 -1
- package/dist/plugins/task-attachment-store.d.ts +2 -2
- package/dist/plugins/task-attachment-store.js +37 -52
- package/dist/plugins/tool-result-store-sql.d.ts +9 -8
- package/dist/plugins/tool-result-store-sql.js +23 -2
- package/dist/plugins/wiring-section-codec.d.ts +32 -0
- package/dist/plugins/wiring-section-codec.js +55 -0
- package/dist/plugins/workspace-fill.d.ts +1 -1
- package/dist/rule-entry-list.d.ts +3 -2
- package/dist/rule-entry-list.js +5 -1
- package/dist/rules-consent.d.ts +28 -5
- package/dist/rules-consent.js +40 -28
- package/dist/run-local.js +9 -5
- package/dist/runs.d.ts +12 -8
- package/dist/runs.js +6 -7
- package/dist/runtime-governance.d.ts +1 -1
- package/dist/server-secret-env.d.ts +4 -0
- package/dist/session-error-wire.d.ts +12 -0
- package/dist/session-error-wire.js +6 -0
- package/dist/session-incarnation.d.ts +1 -1
- package/dist/session-notify-door.d.ts +106 -0
- package/dist/session-notify-door.js +93 -0
- package/dist/session-sync.d.ts +9 -14
- package/dist/session-sync.js +5 -5
- package/dist/session-workspace.d.ts +3 -13
- package/dist/session-workspace.js +0 -9
- package/dist/standing-answer-grant.d.ts +5 -0
- package/dist/standing-answer-grant.js +10 -0
- package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
- package/dist/stores-sql/session-policy-store-sql.js +3 -13
- package/dist/task-mcp.d.ts +1 -1
- package/dist/task-settings.d.ts +11 -8
- package/dist/task-settings.js +31 -11
- package/dist/tool-approval.d.ts +52 -34
- package/dist/tool-approval.js +66 -57
- package/dist/trace/core-keyset-guard.d.ts +11 -8
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +3 -1
- package/dist/trace/exit-seat.d.ts +7 -1
- package/dist/trace/exit-seat.js +1 -0
- package/dist/trace/ledger-sink.d.ts +2 -2
- package/dist/trace/model-segment.d.ts +55 -25
- package/dist/trace/model-segment.js +101 -33
- package/dist/trace/project.d.ts +7 -4
- package/dist/trace/project.js +6 -0
- package/dist/trace/redact.d.ts +61 -9
- package/dist/trace/redact.js +1490 -468
- package/dist/trace/self-minted-wire.d.ts +510 -38
- package/dist/trace/self-minted-wire.js +165 -17
- package/dist/trace/wire-projection-faces.d.ts +1 -1
- package/dist/trace/wire-projection-faces.js +1 -1
- package/dist/verifier-pause-card.d.ts +33 -5
- package/dist/verifier-pause-card.js +15 -10
- package/dist/web-admin-client/apply-effective.d.ts +12 -16
- package/dist/web-admin-client/apply-effective.js +90 -10
- package/dist/workspace-space.d.ts +19 -3
- package/dist/workspace-space.js +8 -4
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
- package/package.json +3 -2
- package/wire-changes/server@7.143.0.json +1167 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { externalNotificationInboxEntry } from "../../orchestration/workflow-completion-inbox.js";
|
|
2
1
|
import { sendJson, sendError } from "../send.js";
|
|
3
2
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal, sessionCallerOf } from "../principal-gate.js";
|
|
4
3
|
import { ownerlessRowOpen, sessionOwnerCellOf } from "../../security.js";
|
|
5
4
|
import { sendResumeOutcome } from "../route-ctx.js";
|
|
6
5
|
import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
|
|
7
6
|
import * as facts from "../../capability-facts.js";
|
|
7
|
+
import { readResumeRestatement } from "../resume-restatement.js";
|
|
8
8
|
export async function handleNotifyWake(req, res, match, ctx) {
|
|
9
9
|
const { deps } = ctx;
|
|
10
|
-
const { steerableRuns } = ctx.registry;
|
|
11
10
|
const { readJson, rateLimited, quotaExceeded, leaseDenied, safeDecode } = ctx.helpers;
|
|
12
11
|
const { resumeWake } = ctx.legs;
|
|
13
12
|
switch (match.decl.id) {
|
|
@@ -57,11 +56,12 @@ export async function handleNotifyWake(req, res, match, ctx) {
|
|
|
57
56
|
sendError(res, 501, "capability.session_ownership_required", "external notify requires the session-ownership face (sessionStorage.headOf)");
|
|
58
57
|
return;
|
|
59
58
|
}
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
59
|
+
const notifyHead = await owned.sessionStorage.headOf(notifySession);
|
|
60
|
+
if (notifyHead === undefined) {
|
|
62
61
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
63
62
|
return;
|
|
64
63
|
}
|
|
64
|
+
const notifyOwner = notifyHead.owner;
|
|
65
65
|
const notifyOperator = explicitOperatorOk(principal, deps.config);
|
|
66
66
|
if (!notifyOperator && notifyOwner !== principal && !ownerlessRowOpen(await sessionOwnerCellOf(owned.sessionStorage, notifySession, notifyOwner), sessionCallerOf(req, deps))) {
|
|
67
67
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
@@ -75,29 +75,26 @@ export async function handleNotifyWake(req, res, match, ctx) {
|
|
|
75
75
|
...(nb.seq !== undefined ? { seq: nb.seq } : {}),
|
|
76
76
|
...(nb.source !== undefined ? { source: nb.source } : {}),
|
|
77
77
|
};
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
await liveStream.notify(payload, { priority: "next" });
|
|
78
|
+
const out = await ctx.legs.sessionNotify.deliver({ sessionId: notifySession, occupant: notifyHead, ownerlessEntryOwner: principal ?? null }, payload);
|
|
79
|
+
switch (out.delivery) {
|
|
80
|
+
case "live":
|
|
83
81
|
sendJson(res, 200, { sessionId: notifySession, delivery: "live" });
|
|
84
82
|
return;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
83
|
+
case "refused":
|
|
84
|
+
sendError(res, 400, out.code, out.message);
|
|
85
|
+
return;
|
|
86
|
+
case "no_inbox":
|
|
87
|
+
sendError(res, 501, "capability.workflow_store_required", "external notify requires the workflow completion inbox (WORKFLOW_RUN_STORE)");
|
|
88
|
+
return;
|
|
89
|
+
case "parked":
|
|
90
|
+
sendJson(res, 202, { sessionId: notifySession, delivery: "parked", note: "no live stream — parked in the session inbox; drained as a task_notification on the next stream open" });
|
|
91
|
+
return;
|
|
92
|
+
case "withdrawn":
|
|
93
|
+
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
94
|
+
return;
|
|
95
|
+
default:
|
|
96
|
+
return assertNever(out);
|
|
97
97
|
}
|
|
98
|
-
await deps.workflowCompletionInbox.enqueue(externalNotificationInboxEntry(notifySession, notifyOwner ?? principal ?? null, { task_type: "external", ...payload }, Date.now()));
|
|
99
|
-
sendJson(res, 202, { sessionId: notifySession, delivery: "parked", note: "no live stream — parked in the session inbox; drained as a task_notification on the next stream open" });
|
|
100
|
-
return;
|
|
101
98
|
}
|
|
102
99
|
case "session-wake": {
|
|
103
100
|
if (!facts.checkpointPark(deps).available) {
|
|
@@ -121,7 +118,8 @@ export async function handleNotifyWake(req, res, match, ctx) {
|
|
|
121
118
|
sendError(res, 400, "request.field_invalid", "message must be a non-empty string when present");
|
|
122
119
|
return;
|
|
123
120
|
}
|
|
124
|
-
const
|
|
121
|
+
const restatement = readResumeRestatement(wakeBody);
|
|
122
|
+
const out = await resumeWake(wakeSession, wakeBody.message, principal, req, true, undefined, undefined, restatement);
|
|
125
123
|
sendResumeOutcome(res, out, deps.logger);
|
|
126
124
|
return;
|
|
127
125
|
}
|
|
@@ -18,23 +18,33 @@ const RULES_PAGE_DEFAULT = 50;
|
|
|
18
18
|
const RULES_PAGE_MAX = 200;
|
|
19
19
|
const MAX_LAYER_CONTENT_CHARS = 16_384;
|
|
20
20
|
const MAX_LAYERS = 3;
|
|
21
|
+
const CC_IMPORT_ROOTED_LAYERS = IMPORTED_SETTINGS_LAYERS.filter((l) => l !== "userSettings");
|
|
21
22
|
const CcImportPrepareBodySchema = z
|
|
22
23
|
.object({
|
|
23
24
|
layers: z
|
|
24
|
-
.array(z
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
.array(z.discriminatedUnion("layer", [
|
|
26
|
+
z
|
|
27
|
+
.object({
|
|
28
|
+
layer: z.literal("userSettings"),
|
|
29
|
+
path: z.string().min(1).max(1024),
|
|
30
|
+
content: z.string().max(MAX_LAYER_CONTENT_CHARS),
|
|
31
|
+
})
|
|
32
|
+
.strict(),
|
|
33
|
+
z
|
|
34
|
+
.object({
|
|
35
|
+
layer: z.enum(CC_IMPORT_ROOTED_LAYERS),
|
|
36
|
+
path: z.string().min(1).max(1024),
|
|
37
|
+
root: z.string().max(1024),
|
|
38
|
+
content: z.string().max(MAX_LAYER_CONTENT_CHARS),
|
|
39
|
+
})
|
|
40
|
+
.strict(),
|
|
41
|
+
]))
|
|
32
42
|
.min(1)
|
|
33
43
|
.max(MAX_LAYERS),
|
|
34
44
|
})
|
|
35
45
|
.strict();
|
|
36
46
|
function ccImportRootRefused(layer) {
|
|
37
|
-
return !isValidDurableScope({ kind: "project", root: layer.root });
|
|
47
|
+
return layer.root !== undefined && !isValidDurableScope({ kind: "project", root: layer.root });
|
|
38
48
|
}
|
|
39
49
|
const TicketStringSchema = z.string().min(1).max(190).refine((t) => !hasControlOrBidi(t));
|
|
40
50
|
const CcImportRedeemBodySchema = z.object({ ticket: TicketStringSchema }).strict();
|
|
@@ -122,9 +132,21 @@ export async function handleRules(req, res, match, ctx) {
|
|
|
122
132
|
}
|
|
123
133
|
const prepared = await lane.prepareLocalImport(principal, parsed.data.rows);
|
|
124
134
|
if (!prepared.ok) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
switch (prepared.reason) {
|
|
136
|
+
case "too-many-rows":
|
|
137
|
+
case "batch-too-large":
|
|
138
|
+
logger.info("local rule import batch refused", prepared.reason === "too-many-rows" ? { reason: prepared.reason, rows: prepared.rows } : { reason: prepared.reason, bytes: prepared.bytes });
|
|
139
|
+
sendError(res, 413, "request.payload_too_large", "too many rule rows in this batch — split it and import again");
|
|
140
|
+
return;
|
|
141
|
+
case "tightening-write-failed":
|
|
142
|
+
logger.info("local rule import tightening write failed", { detail: prepared.detail, committed: prepared.committed });
|
|
143
|
+
sendRuleWriteFailure(res, "import-tightening", prepared.committed);
|
|
144
|
+
return;
|
|
145
|
+
default: {
|
|
146
|
+
const unknown = prepared;
|
|
147
|
+
throw new Error(`unknown local import refusal: ${JSON.stringify(unknown)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
128
150
|
}
|
|
129
151
|
sendJson(res, 200, { preview: prepared.preview, ...(prepared.ticket !== undefined ? { ticket: prepared.ticket, expiresAtMs: prepared.expiresAtMs } : {}) });
|
|
130
152
|
return;
|
|
@@ -157,7 +179,7 @@ export async function handleRules(req, res, match, ctx) {
|
|
|
157
179
|
if (match.decl.id === "rules-cc-import-prepare") {
|
|
158
180
|
const parsed = CcImportPrepareBodySchema.safeParse(rawBody);
|
|
159
181
|
if (!parsed.success) {
|
|
160
|
-
sendError(res, 400, "request.body_shape", "body must be {layers:[{layer,path,root,content}]}");
|
|
182
|
+
sendError(res, 400, "request.body_shape", "body must be {layers:[{layer:\"userSettings\",path,content} | {layer:\"projectSettings\"|\"localSettings\",path,root,content}]}");
|
|
161
183
|
return;
|
|
162
184
|
}
|
|
163
185
|
const refusedLayer = parsed.data.layers.findIndex(ccImportRootRefused);
|
|
@@ -268,6 +290,19 @@ function sendRuleWriteFailure(res, verb, committed) {
|
|
|
268
290
|
throw new Error(`unknown rule-write committed word: ${JSON.stringify(unknownWord)}`);
|
|
269
291
|
}
|
|
270
292
|
}
|
|
293
|
+
case "import-tightening":
|
|
294
|
+
switch (committed) {
|
|
295
|
+
case "no":
|
|
296
|
+
sendError(res, 503, "state.rule_write_failed", "a deny / ask row of this batch was not written (that row is not in the store); deny / ask rows before it in the batch may already be in force, and the batch's allow rows were withheld (no ticket) — send the same batch again (rows already in force answer no-op)", { committed });
|
|
297
|
+
return;
|
|
298
|
+
case "unknown":
|
|
299
|
+
sendError(res, 503, "state.rule_write_failed", "a deny / ask row of this batch may or may not have landed; deny / ask rows before it in the batch may already be in force, and the batch's allow rows were withheld (no ticket) — reconcile with GET /v1/rules, or send the same batch again (rows already in force answer no-op)", { committed });
|
|
300
|
+
return;
|
|
301
|
+
default: {
|
|
302
|
+
const unknownWord = committed;
|
|
303
|
+
throw new Error(`unknown rule-write committed word: ${JSON.stringify(unknownWord)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
271
306
|
case "revoke":
|
|
272
307
|
switch (committed) {
|
|
273
308
|
case "no":
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -13,7 +13,7 @@ import { idemCallerOf, idemKeyOf, inflightKeyOf, submissionIdemOf } from "../ide
|
|
|
13
13
|
import { streamSseLog } from "../sse-log.js";
|
|
14
14
|
import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
15
15
|
import { buildApprovalPreamble, buildApprovalPreambleSseFrames, readClosedWordSeat, storedCardDemandsAPerson } from "../../approval-card.js";
|
|
16
|
-
import { heldAskOf,
|
|
16
|
+
import { heldAskOf, liveHostSource, readDecider, retiredDecisionKeyFacts, DECIDED_BY_NOT_IN_PROOF, POLICY_CANNOT_ALLOW_PERSON_ASK } from "../../host-decision.js";
|
|
17
17
|
import { ASK_DECISION_CONSUMER_ABSENT, approvalPreambleReplays, outcomeOfDecidedRow, presentedApprovalFrameFamily, resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
18
18
|
import { MAX_DECISION_NOTE_CHARS, noteLandedOnRow, noteRecordedReceipt } from "../../approval-ask-machine.js";
|
|
19
19
|
import { askRowEndingCause } from "../../approval-deny-reasons.js";
|
|
@@ -57,6 +57,7 @@ const AskDecisionBodySchema = z
|
|
|
57
57
|
.optional(),
|
|
58
58
|
actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
|
|
59
59
|
decidedBy: z.unknown().optional(),
|
|
60
|
+
decidedVia: z.unknown().optional(),
|
|
60
61
|
})
|
|
61
62
|
.strict();
|
|
62
63
|
const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
|
|
@@ -658,7 +659,7 @@ export async function handleRuns(req, res, match, ctx) {
|
|
|
658
659
|
state: { activeTools: [], nestedStats: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0, anyUnpriced: false } },
|
|
659
660
|
status: "pending",
|
|
660
661
|
createdAt: Date.now(),
|
|
661
|
-
|
|
662
|
+
sourceSessionId: run.sessionId,
|
|
662
663
|
});
|
|
663
664
|
const stillRead = await readResumeCtx(cs.getCtx(run.sessionId), { sessionId: run.sessionId, opener: deps.checkpointCtxSealing });
|
|
664
665
|
if (stillRead.kind === "corrupt" || stillRead.kind === "unsealable") {
|
|
@@ -1320,17 +1321,17 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1320
1321
|
sendError(res, 400, "request.body_shape", retired.error, retired.extra);
|
|
1321
1322
|
return;
|
|
1322
1323
|
}
|
|
1323
|
-
sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy");
|
|
1324
|
+
sendError(res, 400, "request.body_shape", "invalid ask decision body — expected decision=approve|deny plus optional updatedInput / note / idempotencyKey / actor.label / decidedBy / decidedVia");
|
|
1324
1325
|
return;
|
|
1325
1326
|
}
|
|
1326
1327
|
const body = parsedBody.data;
|
|
1327
|
-
const decidedByReading =
|
|
1328
|
+
const decidedByReading = readDecider(body, deps.config.directDoorActive);
|
|
1328
1329
|
switch (decidedByReading.kind) {
|
|
1329
1330
|
case "not_in_proof":
|
|
1330
1331
|
sendError(res, 400, "decided_by_not_in_proof", DECIDED_BY_NOT_IN_PROOF);
|
|
1331
1332
|
return;
|
|
1332
1333
|
case "outside_closed_set":
|
|
1333
|
-
sendError(res, 400, "request.field_invalid",
|
|
1334
|
+
sendError(res, 400, "request.field_invalid", decidedByReading.message);
|
|
1334
1335
|
return;
|
|
1335
1336
|
case "accepted":
|
|
1336
1337
|
break;
|
|
@@ -1339,7 +1340,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1339
1340
|
throw new Error(`unhandled decidedBy reading: ${JSON.stringify(unreachable)}`);
|
|
1340
1341
|
}
|
|
1341
1342
|
}
|
|
1342
|
-
const
|
|
1343
|
+
const decider = decidedByReading.decider;
|
|
1343
1344
|
const hasUpdatedInput = typeof rawBody === "object" && rawBody !== null && "updatedInput" in rawBody;
|
|
1344
1345
|
const taskId = match.params.taskId;
|
|
1345
1346
|
const askId = match.params.askId;
|
|
@@ -1473,7 +1474,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1473
1474
|
sendTerminal(row0);
|
|
1474
1475
|
return;
|
|
1475
1476
|
}
|
|
1476
|
-
if (body.decision === "approve" && policyCannotAnswer(heldAskOf(storedCardDemandsAPerson(row0.cardJson)), decidedBy)) {
|
|
1477
|
+
if (body.decision === "approve" && policyCannotAnswer(heldAskOf(storedCardDemandsAPerson(row0.cardJson)), decider.decidedBy)) {
|
|
1477
1478
|
sendError(res, 400, "request.field_conflict", POLICY_CANNOT_ALLOW_PERSON_ASK, fieldConflictFacts("policy_person_ask", ["decidedBy", "decision"]));
|
|
1478
1479
|
return;
|
|
1479
1480
|
}
|
|
@@ -1485,7 +1486,7 @@ export async function handleRunVerbs(req, res, match, ctx) {
|
|
|
1485
1486
|
decisionActor: actor,
|
|
1486
1487
|
decisionNote: body.note ?? null,
|
|
1487
1488
|
idempotencyKey: body.idempotencyKey ?? null,
|
|
1488
|
-
...(
|
|
1489
|
+
...liveHostSource(decider),
|
|
1489
1490
|
...(body.decision === "approve" && hasUpdatedInput ? { updatedInput: body.updatedInput } : {}),
|
|
1490
1491
|
});
|
|
1491
1492
|
}
|
|
@@ -13,9 +13,14 @@
|
|
|
13
13
|
* R5 可用性 = 能力事实 `sessionMemorySync`(本机同步腿 ∧ 持久 run 账面 ∧ 会话头读),与能力位同读一只;不从后端名推。
|
|
14
14
|
*
|
|
15
15
|
* ── 读集(这个会话读哪几层)────────────────────────────────────────────────────────────────────
|
|
16
|
-
* =
|
|
17
|
-
* `SettledLegMemoryObservation`):`mounted` ⇒ `scopes[].scope` 按服务序;`
|
|
18
|
-
*
|
|
16
|
+
* = 会话**最近一条已结腿**的记忆观测(run 店 `latestMemoryObservationForSession`;规则属主 `store-contracts.ts` 的
|
|
17
|
+
* `SettledLegMemoryObservation`,四态闭集 `LegMemoryObservation`):`mounted` ⇒ `scopes[].scope` 按服务序;`none`(那一腿没配记忆:`no-spec` / `disabled`)⇒ `[]` —— 是值;
|
|
18
|
+
* `memoryless`(S-2855B:那一腿配了记忆而没挂上 —— 它本该读哪几层在挂载失败时不可知,答 `[]` 就是把故障洗成「不读记忆」)与
|
|
19
|
+
* `absent`([ref] 空观测:那一腿的记录里没有观测 —— 因由只说记录里有什么,不推断它停在哪一步)⇒ 读集取不到。**不往更早的腿找**(一条律,一只读法)。
|
|
20
|
+
* 读集取不到 ⇒ 409 `conflict.session_memory_unobserved`,体恒带 `reason`(闭集 {@link SESSION_MEMORY_UNOBSERVED_REASONS},判序 = 读的先后):
|
|
21
|
+
* 会话还没有已结腿 ⇒ `no_settled_run`;那一腿是空观测 ⇒ 它的因由(`run_result_missing` / `run_result_unobserved`);那一腿没挂上记忆 ⇒ `memory_not_mounted`
|
|
22
|
+
* (引擎的两只因由 `mount-failed` / `no-backend` 不在本体上复述 —— 它们在那条 run 的结果里,`GET /v1/runs/:id` 的 `result.effectiveMemoryScopes.reason`);观测在、会话没有叶子 ⇒ `no_leaf`。
|
|
23
|
+
* 句子由 `reason` 定(一张表 {@link SESSION_MEMORY_UNOBSERVED_MESSAGES});有在飞 run 时另带 `activeTaskId`。
|
|
19
24
|
* 路由不重铸 spec:spec 只在受理时铸、不落会话行,重铸 = 重启前后两种答案。
|
|
20
25
|
*
|
|
21
26
|
* ── 门序(编号即执行序)──────────────────────────────────────────────────────────────────────────
|
|
@@ -46,11 +51,34 @@
|
|
|
46
51
|
*/
|
|
47
52
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
48
53
|
import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
|
|
49
|
-
import type
|
|
54
|
+
import { type SettledLegMemoryObservation } from "../../plugins/store-contracts.js";
|
|
50
55
|
/** 收割栅栏的句(本服务铸;码 `session_active` 是引擎报告闭集的成员)。 */
|
|
51
56
|
export declare const SESSION_ACTIVE_SYNC_MESSAGE = "a run in this session is in flight, or the session moved on after the memory it reads was observed (a run was admitted or its transcript advanced) \u2014 wait for it to finish on its own, then sync again (do not cancel it to sync sooner)";
|
|
52
|
-
/**
|
|
53
|
-
|
|
57
|
+
/**
|
|
58
|
+
* 409 `conflict.session_memory_unobserved` 体上的 `reason`(闭集;判序 = 读的先后):会话没有已结腿 → 那一腿的观测取不出读集(空观测的因由,词表属主 = `store-contracts.ts`
|
|
59
|
+
* 的 `MEMORY_OBSERVATION_ABSENT_REASONS`,本表不另写;或引擎观测说配了记忆而没挂上)→ 观测在、会话没有叶子。自铸 wire 值登记 = `trace/self-minted-wire.ts` 的 `sessionMemoryUnobservedReason`。
|
|
60
|
+
*/
|
|
61
|
+
export declare const SESSION_MEMORY_UNOBSERVED_REASONS: readonly ["no_settled_run", "run_result_missing", "run_result_unobserved", "memory_not_mounted", "no_leaf"];
|
|
62
|
+
export type SessionMemoryUnobservedReason = (typeof SESSION_MEMORY_UNOBSERVED_REASONS)[number];
|
|
63
|
+
/** 每个 `reason` 的句子(wire 文案,冻结在 `test/api-error-text-freeze.test.ts` 的常量锚格;句子由 `reason` 定,调用点不另写一句)。 */
|
|
64
|
+
export declare const SESSION_MEMORY_UNOBSERVED_MESSAGES: {
|
|
65
|
+
readonly no_settled_run: "this session has no settled run with a memory observation yet, so there is nothing to say about which memory it reads — run a task in it (or let the run in flight finish), then sync";
|
|
66
|
+
readonly run_result_missing: "the latest settled run in this session has no memory observation on record, and an earlier run is never read in its place — run another task in this session (if that run was refused, fix what refused it first), then sync";
|
|
67
|
+
readonly run_result_unobserved: "the latest settled run in this session has no memory observation on record, and an earlier run is never read in its place — run another task in this session (if that run was refused, fix what refused it first), then sync";
|
|
68
|
+
readonly memory_not_mounted: "memory was configured for the latest settled run in this session but did not mount, so which memory this session reads is unknown, and an earlier run is never read in its place — run another task in this session (if its memory fails to mount again, fix that first), then sync";
|
|
69
|
+
readonly no_leaf: "this session has no transcript yet, so there is no revision to sync from — run a task in it (or let the run in flight finish), then sync";
|
|
70
|
+
};
|
|
71
|
+
/** 第 7 步的读数:读集(观测所在那一腿 + 层键),或取不到的那一臂(叶子臂在调用点判 —— 它读的是头,不是观测)。 */
|
|
72
|
+
export type SessionReadSet = {
|
|
73
|
+
readonly kind: "read";
|
|
74
|
+
readonly taskId: string;
|
|
75
|
+
readonly scopes: readonly string[];
|
|
76
|
+
} | {
|
|
77
|
+
readonly kind: "unobserved";
|
|
78
|
+
readonly reason: Exclude<SessionMemoryUnobservedReason, "no_leaf">;
|
|
79
|
+
};
|
|
80
|
+
/** 读集 = 最近一条已结腿的记忆观测(四态穷举;引擎加一态 / 本服务加一态 ⇒ 编译红):只有 `mounted` 与 `none` 说得出读集。 */
|
|
81
|
+
export declare function sessionReadSetOf(observed: SettledLegMemoryObservation | undefined): SessionReadSet;
|
|
54
82
|
export declare function handleSessionMemorySync(req: IncomingMessage, res: ServerResponse, match: RouteMatch<SessionMemorySyncRouteId>, ctx: RouteCtx): Promise<void>;
|
|
55
83
|
export declare const SESSION_MEMORY_SYNC_ROUTES: readonly [{
|
|
56
84
|
readonly id: "session-memory-sync";
|
|
@@ -7,14 +7,30 @@ import * as facts from "../../capability-facts.js";
|
|
|
7
7
|
import { sessionOwnerCellOf } from "../../security.js";
|
|
8
8
|
import { leafOfAdmittedHead } from "../../session-incarnation.js";
|
|
9
9
|
import { declaredMemorySyncIdentityFlaw, memorySyncIdentityRule } from "../../memory-sync-identity.js";
|
|
10
|
+
import { MEMORY_OBSERVATION_ABSENT_REASONS } from "../../plugins/store-contracts.js";
|
|
10
11
|
export const SESSION_ACTIVE_SYNC_MESSAGE = "a run in this session is in flight, or the session moved on after the memory it reads was observed (a run was admitted or its transcript advanced) — wait for it to finish on its own, then sync again (do not cancel it to sync sooner)";
|
|
11
|
-
export
|
|
12
|
+
export const SESSION_MEMORY_UNOBSERVED_REASONS = ["no_settled_run", ...MEMORY_OBSERVATION_ABSENT_REASONS, "memory_not_mounted", "no_leaf"];
|
|
13
|
+
const LATEST_RUN_UNOBSERVED_MESSAGE = "the latest settled run in this session has no memory observation on record, and an earlier run is never read in its place — run another task in this session (if that run was refused, fix what refused it first), then sync";
|
|
14
|
+
export const SESSION_MEMORY_UNOBSERVED_MESSAGES = {
|
|
15
|
+
no_settled_run: "this session has no settled run with a memory observation yet, so there is nothing to say about which memory it reads — run a task in it (or let the run in flight finish), then sync",
|
|
16
|
+
run_result_missing: LATEST_RUN_UNOBSERVED_MESSAGE,
|
|
17
|
+
run_result_unobserved: LATEST_RUN_UNOBSERVED_MESSAGE,
|
|
18
|
+
memory_not_mounted: "memory was configured for the latest settled run in this session but did not mount, so which memory this session reads is unknown, and an earlier run is never read in its place — run another task in this session (if its memory fails to mount again, fix that first), then sync",
|
|
19
|
+
no_leaf: "this session has no transcript yet, so there is no revision to sync from — run a task in it (or let the run in flight finish), then sync",
|
|
20
|
+
};
|
|
21
|
+
export function sessionReadSetOf(observed) {
|
|
22
|
+
if (observed === undefined)
|
|
23
|
+
return { kind: "unobserved", reason: "no_settled_run" };
|
|
24
|
+
const { taskId, observation } = observed;
|
|
12
25
|
switch (observation.state) {
|
|
13
26
|
case "mounted":
|
|
14
|
-
return observation.scopes.map((s) => s.scope);
|
|
15
|
-
case "memoryless":
|
|
27
|
+
return { kind: "read", taskId, scopes: observation.scopes.map((s) => s.scope) };
|
|
16
28
|
case "none":
|
|
17
|
-
return [];
|
|
29
|
+
return { kind: "read", taskId, scopes: [] };
|
|
30
|
+
case "memoryless":
|
|
31
|
+
return { kind: "unobserved", reason: "memory_not_mounted" };
|
|
32
|
+
case "absent":
|
|
33
|
+
return { kind: "unobserved", reason: observation.reason };
|
|
18
34
|
default:
|
|
19
35
|
return assertNever(observation);
|
|
20
36
|
}
|
|
@@ -101,17 +117,18 @@ export async function handleSessionMemorySync(req, res, match, ctx) {
|
|
|
101
117
|
sendError(res, 400, "request.field_invalid", `target ${memorySyncIdentityRule(flaw)} — send the target engine's memorySyncServe.identity exactly as GET /v1/capabilities publishes it`, { field: "target", reason: flaw });
|
|
102
118
|
return;
|
|
103
119
|
}
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
120
|
+
const readSet = sessionReadSetOf(await runStore.latestMemoryObservationForSession(sessionId));
|
|
121
|
+
if (readSet.kind === "unobserved" || sourceRevision === null) {
|
|
122
|
+
const reason = readSet.kind === "unobserved" ? readSet.reason : "no_leaf";
|
|
106
123
|
const activeTaskId = await runStore.getActiveTaskId(sessionId);
|
|
107
|
-
sendError(res, 409, "conflict.session_memory_unobserved",
|
|
124
|
+
sendError(res, 409, "conflict.session_memory_unobserved", SESSION_MEMORY_UNOBSERVED_MESSAGES[reason], { reason, ...(activeTaskId !== undefined ? { activeTaskId } : {}) });
|
|
108
125
|
return;
|
|
109
126
|
}
|
|
110
|
-
const scopes =
|
|
127
|
+
const { taskId: observedTaskId, scopes } = readSet;
|
|
111
128
|
const quiet = async () => {
|
|
112
129
|
if ((await runStore.getActiveTaskId(sessionId)) !== undefined)
|
|
113
130
|
return false;
|
|
114
|
-
if ((await runStore.latestRunForSession(sessionId))?.taskId !==
|
|
131
|
+
if ((await runStore.latestRunForSession(sessionId))?.taskId !== observedTaskId)
|
|
115
132
|
return false;
|
|
116
133
|
const now = await readHead();
|
|
117
134
|
return now !== undefined && leafOfAdmittedHead(now) === sourceRevision;
|
|
@@ -129,7 +146,7 @@ export async function handleSessionMemorySync(req, res, match, ctx) {
|
|
|
129
146
|
report = reportOf({ target, sourceRevision, scopes, layers: [] });
|
|
130
147
|
}
|
|
131
148
|
else {
|
|
132
|
-
report = await mergedRound(leg, JSON.stringify([sessionId, sourceRevision,
|
|
149
|
+
report = await mergedRound(leg, JSON.stringify([sessionId, sourceRevision, observedTaskId, target]), async () => {
|
|
133
150
|
const round = await leg.syncOnce("session-trigger", { reading: scopes });
|
|
134
151
|
if (!(await quiet()))
|
|
135
152
|
return fenced();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { once } from "node:events";
|
|
2
|
-
import { randomBytes
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, validateFileHistoryExport } from "@sema-agent/core";
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
5
5
|
import { FileHistoryUnreadableError, blobGetterOf, exportSessionManifest, exportedFileHistory, fileHistoryImportFace, replayFileHistory, replayPolicyRecords, wipeSessionAttendants } from "../../session-sync.js";
|
|
@@ -222,6 +222,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
222
222
|
return;
|
|
223
223
|
}
|
|
224
224
|
const fs = blobs.historyStore;
|
|
225
|
+
const exchange = blobs.blobExchange;
|
|
225
226
|
let envelope;
|
|
226
227
|
try {
|
|
227
228
|
envelope = await exportedFileHistory(fs, sessionId);
|
|
@@ -238,7 +239,7 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
238
239
|
}
|
|
239
240
|
let bytes;
|
|
240
241
|
try {
|
|
241
|
-
bytes = await
|
|
242
|
+
bytes = await exchange.get(hash);
|
|
242
243
|
}
|
|
243
244
|
catch (e) {
|
|
244
245
|
if (sendPullUnreadable(req, res, deps.logger, e))
|
|
@@ -268,16 +269,15 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
268
269
|
sendError(res, 400, "request.id_invalid", "blob hash must be 64 lowercase-hex chars");
|
|
269
270
|
return;
|
|
270
271
|
}
|
|
271
|
-
const fs = blobs.historyStore;
|
|
272
272
|
const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
|
|
273
|
-
|
|
274
|
-
sendError(res, 400, "request.hash_mismatch", "blob hash mismatch");
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
const put = await fs.putBlob(hash, new Uint8Array(body));
|
|
273
|
+
const put = await blobs.blobExchange.put(hash, new Uint8Array(body));
|
|
278
274
|
if (!put.ok) {
|
|
279
|
-
if (put.error.
|
|
280
|
-
sendError(res, 413, "blob_too_large_for_sql", put.error.message, { wallSource: put.error.wallSource });
|
|
275
|
+
if (put.error.blobCap !== undefined) {
|
|
276
|
+
sendError(res, 413, "blob_too_large_for_sql", put.error.message, { wallSource: put.error.blobCap.wallSource });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (put.error.code === "adopt_failed") {
|
|
280
|
+
sendError(res, 400, "request.hash_mismatch", "blob hash mismatch");
|
|
281
281
|
return;
|
|
282
282
|
}
|
|
283
283
|
sendError(res, 502, put.error.code, "blob store write failed");
|
|
@@ -751,15 +751,8 @@ async function firstMissingBlob(blobFaces, referenced) {
|
|
|
751
751
|
return undefined;
|
|
752
752
|
if (!blobFaces.available)
|
|
753
753
|
return referenced[0];
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
const present = await store.hasBlobs([...referenced]);
|
|
757
|
-
return referenced.find((h) => !present.has(h));
|
|
758
|
-
}
|
|
759
|
-
for (const hash of referenced)
|
|
760
|
-
if (!(await store.getBlob(hash)))
|
|
761
|
-
return hash;
|
|
762
|
-
return undefined;
|
|
754
|
+
const present = await blobFaces.blobExchange.has(referenced);
|
|
755
|
+
return referenced.find((h) => !present.has(h));
|
|
763
756
|
}
|
|
764
757
|
function sendSessionSyncUnavailable(res, why) {
|
|
765
758
|
switch (why) {
|
|
@@ -779,7 +772,7 @@ function sendSessionSyncUnavailable(res, why) {
|
|
|
779
772
|
case "policy_purge_absent":
|
|
780
773
|
sendError(res, 501, "capability.policy_purge_required", "session sync is not available on this deployment: the session-policy store has no deleteBySession purge verb, so an overwrite-dst import could not wipe the destination's stale policy rows. Every in-tree policy store carries it since core 1.423 — upgrade the policy store");
|
|
781
774
|
return;
|
|
782
|
-
case "
|
|
775
|
+
case "blob_exchange_absent":
|
|
783
776
|
sendError(res, 501, "capability.snapshot_store_required", "file-history blob transfer is not available on this deployment: its file-history store has no blob faces (SQL backends only; capabilities.sessionSyncFileHistory is false here). Sync the conversation without fileHistory");
|
|
784
777
|
return;
|
|
785
778
|
default:
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `sse.sseConnections` / `sse.sseOwnerLookups`——`let` 无法跨模块共享自增。判据/时机/配对释放逐字未动。
|
|
13
13
|
*/
|
|
14
14
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
15
|
-
import {
|
|
15
|
+
import type { SessionMemoryView } from "../../memory-operator-faces.js";
|
|
16
16
|
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
17
17
|
/** 单域独占状态。每个 server 实例一份——模块级会把连接坑/缓存跨实例串味。 */
|
|
18
18
|
export interface SessionsLocal {
|
|
@@ -42,7 +42,7 @@ export interface SessionsLocal {
|
|
|
42
42
|
* mcp/a2a 按 (scenario, principal) 分键是因为它们的面按人裁,这张面不按人裁,分键只会白扩表)。 */
|
|
43
43
|
memoryStatusCache: Map<string, {
|
|
44
44
|
at: number;
|
|
45
|
-
status: Promise<
|
|
45
|
+
status: Promise<SessionMemoryView>;
|
|
46
46
|
}>;
|
|
47
47
|
}
|
|
48
48
|
export declare function createSessionsLocal(): SessionsLocal;
|
|
@@ -8,13 +8,12 @@ import { recordFailOpen } from "../../observability/fail-open.js";
|
|
|
8
8
|
import { MCP_DIAL_DEADLINE_MS, raceDeadline } from "../mcp-dial-deadline.js";
|
|
9
9
|
import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
|
|
10
10
|
import { readLayerSeat, sentMemoryLayerRefusal } from "../memory-layer-reply.js";
|
|
11
|
-
import { foldPlaneStatuses } from "../../memory-operator-faces.js";
|
|
12
11
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
|
|
13
12
|
import { readWritableSessionPermissionRules } from "../../session-policy-wire.js";
|
|
14
13
|
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders, httpErrorBody } from "../send.js";
|
|
15
14
|
import { fleetViewOf, gatedPrincipal, explicitOperatorOk, sessionCallerOf, sessionOwnerReadable, sendPrincipalRefusal, sendUnauthorized } from "../principal-gate.js";
|
|
16
15
|
import { headReadOf, leafOfAdmittedHead, sessionFaceOfHead } from "../../session-incarnation.js";
|
|
17
|
-
import { runRowReadableOnSessionFace } from "../owner-access.js";
|
|
16
|
+
import { foldOwnerUnreadable, runRowReadableOnSessionFace } from "../owner-access.js";
|
|
18
17
|
import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
|
|
19
18
|
import * as facts from "../../capability-facts.js";
|
|
20
19
|
import { rosterAgentId } from "./agents-roster.js";
|
|
@@ -84,7 +83,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
84
83
|
const readOwnedSessionView = async (sessionAudit, sessionId, readOpts) => {
|
|
85
84
|
const headOf = headReadOf(deps.sessionStorage);
|
|
86
85
|
if (headOf !== undefined) {
|
|
87
|
-
const owner = (await headOf(sessionId))?.owner;
|
|
86
|
+
const owner = (await foldOwnerUnreadable(deps.logger, match.decl.id, sessionId, () => headOf(sessionId)))?.owner;
|
|
88
87
|
if (owner === undefined) {
|
|
89
88
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
90
89
|
return undefined;
|
|
@@ -274,7 +273,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
274
273
|
sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
|
|
275
274
|
return;
|
|
276
275
|
}
|
|
277
|
-
const head = await headOf(headSession);
|
|
276
|
+
const head = await foldOwnerUnreadable(deps.logger, match.decl.id, headSession, () => headOf(headSession));
|
|
278
277
|
if (head === undefined) {
|
|
279
278
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
280
279
|
return;
|
|
@@ -304,7 +303,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
304
303
|
sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
|
|
305
304
|
return;
|
|
306
305
|
}
|
|
307
|
-
const reading = await placementFact.seat.read(placementSession);
|
|
306
|
+
const reading = await foldOwnerUnreadable(deps.logger, match.decl.id, placementSession, () => placementFact.seat.read(placementSession));
|
|
308
307
|
if (reading === undefined) {
|
|
309
308
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
310
309
|
return;
|
|
@@ -322,7 +321,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
322
321
|
sendError(res, 501, "capability.session_store_required", "session events require a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
323
322
|
return;
|
|
324
323
|
}
|
|
325
|
-
const readHead = (id) => evFact.sessionStorage.headOf(id);
|
|
324
|
+
const readHead = (id) => foldOwnerUnreadable(deps.logger, match.decl.id, id, () => evFact.sessionStorage.headOf(id));
|
|
326
325
|
const readOwner = async (id) => (await readHead(id))?.owner;
|
|
327
326
|
const watch = evFact.watch;
|
|
328
327
|
const evSession = safeDecode(match.params.sessionId);
|
|
@@ -541,6 +540,10 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
541
540
|
sendError(res, 400, "request.body_shape", "invalid session memory erase body — expected { requestId: string, allowUnevidenced?: boolean } (no `select`: this face erases exactly this session's own contributions)");
|
|
542
541
|
return;
|
|
543
542
|
}
|
|
543
|
+
if ("scope" in raw) {
|
|
544
|
+
sendError(res, 400, "request.body_shape", "invalid session memory erase body — `scope` is not accepted: this face erases this session's own contributions in every memory store of the deployment, so there is no layer to name; resend without `scope`");
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
544
547
|
if ("select" in raw) {
|
|
545
548
|
sendError(res, 400, "request.body_shape", "invalid session memory erase body — `select` is not accepted on the self-service face: the selector is fixed to { sessionId: <this session> } by the path; the operator face POST /v1/memory/erase takes explicit selectors");
|
|
546
549
|
return;
|
|
@@ -553,17 +556,12 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
553
556
|
sendError(res, 400, "request.field_invalid", "allowUnevidenced must be a boolean when present");
|
|
554
557
|
return;
|
|
555
558
|
}
|
|
556
|
-
const at = readLayerSeat(res, Object.prototype.hasOwnProperty.call(raw, "scope"), raw.scope);
|
|
557
|
-
if (at === null)
|
|
558
|
-
return;
|
|
559
559
|
const withheld = (coreMessage) => deps.logger?.warn?.("session_memory_erase_refused", { sessionId, err: coreMessage });
|
|
560
560
|
try {
|
|
561
561
|
const input = { requestId: raw.requestId, select: { sessionId }, ...(raw.allowUnevidenced !== undefined ? { allowUnevidenced: raw.allowUnevidenced } : {}) };
|
|
562
|
-
sendJson(res, 200, await
|
|
562
|
+
sendJson(res, 200, await faces.erase(input));
|
|
563
563
|
}
|
|
564
564
|
catch (err) {
|
|
565
|
-
if (sentMemoryLayerRefusal(res, err, { kind: "session_owner", withheld }))
|
|
566
|
-
return;
|
|
567
565
|
if (sentMemoryComplianceFailure(res, err, { kind: "session_owner", withheld }, deps.logger))
|
|
568
566
|
return;
|
|
569
567
|
deps.logger?.warn?.("session_memory_erase_failed", { sessionId, err: String(err) });
|
|
@@ -601,21 +599,22 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
601
599
|
return;
|
|
602
600
|
try {
|
|
603
601
|
const { memoryStatusCache } = ctx.local.sessions;
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
602
|
+
const reading = face.readingAt(at);
|
|
603
|
+
const cacheKey = JSON.stringify([sessionId, reading.key]);
|
|
604
|
+
const cached = memoryStatusCache.get(cacheKey);
|
|
605
|
+
let pending;
|
|
606
|
+
if (cached && Date.now() - cached.at <= MEMORY_STATUS_TTL_MS)
|
|
607
|
+
pending = cached.status;
|
|
608
|
+
else {
|
|
609
609
|
if (memoryStatusCache.size > 1024)
|
|
610
610
|
memoryStatusCache.clear();
|
|
611
|
-
|
|
612
|
-
const fresh = { at: Date.now(), status };
|
|
611
|
+
pending = reading.status(sessionId);
|
|
612
|
+
const fresh = { at: Date.now(), status: pending };
|
|
613
613
|
memoryStatusCache.set(cacheKey, fresh);
|
|
614
|
-
|
|
614
|
+
pending.catch(() => { if (memoryStatusCache.get(cacheKey) === fresh)
|
|
615
615
|
memoryStatusCache.delete(cacheKey); });
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const s = foldPlaneStatuses(await Promise.all(face.storesAt(at).map(statusOf)));
|
|
616
|
+
}
|
|
617
|
+
const s = await pending;
|
|
619
618
|
sendJson(res, 200, {
|
|
620
619
|
sessionId,
|
|
621
620
|
autoConsolidationArmed: deps.memoryAutoConsolidationArmed,
|