@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
package/dist/http/resume-legs.js
CHANGED
|
@@ -6,9 +6,6 @@ import { parkDecisionFingerprint } from "../orchestration/workflow-park-decision
|
|
|
6
6
|
import { headReadOf } from "../session-incarnation.js";
|
|
7
7
|
import { createSessionWorkspaceRecorder, legSessionWitnessOf, sessionWorkspaceStoreOf } from "../session-workspace.js";
|
|
8
8
|
const DELIVERY_NOT_RECORDED = "workflow park decide: this delivery no longer holds the host session (its claim was released) — the decision was not recorded and this delivery was not started";
|
|
9
|
-
const headUnavailable = async (sessionId) => {
|
|
10
|
-
throw new Error(`parked decide cannot verify which occupant session ${sessionId} belongs to: this deployment's session store has no head read (headOf; every built-in store has one) — refusing rather than resuming unverified`);
|
|
11
|
-
};
|
|
12
9
|
import { HttpError, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
|
|
13
10
|
import { graftParkToolCallId, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
|
|
14
11
|
import { failureCodeOf, nestedPauseOf, sealResultForExit, outwardPlane } from "../terminal.js";
|
|
@@ -50,7 +47,7 @@ import { createImagesLocal } from "./routes/images.js";
|
|
|
50
47
|
import { createCapabilitiesLocal } from "./routes/capabilities.js";
|
|
51
48
|
import { gatedPrincipal, explicitOperatorOk, runIdentityOf } from "./principal-gate.js";
|
|
52
49
|
import { buildActiveRunConflict, resumeEntryForGate } from "./active-run-conflict.js";
|
|
53
|
-
import {
|
|
50
|
+
import { commitExecutionContext, isRootSessionGoneError, resolveExecutionContext, rootSessionAbsentAnswer, stampSpecRoot } from "./execution-context.js";
|
|
54
51
|
import { commitSessionInheritEnv } from "./session-inherit-env.js";
|
|
55
52
|
import { ASK_USER_QUESTION_TOOL_NAME, isApprovalGateKind, isEngineQuestionRow } from "../approval-content-kind.js";
|
|
56
53
|
import { deploymentListAsks } from "../approval.js";
|
|
@@ -102,6 +99,7 @@ const WORKER_SWAP_REDEEMABLE = {
|
|
|
102
99
|
unknown_model_ref: true,
|
|
103
100
|
not_a_model_ref: true,
|
|
104
101
|
unknown_role_word: true,
|
|
102
|
+
no_model_for_role: true,
|
|
105
103
|
};
|
|
106
104
|
function checkpointRowRedeemableElsewhere(reason) {
|
|
107
105
|
return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
|
|
@@ -416,10 +414,30 @@ export function createResumeLegs(ctx) {
|
|
|
416
414
|
checkpointStore: cs,
|
|
417
415
|
registry: defaultTaskRegistry,
|
|
418
416
|
reviveSurface: parkedSurface,
|
|
419
|
-
headOf: headReadOf(deps.sessionStorage) ?? headUnavailable,
|
|
420
417
|
...(deps.parkedKnownAgentTypes ? { knownAgentTypes: deps.parkedKnownAgentTypes } : {}),
|
|
421
418
|
...(deps.parkedReviveInheritedGate ? { rebuildInheritedGate: deps.parkedReviveInheritedGate } : {}),
|
|
422
419
|
...(deps.toolApproval ? { approverSeatFor: windowZeroSeatOf(deps.toolApproval) } : {}),
|
|
420
|
+
execution: {
|
|
421
|
+
resolve: async (root) => {
|
|
422
|
+
let ctx;
|
|
423
|
+
try {
|
|
424
|
+
ctx = await resolveExecutionContext(deps, { kind: "continuation", root, principal: decodeCheckpointScope(cp.scope) });
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
if (isRootSessionGoneError(e))
|
|
428
|
+
return { ok: false, gone: true };
|
|
429
|
+
if (e instanceof HttpError)
|
|
430
|
+
return { ok: false, gone: false, answer: resumeRefusalWithCard(resumeBlockedByPolicyAnswer(httpErrorRefusal(e)), true) };
|
|
431
|
+
throw e;
|
|
432
|
+
}
|
|
433
|
+
const occupant = ctx.occupant;
|
|
434
|
+
if (occupant === undefined) {
|
|
435
|
+
throw new Error(`parked decide: root session ${root} resolved without an occupant reading — the parked revive leg is wired to a session store without an execution placement seat (sessionStorage.executionPlacement), which createHttpServer should have refused at startup; refusing rather than resuming with nobody judging the root session's occupant after the claim`);
|
|
436
|
+
}
|
|
437
|
+
return { ok: true, ctx: { ...ctx, occupant } };
|
|
438
|
+
},
|
|
439
|
+
commitDeps: deps,
|
|
440
|
+
},
|
|
423
441
|
...(deps.logger ? { warn: parkWarn } : {}),
|
|
424
442
|
}, { ...parkedReq, selectedGate: captureSelectedGate() }, { handle: origin.handle, row: origin.row }));
|
|
425
443
|
void ctxPromise.catch(() => { });
|
|
@@ -520,7 +538,7 @@ export function createResumeLegs(ctx) {
|
|
|
520
538
|
hostDecision,
|
|
521
539
|
};
|
|
522
540
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
523
|
-
const driven = await driveResumeIntoRunLog({ peerLease: resolved.peerLease, runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, commitment: { kind: "gate_resolved", ...captureSelectedGate() }, verifyRounds, resumeCtx: { sessionId, ctx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(boundGrant ? { onResumeCommitted: boundGrant } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}), ...(ownerLeaseAdmittedHere ? { ownerLeaseAdmitted: true } : {}) });
|
|
541
|
+
const driven = await driveResumeIntoRunLog({ peerLease: resolved.peerLease, execution: resolved.execution, runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, commitment: { kind: "gate_resolved", ...captureSelectedGate() }, verifyRounds, resumeCtx: { sessionId, ctx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(boundGrant ? { onResumeCommitted: boundGrant } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}), ...(ownerLeaseAdmittedHere ? { ownerLeaseAdmitted: true } : {}) });
|
|
524
542
|
noteLateSettleIfAccepted(driven);
|
|
525
543
|
return driven;
|
|
526
544
|
}
|
|
@@ -528,17 +546,19 @@ export function createResumeLegs(ctx) {
|
|
|
528
546
|
resolved.peerLease.release();
|
|
529
547
|
}
|
|
530
548
|
}
|
|
531
|
-
const ONE_SHOT_SUBMIT_KEYS = ["resumeAt", "restoreFiles", "acceptPartialRestore", "rewindFilesTo", "
|
|
549
|
+
const ONE_SHOT_SUBMIT_KEYS = ["resumeAt", "restoreFiles", "acceptPartialRestore", "rewindFilesTo", "userAttachments", "images"];
|
|
532
550
|
const ONE_SHOT_SUBMIT_STRIP = Object.fromEntries(ONE_SHOT_SUBMIT_KEYS.map((k) => [k, undefined]));
|
|
533
551
|
const resolveResumeSpec = async (ctx, req, auth, lease) => {
|
|
534
552
|
const engineNotices = [];
|
|
535
|
-
const
|
|
553
|
+
const execution = await resolveExecutionContext(deps, { kind: "continuation", root: auth.sessionId, principal: auth.principal });
|
|
554
|
+
const spec = stampSpecRoot(await deps.resolveSpec({ ...ctx.body, ...ONE_SHOT_SUBMIT_STRIP }, req, auth, {
|
|
536
555
|
leg: "resume",
|
|
537
556
|
onNotice: (n) => engineNotices.push(n),
|
|
538
557
|
...(ctx.unattendedLeg === true ? { unattendedLeg: true } : {}),
|
|
539
558
|
lease,
|
|
540
|
-
|
|
541
|
-
|
|
559
|
+
execution,
|
|
560
|
+
}), execution);
|
|
561
|
+
return { spec, engineNotices, execution };
|
|
542
562
|
};
|
|
543
563
|
const refuseStoredBodyReplay = (sessionId, refusal) => {
|
|
544
564
|
deps.logger?.warn?.("resume_refused_stored_body", { sessionId, ...storedBodyReplayLogFields(refusal) });
|
|
@@ -550,8 +570,8 @@ export function createResumeLegs(ctx) {
|
|
|
550
570
|
return { ok: false, answer: refuseStoredBodyReplay(auth.sessionId, replay) };
|
|
551
571
|
const lease = deps.peerDesk.lease("resume");
|
|
552
572
|
try {
|
|
553
|
-
const { spec, engineNotices } = await resolveResumeSpec(ctx, req, auth, lease);
|
|
554
|
-
return { ok: true, spec, engineNotices, peerLease: lease };
|
|
573
|
+
const { spec, engineNotices, execution } = await resolveResumeSpec(ctx, req, auth, lease);
|
|
574
|
+
return { ok: true, spec, engineNotices, execution, peerLease: lease };
|
|
555
575
|
}
|
|
556
576
|
catch (e) {
|
|
557
577
|
lease.release();
|
|
@@ -583,7 +603,7 @@ export function createResumeLegs(ctx) {
|
|
|
583
603
|
}
|
|
584
604
|
async function driveResumeLeg(args, onAccepted) {
|
|
585
605
|
const resumeLegStartedAt = Date.now();
|
|
586
|
-
const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx } = args;
|
|
606
|
+
const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds, resumeCtx, execution } = args;
|
|
587
607
|
const reparkStatus = outcome.gate === "plan_review" ? "needs_review" : "suspended";
|
|
588
608
|
const commitResumeCtx = () => Promise.resolve().then(() => deps.checkpointStore.putCtx(resumeCtx.sessionId, sealCheckpointCtx({ ...resumeCtx.ctx, writerVersion: serviceVersion() }, deps.checkpointCtxSealing, resumeCtx.sessionId))).catch((e) => {
|
|
589
609
|
deps.metrics?.inc("resume_ctx_not_persisted_total");
|
|
@@ -627,12 +647,14 @@ export function createResumeLegs(ctx) {
|
|
|
627
647
|
if (created.existing) {
|
|
628
648
|
throw new Error(`wake fresh run: createRun observed an existing row for the freshly minted taskId ${freshId} (no idempotency key was sent) — refusing to treat it as a busy session`);
|
|
629
649
|
}
|
|
650
|
+
if (created.sessionAbsent)
|
|
651
|
+
return rootSessionAbsentAnswer();
|
|
630
652
|
if (!created.ok) {
|
|
631
653
|
return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, created.activeTaskId) };
|
|
632
654
|
}
|
|
633
|
-
const placementReject = await
|
|
655
|
+
const placementReject = await commitExecutionContext(deps, { kind: "run", runStore: deps.runStore, taskId: freshId }, execution);
|
|
634
656
|
if (placementReject)
|
|
635
|
-
return
|
|
657
|
+
return placementReject;
|
|
636
658
|
taskId = freshId;
|
|
637
659
|
freshRunClaimed = true;
|
|
638
660
|
}
|
|
@@ -643,11 +665,12 @@ export function createResumeLegs(ctx) {
|
|
|
643
665
|
if (deps.toolApproval && taskId) {
|
|
644
666
|
const toolApproval = deps.toolApproval;
|
|
645
667
|
resumeTaskConfig.onAsk = approvalLeg.unattended
|
|
646
|
-
? toolApproval.boundUnattendedAsk({ owner: principal ?? null, taskId, sessionId }, approvalLeg.unattended)
|
|
668
|
+
? toolApproval.boundUnattendedAsk({ owner: principal ?? null, taskId, sessionId, executionLane: execution.lane }, approvalLeg.unattended)
|
|
647
669
|
: toolApproval.boundAsk({
|
|
648
670
|
owner: principal ?? null,
|
|
649
671
|
taskId,
|
|
650
672
|
sessionId,
|
|
673
|
+
executionLane: execution.lane,
|
|
651
674
|
...(approvalLeg.active ? { legKey, ...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}) } : {}),
|
|
652
675
|
});
|
|
653
676
|
}
|
|
@@ -713,6 +736,11 @@ export function createResumeLegs(ctx) {
|
|
|
713
736
|
claimedRow = freshRunClaimed ? true : await deps.runStore.markResuming(taskId, deps.instanceId ?? "default");
|
|
714
737
|
if (!claimedRow)
|
|
715
738
|
return notResumableAnswer();
|
|
739
|
+
if (!freshRunClaimed) {
|
|
740
|
+
const placementReject = await commitExecutionContext(deps, { kind: "run", runStore: deps.runStore, taskId }, execution);
|
|
741
|
+
if (placementReject)
|
|
742
|
+
return placementReject;
|
|
743
|
+
}
|
|
716
744
|
markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, principal);
|
|
717
745
|
fleetPub?.onStart();
|
|
718
746
|
const rs = deps.runStore;
|
|
@@ -1140,6 +1168,7 @@ export function createResumeLegs(ctx) {
|
|
|
1140
1168
|
owner: principal ?? null,
|
|
1141
1169
|
abortSignal: cancelCtrl.signal,
|
|
1142
1170
|
sessionId,
|
|
1171
|
+
executionLane: execution.lane,
|
|
1143
1172
|
emitCard: createApprovalCardEmitter({ appendDurable: (f) => approvalEmit(f) }),
|
|
1144
1173
|
emitRevoke: (f) => approvalEmit(f),
|
|
1145
1174
|
legKey,
|
|
@@ -1328,7 +1357,7 @@ export function createResumeLegs(ctx) {
|
|
|
1328
1357
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1329
1358
|
const verifyRounds = verifyRoundsFromBody(ctx.body);
|
|
1330
1359
|
const selectedGate = durableRowIdentity("decision-receipt", cp.checkpointId, token);
|
|
1331
|
-
return await driveResumeIntoRunLog({ peerLease: resolved.peerLease, runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, commitment: { kind: "gate_resolved", ...selectedGate }, verifyRounds, resumeCtx: { sessionId, ctx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
1360
|
+
return await driveResumeIntoRunLog({ peerLease: resolved.peerLease, execution: resolved.execution, runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, commitment: { kind: "gate_resolved", ...selectedGate }, verifyRounds, resumeCtx: { sessionId, ctx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
1332
1361
|
}
|
|
1333
1362
|
finally {
|
|
1334
1363
|
resolved.peerLease.release();
|
|
@@ -1367,9 +1396,9 @@ export function createResumeLegs(ctx) {
|
|
|
1367
1396
|
return resolved.answer;
|
|
1368
1397
|
try {
|
|
1369
1398
|
const { spec, engineNotices } = resolved;
|
|
1370
|
-
const hostSpec = { ...spec, objective
|
|
1399
|
+
const hostSpec = { ...spec, objective };
|
|
1371
1400
|
const out = await mintAndDriveDurableRun(routeCtxBase, deps.runStore, {
|
|
1372
|
-
prepared: { spec: hostSpec, runner: runnerFor(hostSpec), auth, body: ctx.body, acceptance: createRunAcceptance({}, { objective }), peerLease: resolved.peerLease, ...(engineNotices.length > 0 ? { engineNotices } : {}) },
|
|
1401
|
+
prepared: { spec: hostSpec, runner: runnerFor(hostSpec), auth, body: ctx.body, acceptance: createRunAcceptance({}, { objective }), peerLease: resolved.peerLease, execution: resolved.execution, ...(engineNotices.length > 0 ? { engineNotices } : {}) },
|
|
1373
1402
|
fleetScope: resumeFleetScope(req, auth),
|
|
1374
1403
|
elicitOwner: principal ?? null,
|
|
1375
1404
|
source: "workflow_parked_decide",
|
|
@@ -1455,6 +1484,7 @@ export function createResumeLegs(ctx) {
|
|
|
1455
1484
|
const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
|
|
1456
1485
|
return await driveResumeIntoRunLog({
|
|
1457
1486
|
peerLease: resolved.peerLease,
|
|
1487
|
+
execution: resolved.execution,
|
|
1458
1488
|
runner: runnerFor(spec),
|
|
1459
1489
|
token,
|
|
1460
1490
|
sessionId: cp.sessionId,
|
|
@@ -1581,7 +1611,7 @@ export function createResumeLegs(ctx) {
|
|
|
1581
1611
|
hostDecision,
|
|
1582
1612
|
};
|
|
1583
1613
|
const selectedGate = durableRowIdentity("decision-receipt", cp.checkpointId, token);
|
|
1584
|
-
const out = await driveResumeIntoRunLog({ peerLease: resolved.peerLease, runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, commitment: { kind: "gate_resolved", ...selectedGate }, verifyRounds, resumeCtx: { sessionId, ctx: rebuildCtx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
1614
|
+
const out = await driveResumeIntoRunLog({ peerLease: resolved.peerLease, execution: resolved.execution, runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, commitment: { kind: "gate_resolved", ...selectedGate }, verifyRounds, resumeCtx: { sessionId, ctx: rebuildCtx }, ...(engineNotices.length > 0 ? { engineNotices } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}), ...(req !== undefined ? { callerInitiated: true } : {}) });
|
|
1585
1615
|
return out;
|
|
1586
1616
|
}
|
|
1587
1617
|
finally {
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ import type { VerifyRoundsSpec } from "./verify-rounds.js";
|
|
|
32
32
|
import type { RunAbort } from "../run-cancel-context.js";
|
|
33
33
|
import type { RunIdentity } from "../identity-source.js";
|
|
34
34
|
import type { PeerLease } from "../peer-seat.js";
|
|
35
|
+
import type { RootedTaskSpec } from "./execution-context.js";
|
|
35
36
|
import type { GateCommitment, GateSettlementEvidence } from "../gate-settlement.js";
|
|
36
37
|
/** 跨域可变状态 = lens1 §C2 实测的那 8 条「运行期登记簿」,一个对象。
|
|
37
38
|
* [ref]:`cancelledViaVerb`(按 taskId 键控的「这次是取消动词」旁表)删除 —— 中止成因改挂在每条腿自己的
|
|
@@ -117,11 +118,13 @@ export interface RouteRequestState {
|
|
|
117
118
|
/** B4①(命名化):`prepareSpec` 的返回形(tasks.ts/runs.ts 两域共同消费的属性面)——原为 6 成员内联匿名对象。
|
|
118
119
|
* 命名后好处不只是过 B4 门:两个消费域现在都能对同一个符号做 `import type` 标注,而不是各自重推断结构。 */
|
|
119
120
|
export interface PreparedTaskSubmission {
|
|
120
|
-
spec
|
|
121
|
+
/** 盖过根的 spec(`http/execution-context.ts` `stampSpecRoot` 是唯一写者):`sessionId` 恒在 = 这次提交的根会话。 */
|
|
122
|
+
spec: RootedTaskSpec;
|
|
121
123
|
/** [ref]:本次提交的执行 Runner —— 场景 hands=none 时是无手孪生(不挂 executionEnvFactory)。
|
|
122
124
|
* 两个消费域一律用它,不再直接摸 `deps.runner`(那正是「场景表态了、执行点没听」的缝)。 */
|
|
123
125
|
runner: Runner;
|
|
124
|
-
|
|
126
|
+
/** authorizer 交出的身份与根(必填席,恒在;续跑家族的内部车道交它从 checkpoint 重建的那一只)。 */
|
|
127
|
+
auth: RequestAuth;
|
|
125
128
|
verify?: VerifyRoundsSpec;
|
|
126
129
|
cascade?: boolean;
|
|
127
130
|
jobId?: string;
|
|
@@ -137,13 +140,11 @@ export interface PreparedTaskSubmission {
|
|
|
137
140
|
*/
|
|
138
141
|
warnings?: readonly EntryProblem[];
|
|
139
142
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* ②本腿不是 fresh 根提交(resume 家族根本不经 `prepareSpec`)。在场 ⇒ 每个 `runStore.createRun(`
|
|
144
|
-
* 成功点**必须**把它交给 `commitPlacement`(唯一写入属主;类级机器门钉住每个调用点都接了)。
|
|
143
|
+
* 这次执行的**执行上下文**(唯一解析器 `http/execution-context.ts` `resolveExecutionContext` 的产物:根 · 住客 · 放置态 · 车道 · device 车道的设备意图)。
|
|
144
|
+
* 提交门在装配之前铸(fresh);`/decide` 的宿主新 turn 交续跑折叠器铸的那一只(continuation)。每个 `runStore.createRun(` 成功点**必须**把它交给
|
|
145
|
+
* 认领之后的唯一写口 `commitExecutionContext`(类级机器门钉住每个调用点都接了)。
|
|
145
146
|
*/
|
|
146
|
-
|
|
147
|
+
execution: import("./execution-context.js").RootedExecutionContext;
|
|
147
148
|
/**
|
|
148
149
|
* 这次提交的**受理座**(`run-acceptance.ts`):标题器只在引擎受理之后起。提交门(`prepareSpec`)随 spec 一起铸;
|
|
149
150
|
* 三条提交腿只喂它(持流的形逐帧 `observe`,终局落行之后 `settle` 一次),不再各自判「什么时候起标题」。
|
|
@@ -226,6 +227,11 @@ export interface WorkflowDecisionDelivery {
|
|
|
226
227
|
/** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
|
|
227
228
|
export interface DriveResumeArgs {
|
|
228
229
|
token: CheckpointToken;
|
|
230
|
+
/**
|
|
231
|
+
* 这条续跑的执行上下文(续跑折叠器经唯一解析器铸的 continuation 形:根 = checkpoint 的会话)。本腿认领之后(wake 的建行 / `markResuming` CAS 赢)
|
|
232
|
+
* 交给唯一写口 `commitExecutionContext`(按解析器那一读的住客再断言 writer(L) / absent 落格;住客换了 ⇒ 释放 + 404)。
|
|
233
|
+
*/
|
|
234
|
+
execution: import("./execution-context.js").RootedExecutionContext;
|
|
229
235
|
/**
|
|
230
236
|
* 🔴 **[ref] 合并复审 F2 [high] —— 「本次续跑真正用的那个体」,由本腿在赢下 CAS 之后写回耐久层**
|
|
231
237
|
* (验真后修;必填 —— `driveResumeIntoRunLog` 今天恰有**四个**调用点:legacy `/decide`(`resumeCheckpoint`)、
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* [ref](core 7.32.0 [ref] 的 server 半场;wire 形 [ref] / [ref] / [ref] 定)—— **会话后台任务的两只读口**:
|
|
3
3
|
*
|
|
4
|
-
* `GET /v1/sessions/:id/background` → 200 `{ rows: [{ id, description?, type, status, createdAt, sessionScoped, retained? }] }`
|
|
4
|
+
* `GET /v1/sessions/:id/background` → 200 `{ rows: [{ id, description?, type, status, createdAt, sessionScoped, retained?, sessionId? }] }`
|
|
5
5
|
* `POST /v1/sessions/:id/background/stop` body `{ includeRetained: boolean }` → 200 `{ receipts: [{ id, outcome }] }`
|
|
6
6
|
*
|
|
7
7
|
* 消费方:cli [ref] ①(删前核登记)/ [ref](退出三选一:「Exit and stop tasks」= `includeRetained:true`,`sema kill` =
|
|
@@ -28,6 +28,16 @@
|
|
|
28
28
|
* oracle);显式 operator 可读可停任何会话(监督动作)。零模型工作 ⇒ 不进 billable、不吃 drain 503;停止口是 mutating ⇒
|
|
29
29
|
* `rateLimited`。能力位 `background: { listFace, exitFaces }` 与两口的 501 从同一个谓词推导(会话属主面在场)。
|
|
30
30
|
*
|
|
31
|
+
* ── 结清口:子代结清只有一只协调器 ─────────────────────────────────────────────────────────────────
|
|
32
|
+
* `POST /v1/sessions/:id/children/retire`(无体)→ 200 `{ children: string[] }`:**显式**结清以本会话为根的后台子代(running 停掉、parked
|
|
33
|
+
* 作废卡并对账翻终态),与 `DELETE /v1/sessions/:id` 同调**一只**预检 `probeSessionChildren` 与**一只**协调器 `retireSessionChildren`
|
|
34
|
+
* (`session-children-retire.ts`)、同一个 scope(`sessionChildrenScope`)。预检有本副本结束不了的 ⇒ 409 `conflict.session_children_alive{remaining}`
|
|
35
|
+
* (与删除门同码同投影,一只都不动);停不净(时序失败)⇒ 与删除门同一失败契约(通用 500,重试收敛)。`children` = 首轮观察到、且已全部
|
|
36
|
+
* 结清的子代,每项经名册的 id 投影 `rosterAgentId`(与换绑 / 导入两门 409 体的 `children` 同形同投影)。它不判根认领(停止口也不判):
|
|
37
|
+
* 根 run 活着时它可再派子代 —— 由换绑 / 导入门写事务内的认领读兜。鉴权 / 限流 / 无凭据写门与停止口同。
|
|
38
|
+
* **停止口的已发语义逐字不动**:它只中止 running 的后台 agent / workflow 并收 shell,停驻子代与待决卡留着 —— 「停别的、留卡待决」与
|
|
39
|
+
* 「把树结清」是两个意图、两只口。
|
|
40
|
+
*
|
|
31
41
|
* 🔴 **停止口在无凭据写门内**(`credentialGated: ["POST"]`;车OL F1 = 7.102.0 合并复审 gpt-6 腿 [high]):门的两个合取项它都中
|
|
32
42
|
* —— ① 授权的唯一输入是 principal 头(属主判 `sessionOwnerOk` 与 operator 臂 `explicitOperatorOk` 读的都是 `gatedPrincipal`,
|
|
33
43
|
* 无 service 凭证的部署上它收调用方自报的头);② 不可撤销、爆炸半径跨租户(知道会话 id + 自报它的属主 ⇒ 停掉那个会话的后台
|
|
@@ -53,12 +63,24 @@ export interface SessionBackgroundRow {
|
|
|
53
63
|
createdAt: number;
|
|
54
64
|
sessionScoped: boolean;
|
|
55
65
|
retained?: true;
|
|
66
|
+
/** 后台 agent 行的子会话 id(引擎绑定之后才有;缺席 ⇒ 键不在)。 */
|
|
67
|
+
sessionId?: string;
|
|
56
68
|
}
|
|
57
69
|
/** 停止口的一条受据(`outcome` 逐字透传 core 闭集)。 */
|
|
58
70
|
export interface SessionBackgroundReceipt {
|
|
59
71
|
id: string;
|
|
60
72
|
outcome: BackgroundShellHardKillReceipt["outcome"];
|
|
61
73
|
}
|
|
74
|
+
/** 结清口的路径(换绑 / 导入两门的子代半 409 以它点名下一步;与执行面拒体带 `forkPath` 同一形)。 */
|
|
75
|
+
export declare function childrenRetirePath(sessionId: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* 子代半命中的拒体两键(换绑 409 `device.rebind_active_run`、导入 409 `session_active` 同一只):`children` = 预检那一次枚举观察到的活子代
|
|
78
|
+
* (`probeSessionChildren` 的 `liveChildren`)经名册 id 投影,`retirePath` = 结清口。
|
|
79
|
+
*/
|
|
80
|
+
export declare function liveChildrenRefusal(sessionId: string, liveChildren: readonly string[]): {
|
|
81
|
+
children: string[];
|
|
82
|
+
retirePath: string;
|
|
83
|
+
};
|
|
62
84
|
export declare function handleBackground(req: IncomingMessage, res: ServerResponse, match: RouteMatch<BackgroundRouteId>, ctx: RouteCtx): Promise<void>;
|
|
63
85
|
export declare const BACKGROUND_ROUTES: readonly [{
|
|
64
86
|
readonly id: "session-background-list";
|
|
@@ -71,6 +93,12 @@ export declare const BACKGROUND_ROUTES: readonly [{
|
|
|
71
93
|
readonly label: "/v1/sessions/:id/background/stop";
|
|
72
94
|
readonly credentialGated: readonly ["POST"];
|
|
73
95
|
readonly methods: readonly ["POST"];
|
|
96
|
+
}, {
|
|
97
|
+
readonly id: "session-children-retire";
|
|
98
|
+
readonly pattern: RegExp;
|
|
99
|
+
readonly label: "/v1/sessions/:id/children/retire";
|
|
100
|
+
readonly credentialGated: readonly ["POST"];
|
|
101
|
+
readonly methods: readonly ["POST"];
|
|
74
102
|
}];
|
|
75
103
|
/** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
|
|
76
104
|
export type BackgroundRouteId = RouteIdsOf<typeof BACKGROUND_ROUTES>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defaultTaskRegistry } from "@sema-agent/core";
|
|
3
3
|
import { reapSessionBackgroundHard } from "../../background-session-reap.js";
|
|
4
|
+
import { probeSessionChildren, retireSessionChildren, sessionChildrenScope, SessionChildrenStopIncompleteError } from "../../session-children-retire.js";
|
|
5
|
+
import { rosterAgentId } from "./agents-roster.js";
|
|
4
6
|
import { redactSecrets } from "../../trace/redact.js";
|
|
5
7
|
import { sendJson, sendError } from "../send.js";
|
|
6
8
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
@@ -20,6 +22,7 @@ const ROW_KEY_DISPOSITION = {
|
|
|
20
22
|
description: "keep",
|
|
21
23
|
toolUseId: "strip",
|
|
22
24
|
updatedAt: "strip",
|
|
25
|
+
sessionId: "keep",
|
|
23
26
|
};
|
|
24
27
|
void ROW_KEY_DISPOSITION;
|
|
25
28
|
function projectRow(r) {
|
|
@@ -31,13 +34,20 @@ function projectRow(r) {
|
|
|
31
34
|
createdAt: r.createdAt,
|
|
32
35
|
sessionScoped: r.sessionScoped,
|
|
33
36
|
...(r.retained === true ? { retained: true } : {}),
|
|
37
|
+
...(r.sessionId !== undefined ? { sessionId: r.sessionId } : {}),
|
|
34
38
|
};
|
|
35
39
|
}
|
|
40
|
+
export function childrenRetirePath(sessionId) {
|
|
41
|
+
return `/v1/sessions/${encodeURIComponent(sessionId)}/children/retire`;
|
|
42
|
+
}
|
|
43
|
+
export function liveChildrenRefusal(sessionId, liveChildren) {
|
|
44
|
+
return { children: liveChildren.map((handle) => rosterAgentId({ handle })), retirePath: childrenRetirePath(sessionId) };
|
|
45
|
+
}
|
|
36
46
|
const StopBodySchema = z.object({ includeRetained: z.boolean() }).strict();
|
|
37
47
|
export async function handleBackground(req, res, match, ctx) {
|
|
38
48
|
const { deps } = ctx;
|
|
39
49
|
const id = match.decl.id;
|
|
40
|
-
if (id === "session-background-stop" && ctx.helpers.rateLimited(req, res))
|
|
50
|
+
if ((id === "session-background-stop" || id === "session-children-retire") && ctx.helpers.rateLimited(req, res))
|
|
41
51
|
return;
|
|
42
52
|
const principal = gatedPrincipal(req, deps.config);
|
|
43
53
|
if (deps.config.requirePrincipal && principal === undefined) {
|
|
@@ -71,7 +81,7 @@ export async function handleBackground(req, res, match, ctx) {
|
|
|
71
81
|
const operator = explicitOperatorOk(principal, deps.config);
|
|
72
82
|
if (!operator && !ctx.helpers.sessionOwnerOk(req, res, await sessionOwnerCellOf(owned.sessionStorage, sessionId, owner)))
|
|
73
83
|
return;
|
|
74
|
-
const scope = owner
|
|
84
|
+
const scope = sessionChildrenScope(owner);
|
|
75
85
|
switch (id) {
|
|
76
86
|
case "session-background-list": {
|
|
77
87
|
const rows = defaultTaskRegistry.list({ owner: sessionId, sessionId, scope }, { limit: LIST_LIMIT }).map(projectRow);
|
|
@@ -83,6 +93,22 @@ export async function handleBackground(req, res, match, ctx) {
|
|
|
83
93
|
sendJson(res, 200, { receipts: receipts.map((r) => ({ id: r.taskId, outcome: r.outcome })) });
|
|
84
94
|
return;
|
|
85
95
|
}
|
|
96
|
+
case "session-children-retire": {
|
|
97
|
+
const sources = { agentStore: deps.backgroundAgentStore, checkpointStore: deps.checkpointStore, registry: defaultTaskRegistry };
|
|
98
|
+
const reach = await probeSessionChildren(sources, sessionId, scope);
|
|
99
|
+
if (!reach.reachable) {
|
|
100
|
+
deps.logger?.warn("session_children_retire_unreachable", { sessionId, remaining: reach.unreachableHandles.length, handles: reach.unreachableHandles });
|
|
101
|
+
sendError(res, 409, "conflict.session_children_alive", "this session still has background agents running where this replica cannot stop them (on another replica, for example), so nothing was retired — `remaining` lists their ids (the same ids `GET /v1/agents/roster` shows); retry once they finish, or stop them where they run", { remaining: reach.unreachableHandles.map((handle) => rosterAgentId({ handle })) });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const out = await retireSessionChildren(sources, sessionId, scope);
|
|
105
|
+
if (out.stopped === "incomplete") {
|
|
106
|
+
deps.logger?.warn("session_children_retire_incomplete", { sessionId, remaining: out.remainingHandles.length, handles: out.remainingHandles });
|
|
107
|
+
throw new SessionChildrenStopIncompleteError(sessionId, out.remainingHandles.length);
|
|
108
|
+
}
|
|
109
|
+
sendJson(res, 200, { children: out.observed.map((handle) => rosterAgentId({ handle })) });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
86
112
|
default:
|
|
87
113
|
return assertNever(id);
|
|
88
114
|
}
|
|
@@ -90,5 +116,6 @@ export async function handleBackground(req, res, match, ctx) {
|
|
|
90
116
|
export const BACKGROUND_ROUTES = [
|
|
91
117
|
{ id: "session-background-list", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/background$/, label: "/v1/sessions/:id/background", methods: ["GET"] },
|
|
92
118
|
{ id: "session-background-stop", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/background\/stop$/, label: "/v1/sessions/:id/background/stop", credentialGated: ["POST"], methods: ["POST"] },
|
|
119
|
+
{ id: "session-children-retire", pattern: /^\/v1\/sessions\/(?<sessionId>[^/]+)\/children\/retire$/, label: "/v1/sessions/:id/children/retire", credentialGated: ["POST"], methods: ["POST"] },
|
|
93
120
|
];
|
|
94
121
|
//# sourceMappingURL=background.js.map
|
|
@@ -8,19 +8,19 @@
|
|
|
8
8
|
* **hub 在场性**判 —— v1 部署形 = 单副本(基线稿 §4.6),汇聚端的活连接集就是本副本的 presence
|
|
9
9
|
* 真源;`name` = displayName,空串诚实缺席;`lastSeenAt` ISO-8601)。**吊销行不列**:revoked 是
|
|
10
10
|
* 终态(重注册 = 新 deviceId),列出来只会被壳当换绑目标然后撞 404。
|
|
11
|
-
* ·
|
|
12
|
-
* `{
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 装配缝加一次店读;独立读面把 device wire 族收在一个文件里,户口与门序自洽。
|
|
16
|
-
* · `POST /v1/devices/sessions/:rootSessionId/rebind` —— 显式换绑动词(v2 稿 §2 门序五步,逐字):
|
|
17
|
-
* ① gatedPrincipal + owner 门(非属主 404 反枚举同串,[ref] 纪律)
|
|
11
|
+
* · 绑定读面在会话族:`GET /v1/sessions/:id/execution-placement`(`routes/sessions.ts`;体 = 执行放置格的 wire 投影,
|
|
12
|
+
* device 车道上是 `{ role: "writer", lane: "device", device: { deviceId, rev } }`)。绑定是根会话行上的执行格,读只有会话店的执行席一只。
|
|
13
|
+
* · `POST /v1/devices/sessions/:rootSessionId/rebind` —— 显式换绑动词(v2 稿 §2 门序五步):
|
|
14
|
+
* ① gatedPrincipal + owner 门 —— 一次行读(会话店执行席:住客 + 格),属主门判**格上**两属主列(非属主 404 反枚举同串,[ref] 纪律)
|
|
18
15
|
* ② 目标设备在场门(不存在 / 不是该域的 / 已吊销 ⇒ 404 **同串** —— 设备 id 也是不可枚举面)
|
|
19
|
-
* ③ 无活跃 run
|
|
20
|
-
* `
|
|
21
|
-
* `device.rebind_active_run` 体带 `activeTaskId`
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
16
|
+
* ③ 无活跃 run 门:池级 `getActiveTaskId` 读一次作**早拒**(零审计行);**保证**在设备店的换绑事务里 —— 同一连接先锁根会话行、
|
|
17
|
+
* 再锁定读 `task_active`,有认领 ⇒ 不写(读写序律;hub 内存在途计数不作门 —— 内存态跨副本不可见);两处同拒 409
|
|
18
|
+
* `device.rebind_active_run` 体带 `activeTaskId`
|
|
19
|
+
* ③′ 子代半(写前一次观察):以根为根的活后台子代(running / parked;`probeSessionChildren` 那一次枚举,scope = 会话属主 `sessionChildrenScope`,
|
|
20
|
+
* 与删除门同读)⇒ 同码 409,体 `children`(名册 id)+ `retirePath`(显式结清口 —— 先结清、再换绑)
|
|
21
|
+
* ④ 前态 CAS(店内单事务:按门那一读的住客 ∧ 格上两属主列 ∧ rev 定址,`execution_rev` 恒变列 + `session_rebound` 审计行;输 = 409
|
|
22
|
+
* `device.rebind_rev_conflict` 体带 `currentRev` —— [ref] currentPending 哲学,壳一跳重定位;门后会话被删并同 id 重登记 ⇒ 住客不符 ⇒ 设备 404,
|
|
23
|
+
* 永不写到新住客的格上)
|
|
24
24
|
* ⑤ 赢 = 200 `{ rootSessionId, deviceId, rev, workspaceCarryover: "none" }`。
|
|
25
25
|
* 幂等形:toDeviceId ≡ 当前 ∧ expectedRev ≡ 当前 rev ⇒ 200 回显不加 rev(真幂等,不写审计)。
|
|
26
26
|
* `workspaceCarryover: "none"` 是 **wire 冻结字面**(v2 稿 §5):换绑只改路由,不搬字节 —— 壳据
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* 才构成一个 device 域;缺 tenant claim 的凭据(纯 principal 头 / 静态 token 直连)**判不出域** ⇒
|
|
35
35
|
* 非 operator 一律响亮 403(fail-closed + loud,[ref]:静默空列表是假健康)——基线稿对 enrollment 的
|
|
36
36
|
* 同向裁定是「缺 tenant/global scope 明确拒注册」。enrollment/管理端点与**首绑写协议**都从
|
|
37
|
-
* 这同一个函数取域(
|
|
37
|
+
* 这同一个函数取域(它住在 `http/execution-context.ts` —— 提交腿的设备准入也要用,放在路由域里是层序倒挂),
|
|
38
38
|
* 不得手抄第二份派生。
|
|
39
39
|
*
|
|
40
40
|
* ── 反枚举 404 的单一铸造点 ──────────────────────────────────────────────────────────────────────
|
|
@@ -81,12 +81,6 @@ export declare const DEVICES_ROUTES: readonly [{
|
|
|
81
81
|
readonly pattern: RegExp;
|
|
82
82
|
readonly label: "/v1/devices/sessions/:id/rebind";
|
|
83
83
|
readonly methods: readonly ["POST"];
|
|
84
|
-
}, {
|
|
85
|
-
readonly id: "device-session";
|
|
86
|
-
readonly pattern: RegExp;
|
|
87
|
-
readonly label: "/v1/devices/sessions/:id";
|
|
88
|
-
readonly credentialGated: readonly ["GET"];
|
|
89
|
-
readonly methods: readonly ["GET"];
|
|
90
84
|
}, {
|
|
91
85
|
readonly prefix: "/v1/devices/";
|
|
92
86
|
readonly credentialGated: readonly ["POST"];
|
|
@@ -3,8 +3,12 @@ import { explicitOperatorOk, gatedPrincipal, sendPrincipalRefusal } from "../pri
|
|
|
3
3
|
import { assertNever } from "../route-ctx.js";
|
|
4
4
|
import * as facts from "../../capability-facts.js";
|
|
5
5
|
import { DEVICE_PLATFORM_OS, DEVICE_PUBKEY_MAX_BYTES, DEVICE_TEXT_LIMITS, assertDeviceIdShape, assertDevicePubkeyShape, assertRootSessionIdShape, isDeviceAbsolutePath, sameDeviceOwner, } from "../../device-store.js";
|
|
6
|
+
import { boundDeviceOf } from "../../execution-placement.js";
|
|
7
|
+
import { defaultTaskRegistry } from "@sema-agent/core";
|
|
8
|
+
import { probeSessionChildren, sessionChildrenScope } from "../../session-children-retire.js";
|
|
9
|
+
import { liveChildrenRefusal } from "./background.js";
|
|
6
10
|
import { DEFAULT_ENROLL_TOKEN_TTL_SEC } from "../../device-enrollment.js";
|
|
7
|
-
import { deviceOwnerDomainOf } from "../
|
|
11
|
+
import { deviceOwnerDomainOf } from "../execution-context.js";
|
|
8
12
|
import { echoKeyName } from "../../request-key-closure.js";
|
|
9
13
|
const DEVICE_NOT_FOUND_MESSAGE = "device not found";
|
|
10
14
|
const NO_DEVICE_DOMAIN_MESSAGE = "device management needs a caller domain: this credential carries no verified tenant scope claim (registry auth-bridge JWT `scope`) and is not an explicit operator — connect through the registry auth bridge, or configure OPERATOR_PRINCIPALS";
|
|
@@ -32,9 +36,6 @@ function deviceListRow(row, online) {
|
|
|
32
36
|
...(row.lastSeenAtMs !== null ? { lastSeenAt: new Date(row.lastSeenAtMs).toISOString() } : {}),
|
|
33
37
|
};
|
|
34
38
|
}
|
|
35
|
-
function boundDeviceOf(row) {
|
|
36
|
-
return { deviceId: row.deviceId, rev: row.rev, boundAt: new Date(row.boundAtMs).toISOString() };
|
|
37
|
-
}
|
|
38
39
|
function sendDeviceRejection(res, rej) {
|
|
39
40
|
if (rej.retryAfterSec !== undefined)
|
|
40
41
|
res.setHeader("retry-after", String(rej.retryAfterSec));
|
|
@@ -112,7 +113,6 @@ function parseEnrollBody(res, body) {
|
|
|
112
113
|
export async function handleDevices(req, res, match, ctx) {
|
|
113
114
|
switch (match.decl.id) {
|
|
114
115
|
case "devices-list":
|
|
115
|
-
case "device-session":
|
|
116
116
|
case "device-session-rebind":
|
|
117
117
|
case "device-enroll-tokens":
|
|
118
118
|
case "device-enroll":
|
|
@@ -248,31 +248,19 @@ export async function handleDevices(req, res, match, ctx) {
|
|
|
248
248
|
return;
|
|
249
249
|
}
|
|
250
250
|
const rootSessionId = decoded;
|
|
251
|
-
if (match.decl.id === "device-session") {
|
|
252
|
-
if (!operator && domain === undefined) {
|
|
253
|
-
sendError(res, 403, "auth.forbidden", NO_DEVICE_DOMAIN_MESSAGE);
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
const binding = await store.getSessionBinding(rootSessionId);
|
|
257
|
-
if (!binding || (!operator && !sameDeviceOwner(binding.owner, domain))) {
|
|
258
|
-
sendDeviceNotFound(res);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
sendJson(res, 200, { rootSessionId, boundDevice: boundDeviceOf(binding) });
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
251
|
if (!operator && domain === undefined) {
|
|
265
252
|
sendError(res, 403, "auth.forbidden", NO_DEVICE_DOMAIN_MESSAGE);
|
|
266
253
|
return;
|
|
267
254
|
}
|
|
268
|
-
const
|
|
269
|
-
|
|
255
|
+
const placement = await enrollment.readPlacement(rootSessionId);
|
|
256
|
+
const binding = placement === undefined ? undefined : boundDeviceOf(placement.cell);
|
|
257
|
+
if (placement === undefined || !binding || (!operator && !sameDeviceOwner(binding.owner, domain))) {
|
|
270
258
|
sendDeviceNotFound(res);
|
|
271
259
|
return;
|
|
272
260
|
}
|
|
273
261
|
const body = (await readJson(req));
|
|
274
262
|
if (typeof body !== "object" || body === null || typeof body.toDeviceId !== "string" || body.expectedRev === undefined) {
|
|
275
|
-
sendError(res, 400, "request.body_shape", "body must be { toDeviceId: string, expectedRev: number } — expectedRev is the
|
|
263
|
+
sendError(res, 400, "request.body_shape", "body must be { toDeviceId: string, expectedRev: number } — expectedRev is the device rev read from GET /v1/sessions/{sessionId}/execution-placement (explicit prior state; two concurrent operators must not silently stomp each other)");
|
|
276
264
|
return;
|
|
277
265
|
}
|
|
278
266
|
if (typeof body.expectedRev !== "number" || !Number.isInteger(body.expectedRev) || body.expectedRev < 0) {
|
|
@@ -295,16 +283,24 @@ export async function handleDevices(req, res, match, ctx) {
|
|
|
295
283
|
sendError(res, 409, "device.rebind_active_run", "this session has a non-terminal run — rebinding now would leave in-flight work addressed at another workspace; cancel the run (or let it finish), then retry", { activeTaskId });
|
|
296
284
|
return;
|
|
297
285
|
}
|
|
298
|
-
const
|
|
286
|
+
const tree = await probeSessionChildren({ agentStore: deps.backgroundAgentStore, checkpointStore: deps.checkpointStore, registry: defaultTaskRegistry }, rootSessionId, sessionChildrenScope(placement.occupant.owner));
|
|
287
|
+
if (tree.liveChildren.length > 0) {
|
|
288
|
+
sendError(res, 409, "device.rebind_active_run", "this session still has background agents running or waiting on an approval under it — rebinding now would leave them addressed at the old device; retire them (POST retirePath) or let them finish, then retry", liveChildrenRefusal(rootSessionId, tree.liveChildren));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const result = await store.rebindDeviceExecution({ root: rootSessionId, occupant: placement.occupant, owner: binding.owner, expectedRev, targetDeviceId: toDeviceId });
|
|
299
292
|
switch (result.outcome) {
|
|
300
293
|
case "rebound":
|
|
301
294
|
case "idempotent":
|
|
302
|
-
sendJson(res, 200, { rootSessionId, deviceId: result.
|
|
295
|
+
sendJson(res, 200, { rootSessionId, deviceId: result.device.id, rev: result.device.rev, workspaceCarryover: "none" });
|
|
303
296
|
return;
|
|
304
297
|
case "rev_conflict":
|
|
305
|
-
sendError(res, 409, "device.rebind_rev_conflict", "expectedRev does not match the binding's current rev — the session↔device binding changed since it was read; re-read it and retry with currentRev", { currentRev: result.
|
|
298
|
+
sendError(res, 409, "device.rebind_rev_conflict", "expectedRev does not match the binding's current rev — the session↔device binding changed since it was read; re-read it and retry with currentRev", { currentRev: result.device.rev });
|
|
299
|
+
return;
|
|
300
|
+
case "active_run":
|
|
301
|
+
sendError(res, 409, "device.rebind_active_run", "this session has a non-terminal run — rebinding now would leave in-flight work addressed at another workspace; cancel the run (or let it finish), then retry", { activeTaskId: result.activeTaskId });
|
|
306
302
|
return;
|
|
307
|
-
case "
|
|
303
|
+
case "gone":
|
|
308
304
|
case "device_not_found":
|
|
309
305
|
sendDeviceNotFound(res);
|
|
310
306
|
return;
|
|
@@ -321,7 +317,6 @@ export const DEVICES_ROUTES = [
|
|
|
321
317
|
{ id: "device-enroll", path: "/v1/devices/enroll", credentialGated: ["POST"], methods: ["POST"] },
|
|
322
318
|
{ id: "device-revoke", pattern: /^\/v1\/devices\/(?<deviceId>[^/]+)\/revoke$/, label: "/v1/devices/:deviceId/revoke", methods: ["POST"] },
|
|
323
319
|
{ id: "device-session-rebind", pattern: /^\/v1\/devices\/sessions\/(?<rootSessionId>[^/]+)\/rebind$/, label: "/v1/devices/sessions/:id/rebind", methods: ["POST"] },
|
|
324
|
-
{ id: "device-session", pattern: /^\/v1\/devices\/sessions\/(?<rootSessionId>[^/]+)$/, label: "/v1/devices/sessions/:id", credentialGated: ["GET"], methods: ["GET"] },
|
|
325
320
|
{ prefix: "/v1/devices/", credentialGated: ["POST"] },
|
|
326
321
|
];
|
|
327
322
|
//# sourceMappingURL=devices.js.map
|