@sema-agent/server 7.138.0 → 7.139.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 +276 -7
- package/README.md +7 -4
- package/README.zh-CN.md +6 -4
- package/USAGE.md +73 -29
- package/deploy/sema-up/README.md +1 -1
- package/deploy/sema-up/lib/lanes.json +3 -2
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +20 -20
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/arms.js +1 -3
- package/dist/bench/s1/run-firm.js +2 -2
- package/dist/boot/device-lane.d.ts +8 -3
- package/dist/boot/device-lane.js +7 -2
- package/dist/boot/execution-env.d.ts +4 -2
- package/dist/boot/execution-env.js +16 -24
- package/dist/boot/execution-lane.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +3 -2
- package/dist/boot/governance-seams.js +9 -11
- package/dist/boot/memory-consolidation.js +2 -2
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +8 -9
- package/dist/boot/retention-lane.d.ts +5 -5
- package/dist/boot/retention-lane.js +0 -6
- package/dist/boot/runner-deps.d.ts +3 -1
- package/dist/boot/runner-deps.js +2 -1
- package/dist/boot/session-faces.d.ts +0 -3
- package/dist/boot/session-faces.js +15 -8
- package/dist/boot/stage-02-web-admin-pull.js +2 -2
- package/dist/boot/stage-05-execution-env.js +2 -2
- package/dist/boot/stage-06-runners.js +0 -1
- package/dist/boot/stage-09-leader.js +1 -2
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.js +4 -4
- package/dist/boot/web-admin.js +2 -2
- package/dist/capabilities/mcp-probe.js +1 -1
- package/dist/capabilities/scenarios.d.ts +1 -1
- package/dist/capability-facts.d.ts +15 -7
- package/dist/capability-facts.js +8 -3
- package/dist/config-catalog.js +7 -7
- package/dist/config-invariants.d.ts +23 -3
- package/dist/config-invariants.js +42 -0
- package/dist/config-provider.d.ts +19 -24
- package/dist/config-provider.js +27 -12
- package/dist/config-types.d.ts +10 -5
- package/dist/config.d.ts +33 -0
- package/dist/config.js +44 -15
- package/dist/delegated-session-owner.d.ts +2 -2
- package/dist/deployment-shape.d.ts +1 -1
- package/dist/device-enrollment.d.ts +41 -2
- package/dist/device-enrollment.js +27 -12
- package/dist/device-store.d.ts +58 -85
- package/dist/device-store.js +19 -49
- package/dist/device-ws-hub.js +2 -1
- package/dist/env-name-allowlist-knobs.d.ts +5 -4
- package/dist/exec-cut.d.ts +60 -0
- package/dist/exec-cut.js +36 -0
- package/dist/execution-lane-caps.d.ts +19 -0
- package/dist/execution-lane-caps.js +10 -0
- package/dist/execution-placement.d.ts +259 -0
- package/dist/execution-placement.js +199 -0
- package/dist/field-conflict.d.ts +3 -2
- package/dist/field-conflict.js +1 -0
- package/dist/hosted-posture.d.ts +2 -2
- package/dist/http/admission.js +8 -19
- package/dist/http/checkpoint-ctx.d.ts +21 -12
- package/dist/http/checkpoint-ctx.js +4 -2
- package/dist/http/claimed-run.d.ts +1 -1
- package/dist/http/durable-run-leg.js +9 -5
- package/dist/http/execution-context.d.ts +218 -0
- package/dist/http/execution-context.js +330 -0
- package/dist/http/resume-legs.js +50 -20
- package/dist/http/route-ctx.d.ts +14 -8
- package/dist/http/routes/background.d.ts +29 -1
- package/dist/http/routes/background.js +29 -2
- package/dist/http/routes/devices.d.ts +13 -19
- package/dist/http/routes/devices.js +21 -26
- package/dist/http/routes/session-sync.js +110 -15
- package/dist/http/routes/sessions.d.ts +6 -0
- package/dist/http/routes/sessions.js +115 -11
- package/dist/http/routes/side-query.d.ts +12 -4
- package/dist/http/routes/side-query.js +4 -5
- package/dist/http/routes/tasks.js +37 -31
- package/dist/http/server.d.ts +35 -19
- package/dist/http/server.js +11 -0
- package/dist/http/session-inherit-env.d.ts +3 -4
- package/dist/http/session-inherit-env.js +1 -1
- package/dist/http/wire-types.d.ts +8 -12
- package/dist/index.d.ts +1 -0
- package/dist/memory-sync-client.d.ts +53 -14
- package/dist/memory-sync-client.js +196 -116
- package/dist/memory-sync.d.ts +1 -1
- package/dist/memory-sync.js +3 -2
- package/dist/observability/cost-taxonomy.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +12 -0
- package/dist/observability/fail-open.js +12 -0
- package/dist/observability/run-terminal.js +0 -1
- package/dist/orchestration/workflow-notify-journal.js +4 -4
- package/dist/org-memory-admission.d.ts +4 -3
- package/dist/org-memory-admission.js +6 -6
- package/dist/parked-decide.d.ts +34 -9
- package/dist/parked-decide.js +12 -19
- package/dist/parked-revive-surface.d.ts +12 -15
- package/dist/parked-revive-surface.js +3 -6
- package/dist/peer-seat.d.ts +13 -7
- package/dist/peer-seat.js +37 -3
- package/dist/plugins/approval-ask-store-file.d.ts +7 -0
- package/dist/plugins/approval-ask-store-file.js +11 -0
- package/dist/plugins/approval-ask-store-memory.d.ts +6 -0
- package/dist/plugins/approval-ask-store-memory.js +12 -0
- package/dist/plugins/approval-ask-store-sql.d.ts +22 -5
- package/dist/plugins/approval-ask-store-sql.js +30 -12
- package/dist/plugins/caching-session-store.d.ts +6 -2
- package/dist/plugins/caching-session-store.js +9 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +6 -5
- package/dist/plugins/checkpoint-store-sql.js +5 -7
- package/dist/plugins/device-store-sql.d.ts +25 -51
- package/dist/plugins/device-store-sql.js +49 -103
- package/dist/plugins/execution-placement-sql.d.ts +151 -0
- package/dist/plugins/execution-placement-sql.js +223 -0
- package/dist/plugins/file-history-store-sql.d.ts +7 -6
- package/dist/plugins/file-history-store-sql.js +5 -5
- package/dist/plugins/file-run-store.d.ts +6 -1
- package/dist/plugins/file-run-store.js +4 -0
- package/dist/plugins/fork-routing-session-store.d.ts +6 -0
- package/dist/plugins/fork-routing-session-store.js +16 -0
- package/dist/plugins/local-session-store.d.ts +149 -50
- package/dist/plugins/local-session-store.js +382 -107
- package/dist/plugins/memory-key-guards.d.ts +4 -0
- package/dist/plugins/memory-key-guards.js +1 -0
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/one-time-migrations.d.ts +111 -3
- package/dist/plugins/one-time-migrations.js +380 -10
- package/dist/plugins/outcome-ledger-sql.d.ts +2 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -15
- package/dist/plugins/permission-rule-store-sql.js +13 -16
- package/dist/plugins/pg-pool.d.ts +7 -3
- package/dist/plugins/pg-session-storage.d.ts +31 -20
- package/dist/plugins/pg-session-storage.js +120 -52
- package/dist/plugins/remote-env-adb.d.ts +20 -4
- package/dist/plugins/remote-env-adb.js +70 -62
- package/dist/plugins/remote-env-device.d.ts +30 -12
- package/dist/plugins/remote-env-device.js +18 -17
- package/dist/plugins/remote-env-e2b.d.ts +18 -4
- package/dist/plugins/remote-env-e2b.js +41 -42
- package/dist/plugins/remote-env-file-error.d.ts +2 -0
- package/dist/plugins/remote-env-file-error.js +1 -1
- package/dist/plugins/remote-env-host.d.ts +21 -8
- package/dist/plugins/remote-env-host.js +66 -70
- package/dist/plugins/remote-env-k8s.d.ts +11 -16
- package/dist/plugins/remote-env-k8s.js +61 -54
- package/dist/plugins/remote-env-local-docker.d.ts +29 -6
- package/dist/plugins/remote-env-local-docker.js +46 -59
- package/dist/plugins/remote-env-ssh.d.ts +16 -4
- package/dist/plugins/remote-env-ssh.js +72 -56
- package/dist/plugins/remote-shell.d.ts +100 -3
- package/dist/plugins/remote-shell.js +166 -2
- package/dist/plugins/retention-store-sql.d.ts +9 -6
- package/dist/plugins/retention-store-sql.js +7 -1
- package/dist/plugins/run-store-sql.d.ts +13 -3
- package/dist/plugins/run-store-sql.js +5 -0
- package/dist/plugins/schema-consistency.d.ts +15 -5
- package/dist/plugins/schema-consistency.js +14 -8
- package/dist/plugins/schema-drift-sentence.d.ts +27 -0
- package/dist/plugins/schema-drift-sentence.js +38 -0
- package/dist/plugins/session-fork.d.ts +54 -0
- package/dist/plugins/session-fork.js +38 -1
- package/dist/plugins/session-row-sql.d.ts +22 -0
- package/dist/plugins/session-row-sql.js +81 -0
- package/dist/plugins/session-store.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +19 -7
- package/dist/plugins/store-contracts.d.ts +25 -5
- package/dist/plugins/tidb-pool.d.ts +12 -7
- package/dist/plugins/tidb-pool.js +9 -3
- package/dist/plugins/tidb-session-store.d.ts +57 -26
- package/dist/plugins/tidb-session-store.js +118 -50
- package/dist/plugins/tool-result-store-sql.d.ts +3 -2
- package/dist/plugins/tool-result-store-sql.js +18 -10
- package/dist/project-memory.d.ts +9 -15
- package/dist/project-memory.js +5 -4
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +6 -7
- package/dist/request-value-closure.js +1 -3
- package/dist/run-local.d.ts +6 -0
- package/dist/run-local.js +24 -20
- package/dist/runs.d.ts +2 -1
- package/dist/runs.js +3 -1
- package/dist/security.d.ts +38 -12
- package/dist/security.js +8 -2
- package/dist/server-secret-env.d.ts +9 -5
- package/dist/session-children-retire.d.ts +27 -9
- package/dist/session-children-retire.js +9 -5
- package/dist/session-incarnation.d.ts +19 -9
- package/dist/session-incarnation.js +4 -2
- package/dist/session-row.d.ts +114 -5
- package/dist/session-row.js +55 -5
- package/dist/session-titler.d.ts +2 -2
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-cwd.d.ts +6 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/tool-approval.d.ts +6 -0
- package/dist/tool-approval.js +8 -1
- package/dist/trace/project.d.ts +1 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/self-minted-wire.d.ts +111 -2
- package/dist/trace/self-minted-wire.js +38 -1
- package/dist/url-refusal.d.ts +1 -1
- package/dist/web-admin-client/apply-effective.d.ts +17 -3
- package/dist/web-admin-client/apply-effective.js +20 -10
- package/dist/web-admin-client/skills-mcp.js +3 -2
- package/dist/web-admin-client/types.d.ts +6 -0
- package/package.json +3 -3
- package/wire-changes/server@7.139.0.json +348 -0
- package/dist/http/device-placement.d.ts +0 -175
- package/dist/http/device-placement.js +0 -145
|
@@ -18,6 +18,10 @@ import { assertNever, SESSION_NOT_FOUND_MESSAGE } from "../route-ctx.js";
|
|
|
18
18
|
import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
|
|
19
19
|
import { readSessionPinned, readSessionTitle } from "../../session-row.js";
|
|
20
20
|
import { sessionFaceOfHead } from "../../session-incarnation.js";
|
|
21
|
+
import { defaultTaskRegistry } from "@sema-agent/core";
|
|
22
|
+
import { SessionActiveRunError, SessionForkInProgressError, SessionWriterPresentError } from "../../execution-placement.js";
|
|
23
|
+
import { probeSessionChildren, sessionChildrenScope } from "../../session-children-retire.js";
|
|
24
|
+
import { liveChildrenRefusal } from "./background.js";
|
|
21
25
|
export function createSessionSyncLocal() {
|
|
22
26
|
const pendingStagings = new Map();
|
|
23
27
|
const importLeases = new Map();
|
|
@@ -90,7 +94,46 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
90
94
|
const replaySessionRow = async (row, face) => {
|
|
91
95
|
if (Object.keys(row).length === 0)
|
|
92
96
|
return true;
|
|
93
|
-
|
|
97
|
+
if (face === undefined)
|
|
98
|
+
return false;
|
|
99
|
+
const replayed = await sessionStorage.writeSessionRowCells(sessionId, row, { writer: "replay", row: { row: face } });
|
|
100
|
+
switch (replayed.kind) {
|
|
101
|
+
case "written":
|
|
102
|
+
return true;
|
|
103
|
+
case "absent":
|
|
104
|
+
return false;
|
|
105
|
+
default: {
|
|
106
|
+
const unreachable = replayed;
|
|
107
|
+
throw new Error(`session-sync row replay: no answer for the row write outcome ${JSON.stringify(unreachable)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const liveChildrenOf = async (face) => face === undefined
|
|
112
|
+
? []
|
|
113
|
+
: (await probeSessionChildren({ agentStore: deps.backgroundAgentStore, checkpointStore: deps.checkpointStore, registry: defaultTaskRegistry }, sessionId, sessionChildrenScope(face.owner))).liveChildren;
|
|
114
|
+
const activityOf = async (face) => {
|
|
115
|
+
const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
|
|
116
|
+
if (activeTaskId)
|
|
117
|
+
return { kind: "run", activeTaskId };
|
|
118
|
+
const liveChildren = await liveChildrenOf(face);
|
|
119
|
+
return liveChildren.length > 0 ? { kind: "children", liveChildren } : undefined;
|
|
120
|
+
};
|
|
121
|
+
const sendActivity = (activity, moment) => {
|
|
122
|
+
switch (activity.kind) {
|
|
123
|
+
case "run":
|
|
124
|
+
if (moment === "open")
|
|
125
|
+
sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId: activity.activeTaskId });
|
|
126
|
+
else
|
|
127
|
+
sendError(res, 409, "session_active", "session started an active run while the import streamed; sync after it settles", { activeTaskId: activity.activeTaskId });
|
|
128
|
+
return;
|
|
129
|
+
case "children":
|
|
130
|
+
sendError(res, 409, "session_active", "session still has background agents running or waiting on an approval under it; retire them (POST retirePath) or let them finish, then sync", liveChildrenRefusal(sessionId, activity.liveChildren));
|
|
131
|
+
return;
|
|
132
|
+
default: {
|
|
133
|
+
const unhandled = activity;
|
|
134
|
+
throw new Error(`session sync: unknown session activity ${JSON.stringify(unhandled)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
94
137
|
};
|
|
95
138
|
const canReplayHistory = fileHistoryImportFace(historyStore) !== undefined;
|
|
96
139
|
const replayHistory = async (envelope, allowReplace) => {
|
|
@@ -339,15 +382,56 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
339
382
|
if (!payloadVerified)
|
|
340
383
|
digestSaysDiffer = true;
|
|
341
384
|
}
|
|
385
|
+
const placed = gateFace === undefined ? undefined : await sessionStorage.executionPlacement?.read(sessionId);
|
|
386
|
+
if (placed !== undefined) {
|
|
387
|
+
switch (placed.cell.role) {
|
|
388
|
+
case "absent":
|
|
389
|
+
case "mirror":
|
|
390
|
+
break;
|
|
391
|
+
case "writer":
|
|
392
|
+
sendSessionWriter(res);
|
|
393
|
+
return;
|
|
394
|
+
case "forking":
|
|
395
|
+
sendError(res, 409, "conflict.fork_in_progress", new SessionForkInProgressError(sessionId).message);
|
|
396
|
+
return;
|
|
397
|
+
default: {
|
|
398
|
+
const unknownRole = placed.cell;
|
|
399
|
+
throw new Error(`session sync: unknown execution placement ${JSON.stringify(unknownRole)}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const activeNow = await activityOf(gateFace);
|
|
404
|
+
if (activeNow !== undefined) {
|
|
405
|
+
sendActivity(activeNow, "open");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
342
408
|
if (relA.relation === "identical" && !digestSaysDiffer) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
409
|
+
const seat = sessionStorage.executionPlacement;
|
|
410
|
+
if (seat !== undefined) {
|
|
411
|
+
if (gateFace === undefined) {
|
|
412
|
+
sendError(res, 409, "conflict", "sync conflict");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const mirrored = await seat.markMirror(sessionId, gateFace);
|
|
416
|
+
switch (mirrored.outcome) {
|
|
417
|
+
case "mirrored":
|
|
418
|
+
break;
|
|
419
|
+
case "writer":
|
|
420
|
+
sendSessionWriter(res);
|
|
421
|
+
return;
|
|
422
|
+
case "gone":
|
|
423
|
+
sendError(res, 409, "conflict", "sync conflict");
|
|
424
|
+
return;
|
|
425
|
+
case "active":
|
|
426
|
+
sendActivity({ kind: "run", activeTaskId: mirrored.activeTaskId }, "open");
|
|
348
427
|
return;
|
|
428
|
+
default: {
|
|
429
|
+
const unhandled = mirrored;
|
|
430
|
+
throw new Error(`session sync: unknown mirror outcome ${JSON.stringify(unhandled)}`);
|
|
349
431
|
}
|
|
350
432
|
}
|
|
433
|
+
}
|
|
434
|
+
if (fileHistory !== null && canReplayHistory) {
|
|
351
435
|
const failed = await replayHistory(fileHistory, resolution === "overwrite-dst");
|
|
352
436
|
if (failed !== undefined) {
|
|
353
437
|
sendFileHistoryReplayFailure(res, failed);
|
|
@@ -380,11 +464,6 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
380
464
|
});
|
|
381
465
|
return;
|
|
382
466
|
}
|
|
383
|
-
const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
|
|
384
|
-
if (activeTaskId) {
|
|
385
|
-
sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId });
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
467
|
const heldLease = importLeases.get(sessionId);
|
|
389
468
|
if (heldLease) {
|
|
390
469
|
const staleSecRaw = deps.config.syncImportLeaseStaleSec;
|
|
@@ -511,8 +590,9 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
511
590
|
let mayPushAtCommit;
|
|
512
591
|
let activeAtCommit;
|
|
513
592
|
try {
|
|
514
|
-
|
|
515
|
-
|
|
593
|
+
const gateAtCommit = await judgeSessionGate();
|
|
594
|
+
mayPushAtCommit = gateAtCommit.mayPush;
|
|
595
|
+
activeAtCommit = mayPushAtCommit ? await activityOf(gateAtCommit.face) : undefined;
|
|
516
596
|
}
|
|
517
597
|
catch (e) {
|
|
518
598
|
await pending.handle.abort().catch(() => undefined);
|
|
@@ -525,10 +605,10 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
525
605
|
sendSessionAbsent();
|
|
526
606
|
return;
|
|
527
607
|
}
|
|
528
|
-
if (activeAtCommit) {
|
|
608
|
+
if (activeAtCommit !== undefined) {
|
|
529
609
|
await pending.handle.abort().catch(() => undefined);
|
|
530
610
|
cleanupStaging(stagingId);
|
|
531
|
-
|
|
611
|
+
sendActivity(activeAtCommit, "streamed");
|
|
532
612
|
return;
|
|
533
613
|
}
|
|
534
614
|
try {
|
|
@@ -567,6 +647,18 @@ export async function handleSessionSync(req, res, match, ctx) {
|
|
|
567
647
|
sendError(res, 409, "conflict", "sync conflict", { relation: e.relation });
|
|
568
648
|
return;
|
|
569
649
|
}
|
|
650
|
+
if (e instanceof SessionWriterPresentError) {
|
|
651
|
+
sendSessionWriter(res);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (e instanceof SessionForkInProgressError) {
|
|
655
|
+
sendError(res, 409, "conflict.fork_in_progress", e.message);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (e instanceof SessionActiveRunError) {
|
|
659
|
+
sendActivity({ kind: "run", activeTaskId: e.activeTaskId }, "streamed");
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
570
662
|
if (sendSessionPolicyError(res, e))
|
|
571
663
|
return;
|
|
572
664
|
if (sendSessionError(res, e, "import")) {
|
|
@@ -635,6 +727,9 @@ function sendPullUnreadable(req, res, logger, e) {
|
|
|
635
727
|
}
|
|
636
728
|
}
|
|
637
729
|
}
|
|
730
|
+
function sendSessionWriter(res) {
|
|
731
|
+
sendError(res, 409, "conflict.session_writer", "this session is written on this deployment (its execution placement is a writer here) — an import cannot turn it into a read-only mirror; fork it to push your copy, or pull instead; nothing was written");
|
|
732
|
+
}
|
|
638
733
|
function sendMissingBlob(res) {
|
|
639
734
|
sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
|
|
640
735
|
}
|
|
@@ -74,6 +74,12 @@ export declare const SESSIONS_ROUTES: readonly [{
|
|
|
74
74
|
readonly pattern: RegExp;
|
|
75
75
|
readonly label: "/v1/sessions/:id/$verb";
|
|
76
76
|
readonly methods: readonly ["GET"];
|
|
77
|
+
}, {
|
|
78
|
+
readonly id: "session-execution-placement";
|
|
79
|
+
readonly pattern: RegExp;
|
|
80
|
+
readonly label: "/v1/sessions/:id/execution-placement";
|
|
81
|
+
readonly credentialGated: readonly ["GET"];
|
|
82
|
+
readonly methods: readonly ["GET"];
|
|
77
83
|
}, {
|
|
78
84
|
readonly id: "session-events";
|
|
79
85
|
readonly pattern: RegExp;
|
|
@@ -18,6 +18,10 @@ import * as facts from "../../capability-facts.js";
|
|
|
18
18
|
import { rosterAgentId } from "./agents-roster.js";
|
|
19
19
|
import { projectSessionListRow } from "./sessions-list.js";
|
|
20
20
|
import { readSessionRowPatch } from "../../session-row.js";
|
|
21
|
+
import { forkPathOf } from "../execution-context.js";
|
|
22
|
+
import { executionPlacementView } from "../../execution-placement.js";
|
|
23
|
+
import { ForkTargetUnsettledError, forkKeyOf } from "../../plugins/session-fork.js";
|
|
24
|
+
import { fieldConflictFacts } from "../../field-conflict.js";
|
|
21
25
|
import { buildBoundedToolNames } from "../../panel-tool-names.js";
|
|
22
26
|
import { projectMcpAuthorization } from "../../mcp-authorization-face.js";
|
|
23
27
|
const MCP_STATUS_TTL_MS = 5_000;
|
|
@@ -172,7 +176,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
172
176
|
mcpStatusCache.clear();
|
|
173
177
|
const panel = (async () => {
|
|
174
178
|
const stopDial = new AbortController();
|
|
175
|
-
const matP = materializeMcpTools(specs, principal, undefined, undefined, undefined, deps.mcpRevocations, undefined, undefined, undefined, stopDial.signal, undefined, deps.peerDesk.seat
|
|
179
|
+
const matP = materializeMcpTools(specs, principal, undefined, undefined, undefined, deps.mcpRevocations, undefined, undefined, undefined, stopDial.signal, undefined, deps.peerDesk.seat);
|
|
176
180
|
const mat = await raceDeadline(matP, MCP_DIAL_DEADLINE_MS).catch(() => null);
|
|
177
181
|
if (!mat) {
|
|
178
182
|
stopDial.abort();
|
|
@@ -221,7 +225,7 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
221
225
|
if (a2aStatusCache.size > 1024)
|
|
222
226
|
a2aStatusCache.clear();
|
|
223
227
|
const panel = (async () => {
|
|
224
|
-
const matP = materializeA2aTools(specs, principal, undefined, undefined, deps.peerDesk.seat
|
|
228
|
+
const matP = materializeA2aTools(specs, principal, undefined, undefined, deps.peerDesk.seat);
|
|
225
229
|
let timer;
|
|
226
230
|
const mat = await Promise.race([matP.catch(() => null), new Promise((r) => { timer = setTimeout(() => r(null), A2A_MATERIALIZE_TIMEOUT_MS); })]);
|
|
227
231
|
if (timer)
|
|
@@ -286,6 +290,29 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
286
290
|
return;
|
|
287
291
|
}
|
|
288
292
|
}
|
|
293
|
+
{
|
|
294
|
+
if (match.decl.id === "session-execution-placement") {
|
|
295
|
+
const placementFact = facts.executionPlacementRead(deps);
|
|
296
|
+
if (!placementFact.available) {
|
|
297
|
+
sendError(res, 501, "capability.session_store_required", "the execution placement read requires a durable session store (DB_BACKEND=mysql|pg|local)");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const placementSession = safeDecode(match.params.sessionId);
|
|
301
|
+
if (placementSession === null) {
|
|
302
|
+
sendError(res, 400, "request.path_malformed", "session id segment is not valid percent-encoding");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const reading = await placementFact.seat.read(placementSession);
|
|
306
|
+
if (reading === undefined) {
|
|
307
|
+
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (!explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config) && !sessionOwnerOk(req, res, await sessionOwnerCellOf(deps.sessionStorage, placementSession, reading.occupant.owner)))
|
|
311
|
+
return;
|
|
312
|
+
sendJson(res, 200, executionPlacementView(reading.cell));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
289
316
|
{
|
|
290
317
|
if (match.decl.id === "session-events") {
|
|
291
318
|
const evFact = facts.sessionEventStream(deps);
|
|
@@ -675,13 +702,25 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
675
702
|
sendError(res, 400, read.problem.code, read.problem.message);
|
|
676
703
|
return;
|
|
677
704
|
}
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
705
|
+
const outcome = await store.writeSessionRowCells(sessionId, read.patch, { writer: "client", row: { row: face }, ...(read.expectedRowTag !== undefined ? { rowTag: read.expectedRowTag } : {}) });
|
|
706
|
+
switch (outcome.kind) {
|
|
707
|
+
case "written":
|
|
708
|
+
sendJson(res, 200, projectSessionListRow(outcome.row));
|
|
709
|
+
return;
|
|
710
|
+
case "changed":
|
|
711
|
+
sendError(res, 409, "conflict.session_row_changed", "the session row changed since the rowTag you sent was read — nothing was written; currentRow is the row as it stands now: decide again, then send its rowTag", { currentRow: projectSessionListRow(outcome.row) });
|
|
712
|
+
return;
|
|
713
|
+
case "mirror":
|
|
714
|
+
sendError(res, 409, "conflict.session_mirror", "this session is a read-only mirror of a session whose writer is elsewhere — it cannot be renamed or pinned here; fork it (forkPath) to continue here; nothing was written", { forkPath: forkPathOf(sessionId) });
|
|
715
|
+
return;
|
|
716
|
+
case "absent":
|
|
717
|
+
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
718
|
+
return;
|
|
719
|
+
default: {
|
|
720
|
+
const unknownOutcome = outcome;
|
|
721
|
+
throw new Error(`PATCH /v1/sessions/:id: no answer for the row write outcome ${JSON.stringify(unknownOutcome)}`);
|
|
722
|
+
}
|
|
682
723
|
}
|
|
683
|
-
sendJson(res, 200, projectSessionListRow(row));
|
|
684
|
-
return;
|
|
685
724
|
}
|
|
686
725
|
{
|
|
687
726
|
if (match.decl.id === "session-fork") {
|
|
@@ -712,12 +751,57 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
712
751
|
return;
|
|
713
752
|
}
|
|
714
753
|
const newOwner = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
|
|
715
|
-
const
|
|
716
|
-
if (
|
|
754
|
+
const keyed = readForkTarget(await readJson(req), sourceId);
|
|
755
|
+
if (keyed.kind === "refused") {
|
|
756
|
+
sendError(res, 400, keyed.errorCode, keyed.message, keyed.facts);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (keyed.kind === "none") {
|
|
760
|
+
const newId = await fork(sourceId, newOwner);
|
|
761
|
+
if (newId === null) {
|
|
762
|
+
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
sendJson(res, 200, { sessionId: newId });
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const forkTo = forkFact.sessionStorage.forkTo?.bind(forkFact.sessionStorage);
|
|
769
|
+
if (forkTo === undefined) {
|
|
770
|
+
sendError(res, 501, "capability.session_store_required", "a keyed fork (forkId + forkNonce) requires a session store that creates the target row atomically (DB_BACKEND=mysql|pg|local)");
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
let outcome;
|
|
774
|
+
try {
|
|
775
|
+
outcome = await forkTo(sourceId, newOwner, { id: keyed.forkId, key: forkKeyOf(sourceId, keyed.forkNonce) });
|
|
776
|
+
}
|
|
777
|
+
catch (e) {
|
|
778
|
+
if (e instanceof ForkTargetUnsettledError) {
|
|
779
|
+
sendError(res, 503, "state.fork_target_unsettled", e.message, { permanence: "transient" });
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
throw e;
|
|
783
|
+
}
|
|
784
|
+
if (outcome === null) {
|
|
717
785
|
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
718
786
|
return;
|
|
719
787
|
}
|
|
720
|
-
|
|
788
|
+
switch (outcome.outcome) {
|
|
789
|
+
case "created":
|
|
790
|
+
sendJson(res, 200, { sessionId: outcome.id });
|
|
791
|
+
return;
|
|
792
|
+
case "replayed":
|
|
793
|
+
sendJson(res, 200, { sessionId: outcome.id, replayed: true });
|
|
794
|
+
return;
|
|
795
|
+
case "occupied":
|
|
796
|
+
if (!outcome.sameOwner) {
|
|
797
|
+
sendError(res, 404, "not_found.session", SESSION_NOT_FOUND_MESSAGE);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
sendError(res, 409, "conflict.fork_id_taken", "forkId already names another session of yours (a different fork, or a session that was not created by this fork request) — pick a new forkId and forkNonce; nothing was forked", { forkId: keyed.forkId });
|
|
801
|
+
return;
|
|
802
|
+
default:
|
|
803
|
+
assertNever(outcome);
|
|
804
|
+
}
|
|
721
805
|
return;
|
|
722
806
|
}
|
|
723
807
|
}
|
|
@@ -853,6 +937,7 @@ export const SESSIONS_ROUTES = [
|
|
|
853
937
|
{ id: "session-mcp", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/mcp$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
|
|
854
938
|
{ id: "session-a2a", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/a2a$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
|
|
855
939
|
{ id: "session-head", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/head$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
|
|
940
|
+
{ id: "session-execution-placement", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/execution-placement$/, label: "/v1/sessions/:id/execution-placement", credentialGated: ["GET"], methods: ["GET"] },
|
|
856
941
|
{ id: "session-events", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/events$/, label: "/v1/sessions/:id/$verb", methods: ["GET"], subscription: true },
|
|
857
942
|
{ id: "session-fork", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/fork$/, label: "/v1/sessions/:id/$verb", methods: ["POST"] },
|
|
858
943
|
{ id: "session-policy-get", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/policy$/, label: "/v1/sessions/:id/$verb", methods: ["GET"] },
|
|
@@ -864,4 +949,23 @@ export const SESSIONS_ROUTES = [
|
|
|
864
949
|
{ id: "session-patch", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)$/, label: "/v1/sessions/:id", methods: ["PATCH"] },
|
|
865
950
|
{ id: "session-delete", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)$/, label: "/v1/sessions/:id", methods: ["DELETE"] },
|
|
866
951
|
];
|
|
952
|
+
function readForkTarget(raw, sourceId) {
|
|
953
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
954
|
+
return { kind: "refused", errorCode: "request.field_invalid", message: "the fork body must be an object { forkId?, forkNonce? }" };
|
|
955
|
+
const { forkId, forkNonce } = raw;
|
|
956
|
+
if (forkId === undefined && forkNonce === undefined)
|
|
957
|
+
return { kind: "none" };
|
|
958
|
+
if (forkId === undefined || forkNonce === undefined) {
|
|
959
|
+
return { kind: "refused", errorCode: "request.field_conflict", message: "forkId and forkNonce come together — send both (a keyed, retryable fork) or neither (a fork under a freshly minted id)", facts: fieldConflictFacts("fork_target", ["forkId", "forkNonce"]) };
|
|
960
|
+
}
|
|
961
|
+
if (typeof forkId !== "string" || !isUuidShape(forkId))
|
|
962
|
+
return { kind: "refused", errorCode: "request.id_invalid", message: "forkId must be a canonical uuid (the id the forked session will have)" };
|
|
963
|
+
if (forkId === sourceId)
|
|
964
|
+
return { kind: "refused", errorCode: "request.field_invalid", message: "forkId must name a new session — it equals the session being forked" };
|
|
965
|
+
if (typeof forkNonce !== "string" || forkNonce.length === 0 || forkNonce.length > FORK_NONCE_MAX_CHARS) {
|
|
966
|
+
return { kind: "refused", errorCode: "request.field_invalid", message: `forkNonce must be a non-empty string of at most ${FORK_NONCE_MAX_CHARS} characters (the caller's key for this fork request; a retry sends the same one)` };
|
|
967
|
+
}
|
|
968
|
+
return { kind: "keyed", forkId, forkNonce };
|
|
969
|
+
}
|
|
970
|
+
const FORK_NONCE_MAX_CHARS = 255;
|
|
867
971
|
//# sourceMappingURL=sessions.js.map
|
|
@@ -8,10 +8,18 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
10
10
|
import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/**
|
|
12
|
+
* [ref] —— core 前置校验异常的分类(400 客户端可改 / 500 我方缺陷):**只读引擎的码**。
|
|
13
|
+
* 引擎角色族的拒(未知模型引用 / 不是模型引用 / 角色词出表 / 角色链给不出模型)一律带码 `config.unknown_role`(`detail.reason` 分臂;
|
|
14
|
+
* core 7.60.0 [ref] 起最后一臂也带码)—— 这是引擎写明的判别位,本路由的 400 只认它;句子不读(修前是一张按英文开头匹配的两行前缀表,
|
|
15
|
+
* 引擎改一个字就会把客户端错误静默翻成 500)。失败方向:别的码 / 没有码一律 500(响亮、可告警、可重试),绝不把不认识的异常猜成客户端错误。
|
|
16
|
+
* 跨仓锚:test/side-query-http.test.ts 调装机 core 的两个导出解析器真抛,拿真错误过本谓词。
|
|
17
|
+
*
|
|
18
|
+
* 🪦 原表还有第三臂 `requires a non-empty messages array`(core `core/side-query.js`)—— 不可达(本路由在调 core 之前就有
|
|
19
|
+
* `!Array.isArray(body.messages) || body.messages.length === 0 ⇒ 400 request.body_shape` 的前拦),已删;前拦一旦被摘,
|
|
20
|
+
* side-query-http 的「空/非数组 messages → 400 request.body_shape」两格先红。
|
|
21
|
+
*/
|
|
22
|
+
export declare function isCorePreflightValidationError(err: unknown): boolean;
|
|
15
23
|
/** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。 */
|
|
16
24
|
export declare function handleSideQuery(req: IncomingMessage, res: ServerResponse, match: RouteMatch<SideQueryRouteId>, ctx: RouteCtx): Promise<void>;
|
|
17
25
|
export declare const SIDE_QUERY_ROUTES: readonly [{
|
|
@@ -4,15 +4,14 @@ import { echoCallerText } from "../../caller-text-echo.js";
|
|
|
4
4
|
import { isAtMentionable, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../../model-select.js";
|
|
5
5
|
import { echoModelRef, modelUnavailableMessage } from "../../model-entry-refusal.js";
|
|
6
6
|
import { redactSecrets } from "../../trace/redact.js";
|
|
7
|
+
import { errorCodeOf } from "../../observability/err-text.js";
|
|
7
8
|
import { admitProviderEcho, redactErrorMessage } from "../../observability/run-terminal.js";
|
|
8
9
|
import { sendJson, sendError, sendHttpError, msg } from "../send.js";
|
|
9
10
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
10
11
|
import { assertNever } from "../route-ctx.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return CORE_PREFLIGHT_VALIDATION_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
12
|
+
export function isCorePreflightValidationError(err) {
|
|
13
|
+
return errorCodeOf(err) === "config.unknown_role";
|
|
14
14
|
}
|
|
15
|
-
export const CORE_PREFLIGHT_VALIDATION_ANCHORS = CORE_PREFLIGHT_VALIDATION_PREFIXES;
|
|
16
15
|
const MODEL_ROLE_ECHO_CHARS = 80;
|
|
17
16
|
function isModelRole(value) {
|
|
18
17
|
return typeof value === "string" && MODEL_ROLES.includes(value);
|
|
@@ -105,7 +104,7 @@ export async function handleSideQuery(req, res, match, ctx) {
|
|
|
105
104
|
deps.metrics?.inc?.("side_query_total", { result: "error" });
|
|
106
105
|
if (!res.writableEnded && !ac.signal.aborted) {
|
|
107
106
|
const m = msg(e);
|
|
108
|
-
const isValidation = isCorePreflightValidationError(
|
|
107
|
+
const isValidation = isCorePreflightValidationError(e);
|
|
109
108
|
sendError(res, isValidation ? 400 : 500, isValidation ? "request.field_invalid" : "internal.error", redactSecrets(m));
|
|
110
109
|
}
|
|
111
110
|
}
|