@sema-agent/server 7.51.0 → 7.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +103 -24
- package/dist/adoption/plan.d.ts +4 -4
- package/dist/adoption/plan.js +11 -0
- package/dist/approval-card.d.ts +26 -2
- package/dist/approval-card.js +9 -0
- package/dist/approval-hmac.d.ts +1 -1
- package/dist/approval-reconciler.d.ts +2 -2
- package/dist/approval-reconciler.js +1 -1
- package/dist/bench/l8/artifact.d.ts +1 -1
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/live-deps.d.ts +1 -1
- package/dist/bench/s1/oracle.d.ts +1 -1
- package/dist/bench/s1/row.d.ts +1 -1
- package/dist/boot/budget-tracing.d.ts +1 -1
- package/dist/boot/budget-tracing.js +2 -5
- package/dist/boot/coordinators.js +3 -1
- package/dist/boot/deferred-sandbox-path-env.d.ts +1 -1
- package/dist/boot/device-lane.js +2 -0
- package/dist/boot/engine-lease.d.ts +1 -1
- package/dist/boot/execution-env.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +2 -2
- package/dist/boot/leader.d.ts +1 -1
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/memory-consolidation.d.ts +6 -6
- package/dist/boot/org-memory.d.ts +1 -1
- package/dist/boot/parked-revive-gate.d.ts +3 -3
- package/dist/boot/reapers.d.ts +2 -2
- package/dist/boot/reapers.js +17 -4
- package/dist/boot/resolve-spec.d.ts +12 -0
- package/dist/boot/resolve-spec.js +29 -7
- package/dist/boot/retention-lane.d.ts +2 -2
- package/dist/boot/runner-deps.d.ts +17 -7
- package/dist/boot/runner-deps.js +4 -2
- package/dist/boot/runtime-caps.d.ts +23 -0
- package/dist/boot/runtime-caps.js +24 -2
- package/dist/boot/session-faces.d.ts +1 -1
- package/dist/boot/session-faces.js +14 -3
- package/dist/boot/stores.js +31 -1
- package/dist/boot/task-list-lane.d.ts +2 -2
- package/dist/brain.js +51 -31
- package/dist/budget.d.ts +2 -2
- package/dist/capabilities/center-plugins.d.ts +1 -1
- package/dist/capabilities/collab-workflows.d.ts +1 -1
- package/dist/capabilities/hands-lane.d.ts +3 -3
- package/dist/capabilities/scenarios.d.ts +3 -3
- package/dist/capabilities/skills.d.ts +1 -1
- package/dist/config-center/hot-keys-registry.js +5 -0
- package/dist/config-center/http-client.d.ts +2 -2
- package/dist/config-center/read-face.d.ts +1 -1
- package/dist/config-center/restart-signal.d.ts +1 -1
- package/dist/config-types.d.ts +100 -25
- package/dist/config.d.ts +11 -3
- package/dist/config.js +91 -15
- package/dist/deployment-governance.d.ts +1 -1
- package/dist/device-enrollment.d.ts +2 -2
- package/dist/device-store.d.ts +3 -3
- package/dist/device-ws-hub.d.ts +4 -2
- package/dist/device-ws-hub.js +9 -3
- package/dist/device-ws-protocol.d.ts +2 -2
- package/dist/env-facts.d.ts +1 -1
- package/dist/execution-lane-caps.d.ts +1 -1
- package/dist/fleet/fleet-bus.d.ts +5 -2
- package/dist/fleet/fleet-reconciler.d.ts +1 -1
- package/dist/fleet/fleet-terminal-window.d.ts +1 -1
- package/dist/hooks/hook-runner.d.ts +8 -1
- package/dist/http/active-run-conflict.d.ts +2 -2
- package/dist/http/principal-gate.d.ts +1 -1
- package/dist/http/route-ctx.d.ts +27 -5
- package/dist/http/routes/admin-memory-optout.d.ts +48 -0
- package/dist/http/routes/admin-memory-optout.js +75 -0
- package/dist/http/routes/agents-roster.d.ts +1 -1
- package/dist/http/routes/approvals-assistant.js +21 -21
- package/dist/http/routes/capabilities.js +20 -9
- package/dist/http/routes/diagnostics.d.ts +1 -1
- package/dist/http/routes/leader.d.ts +1 -1
- package/dist/http/routes/memory-bundle.d.ts +1 -1
- package/dist/http/routes/memory-bundle.js +2 -1
- package/dist/http/routes/memory-compliance.d.ts +4 -4
- package/dist/http/routes/memory-origin.d.ts +4 -4
- package/dist/http/routes/memory-origin.js +3 -1
- package/dist/http/routes/memory-policy.js +6 -2
- package/dist/http/routes/retention-ops.d.ts +1 -1
- package/dist/http/routes/rules.d.ts +5 -2
- package/dist/http/routes/rules.js +84 -5
- package/dist/http/routes/run-memory-capture.d.ts +34 -0
- package/dist/http/routes/run-memory-capture.js +97 -0
- package/dist/http/routes/runs.d.ts +1 -0
- package/dist/http/routes/runs.js +212 -6
- package/dist/http/routes/session-sync.d.ts +4 -6
- package/dist/http/routes/session-sync.js +60 -47
- package/dist/http/routes/sessions.js +61 -188
- package/dist/http/routes/shared-memory.d.ts +1 -1
- package/dist/http/routes/tasks.js +1 -1
- package/dist/http/routes/workflows.js +19 -0
- package/dist/http/server.d.ts +35 -13
- package/dist/http/server.js +110 -14
- package/dist/http/verify-rounds.d.ts +1 -1
- package/dist/http/wire-types.d.ts +41 -8
- package/dist/leader/endpoint.d.ts +1 -1
- package/dist/leader/leader.d.ts +1 -1
- package/dist/leader/repair-oracle.d.ts +2 -2
- package/dist/leader/repair-oracle.js +1 -1
- package/dist/main.js +14 -6
- package/dist/memory-bundle-engine.d.ts +1 -1
- package/dist/memory-operator-faces.d.ts +5 -5
- package/dist/memory-origin-fence.d.ts +35 -0
- package/dist/memory-origin-fence.js +66 -0
- package/dist/memory-posture.d.ts +13 -1
- package/dist/memory-posture.js +1 -0
- package/dist/memory-scope.d.ts +6 -1
- package/dist/memory-scope.js +3 -2
- package/dist/model-select.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +17 -5
- package/dist/observability/fail-open.js +17 -5
- package/dist/orchestration/hardened-vm-runner.d.ts +2 -2
- package/dist/orchestration/workflow-completion-inbox.d.ts +2 -2
- package/dist/plugins/background-shell-support.d.ts +2 -2
- package/dist/plugins/blob-backend.d.ts +7 -2
- package/dist/plugins/blob-backend.js +22 -18
- package/dist/plugins/caching-session-store.d.ts +7 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
- package/dist/plugins/file-history-store-sql.d.ts +170 -0
- package/dist/plugins/file-history-store-sql.js +827 -0
- package/dist/plugins/file-run-store.d.ts +44 -5
- package/dist/plugins/file-run-store.js +85 -13
- package/dist/plugins/fork-routing-session-store.d.ts +1 -1
- package/dist/plugins/fork-routing-session-store.js +2 -2
- package/dist/plugins/leader-run-store-sql.d.ts +1 -1
- package/dist/plugins/local-session-store.d.ts +9 -1
- package/dist/plugins/memory-embedder-fingerprint.d.ts +1 -1
- package/dist/plugins/memory-optout-grant-store-sql.d.ts +139 -0
- package/dist/plugins/memory-optout-grant-store-sql.js +157 -0
- package/dist/plugins/memory-origin-law.d.ts +2 -2
- package/dist/plugins/memory-run-store.d.ts +4 -3
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/permission-rule-store-file.d.ts +16 -6
- package/dist/plugins/permission-rule-store-file.js +77 -13
- package/dist/plugins/permission-rule-store-sql.d.ts +156 -8
- package/dist/plugins/permission-rule-store-sql.js +123 -21
- package/dist/plugins/pg-pool.js +59 -0
- package/dist/plugins/pg-safe-json.d.ts +1 -1
- package/dist/plugins/remote-env-device.d.ts +1 -1
- package/dist/plugins/remote-env-k8s.d.ts +1 -1
- package/dist/plugins/retention-lane-store-sql.d.ts +3 -3
- package/dist/plugins/retention-store-sql.d.ts +7 -7
- package/dist/plugins/retention-store-sql.js +6 -0
- package/dist/plugins/run-store-sql.d.ts +9 -3
- package/dist/plugins/run-store-sql.js +2 -2
- package/dist/plugins/send-user-file.d.ts +1 -1
- package/dist/plugins/session-placement.d.ts +1 -1
- package/dist/plugins/sql-errors.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +59 -29
- package/dist/plugins/store-backend.js +75 -15
- package/dist/plugins/store-contracts.d.ts +5 -3
- package/dist/plugins/task-list-store-sql.d.ts +1 -1
- package/dist/plugins/tidb-pool.js +59 -0
- package/dist/plugins/web-search.d.ts +1 -1
- package/dist/prompts-domain-validate.js +1 -1
- package/dist/resource-suspend.d.ts +1 -1
- package/dist/resource-window.d.ts +3 -3
- package/dist/rules-consent.d.ts +231 -9
- package/dist/rules-consent.js +411 -13
- package/dist/run-local.js +29 -4
- package/dist/runs.d.ts +15 -1
- package/dist/runs.js +19 -0
- package/dist/runtime-caps-resolver.d.ts +36 -1
- package/dist/runtime-caps-resolver.js +25 -0
- package/dist/runtime-governance.d.ts +1 -1
- package/dist/security.d.ts +5 -5
- package/dist/security.js +1 -1
- package/dist/session-sync.d.ts +52 -37
- package/dist/session-sync.js +19 -16
- package/dist/shared-memory-scope-authorizer.d.ts +1 -1
- package/dist/sighup-idle.d.ts +1 -1
- package/dist/spec-fields.d.ts +22 -1
- package/dist/spec-fields.js +11 -0
- package/dist/store-live-probe.d.ts +2 -2
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-cwd.d.ts +1 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/task-settings.d.ts +3 -3
- package/dist/task-workflow.d.ts +1 -1
- package/dist/text-bidi.d.ts +2 -2
- package/dist/tool-approval.d.ts +191 -9
- package/dist/tool-approval.js +248 -29
- package/dist/trace/core-keyset-guard.d.ts +3 -3
- package/dist/trace/engine-notice-wire.d.ts +6 -4
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/ledger-events.d.ts +1 -1
- package/dist/trace/ledger-sink.d.ts +2 -2
- package/dist/trace/project.d.ts +8 -2
- package/dist/trace/project.js +10 -0
- package/dist/turn-activity.d.ts +1 -1
- package/dist/wall-clock-jump-guard.d.ts +3 -3
- package/package.json +3 -3
- package/dist/http/tar.d.ts +0 -17
- package/dist/http/tar.js +0 -53
- package/dist/http/workspace-content.d.ts +0 -11
- package/dist/http/workspace-content.js +0 -20
- package/dist/plugins/file-snapshot-store-sql.d.ts +0 -170
- package/dist/plugins/file-snapshot-store-sql.js +0 -213
package/dist/http/routes/runs.js
CHANGED
|
@@ -15,7 +15,7 @@ import { scopedIdempotencyKey } from "../idempotency.js";
|
|
|
15
15
|
import { streamSseLog } from "../sse-log.js";
|
|
16
16
|
import { bindSseLifecycle } from "../sse-lifecycle.js";
|
|
17
17
|
import { buildApprovalPreamble, buildApprovalPreambleSseFrames } from "../../approval-card.js";
|
|
18
|
-
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
18
|
+
import { ASK_DECISION_CONSUMER_ABSENT, resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
19
19
|
import { MAX_DECISION_NOTE_CHARS } from "../../approval-ask-machine.js";
|
|
20
20
|
import { normalizeRunEventType } from "../../trace/project.js";
|
|
21
21
|
import { sendJson, sendError, httpErrorCode, sseHeaders } from "../send.js";
|
|
@@ -30,6 +30,7 @@ export const RUN_CANCEL_RE = /^\/v1\/runs\/([^/]+)\/cancel$/;
|
|
|
30
30
|
export const RUN_STEER_RE = /^\/v1\/runs\/([^/]+)\/steer$/;
|
|
31
31
|
export const RUN_DETACH_RE = /^\/v1\/runs\/([^/]+)\/detach$/;
|
|
32
32
|
export const RUN_COMPACT_RE = /^\/v1\/runs\/([^/]+)\/compact$/;
|
|
33
|
+
export const RUN_INTERRUPT_RE = /^\/v1\/runs\/([^/]+)\/interrupt$/;
|
|
33
34
|
export const ELICIT_RESPOND_RE = /^\/v1\/elicitations\/([^/]+)\/respond$/;
|
|
34
35
|
export const QUESTION_RESPOND_RE = /^\/v1\/questions\/([^/]+)\/respond$/;
|
|
35
36
|
export const TOOL_APPROVAL_RESPOND_RE = /^\/v1\/tool-approvals\/([^/]+)\/respond$/;
|
|
@@ -51,6 +52,20 @@ const AskDecisionBodySchema = z
|
|
|
51
52
|
actor: z.object({ label: z.string().max(ASK_ACTOR_LABEL_MAX) }).strict().optional(),
|
|
52
53
|
})
|
|
53
54
|
.strict();
|
|
55
|
+
const ASK_DECISION_CONFIRM_TIMEOUT_MS = 2_000;
|
|
56
|
+
function withDeadline(op, timeoutMs, label) {
|
|
57
|
+
let timer;
|
|
58
|
+
return Promise.race([
|
|
59
|
+
op,
|
|
60
|
+
new Promise((_resolve, reject) => {
|
|
61
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
62
|
+
timer.unref?.();
|
|
63
|
+
}),
|
|
64
|
+
]).finally(() => {
|
|
65
|
+
if (timer !== undefined)
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
54
69
|
export function projectDecisionActor(raw) {
|
|
55
70
|
const parsed = AskDecisionActorSchema.safeParse(raw);
|
|
56
71
|
return parsed.success ? parsed.data : undefined;
|
|
@@ -288,7 +303,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
288
303
|
}
|
|
289
304
|
}
|
|
290
305
|
else {
|
|
291
|
-
const claimed = await rs.markResuming(taskId);
|
|
306
|
+
const claimed = await rs.markResuming(taskId, deps.instanceId ?? "default");
|
|
292
307
|
if (!claimed) {
|
|
293
308
|
const now = await rs.getRun(taskId);
|
|
294
309
|
const st = now?.status;
|
|
@@ -387,7 +402,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
387
402
|
const taskId = steerMatch[1];
|
|
388
403
|
let body;
|
|
389
404
|
try {
|
|
390
|
-
body = (await readJson(req));
|
|
405
|
+
body = ((await readJson(req)) ?? {});
|
|
391
406
|
}
|
|
392
407
|
catch (e) {
|
|
393
408
|
if (e instanceof HttpError) {
|
|
@@ -482,6 +497,10 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
482
497
|
sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : "invalid steering content");
|
|
483
498
|
return;
|
|
484
499
|
}
|
|
500
|
+
if (code === "steering.blocked_by_hook") {
|
|
501
|
+
sendError(res, 422, "steering.blocked_by_hook", e instanceof Error ? e.message : "the deployment's userPromptSubmit hook refused this steering input (fail-closed)");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
485
504
|
if (code === "steering.duplicate_input_id") {
|
|
486
505
|
parkDuplicateKey = e instanceof Error ? e.message : undefined;
|
|
487
506
|
sendDuplicateKey();
|
|
@@ -529,7 +548,13 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
529
548
|
return;
|
|
530
549
|
}
|
|
531
550
|
if (run.status === "running") {
|
|
532
|
-
|
|
551
|
+
const provenElsewhere = typeof run.instanceId === "string" && run.instanceId.length > 0 && deps.instanceId !== undefined && run.instanceId !== deps.instanceId;
|
|
552
|
+
if (provenElsewhere) {
|
|
553
|
+
sendNotRunning("run is live on another replica — a live steer must reach the replica holding the stream; resend it there, or use POST /v1/runs/:id/cancel for a run-level stop");
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
sendNotRunning("no live steerable face for this run on this replica — verify/cascade execution lanes expose none (those legs return a result, not a live stream); use POST /v1/runs/:id/cancel for a run-level stop");
|
|
557
|
+
}
|
|
533
558
|
return;
|
|
534
559
|
}
|
|
535
560
|
if (deps.checkpointStore && deps.sessionStorage?.getLeafId) {
|
|
@@ -639,6 +664,152 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
639
664
|
sendNotRunning(`run is ${run.status} — not accepting steers`);
|
|
640
665
|
return;
|
|
641
666
|
}
|
|
667
|
+
const interruptMatch = req.method === "POST" ? RUN_INTERRUPT_RE.exec(url) : null;
|
|
668
|
+
if (interruptMatch) {
|
|
669
|
+
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
670
|
+
return;
|
|
671
|
+
if (!deps.runStore) {
|
|
672
|
+
sendError(res, 501, "capability.run_store_required", "async runs require a durable run store (DB_BACKEND=mysql|pg|local)");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
676
|
+
if (deps.config.requirePrincipal && principal === undefined) {
|
|
677
|
+
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const taskId = interruptMatch[1];
|
|
681
|
+
let interruptRaw;
|
|
682
|
+
try {
|
|
683
|
+
interruptRaw = await readJson(req);
|
|
684
|
+
}
|
|
685
|
+
catch (e) {
|
|
686
|
+
if (e instanceof HttpError) {
|
|
687
|
+
sendError(res, e.status, httpErrorCode(e.status, e.code), e.message);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
sendError(res, 400, "request.invalid_json", "invalid JSON body");
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const interruptNormalized = interruptRaw === null || interruptRaw === undefined ? {} : interruptRaw;
|
|
694
|
+
if (typeof interruptNormalized !== "object" || Array.isArray(interruptNormalized)) {
|
|
695
|
+
sendError(res, 400, "request.body_shape", "body must be {} (bare halt: cut the in-flight turn and stop the run at this boundary) or { text: string (non-empty) } (cut + steer: the text reaches the model after the cut)");
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const interruptBody = interruptNormalized;
|
|
699
|
+
const interruptBare = !("text" in interruptBody);
|
|
700
|
+
if (interruptBare && Object.keys(interruptBody).length > 0) {
|
|
701
|
+
sendError(res, 400, "request.body_shape", "body must be {} (bare halt: cut the in-flight turn and stop the run at this boundary) or { text: string (non-empty) } (cut + steer: the text reaches the model after the cut)");
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
let text = "";
|
|
705
|
+
if (!interruptBare) {
|
|
706
|
+
if (typeof interruptBody.text !== "string" || interruptBody.text.length === 0) {
|
|
707
|
+
sendError(res, 400, "request.body_shape", "body must be {} (bare halt: cut the in-flight turn and stop the run at this boundary) or { text: string (non-empty) } (cut + steer: the text reaches the model after the cut)");
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
text = interruptBody.text;
|
|
711
|
+
}
|
|
712
|
+
const interruptIdemRaw = headerStr(req.headers["idempotency-key"]);
|
|
713
|
+
const interruptScoped = interruptIdemRaw ? scopedIdempotencyKey(interruptIdemRaw, source, principal) : undefined;
|
|
714
|
+
const interruptInputId = interruptScoped ? `idem-${createHash("sha256").update(interruptScoped).digest("hex").slice(0, 32)}` : uuidv7();
|
|
715
|
+
const interruptMessageId = interruptInputId;
|
|
716
|
+
const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
717
|
+
if (!interruptBare) {
|
|
718
|
+
try {
|
|
719
|
+
validatePendingSteer({ text, trusted, inputId: interruptInputId });
|
|
720
|
+
}
|
|
721
|
+
catch (e) {
|
|
722
|
+
if (e instanceof CheckpointError && e.code === "steering.invalid_content") {
|
|
723
|
+
sendError(res, 422, "steering.invalid_content", e.message);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
throw e;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const run = await deps.runStore.getRun(taskId);
|
|
730
|
+
if (!run) {
|
|
731
|
+
sendError(res, 404, "not_found.run", runNotFoundMessage(taskId));
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (!trusted && run.owner !== null && run.owner !== principal) {
|
|
735
|
+
sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (!runSessionAcceptOk(req, res, run, "run.interrupt"))
|
|
739
|
+
return;
|
|
740
|
+
const sendInterruptNotRunning = (error) => sendError(res, 409, "steering.not_running", error);
|
|
741
|
+
const sendNoLiveTurn = (status, streamJustSettled) => {
|
|
742
|
+
if (isParkedRunStatus(status)) {
|
|
743
|
+
sendError(res, 409, "interrupt.nothing_in_flight", "run is parked on a pending decision — there is no in-flight turn to interrupt; to deliver input use POST /v1/runs/:id/steer (it parks onto the checkpoint and injects on resume)");
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (streamJustSettled) {
|
|
747
|
+
sendInterruptNotRunning("run just finished on this replica — no in-flight turn to interrupt");
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (status === "running") {
|
|
751
|
+
const provenElsewhere = typeof run.instanceId === "string" && run.instanceId.length > 0 && deps.instanceId !== undefined && run.instanceId !== deps.instanceId;
|
|
752
|
+
if (provenElsewhere) {
|
|
753
|
+
sendError(res, 409, "interrupt.not_held", "run is live on another replica — interrupt must reach the replica holding the stream; resend it there, or use POST /v1/runs/:id/cancel for a run-level stop");
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
sendError(res, 409, "interrupt.not_held", "no live interruptible turn face for this run on this replica — verify/cascade execution lanes expose none (those legs return a result, not a live stream); use POST /v1/runs/:id/cancel for a run-level stop");
|
|
757
|
+
}
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
sendInterruptNotRunning(`run is ${status} — not accepting an interrupt`);
|
|
761
|
+
};
|
|
762
|
+
const liveStream = steerableRuns.get(taskId);
|
|
763
|
+
if (!liveStream) {
|
|
764
|
+
sendNoLiveTurn(run.status, false);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (interruptBare) {
|
|
768
|
+
let haltReceipt;
|
|
769
|
+
try {
|
|
770
|
+
haltReceipt = await liveStream.halt();
|
|
771
|
+
}
|
|
772
|
+
catch (e) {
|
|
773
|
+
if (e.code !== "steering.not_running")
|
|
774
|
+
throw e;
|
|
775
|
+
const freshRun = await deps.runStore.getRun(taskId).catch(() => undefined);
|
|
776
|
+
sendNoLiveTurn(freshRun?.status ?? run.status, true);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
sendJson(res, 202, {
|
|
780
|
+
taskId,
|
|
781
|
+
status: run.status,
|
|
782
|
+
turnCut: haltReceipt.turnCut,
|
|
783
|
+
note: "halt accepted — cut + stop: turnCut reports whether THIS call cut a live turn (repeat calls answer turnCut:false once the stop is latched). The run now ends at this boundary as status:\"completed\" with haltedByUser:true — resume by submitting the next objective on the same session. Each real cut is marked by a task.turn_interrupted event with detail.cause:\"user_halt\"; a halt racing the run's natural last moments reports the natural completion, still carrying the haltedByUser seat.",
|
|
784
|
+
});
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
await liveStream.steer(text, { trusted, priority: "now", ...(interruptInputId !== undefined ? { inputId: interruptInputId } : {}) });
|
|
789
|
+
}
|
|
790
|
+
catch (e) {
|
|
791
|
+
const code = e.code;
|
|
792
|
+
if (code === "steering.invalid_content") {
|
|
793
|
+
sendError(res, 422, "steering.invalid_content", e instanceof Error ? e.message : "invalid steering content");
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (code === "steering.blocked_by_hook") {
|
|
797
|
+
sendError(res, 422, "steering.blocked_by_hook", e instanceof Error ? e.message : "the deployment's userPromptSubmit hook refused this input (fail-closed)");
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (code === "steering.duplicate_input_id") {
|
|
801
|
+
sendError(res, 409, "steering.duplicate_input_id", e instanceof Error ? e.message : "this Idempotency-Key was already accepted with different content or at a different tier — reissue with a fresh key");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (code !== "steering.not_running")
|
|
805
|
+
throw e;
|
|
806
|
+
const freshRun = await deps.runStore.getRun(taskId).catch(() => undefined);
|
|
807
|
+
sendNoLiveTurn(freshRun?.status ?? run.status, true);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
sendJson(res, 202, { taskId, status: run.status, delivery: "queued-immediate", messageId: interruptMessageId, note: "interrupt accepted — the cut is best-effort: a task.turn_interrupted event on the run stream is the proof a turn was actually cut; with nothing in flight the text lands at the next turn boundary" });
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
642
813
|
const compactMatch = req.method === "POST" ? RUN_COMPACT_RE.exec(url) : null;
|
|
643
814
|
if (compactMatch) {
|
|
644
815
|
if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
|
|
@@ -722,7 +893,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
|
|
|
722
893
|
const taskId = detachMatch[1];
|
|
723
894
|
let body;
|
|
724
895
|
try {
|
|
725
|
-
body = (await readJson(req));
|
|
896
|
+
body = ((await readJson(req)) ?? {});
|
|
726
897
|
}
|
|
727
898
|
catch (e) {
|
|
728
899
|
if (e instanceof HttpError) {
|
|
@@ -945,7 +1116,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
945
1116
|
}
|
|
946
1117
|
let body;
|
|
947
1118
|
try {
|
|
948
|
-
body = (await readJson(req));
|
|
1119
|
+
body = ((await readJson(req)) ?? {});
|
|
949
1120
|
}
|
|
950
1121
|
catch (e) {
|
|
951
1122
|
if (e instanceof HttpError) {
|
|
@@ -1168,10 +1339,16 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1168
1339
|
}
|
|
1169
1340
|
return true;
|
|
1170
1341
|
};
|
|
1342
|
+
const observedSettled = new Map();
|
|
1171
1343
|
const syncLiveFromDecidedRow = (row) => {
|
|
1172
1344
|
if (!decidedRowIsAuthoritative(row))
|
|
1173
1345
|
return false;
|
|
1346
|
+
if (observedSettled.has(row.askId))
|
|
1347
|
+
return true;
|
|
1348
|
+
if (deps.toolApproval?.editedDecisionInFlight(row.askId) === true)
|
|
1349
|
+
return true;
|
|
1174
1350
|
const settled = deps.toolApproval?.notifyExternalDecision(row.askId, row.decision === "approve");
|
|
1351
|
+
observedSettled.set(row.askId, settled?.settled ?? 0);
|
|
1175
1352
|
if (row.decision === "approve" && (settled?.settled ?? 0) > 0) {
|
|
1176
1353
|
recordFailOpen("server.hitl.row-derived-approve-drops-updated-input", "runs:sync-live-from-decided-row");
|
|
1177
1354
|
}
|
|
@@ -1254,7 +1431,13 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1254
1431
|
sendTerminal(row0);
|
|
1255
1432
|
return;
|
|
1256
1433
|
}
|
|
1434
|
+
if (body.decision === "approve" && hasUpdatedInput && (deps.toolApproval?.liveConsumerCount(askId) ?? 0) === 0) {
|
|
1435
|
+
deps.logger?.warn?.("ask_decision_edit_undeliverable", { taskId, askId });
|
|
1436
|
+
sendError(res, 409, "conflict.ask_edit_undeliverable", "this decision carries an edited input but no live approval waiter for this ask exists on THIS replica — the edit cannot be bound to the decision (the durable ask row has no field for it), so committing would run the ORIGINAL input instead. Nothing was decided: re-send without updatedInput to approve the original as-is, or send the decision to the replica holding this ask (the live POST /v1/tool-approvals/:approvalId/respond leg is always same-replica)");
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1257
1439
|
let outcome;
|
|
1440
|
+
const releaseEditMark = body.decision === "approve" && hasUpdatedInput ? deps.toolApproval?.markEditedDecisionInFlight(askId) : undefined;
|
|
1258
1441
|
try {
|
|
1259
1442
|
outcome = await askStore.decideAsk(askId, row0.batchId, {
|
|
1260
1443
|
decision: body.decision,
|
|
@@ -1270,6 +1453,25 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1270
1453
|
hadIdempotencyKey: body.idempotencyKey !== undefined,
|
|
1271
1454
|
error: err instanceof Error ? err.message : String(err ?? ""),
|
|
1272
1455
|
});
|
|
1456
|
+
if (releaseEditMark !== undefined &&
|
|
1457
|
+
body.idempotencyKey !== undefined &&
|
|
1458
|
+
deps.toolApproval &&
|
|
1459
|
+
deps.toolApproval.editedDecisionInFlightCount(askId) === 1) {
|
|
1460
|
+
try {
|
|
1461
|
+
const confirmed = await withDeadline(askStore.getAsk(askId), ASK_DECISION_CONFIRM_TIMEOUT_MS, "getAsk(ambiguous-commit-confirm)");
|
|
1462
|
+
if (confirmed !== null &&
|
|
1463
|
+
confirmed.state === "DECIDED" &&
|
|
1464
|
+
confirmed.decision === body.decision &&
|
|
1465
|
+
confirmed.idempotencyKey === body.idempotencyKey &&
|
|
1466
|
+
!confirmed.provisional) {
|
|
1467
|
+
const settledNow = deps.toolApproval.notifyExternalDecision(askId, confirmed.decision === "approve", body.updatedInput).settled;
|
|
1468
|
+
deps.logger?.warn?.("ask_decision_edit_settled_after_ambiguous_commit", { taskId, askId, settled: settledNow });
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
catch (confirmErr) {
|
|
1472
|
+
deps.logger?.warn?.("ask_decision_edit_confirm_failed", { taskId, askId, error: confirmErr instanceof Error ? confirmErr.message : String(confirmErr ?? "") });
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1273
1475
|
if (body.idempotencyKey !== undefined) {
|
|
1274
1476
|
sendError(res, 500, "internal.error", "the ask decision store failed around commit — the decision MAY already be persisted; retry with the same idempotencyKey to replay it instead of colliding with it");
|
|
1275
1477
|
}
|
|
@@ -1278,6 +1480,9 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1278
1480
|
}
|
|
1279
1481
|
return;
|
|
1280
1482
|
}
|
|
1483
|
+
finally {
|
|
1484
|
+
releaseEditMark?.();
|
|
1485
|
+
}
|
|
1281
1486
|
if (!outcome.ok) {
|
|
1282
1487
|
if (outcome.row?.state === "DECIDED" && !syncLiveFromDecidedRow(outcome.row))
|
|
1283
1488
|
return;
|
|
@@ -1311,6 +1516,7 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1311
1516
|
decidedAtMs: outcome.row.decidedAtMs,
|
|
1312
1517
|
actor,
|
|
1313
1518
|
...(forwarded ? { updatedInputForwarded: true } : {}),
|
|
1519
|
+
...(settled === 0 ? { consumerState: ASK_DECISION_CONSUMER_ABSENT } : {}),
|
|
1314
1520
|
...decisionNoteEcho(outcome.row),
|
|
1315
1521
|
});
|
|
1316
1522
|
return;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `pendingStagings`(Phase A→B 的 staging 绑定)、`importLeases`(per-session 导入租约)与它的两个操作。
|
|
9
9
|
*/
|
|
10
10
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
11
|
-
import { type SessionRulesRecord } from "@sema-agent/core";
|
|
11
|
+
import { type SessionRulesRecord, type FileHistoryExport } from "@sema-agent/core";
|
|
12
12
|
import { type StagingHandle } from "../../session-sync-kernel.js";
|
|
13
13
|
import type { RouteCtx } from "../route-ctx.js";
|
|
14
14
|
export type PendingStaging = {
|
|
@@ -17,17 +17,15 @@ export type PendingStaging = {
|
|
|
17
17
|
owner: string | null;
|
|
18
18
|
resolution?: "overwrite-dst";
|
|
19
19
|
operatorOk: boolean;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
manifest: Array<[string, string]>;
|
|
23
|
-
}>;
|
|
20
|
+
/** S-15: the session's file-history envelope (or null — no history travelled). Replaces the E19 per-key manifests. */
|
|
21
|
+
fileHistory: FileHistoryExport | null;
|
|
24
22
|
policy: SessionRulesRecord[];
|
|
25
23
|
anchors: Array<{
|
|
26
24
|
eventId: string;
|
|
27
25
|
entryId: string;
|
|
28
26
|
owner: string | null;
|
|
29
27
|
}>;
|
|
30
|
-
/**
|
|
28
|
+
/** 交叉复查 A-3(HIGH):Phase A's DECLARED entry-id set — Phase B verifies the streamed NDJSON matches it
|
|
31
29
|
* (a different tree under the same staging would otherwise commit while the parked snapshots/anchors
|
|
32
30
|
* still reference the DECLARED one). */
|
|
33
31
|
declaredEntryIds: string[];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { once } from "node:events";
|
|
2
2
|
import { randomBytes, createHash } from "node:crypto";
|
|
3
|
-
import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, SessionError, SessionPolicyError } from "@sema-agent/core";
|
|
3
|
+
import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, SessionError, SessionPolicyError, validateFileHistoryExport } from "@sema-agent/core";
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
5
|
-
import { exportSessionManifest,
|
|
5
|
+
import { exportSessionManifest, fileHistoryImportFace, replayFileHistory, replayPolicyRecords, overwriteWipeIncapable, wipeSessionAttendants } from "../../session-sync.js";
|
|
6
6
|
import { classifySyncRelationshipByIds, SyncConflictError } from "../../session-sync-kernel.js";
|
|
7
7
|
import { isUuidShape } from "../../security.js";
|
|
8
8
|
import { sendJson, sendError, msg } from "../send.js";
|
|
@@ -37,7 +37,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
37
37
|
const { readJson, readRawBody, safeDecode, sessionOwnerScopeForWrite, rateLimited } = ctx.helpers;
|
|
38
38
|
const { pendingStagings, importLeases, touchImportLease, cleanupStaging } = ctx.local.sessionSync;
|
|
39
39
|
{
|
|
40
|
-
const sm = /^\/v1\/sessions\/([^/]+)\/sync\/(manifest|entries|import|plan|blobs(?:\/([^/]+))?|
|
|
40
|
+
const sm = /^\/v1\/sessions\/([^/]+)\/sync\/(manifest|entries|import|plan|blobs(?:\/([^/]+))?|history\/blobs\/([^/]+)|import\/([^/]+)\/entries)$/.exec(url);
|
|
41
41
|
if (sm) {
|
|
42
42
|
const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
|
|
43
43
|
if (!deps.backend || !ownerOf) {
|
|
@@ -130,23 +130,27 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
130
130
|
if (sm[4] !== undefined && req.method === "GET") {
|
|
131
131
|
if (!pullGateOk())
|
|
132
132
|
return;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
sendError(res, 400, "request.path_malformed", "invalid snapshot key or blob hash");
|
|
133
|
+
const hash = safeDecode(sm[4]);
|
|
134
|
+
if (hash === null) {
|
|
135
|
+
sendError(res, 400, "request.path_malformed", "invalid blob hash");
|
|
137
136
|
return;
|
|
138
137
|
}
|
|
139
138
|
if (!SHA256_HEX_RE.test(hash)) {
|
|
140
139
|
sendError(res, 400, "request.id_invalid", "blob hash must be 64 lowercase-hex chars");
|
|
141
140
|
return;
|
|
142
141
|
}
|
|
143
|
-
const fs = deps.
|
|
144
|
-
|
|
145
|
-
if (!manifest || ![...manifest.values()].includes(hash)) {
|
|
142
|
+
const fs = deps.fileHistoryStore;
|
|
143
|
+
if (fs?.syncBlobFaces !== true) {
|
|
146
144
|
sendError(res, 404, "not_found.blob", "blob not found");
|
|
147
145
|
return;
|
|
148
146
|
}
|
|
149
|
-
const
|
|
147
|
+
const envelope = typeof fs.exportHistory === "function" ? await fs.exportHistory(sessionId) : null;
|
|
148
|
+
const referenced = envelope !== null && envelope !== undefined && envelope.tracked.some((t) => t.versions.some((v) => v.blobHash === hash));
|
|
149
|
+
if (!referenced) {
|
|
150
|
+
sendError(res, 404, "not_found.blob", "blob not found");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const bytes = await fs.getBlob?.(hash);
|
|
150
154
|
if (!bytes) {
|
|
151
155
|
sendError(res, 404, "not_found.blob", "blob not found");
|
|
152
156
|
return;
|
|
@@ -165,9 +169,9 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
165
169
|
sendError(res, 400, "request.id_invalid", "blob hash must be 64 lowercase-hex chars");
|
|
166
170
|
return;
|
|
167
171
|
}
|
|
168
|
-
const fs = deps.
|
|
169
|
-
if (typeof fs
|
|
170
|
-
sendError(res, 501, "capability.snapshot_store_required", "blob upload requires a durable
|
|
172
|
+
const fs = deps.fileHistoryStore;
|
|
173
|
+
if (fs?.syncBlobFaces !== true || typeof fs.putBlob !== "function") {
|
|
174
|
+
sendError(res, 501, "capability.snapshot_store_required", "blob upload requires a durable file-history store");
|
|
171
175
|
return;
|
|
172
176
|
}
|
|
173
177
|
const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
|
|
@@ -215,11 +219,19 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
215
219
|
sendError(res, 400, "request.field_invalid", "entryIds must be an array of strings");
|
|
216
220
|
return;
|
|
217
221
|
}
|
|
218
|
-
|
|
219
|
-
if (!snapshotsOk || !Array.isArray(body.policy) || !Array.isArray(body.anchors)) {
|
|
222
|
+
if (!Array.isArray(body.policy) || !Array.isArray(body.anchors)) {
|
|
220
223
|
sendError(res, 400, "request.body_shape", "malformed import metadata");
|
|
221
224
|
return;
|
|
222
225
|
}
|
|
226
|
+
let fileHistory = null;
|
|
227
|
+
if (body.fileHistory !== undefined && body.fileHistory !== null) {
|
|
228
|
+
const door = validateFileHistoryExport(body.fileHistory);
|
|
229
|
+
if (!door.ok) {
|
|
230
|
+
sendError(res, 400, "request.field_invalid", `fileHistory: ${door.error.message}`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
fileHistory = body.fileHistory;
|
|
234
|
+
}
|
|
223
235
|
if (body.resolution !== undefined && body.resolution !== "overwrite-dst") {
|
|
224
236
|
sendError(res, 400, "request.field_invalid", "resolution must be 'overwrite-dst'");
|
|
225
237
|
return;
|
|
@@ -227,7 +239,6 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
227
239
|
const resolution = body.resolution;
|
|
228
240
|
const srcLogDigest = typeof body.logDigest === "string" && body.logDigest.length > 0 ? body.logDigest : undefined;
|
|
229
241
|
const entryIds = body.entryIds;
|
|
230
|
-
const snapshots = body.snapshots;
|
|
231
242
|
const policy = body.policy;
|
|
232
243
|
const anchors = body.anchors;
|
|
233
244
|
const dstIds = (await deps.sessionStorage.listEntryIds?.(sessionId)) ?? null;
|
|
@@ -251,16 +262,21 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
251
262
|
digestSaysDiffer = true;
|
|
252
263
|
}
|
|
253
264
|
if (relA.relation === "identical" && !digestSaysDiffer) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (!r.ok) {
|
|
260
|
-
sendError(res, 422, r.error.code === "restore_failed" ? "restore_failed" : "missing_blob", `snapshot import failed (${r.error.code})`);
|
|
265
|
+
if (fileHistory !== null && deps.fileHistoryStore?.syncBlobFaces === true && fileHistoryImportFace(deps.fileHistoryStore) && deps.fileHistoryStore.getBlob) {
|
|
266
|
+
if (resolution === "overwrite-dst") {
|
|
267
|
+
const activeNow = await deps.runStore?.getActiveTaskId(sessionId);
|
|
268
|
+
if (activeNow) {
|
|
269
|
+
sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId: activeNow });
|
|
261
270
|
return;
|
|
262
271
|
}
|
|
263
272
|
}
|
|
273
|
+
const getBlobA = (h) => deps.fileHistoryStore.getBlob(h);
|
|
274
|
+
const r = await replayFileHistory(deps.fileHistoryStore, sessionId, fileHistory, getBlobA, { allowReplace: resolution === "overwrite-dst" });
|
|
275
|
+
const failCode = r.ok ? undefined : r.error.code === "not_found" ? "missing_blob" : r.error.code;
|
|
276
|
+
if (!r.ok) {
|
|
277
|
+
sendError(res, 422, failCode, `file history import failed (${r.error.code})`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
264
280
|
}
|
|
265
281
|
const opOk = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
|
|
266
282
|
if (deps.sessionPolicyStore)
|
|
@@ -299,13 +315,13 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
299
315
|
return;
|
|
300
316
|
}
|
|
301
317
|
}
|
|
302
|
-
const fss = deps.
|
|
303
|
-
|
|
304
|
-
if (canImportSnapshots) {
|
|
318
|
+
const fss = deps.fileHistoryStore;
|
|
319
|
+
if (fileHistory !== null && fileHistoryImportFace(fss) !== undefined) {
|
|
305
320
|
const distinct = new Set();
|
|
306
|
-
for (const
|
|
307
|
-
for (const
|
|
308
|
-
|
|
321
|
+
for (const t of fileHistory.tracked)
|
|
322
|
+
for (const v of t.versions)
|
|
323
|
+
if (v.blobHash !== null)
|
|
324
|
+
distinct.add(v.blobHash);
|
|
309
325
|
const all = [...distinct];
|
|
310
326
|
if (typeof fss?.hasBlobs === "function") {
|
|
311
327
|
const present = await fss.hasBlobs(all);
|
|
@@ -315,9 +331,9 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
315
331
|
return;
|
|
316
332
|
}
|
|
317
333
|
}
|
|
318
|
-
else {
|
|
334
|
+
else if (typeof fss?.getBlob === "function") {
|
|
319
335
|
for (const hash of all) {
|
|
320
|
-
if (!(await
|
|
336
|
+
if (!(await fss.getBlob(hash))) {
|
|
321
337
|
sendError(res, 422, "missing_blob", "missing blob; PUT all referenced blobs before import");
|
|
322
338
|
return;
|
|
323
339
|
}
|
|
@@ -332,13 +348,13 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
332
348
|
importLeases.set(sessionId, { at: Date.now(), stagingId });
|
|
333
349
|
pendingStagings.set(stagingId, {
|
|
334
350
|
handle, realSessionId: sessionId, owner: importingPrincipal, ...(resolution ? { resolution } : {}), operatorOk,
|
|
335
|
-
|
|
351
|
+
fileHistory, policy, anchors, declaredEntryIds: entryIds, openedBy: scope.gateOwner, openedFleetWide: scope.fleetWide,
|
|
336
352
|
});
|
|
337
353
|
sendJson(res, 200, { stagingId, relation: relA.relation });
|
|
338
354
|
return;
|
|
339
355
|
}
|
|
340
|
-
if (sm[
|
|
341
|
-
const stagingId = safeDecode(sm[
|
|
356
|
+
if (sm[5] !== undefined && req.method === "POST") {
|
|
357
|
+
const stagingId = safeDecode(sm[5]);
|
|
342
358
|
if (stagingId === null) {
|
|
343
359
|
sendError(res, 400, "request.id_invalid", "invalid staging id");
|
|
344
360
|
return;
|
|
@@ -352,8 +368,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
352
368
|
sendError(res, 404, "not_found.staging", "staging not found");
|
|
353
369
|
return;
|
|
354
370
|
}
|
|
355
|
-
const getBlob = (h) => deps.
|
|
356
|
-
const importFaceB = fileSnapshotImportFace(deps.fileSnapshotStore);
|
|
371
|
+
const getBlob = (h) => deps.fileHistoryStore?.getBlob ? deps.fileHistoryStore.getBlob(h) : Promise.resolve(undefined);
|
|
357
372
|
const validator = new StreamingImportValidator();
|
|
358
373
|
let lineBuf = "";
|
|
359
374
|
let pendingBatch = [];
|
|
@@ -453,17 +468,15 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
|
|
|
453
468
|
try {
|
|
454
469
|
const committed = await pending.handle.commit(pending.owner, pending.resolution ? { resolution: pending.resolution } : undefined);
|
|
455
470
|
if (pending.resolution === "overwrite-dst" && (committed.relation === "fork" || committed.relation === "stale")) {
|
|
456
|
-
await wipeSessionAttendants({
|
|
471
|
+
await wipeSessionAttendants({ fileHistoryStore: deps.fileHistoryStore, sessionPolicyStore: deps.sessionPolicyStore, resumeAnchorStore: deps.resumeAnchorStore }, sessionId);
|
|
457
472
|
}
|
|
458
|
-
if (
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
473
|
+
if (pending.fileHistory !== null && deps.fileHistoryStore && fileHistoryImportFace(deps.fileHistoryStore)) {
|
|
474
|
+
const r = await replayFileHistory(deps.fileHistoryStore, sessionId, pending.fileHistory, getBlob, { allowReplace: pending.resolution === "overwrite-dst" });
|
|
475
|
+
const failCode = r.ok ? undefined : r.error.code === "not_found" ? "missing_blob" : r.error.code;
|
|
476
|
+
if (!r.ok) {
|
|
477
|
+
sendError(res, 422, failCode, `file history import failed (${r.error.code})`);
|
|
478
|
+
cleanupStaging(stagingId);
|
|
479
|
+
return;
|
|
467
480
|
}
|
|
468
481
|
}
|
|
469
482
|
const policyStore = deps.sessionPolicyStore;
|