@sema-agent/server 7.141.0 → 7.143.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +657 -0
- package/USAGE.md +102 -30
- package/dist/adoption/plan.js +22 -0
- package/dist/approval-ask-audit-store.d.ts +61 -43
- package/dist/approval-ask-audit-store.js +69 -44
- package/dist/approval-ask-machine.d.ts +15 -7
- package/dist/approval-ask-machine.js +10 -4
- package/dist/approval-card.d.ts +126 -27
- package/dist/approval-card.js +103 -23
- package/dist/approval-crash-convergence.d.ts +17 -7
- package/dist/approval-crash-convergence.js +94 -22
- package/dist/approval-deny-reasons.d.ts +34 -30
- package/dist/approval-deny-reasons.js +21 -6
- package/dist/approval-policy-settlement.d.ts +1 -1
- package/dist/approval-queue-kind.d.ts +27 -0
- package/dist/approval-queue-kind.js +6 -0
- package/dist/approval-reconciler.d.ts +41 -9
- package/dist/approval-reconciler.js +45 -42
- package/dist/audit.js +4 -2
- package/dist/auth-bridge.d.ts +9 -6
- package/dist/auth-bridge.js +9 -3
- package/dist/boot/adoption.js +2 -1
- package/dist/boot/budget-tracing.d.ts +17 -1
- package/dist/boot/budget-tracing.js +10 -6
- package/dist/boot/device-lane.d.ts +1 -1
- package/dist/boot/execution-env.d.ts +24 -4
- package/dist/boot/execution-env.js +26 -23
- package/dist/boot/execution-lane.js +2 -1
- package/dist/boot/exit-boundary.js +5 -4
- package/dist/boot/governance-seams.d.ts +26 -41
- package/dist/boot/governance-seams.js +61 -28
- package/dist/boot/late-binds.d.ts +7 -4
- package/dist/boot/late-binds.js +3 -1
- package/dist/boot/leader.js +2 -4
- package/dist/boot/memory-consolidation.d.ts +0 -8
- package/dist/boot/memory-consolidation.js +5 -4
- package/dist/boot/parked-revive-gate.js +2 -1
- package/dist/boot/permission-rules-audit.d.ts +0 -28
- package/dist/boot/permission-rules-audit.js +2 -1
- package/dist/boot/read-deny-anchor-audit.js +2 -1
- package/dist/boot/reapers.js +9 -7
- package/dist/boot/resolve-spec.d.ts +1 -1
- package/dist/boot/resolve-spec.js +19 -10
- package/dist/boot/retention-lane.js +5 -3
- package/dist/boot/runner-deps.d.ts +10 -7
- package/dist/boot/runner-deps.js +20 -29
- package/dist/boot/runtime-caps.js +3 -2
- package/dist/boot/session-faces.js +4 -3
- package/dist/boot/shutdown.d.ts +9 -0
- package/dist/boot/shutdown.js +19 -4
- package/dist/boot/stage-02-web-admin-pull.js +3 -0
- package/dist/boot/stage-03-store-backend.d.ts +4 -1
- package/dist/boot/stage-03-store-backend.js +1 -2
- package/dist/boot/stage-04-stores.d.ts +5 -2
- package/dist/boot/stage-04-stores.js +6 -7
- package/dist/boot/stage-05-execution-env.d.ts +6 -3
- package/dist/boot/stage-05-execution-env.js +8 -6
- package/dist/boot/stage-06-runners.d.ts +6 -3
- package/dist/boot/stage-06-runners.js +3 -4
- package/dist/boot/stage-07-capability-layer.d.ts +6 -3
- package/dist/boot/stage-07-capability-layer.js +7 -2
- package/dist/boot/stage-08-reapers.d.ts +6 -3
- package/dist/boot/stage-08-reapers.js +3 -2
- package/dist/boot/stage-09-leader.d.ts +6 -3
- package/dist/boot/stage-10-http-server.d.ts +6 -3
- package/dist/boot/stage-10-http-server.js +14 -5
- package/dist/boot/stores.d.ts +5 -2
- package/dist/boot/stores.js +10 -9
- package/dist/boot/web-admin.d.ts +5 -1
- package/dist/boot/web-admin.js +48 -36
- package/dist/boot/webfetch-summarize-lane.js +2 -1
- package/dist/boot/workflow-orchestration.js +3 -2
- package/dist/capabilities/builtin-tools.d.ts +2 -1
- package/dist/capabilities/builtin-tools.js +2 -4
- package/dist/capabilities/composite-members.d.ts +2 -1
- package/dist/capabilities/composite-members.js +2 -1
- package/dist/capabilities/hands-lane.d.ts +9 -39
- package/dist/capabilities/hands-lane.js +8 -13
- package/dist/capabilities/mcp-probe.d.ts +3 -2
- package/dist/capabilities/scenarios.d.ts +11 -1
- package/dist/capabilities/scenarios.js +1 -0
- package/dist/capabilities/skills.d.ts +1 -17
- package/dist/capabilities/skills.js +0 -8
- package/dist/capability-facts.d.ts +31 -7
- package/dist/capability-facts.js +27 -4
- package/dist/card-void.d.ts +6 -9
- package/dist/config-catalog.js +2 -1
- package/dist/config-dir-trust.js +58 -29
- package/dist/config-invariants.d.ts +7 -1
- package/dist/config-invariants.js +7 -0
- package/dist/config-provider.d.ts +10 -0
- package/dist/config-provider.js +15 -4
- package/dist/config-root-migration.d.ts +13 -0
- package/dist/config-root-migration.js +99 -0
- package/dist/config-types.d.ts +19 -8
- package/dist/config.d.ts +10 -3
- package/dist/config.js +18 -4
- package/dist/delegated-session-owner.d.ts +5 -3
- package/dist/delegated-session-owner.js +1 -1
- package/dist/device-ws-protocol.d.ts +1 -0
- package/dist/device-ws-protocol.js +2 -1
- package/dist/env-model-table-gate.d.ts +53 -0
- package/dist/env-model-table-gate.js +53 -0
- package/dist/execution-lane-caps.d.ts +2 -1
- package/dist/fleet/fleet-bus.d.ts +34 -55
- package/dist/fleet/fleet-bus.js +35 -100
- package/dist/fleet/fleet-reconciler.d.ts +4 -4
- package/dist/fleet/fleet-reconciler.js +1 -1
- package/dist/fleet/subagent-tail-bus.d.ts +14 -11
- package/dist/fleet/subagent-tail-bus.js +15 -28
- package/dist/hooks/hook-runner.d.ts +6 -0
- package/dist/hooks/hook-runner.js +58 -8
- package/dist/host-decision.d.ts +54 -20
- package/dist/host-decision.js +42 -14
- package/dist/http/admission.d.ts +1 -1
- package/dist/http/admission.js +7 -3
- package/dist/http/approval-sweeps.js +11 -8
- package/dist/http/caller-trust.d.ts +1 -1
- package/dist/http/dispatch.js +4 -2
- package/dist/http/durable-run-leg.js +2 -2
- package/dist/http/engine-refusal.d.ts +16 -0
- package/dist/http/engine-refusal.js +17 -0
- package/dist/http/execution-context.d.ts +2 -2
- package/dist/http/idempotency.d.ts +1 -1
- package/dist/http/memory-compliance-failure-reply.d.ts +1 -3
- package/dist/http/memory-compliance-failure-reply.js +4 -6
- package/dist/http/owner-access.d.ts +11 -0
- package/dist/http/owner-access.js +13 -0
- package/dist/http/principal-gate.d.ts +3 -1
- package/dist/http/principal-gate.js +17 -8
- package/dist/http/resume-legs.d.ts +21 -7
- package/dist/http/resume-legs.js +76 -58
- package/dist/http/resume-restatement.d.ts +39 -0
- package/dist/http/resume-restatement.js +25 -0
- package/dist/http/route-ctx.d.ts +35 -10
- package/dist/http/route-ctx.js +1 -1
- package/dist/http/route-table.js +2 -0
- package/dist/http/routes/approvals-assistant.d.ts +9 -1
- package/dist/http/routes/approvals-assistant.js +142 -31
- package/dist/http/routes/attachments.js +18 -0
- package/dist/http/routes/capabilities.js +16 -4
- package/dist/http/routes/memory-policy.d.ts +2 -2
- package/dist/http/routes/memory-policy.js +27 -7
- package/dist/http/routes/notify-wake.js +23 -25
- package/dist/http/routes/rules.js +48 -13
- package/dist/http/routes/runs.js +12 -11
- package/dist/http/routes/session-memory-sync.d.ts +91 -0
- package/dist/http/routes/session-memory-sync.js +174 -0
- package/dist/http/routes/session-sync.js +13 -20
- package/dist/http/routes/sessions-list.d.ts +2 -1
- package/dist/http/routes/sessions.d.ts +2 -2
- package/dist/http/routes/sessions.js +22 -23
- package/dist/http/routes/tasks.js +34 -54
- package/dist/http/run-read-face.d.ts +7 -0
- package/dist/http/run-read-face.js +9 -3
- package/dist/http/send.d.ts +5 -0
- package/dist/http/send.js +9 -2
- package/dist/http/server.d.ts +27 -15
- package/dist/http/server.js +4 -6
- package/dist/http/stored-body-replay.js +15 -4
- package/dist/http/submit-replay.d.ts +4 -3
- package/dist/http/submit-replay.js +2 -2
- package/dist/http/sync-submit-body.d.ts +21 -4
- package/dist/http/sync-submit-body.js +13 -0
- package/dist/http/wire-types.d.ts +17 -7
- package/dist/identity-source.d.ts +21 -2
- package/dist/identity-source.js +6 -0
- package/dist/leader/leader.js +5 -2
- package/dist/leader/wire.d.ts +7 -1
- package/dist/leader/wire.js +6 -5
- package/dist/leg-facts.d.ts +37 -0
- package/dist/leg-facts.js +15 -0
- package/dist/memory-bundle-engine.d.ts +1 -4
- package/dist/memory-bundle-engine.js +2 -2
- package/dist/memory-incident-sink.d.ts +19 -6
- package/dist/memory-incident-sink.js +8 -4
- package/dist/memory-operator-faces.d.ts +30 -66
- package/dist/memory-operator-faces.js +17 -88
- package/dist/memory-scope-stores.d.ts +38 -40
- package/dist/memory-scope-stores.js +51 -36
- package/dist/memory-scope.d.ts +0 -9
- package/dist/memory-scope.js +0 -9
- package/dist/memory-sync-client.d.ts +21 -21
- package/dist/memory-sync-client.js +62 -43
- package/dist/memory-sync-identity.d.ts +24 -0
- package/dist/memory-sync-identity.js +57 -0
- package/dist/memory-sync.d.ts +27 -5
- package/dist/memory-sync.js +29 -7
- package/dist/model-plane-face.d.ts +13 -7
- package/dist/model-plane-face.js +2 -1
- package/dist/observability/fail-open.d.ts +47 -14
- package/dist/observability/fail-open.js +37 -11
- package/dist/observability/run-terminal.d.ts +92 -34
- package/dist/observability/run-terminal.js +171 -162
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -19
- package/dist/orchestration/workflow-completion-inbox.js +0 -12
- package/dist/orchestration/workflow-notify-journal.d.ts +3 -2
- package/dist/orchestration/workflow-park-decision.d.ts +8 -3
- package/dist/orchestration/workflow-park-decision.js +5 -3
- package/dist/org-delivery-gate.d.ts +11 -0
- package/dist/org-delivery-gate.js +19 -1
- package/dist/parked-decide.d.ts +13 -9
- package/dist/parked-decide.js +7 -7
- package/dist/parked-revive-surface.js +1 -1
- package/dist/peer-inbound-face.d.ts +127 -0
- package/dist/peer-inbound-face.js +158 -0
- package/dist/permission-rule-list.d.ts +13 -9
- package/dist/permission-rule-list.js +7 -3
- package/dist/plugins/approval-ask-store-file.d.ts +14 -4
- package/dist/plugins/approval-ask-store-file.js +64 -11
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -1
- package/dist/plugins/approval-ask-store-memory.js +17 -14
- package/dist/plugins/approval-ask-store-sql.d.ts +70 -23
- package/dist/plugins/approval-ask-store-sql.js +79 -30
- package/dist/plugins/checkpoint-store-sql.d.ts +4 -4
- package/dist/plugins/checkpoint-store-sql.js +9 -7
- package/dist/plugins/file-history-store-sql.d.ts +24 -18
- package/dist/plugins/file-history-store-sql.js +24 -19
- package/dist/plugins/file-run-store.d.ts +35 -1
- package/dist/plugins/file-run-store.js +108 -9
- package/dist/plugins/imported-layer-root-retirement.d.ts +82 -0
- package/dist/plugins/imported-layer-root-retirement.js +96 -0
- package/dist/plugins/local-checkpoint-store.d.ts +1 -1
- package/dist/plugins/local-checkpoint-store.js +2 -2
- package/dist/plugins/local-session-store.d.ts +42 -9
- package/dist/plugins/local-session-store.js +83 -33
- package/dist/plugins/local-task-attachment-store.d.ts +12 -2
- package/dist/plugins/local-task-attachment-store.js +67 -43
- package/dist/plugins/mailbox-recipient-lifecycle.d.ts +9 -31
- package/dist/plugins/mailbox-recipient-lifecycle.js +4 -0
- package/dist/plugins/mailbox-store-sql.d.ts +12 -8
- package/dist/plugins/mailbox-store-sql.js +76 -7
- package/dist/plugins/memory-key-guards.d.ts +3 -3
- package/dist/plugins/memory-run-store.d.ts +6 -1
- package/dist/plugins/memory-run-store.js +11 -1
- package/dist/plugins/memory-sync-store-pg.d.ts +15 -0
- package/dist/plugins/memory-sync-store-pg.js +21 -0
- package/dist/plugins/memory-sync-store-tidb.d.ts +2 -0
- package/dist/plugins/memory-sync-store-tidb.js +13 -1
- package/dist/plugins/one-time-migrations.d.ts +58 -2
- package/dist/plugins/one-time-migrations.js +161 -4
- package/dist/plugins/org-delivery-quota-store.d.ts +1 -1
- package/dist/plugins/org-delivery-quota-store.js +2 -12
- package/dist/plugins/park-commit-watch.d.ts +1 -1
- package/dist/plugins/permission-rule-store-file.d.ts +10 -1
- package/dist/plugins/permission-rule-store-file.js +15 -2
- package/dist/plugins/permission-rule-store-sql.d.ts +31 -1
- package/dist/plugins/permission-rule-store-sql.js +35 -2
- package/dist/plugins/remote-env-device.d.ts +8 -11
- package/dist/plugins/remote-env-device.js +6 -3
- package/dist/plugins/remote-env-e2b.d.ts +23 -4
- package/dist/plugins/remote-env-e2b.js +111 -28
- package/dist/plugins/remote-env-k8s.d.ts +61 -22
- package/dist/plugins/remote-env-k8s.js +133 -33
- package/dist/plugins/remote-env-local-docker.d.ts +15 -3
- package/dist/plugins/remote-env-local-docker.js +40 -12
- package/dist/plugins/retention-store-sql.d.ts +2 -2
- package/dist/plugins/run-store-sql.d.ts +9 -1
- package/dist/plugins/run-store-sql.js +9 -1
- package/dist/plugins/sandbox-read-floor.d.ts +5 -7
- package/dist/plugins/sandbox-read-floor.js +2 -7
- package/dist/plugins/sql-errors.d.ts +10 -1
- package/dist/plugins/sql-errors.js +13 -0
- package/dist/plugins/store-backend.d.ts +33 -35
- package/dist/plugins/store-backend.js +10 -1
- package/dist/plugins/store-contracts.d.ts +50 -1
- package/dist/plugins/store-contracts.js +6 -0
- package/dist/plugins/task-attachment-store.d.ts +112 -14
- package/dist/plugins/task-attachment-store.js +207 -29
- package/dist/plugins/tool-result-store-sql.d.ts +9 -8
- package/dist/plugins/tool-result-store-sql.js +23 -2
- package/dist/plugins/wiring-section-codec.d.ts +32 -0
- package/dist/plugins/wiring-section-codec.js +55 -0
- package/dist/plugins/workspace-fill.d.ts +60 -0
- package/dist/plugins/workspace-fill.js +106 -0
- package/dist/project-bundle.d.ts +38 -0
- package/dist/project-bundle.js +609 -0
- package/dist/rule-entry-list.d.ts +3 -2
- package/dist/rule-entry-list.js +5 -1
- package/dist/rules-consent.d.ts +28 -5
- package/dist/rules-consent.js +40 -28
- package/dist/run-local.js +11 -13
- package/dist/runs.d.ts +12 -8
- package/dist/runs.js +6 -8
- package/dist/runtime-governance.d.ts +1 -1
- package/dist/sandbox-egress-policy.d.ts +4 -1
- package/dist/sandbox-egress-policy.js +3 -1
- package/dist/sandbox-git-plane.d.ts +31 -12
- package/dist/sandbox-git-plane.js +24 -4
- package/dist/security.d.ts +9 -0
- package/dist/security.js +8 -0
- package/dist/server-secret-env.d.ts +4 -0
- package/dist/session-error-wire.d.ts +36 -0
- package/dist/session-error-wire.js +13 -0
- package/dist/session-incarnation.d.ts +1 -1
- package/dist/session-notify-door.d.ts +106 -0
- package/dist/session-notify-door.js +93 -0
- package/dist/session-sync.d.ts +9 -14
- package/dist/session-sync.js +5 -5
- package/dist/session-workspace.d.ts +3 -13
- package/dist/session-workspace.js +0 -9
- package/dist/standing-answer-grant.d.ts +5 -0
- package/dist/standing-answer-grant.js +10 -0
- package/dist/stores-sql/session-policy-store-sql.d.ts +4 -2
- package/dist/stores-sql/session-policy-store-sql.js +3 -13
- package/dist/task-mcp.d.ts +1 -1
- package/dist/task-settings.d.ts +21 -10
- package/dist/task-settings.js +46 -13
- package/dist/terminal.d.ts +39 -19
- package/dist/terminal.js +65 -22
- package/dist/tool-approval.d.ts +56 -37
- package/dist/tool-approval.js +78 -71
- package/dist/trace/core-keyset-guard.d.ts +134 -14
- package/dist/trace/engine-notice-wire.d.ts +9 -3
- package/dist/trace/engine-notice-wire.js +7 -3
- package/dist/trace/exit-seat.d.ts +9 -2
- package/dist/trace/exit-seat.js +1 -0
- package/dist/trace/ledger-sink.d.ts +10 -6
- package/dist/trace/ledger-sink.js +2 -2
- package/dist/trace/model-segment.d.ts +55 -25
- package/dist/trace/model-segment.js +101 -33
- package/dist/trace/project.d.ts +25 -5
- package/dist/trace/project.js +22 -0
- package/dist/trace/redact.d.ts +61 -9
- package/dist/trace/redact.js +1490 -468
- package/dist/trace/self-minted-wire.d.ts +745 -24
- package/dist/trace/self-minted-wire.js +239 -9
- package/dist/trace/wire-projection-faces.d.ts +2 -2
- package/dist/trace/wire-projection-faces.js +2 -2
- package/dist/verifier-pause-card.d.ts +33 -5
- package/dist/verifier-pause-card.js +15 -10
- package/dist/web-admin-client/apply-effective.d.ts +12 -16
- package/dist/web-admin-client/apply-effective.js +109 -9
- package/dist/web-admin-client/effective-keys.js +2 -0
- package/dist/web-admin-client/types.d.ts +4 -0
- package/dist/workspace-space.d.ts +19 -3
- package/dist/workspace-space.js +8 -4
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +3 -1
- package/package.json +3 -2
- package/wire-changes/server@7.142.0.json +639 -0
- package/wire-changes/server@7.143.0.json +1167 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ensureIndexes } from "./ensure-index.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { attachmentKindOf, PROJECT_BUNDLE_MIME } from "../project-bundle.js";
|
|
5
|
+
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
6
|
+
import { isDupKeyError, retryOnDeadlock } from "./sql-errors.js";
|
|
3
7
|
export const TaskAttachmentRecordSchema = z.object({
|
|
4
8
|
id: z.string(),
|
|
5
9
|
owner: z.string(),
|
|
@@ -36,42 +40,151 @@ function rowToRecord(r) {
|
|
|
36
40
|
...(r.bound_at_ms !== null && r.bound_at_ms !== undefined ? { boundAtMs: Number(r.bound_at_ms) } : {}),
|
|
37
41
|
};
|
|
38
42
|
}
|
|
43
|
+
export function bindRowsInProcess(rows, owner, ids, sessionId, boundAtMs) {
|
|
44
|
+
const bound = [];
|
|
45
|
+
const refused = [];
|
|
46
|
+
const claimed = [];
|
|
47
|
+
for (const id of new Set(ids)) {
|
|
48
|
+
const row = rows.get(id);
|
|
49
|
+
if (row === undefined || row.owner !== owner)
|
|
50
|
+
continue;
|
|
51
|
+
if (row.sessionId !== undefined && row.sessionId !== sessionId) {
|
|
52
|
+
refused.push({ kind: "bound_elsewhere", id });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (row.sessionId === undefined) {
|
|
56
|
+
if (attachmentKindOf(row.mime) === "project_bundle" && [...rows.values()].some((r) => r.sessionId === sessionId && attachmentKindOf(r.mime) === "project_bundle")) {
|
|
57
|
+
refused.push({ kind: "bundle_seed_fixed", id });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
row.sessionId = sessionId;
|
|
61
|
+
row.boundAtMs = boundAtMs;
|
|
62
|
+
claimed.push(row);
|
|
63
|
+
}
|
|
64
|
+
bound.push(id);
|
|
65
|
+
}
|
|
66
|
+
return { outcome: { bound, refused }, claimed };
|
|
67
|
+
}
|
|
68
|
+
export const ATTACHMENT_BIND_HTTP_STATUS = {
|
|
69
|
+
"attachment.bound_elsewhere": 409,
|
|
70
|
+
"attachment.bundle_lane_not_isolated": 409,
|
|
71
|
+
"attachment.bundle_seed_fixed": 409,
|
|
72
|
+
};
|
|
73
|
+
export function attachmentBindRefusalAnswer(r) {
|
|
74
|
+
const answer = (errorCode, message, extra) => ({ status: ATTACHMENT_BIND_HTTP_STATUS[errorCode], errorCode, message, extra });
|
|
75
|
+
switch (r.kind) {
|
|
76
|
+
case "bound_elsewhere":
|
|
77
|
+
return answer("attachment.bound_elsewhere", `attachment ${r.id} is bound to another session — an attachment, once bound, stays with that session; upload the file again for this one`, { id: r.id });
|
|
78
|
+
case "bundle_lane_not_isolated":
|
|
79
|
+
return answer("attachment.bundle_lane_not_isolated", `attachment ${r.id} is a project bundle, and this run's execution lane '${r.lane}' is not an isolated sandbox — a project bundle is unpacked only inside an isolated lane (e2b / k8s / local-docker)`, { id: r.id, lane: r.lane });
|
|
80
|
+
case "bundle_seed_fixed":
|
|
81
|
+
return answer("attachment.bundle_seed_fixed", `attachment ${r.id} is a project bundle, and this session already has its seed (the earliest bound project bundle) — a session takes one project bundle`, { id: r.id });
|
|
82
|
+
default: {
|
|
83
|
+
const unreachable = r;
|
|
84
|
+
throw new Error(`unhandled attachment bind refusal: ${String(unreachable.kind)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export async function listAttachmentsBySession(store, sessionId) {
|
|
89
|
+
return (await store.listBySession(sessionId)).filter((r) => attachmentKindOf(r.mime) === "attachment");
|
|
90
|
+
}
|
|
91
|
+
function seedRowOf(rows) {
|
|
92
|
+
return rows
|
|
93
|
+
.filter((r) => attachmentKindOf(r.mime) === "project_bundle")
|
|
94
|
+
.sort((a, b) => (a.boundAtMs ?? 0) - (b.boundAtMs ?? 0) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))[0];
|
|
95
|
+
}
|
|
96
|
+
export async function seedOfSession(store, sessionId) {
|
|
97
|
+
const record = seedRowOf(await store.listBySession(sessionId));
|
|
98
|
+
if (record === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
const bytes = await store.getContent(record.owner, record.id);
|
|
101
|
+
if (!bytes)
|
|
102
|
+
return { record, broken: `the session's project bundle ${record.id} (${record.name}) has no content in the attachment store` };
|
|
103
|
+
const sha = createHash("sha256").update(bytes).digest("hex");
|
|
104
|
+
if (bytes.byteLength !== record.sizeBytes || sha !== record.sha256) {
|
|
105
|
+
return { record, broken: `the session's project bundle ${record.id} (${record.name}) does not match its upload digest (${bytes.byteLength} bytes, sha256 ${sha}; recorded ${record.sizeBytes} bytes, sha256 ${record.sha256})` };
|
|
106
|
+
}
|
|
107
|
+
return { record, bytes };
|
|
108
|
+
}
|
|
39
109
|
export async function bindAttachmentsForTask(opts) {
|
|
40
110
|
const found = [];
|
|
111
|
+
let seedCandidate;
|
|
41
112
|
for (const id of opts.ids) {
|
|
42
113
|
const rec = await opts.store.get(opts.owner, id);
|
|
43
114
|
if (!rec) {
|
|
44
115
|
opts.onMissing(id);
|
|
45
116
|
continue;
|
|
46
117
|
}
|
|
118
|
+
if (rec.sessionId !== undefined && rec.sessionId !== opts.sessionId) {
|
|
119
|
+
opts.onRefused({ kind: "bound_elsewhere", id });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (attachmentKindOf(rec.mime) === "project_bundle" && rec.sessionId !== opts.sessionId) {
|
|
123
|
+
if (!opts.lane.isolated) {
|
|
124
|
+
opts.onRefused({ kind: "bundle_lane_not_isolated", id, lane: opts.lane.word });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (seedCandidate !== undefined) {
|
|
128
|
+
opts.onRefused({ kind: "bundle_seed_fixed", id });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
seedCandidate = id;
|
|
132
|
+
}
|
|
47
133
|
found.push(rec);
|
|
48
134
|
}
|
|
49
135
|
if (found.length === 0)
|
|
50
136
|
return { notice: undefined };
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
137
|
+
const groups = seedCandidate === undefined ? [found] : [found.filter((r) => r.id === seedCandidate), found.filter((r) => r.id !== seedCandidate)];
|
|
138
|
+
const bound = new Set();
|
|
139
|
+
for (const group of groups) {
|
|
140
|
+
if (group.length === 0)
|
|
141
|
+
continue;
|
|
142
|
+
const outcome = await opts.store.bind(opts.owner, group.map((r) => r.id), opts.sessionId, opts.nowMs);
|
|
143
|
+
for (const id of outcome.bound)
|
|
144
|
+
bound.add(id);
|
|
145
|
+
const refused = new Set(outcome.refused.map((r) => r.id));
|
|
146
|
+
for (const r of outcome.refused)
|
|
147
|
+
opts.onRefused(r);
|
|
148
|
+
for (const r of group)
|
|
149
|
+
if (!bound.has(r.id) && !refused.has(r.id))
|
|
150
|
+
opts.onMissing(r.id);
|
|
151
|
+
}
|
|
152
|
+
const held = found.filter((r) => bound.has(r.id));
|
|
153
|
+
const attachments = held.filter((r) => attachmentKindOf(r.mime) === "attachment");
|
|
154
|
+
if (attachments.length === 0)
|
|
155
|
+
return { notice: undefined };
|
|
156
|
+
const paths = materializedRelPaths(await listAttachmentsBySession(opts.store, opts.sessionId));
|
|
157
|
+
const lines = attachments.map((r) => `- ${paths.get(r.id) ?? `attachments/${r.name}`} (${r.mime}, ${r.sizeBytes} bytes)`);
|
|
158
|
+
return { notice: `[attachments] ${attachments.length} uploaded file(s) are available in the \`attachments/\` directory of your working directory:\n${lines.join("\n")}` };
|
|
159
|
+
}
|
|
160
|
+
export async function attachmentPlacementsOf(store, sessionId) {
|
|
161
|
+
const atts = await listAttachmentsBySession(store, sessionId);
|
|
162
|
+
const paths = materializedRelPaths(atts);
|
|
163
|
+
return atts.map((record) => ({ record, relPath: paths.get(record.id) }));
|
|
164
|
+
}
|
|
165
|
+
export async function attachmentBytesOf(store, rec) {
|
|
166
|
+
const bytes = await store.getContent(rec.owner, rec.id);
|
|
167
|
+
if (!bytes)
|
|
168
|
+
throw new Error(`attachment materialization failed: content missing for ${rec.id} (${rec.name})`);
|
|
169
|
+
return bytes;
|
|
56
170
|
}
|
|
57
171
|
export async function materializeAttachmentsInto(env, store, sessionId) {
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
172
|
+
const placements = await attachmentPlacementsOf(store, sessionId);
|
|
173
|
+
if (placements.length === 0)
|
|
60
174
|
return 0;
|
|
61
|
-
const paths = materializedRelPaths(atts);
|
|
62
175
|
const mk = await env.createDir("attachments", { recursive: true });
|
|
63
176
|
if (!mk.ok)
|
|
64
177
|
throw new Error(`attachment materialization failed (createDir attachments/): ${String(mk.error?.message ?? "unknown")}`);
|
|
65
|
-
for (const
|
|
66
|
-
const bytes = await store
|
|
67
|
-
|
|
68
|
-
throw new Error(`attachment materialization failed: content missing for ${a.id} (${a.name})`);
|
|
69
|
-
const w = await env.writeFile(paths.get(a.id), bytes);
|
|
178
|
+
for (const p of placements) {
|
|
179
|
+
const bytes = await attachmentBytesOf(store, p.record);
|
|
180
|
+
const w = await env.writeFile(p.relPath, bytes);
|
|
70
181
|
if (!w.ok)
|
|
71
|
-
throw new Error(`attachment materialization failed (write ${
|
|
182
|
+
throw new Error(`attachment materialization failed (write ${p.relPath}): ${String(w.error?.message ?? "unknown")}`);
|
|
72
183
|
}
|
|
73
|
-
return
|
|
184
|
+
return placements.length;
|
|
74
185
|
}
|
|
186
|
+
const SEED_KEY = "uq_att_seed";
|
|
187
|
+
const SEED_KEY_EXPR = `CASE WHEN mime = '${PROJECT_BUNDLE_MIME}' THEN session_id END`;
|
|
75
188
|
export async function ensureTiDBTaskAttachmentSchema(pool) {
|
|
76
189
|
await pool.query(`CREATE TABLE IF NOT EXISTS ${TASK_ATTACHMENT_TABLE} (
|
|
77
190
|
id VARCHAR(64) NOT NULL,
|
|
@@ -85,15 +198,19 @@ export async function ensureTiDBTaskAttachmentSchema(pool) {
|
|
|
85
198
|
session_id VARCHAR(190) NULL,
|
|
86
199
|
created_at_ms BIGINT NOT NULL,
|
|
87
200
|
bound_at_ms BIGINT NULL,
|
|
201
|
+
-- 种子键:项目包行的会话,其余行 NULL(引擎推导,没有写者);唯一 ⇒ 一只会话一只种子
|
|
202
|
+
seed_session_id VARCHAR(190) GENERATED ALWAYS AS (${SEED_KEY_EXPR}) VIRTUAL,
|
|
88
203
|
PRIMARY KEY (id),
|
|
89
204
|
KEY idx_att_session (session_id),
|
|
90
|
-
KEY idx_att_unbound (created_at_ms)
|
|
205
|
+
KEY idx_att_unbound (created_at_ms),
|
|
206
|
+
UNIQUE KEY ${SEED_KEY} (seed_session_id)
|
|
91
207
|
) COLLATE utf8mb4_bin`);
|
|
92
208
|
await ensureIndexes((text, params) => pool.query(text, params), "mysql", TASK_ATTACHMENT_INDEXES);
|
|
93
209
|
}
|
|
94
210
|
export const TASK_ATTACHMENT_INDEXES = [
|
|
95
211
|
{ table: `${TASK_ATTACHMENT_TABLE}`, name: "idx_att_session", columns: ["session_id"] },
|
|
96
212
|
{ table: `${TASK_ATTACHMENT_TABLE}`, name: "idx_att_unbound", columns: ["created_at_ms"] },
|
|
213
|
+
{ table: `${TASK_ATTACHMENT_TABLE}`, name: SEED_KEY, columns: ["seed_session_id"], unique: true },
|
|
97
214
|
];
|
|
98
215
|
export async function ensurePgTaskAttachmentSchema(q) {
|
|
99
216
|
await q(`CREATE TABLE IF NOT EXISTS ${TASK_ATTACHMENT_TABLE} (
|
|
@@ -107,16 +224,81 @@ export async function ensurePgTaskAttachmentSchema(q) {
|
|
|
107
224
|
session_id VARCHAR(190) COLLATE "C",
|
|
108
225
|
created_at_ms BIGINT NOT NULL,
|
|
109
226
|
bound_at_ms BIGINT,
|
|
227
|
+
-- 种子键(同 MySQL 侧注)
|
|
228
|
+
seed_session_id VARCHAR(190) COLLATE "C" GENERATED ALWAYS AS (${SEED_KEY_EXPR}) STORED,
|
|
110
229
|
PRIMARY KEY (id)
|
|
111
230
|
)`);
|
|
112
231
|
await ensureIndexes(q, "pg", TASK_ATTACHMENT_INDEXES);
|
|
113
232
|
}
|
|
233
|
+
async function bindOnSql(db, owner, ids, sessionId, boundAtMs) {
|
|
234
|
+
const list = [...new Set(ids)];
|
|
235
|
+
if (list.length === 0)
|
|
236
|
+
return { bound: [], refused: [] };
|
|
237
|
+
const mysql = db.dialect === "mysql";
|
|
238
|
+
const q = (mysqlSql, pgSql) => (mysql ? mysqlSql : pgSql);
|
|
239
|
+
const ownRows = (columns, of) => db.query(q(`SELECT ${columns} FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = ? AND id IN (${of.map(() => "?").join(",")})`, `SELECT ${columns} FROM ${TASK_ATTACHMENT_TABLE} WHERE owner = $1 AND id = ANY($2::varchar[])`), mysql ? [owner, ...of] : [owner, of]);
|
|
240
|
+
const kindOf = new Map((await ownRows("id, mime", list)).rows.map((r) => [String(r.id), attachmentKindOf(String(r.mime))]));
|
|
241
|
+
const claim = (of) => retryOnDeadlock(db.dialect, () => inTransaction(db, (conn) => conn.query(q(`UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = CASE WHEN session_id IS NULL THEN ? ELSE bound_at_ms END, session_id = ? WHERE owner = ? AND id IN (${of.map(() => "?").join(",")}) AND (session_id IS NULL OR session_id = ?)`, `UPDATE ${TASK_ATTACHMENT_TABLE} SET bound_at_ms = CASE WHEN session_id IS NULL THEN $2::bigint ELSE bound_at_ms END, session_id = $1::varchar WHERE owner = $3 AND id = ANY($4::varchar[]) AND (session_id IS NULL OR session_id = $1::varchar)`), mysql ? [boundAtMs, sessionId, owner, ...of, sessionId] : [sessionId, boundAtMs, owner, of])));
|
|
242
|
+
const attachments = list.filter((id) => kindOf.get(id) === "attachment");
|
|
243
|
+
if (attachments.length > 0)
|
|
244
|
+
await claim(attachments);
|
|
245
|
+
const seedFixed = new Set();
|
|
246
|
+
for (const id of list) {
|
|
247
|
+
if (kindOf.get(id) !== "project_bundle")
|
|
248
|
+
continue;
|
|
249
|
+
try {
|
|
250
|
+
await claim([id]);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
if (!isSeedKeyConflict(db.dialect, err))
|
|
254
|
+
throw err;
|
|
255
|
+
seedFixed.add(id);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const holderOf = new Map((await ownRows("id, session_id", list)).rows.map((r) => [String(r.id), r.session_id === null || r.session_id === undefined ? undefined : String(r.session_id)]));
|
|
259
|
+
const bound = [];
|
|
260
|
+
const refused = [];
|
|
261
|
+
for (const id of list) {
|
|
262
|
+
if (seedFixed.has(id))
|
|
263
|
+
refused.push({ kind: "bundle_seed_fixed", id });
|
|
264
|
+
else if (holderOf.get(id) === sessionId)
|
|
265
|
+
bound.push(id);
|
|
266
|
+
else if (holderOf.get(id) !== undefined)
|
|
267
|
+
refused.push({ kind: "bound_elsewhere", id });
|
|
268
|
+
}
|
|
269
|
+
return { bound, refused };
|
|
270
|
+
}
|
|
271
|
+
async function inTransaction(db, work) {
|
|
272
|
+
const conn = await db.connect();
|
|
273
|
+
try {
|
|
274
|
+
await conn.begin();
|
|
275
|
+
const out = await work(conn);
|
|
276
|
+
await conn.commit();
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
await conn.rollback().catch(() => undefined);
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
conn.release();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function isSeedKeyConflict(dialect, err) {
|
|
288
|
+
if (!isDupKeyError(dialect, err) || err === null || typeof err !== "object")
|
|
289
|
+
return false;
|
|
290
|
+
if (dialect === "pg")
|
|
291
|
+
return Reflect.get(err, "constraint") === SEED_KEY;
|
|
292
|
+
return new RegExp(`\\b${SEED_KEY}\\b`).test(String(Reflect.get(err, "sqlMessage") ?? Reflect.get(err, "message") ?? ""));
|
|
293
|
+
}
|
|
114
294
|
export class TiDBTaskAttachmentStore {
|
|
115
295
|
pool;
|
|
116
296
|
bytes;
|
|
297
|
+
db;
|
|
117
298
|
constructor(pool, bytes) {
|
|
118
299
|
this.pool = pool;
|
|
119
300
|
this.bytes = bytes;
|
|
301
|
+
this.db = mysqlDriver(pool);
|
|
120
302
|
}
|
|
121
303
|
async put(rec) {
|
|
122
304
|
await this.bytes.putBlob(rec.sha256, rec.content);
|
|
@@ -144,11 +326,8 @@ export class TiDBTaskAttachmentStore {
|
|
|
144
326
|
}
|
|
145
327
|
}
|
|
146
328
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return 0;
|
|
150
|
-
const [res] = await this.pool.query(`UPDATE ${TASK_ATTACHMENT_TABLE} SET session_id = ?, bound_at_ms = ? WHERE owner = ? AND id IN (${ids.map(() => "?").join(",")})`, [sessionId, boundAtMs, owner, ...ids]);
|
|
151
|
-
return res.affectedRows ?? 0;
|
|
329
|
+
bind(owner, ids, sessionId, boundAtMs) {
|
|
330
|
+
return bindOnSql(this.db, owner, ids, sessionId, boundAtMs);
|
|
152
331
|
}
|
|
153
332
|
async listBySession(sessionId) {
|
|
154
333
|
const [rows] = await this.pool.query(`SELECT ${COLS} FROM ${TASK_ATTACHMENT_TABLE} WHERE session_id = ? ORDER BY created_at_ms ASC, id ASC`, [sessionId]);
|
|
@@ -229,11 +408,13 @@ export class TiDBTaskAttachmentStore {
|
|
|
229
408
|
}
|
|
230
409
|
}
|
|
231
410
|
export class PgTaskAttachmentStore {
|
|
232
|
-
q;
|
|
233
411
|
bytes;
|
|
234
|
-
|
|
235
|
-
|
|
412
|
+
db;
|
|
413
|
+
q;
|
|
414
|
+
constructor(pool, bytes) {
|
|
236
415
|
this.bytes = bytes;
|
|
416
|
+
this.db = pgDriver(pool);
|
|
417
|
+
this.q = async (text, params) => ({ rows: (await pool.query(text, params)).rows });
|
|
237
418
|
}
|
|
238
419
|
async put(rec) {
|
|
239
420
|
await this.bytes.putBlob(rec.sha256, rec.content);
|
|
@@ -261,11 +442,8 @@ export class PgTaskAttachmentStore {
|
|
|
261
442
|
}
|
|
262
443
|
}
|
|
263
444
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return 0;
|
|
267
|
-
const res = await this.q(`UPDATE ${TASK_ATTACHMENT_TABLE} SET session_id = $1, bound_at_ms = $2 WHERE owner = $3 AND id = ANY($4::varchar[]) RETURNING id`, [sessionId, boundAtMs, owner, ids]);
|
|
268
|
-
return res.rows.length;
|
|
445
|
+
bind(owner, ids, sessionId, boundAtMs) {
|
|
446
|
+
return bindOnSql(this.db, owner, ids, sessionId, boundAtMs);
|
|
269
447
|
}
|
|
270
448
|
async listBySession(sessionId) {
|
|
271
449
|
const res = await this.q(`SELECT ${COLS} FROM ${TASK_ATTACHMENT_TABLE} WHERE session_id = $1 ORDER BY created_at_ms ASC, id ASC`, [sessionId]);
|
|
@@ -28,7 +28,6 @@ export declare function ensureSchema(pool: PgPool | PgPoolClient): Promise<void>
|
|
|
28
28
|
export declare function assertToolResultProvenanceSchema(query: (sql: string) => Promise<{
|
|
29
29
|
rows: Record<string, unknown>[];
|
|
30
30
|
}>, dialect: SqlDialect): Promise<void>;
|
|
31
|
-
/** Dual-dialect durable ToolResultStore. See the file header for the dialect-delta ledger. */
|
|
32
31
|
export declare class SqlToolResultStore implements ToolResultStore {
|
|
33
32
|
protected readonly db: SqlDriver;
|
|
34
33
|
/** 托管留存声明 = 装配交给它的词(后端按扫车道开不开给;不给 ⇒ `"none"`)。读法与「为什么 SQL 店答 managed 而三方法的实现体在
|
|
@@ -73,14 +72,16 @@ export declare class SqlToolResultStore implements ToolResultStore {
|
|
|
73
72
|
offset?: number;
|
|
74
73
|
limit?: number;
|
|
75
74
|
}): Promise<ToolResultSlice | undefined>;
|
|
76
|
-
/**
|
|
75
|
+
/** core `ToolResultStore.reap` (core 7.61.0 — the one age bound every backend reads): remove the results stored more than
|
|
76
|
+
* `opts.maxAgeMs` before `now`. No bound ⇒ nothing removed; a bound that cannot be evaluated (`maxAgeMs` not a finite
|
|
77
|
+
* number ≥ 0, `now` not a finite number) ⇒ refused, coded `config.retention_policy_invalid`, before any row is touched;
|
|
78
|
+
* age alone decides (ownership is not consulted — the row and its owner columns go together). Returns rows removed.
|
|
77
79
|
* C6: a real DB error must NOT collapse into the same `0` a legitimate "nothing was old enough" returns —
|
|
78
|
-
* those are different facts (query failed vs. query succeeded on an empty set)
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
reapOlderThan(cutoffMs: number): Promise<number>;
|
|
80
|
+
* those are different facts (query failed vs. query succeeded on an empty set). Let the error propagate — the
|
|
81
|
+
* periodic-sweep call site (boot/reapers.ts) wraps this call in its throttled catch so the reap loop never dies. */
|
|
82
|
+
reap(now: number, opts?: {
|
|
83
|
+
maxAgeMs?: number;
|
|
84
|
+
}): Promise<number>;
|
|
84
85
|
/**
|
|
85
86
|
* E21 (§0.5 session delete) — purge offloaded tool results for one session. Selection is by the row's
|
|
86
87
|
* **recorded provenance** (`owner_session_id`, the [ref] column `put` writes in the same statement as the
|
|
@@ -55,6 +55,24 @@ export async function assertToolResultProvenanceSchema(query, dialect) {
|
|
|
55
55
|
(await describeProbedTableDrift(query, dialect, ["tool_result"])));
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
function toolResultAgeCutoff(now, opts) {
|
|
59
|
+
const refuse = (what, v) => {
|
|
60
|
+
throw Object.assign(new Error(`ToolResultStore.reap: ${what} must be a finite number${what === "maxAgeMs" ? " of milliseconds of 0 or more" : ""} (got ${typeof v === "string" ? JSON.stringify(v) : String(v)}) — a retention bound that cannot be evaluated is refused, never read as "no bound"`), {
|
|
61
|
+
code: "config.retention_policy_invalid",
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
if (opts !== undefined) {
|
|
65
|
+
for (const key of Object.keys(opts))
|
|
66
|
+
if (key !== "maxAgeMs")
|
|
67
|
+
refuse(`the bound "${key}" (only maxAgeMs is taken)`, opts[key]);
|
|
68
|
+
}
|
|
69
|
+
const maxAgeMs = opts?.maxAgeMs;
|
|
70
|
+
if (maxAgeMs !== undefined && (typeof maxAgeMs !== "number" || !Number.isFinite(maxAgeMs) || maxAgeMs < 0))
|
|
71
|
+
refuse("maxAgeMs", maxAgeMs);
|
|
72
|
+
if (typeof now !== "number" || !Number.isFinite(now))
|
|
73
|
+
refuse("now", now);
|
|
74
|
+
return maxAgeMs === undefined ? undefined : now - maxAgeMs;
|
|
75
|
+
}
|
|
58
76
|
export class SqlToolResultStore {
|
|
59
77
|
db;
|
|
60
78
|
retention;
|
|
@@ -126,8 +144,11 @@ export class SqlToolResultStore {
|
|
|
126
144
|
return undefined;
|
|
127
145
|
return { content: String(rows[0].slice ?? ""), offset, totalChars: Number(rows[0].total) };
|
|
128
146
|
}
|
|
129
|
-
async
|
|
130
|
-
const
|
|
147
|
+
async reap(now, opts) {
|
|
148
|
+
const cutoff = toolResultAgeCutoff(now, opts);
|
|
149
|
+
if (cutoff === undefined)
|
|
150
|
+
return 0;
|
|
151
|
+
const { affected } = await this.db.query(this.q("DELETE FROM tool_result WHERE created_at < ?", "DELETE FROM tool_result WHERE created_at < $1"), [new Date(cutoff)]);
|
|
131
152
|
return affected;
|
|
132
153
|
}
|
|
133
154
|
async deleteBySession(sessionId) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** 块引用的唯一键。 */
|
|
2
|
+
export declare const WIRING_SECTION_REF_KEY = "$blob";
|
|
3
|
+
/** 一只段体:摘要(`sha256:<64 hex>`)+ 段值本身(JSON 容器)。 */
|
|
4
|
+
export interface WiringSectionBlob {
|
|
5
|
+
readonly digest: string;
|
|
6
|
+
readonly body: object;
|
|
7
|
+
}
|
|
8
|
+
/** {@link splitForStore} 的产物:要落账本的那一形 + 本帧引用到的段体(按摘要去重,首见为准)。 */
|
|
9
|
+
export interface WiringSectionSplit {
|
|
10
|
+
readonly stored: unknown;
|
|
11
|
+
readonly blobs: readonly WiringSectionBlob[];
|
|
12
|
+
}
|
|
13
|
+
/** 装回失败的一段:段名 + 它的引用(引用值不是串时记 `<not-a-digest>`)。 */
|
|
14
|
+
export interface UnresolvedWiringSection {
|
|
15
|
+
readonly section: string;
|
|
16
|
+
readonly digest: string;
|
|
17
|
+
}
|
|
18
|
+
/** {@link joinFromStore} 的结局:整帧,或至少一段装不回(永不交出缺段的帧)。 */
|
|
19
|
+
export type WiringSectionJoin = {
|
|
20
|
+
readonly kind: "joined";
|
|
21
|
+
readonly data: unknown;
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: "unresolved";
|
|
24
|
+
readonly missing: readonly UnresolvedWiringSection[];
|
|
25
|
+
};
|
|
26
|
+
/** 段体的店键:段体(一只 JSON 值)规范 JSON 的 sha256 全长。 */
|
|
27
|
+
export declare function wiringSectionDigest(body: object): string;
|
|
28
|
+
/** 写口:清单帧里 JSON 形是容器的段换成块引用(段体是那一段的 JSON 形,与原值不共享);别的型、或 `data` 不是对象 ⇒ 原样、零块。 */
|
|
29
|
+
export declare function splitForStore(type: string, data: unknown): WiringSectionSplit;
|
|
30
|
+
/** 读口:清单行里恰为单键 `$blob` 对象的顶层段换回段体;任一段在 `lookup` 里找不到 ⇒ `unresolved`(点名每一段)。 */
|
|
31
|
+
export declare function joinFromStore(type: string, data: unknown, lookup: (digest: string) => object | undefined): WiringSectionJoin;
|
|
32
|
+
//# sourceMappingURL=wiring-section-codec.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { canonicalJsonStringify } from "@sema-agent/core";
|
|
3
|
+
export const WIRING_SECTION_REF_KEY = "$blob";
|
|
4
|
+
const WIRING_MANIFEST = "wiring_manifest";
|
|
5
|
+
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6
|
+
export function wiringSectionDigest(body) {
|
|
7
|
+
return `sha256:${createHash("sha256").update(canonicalJsonStringify(body), "utf8").digest("hex")}`;
|
|
8
|
+
}
|
|
9
|
+
function jsonContainerOf(value) {
|
|
10
|
+
if (typeof value !== "object" || value === null)
|
|
11
|
+
return undefined;
|
|
12
|
+
const text = JSON.stringify(value);
|
|
13
|
+
if (text === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
const body = JSON.parse(text);
|
|
16
|
+
return typeof body === "object" && body !== null ? body : undefined;
|
|
17
|
+
}
|
|
18
|
+
export function splitForStore(type, data) {
|
|
19
|
+
if (type !== WIRING_MANIFEST || !isRecord(data))
|
|
20
|
+
return { stored: data, blobs: [] };
|
|
21
|
+
const blobs = new Map();
|
|
22
|
+
const stored = Object.fromEntries(Object.entries(data).map(([section, value]) => {
|
|
23
|
+
const body = jsonContainerOf(value);
|
|
24
|
+
if (body === undefined)
|
|
25
|
+
return [section, value];
|
|
26
|
+
const digest = wiringSectionDigest(body);
|
|
27
|
+
if (!blobs.has(digest))
|
|
28
|
+
blobs.set(digest, body);
|
|
29
|
+
return [section, { [WIRING_SECTION_REF_KEY]: digest }];
|
|
30
|
+
}));
|
|
31
|
+
return { stored, blobs: [...blobs].map(([digest, body]) => ({ digest, body })) };
|
|
32
|
+
}
|
|
33
|
+
export function joinFromStore(type, data, lookup) {
|
|
34
|
+
if (type !== WIRING_MANIFEST || !isRecord(data))
|
|
35
|
+
return { kind: "joined", data };
|
|
36
|
+
let refs = 0;
|
|
37
|
+
const missing = [];
|
|
38
|
+
const entries = Object.entries(data).map(([section, value]) => {
|
|
39
|
+
if (!isRecord(value))
|
|
40
|
+
return [section, value];
|
|
41
|
+
const keys = Object.keys(value);
|
|
42
|
+
if (keys.length !== 1 || keys[0] !== WIRING_SECTION_REF_KEY)
|
|
43
|
+
return [section, value];
|
|
44
|
+
refs += 1;
|
|
45
|
+
const ref = value[WIRING_SECTION_REF_KEY];
|
|
46
|
+
const body = typeof ref === "string" ? lookup(ref) : undefined;
|
|
47
|
+
if (body === undefined)
|
|
48
|
+
missing.push({ section, digest: typeof ref === "string" ? ref : "<not-a-digest>" });
|
|
49
|
+
return [section, body ?? value];
|
|
50
|
+
});
|
|
51
|
+
if (missing.length > 0)
|
|
52
|
+
return { kind: "unresolved", missing };
|
|
53
|
+
return { kind: "joined", data: refs === 0 ? data : Object.fromEntries(entries) };
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=wiring-section-codec.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { RemoteExecutionError, type ExecutionEnvFactoryContext } from "@sema-agent/core";
|
|
2
|
+
import type { Logger } from "../observability/logger.js";
|
|
3
|
+
import { type TaskAttachmentStore } from "./task-attachment-store.js";
|
|
4
|
+
/** 车道一次固定命令的读数(适配器自己的起动身份 —— 与引擎命令同一个执行用户;从不抛)。 */
|
|
5
|
+
export type ChannelRun = {
|
|
6
|
+
readonly ok: true;
|
|
7
|
+
readonly exitCode: number;
|
|
8
|
+
readonly stdout: string;
|
|
9
|
+
readonly stderr: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly error: string;
|
|
13
|
+
};
|
|
14
|
+
/** 车道一次写的读数(建或覆盖;从不抛)。 */
|
|
15
|
+
export type ChannelWrite = {
|
|
16
|
+
readonly ok: true;
|
|
17
|
+
} | {
|
|
18
|
+
readonly ok: false;
|
|
19
|
+
readonly error: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* 恢复点手里那条**已经连上的**通道(不是公开的 `ExecutionEnv` 面)。两件原语,身份都是这只工作区里跑引擎命令的那个执行用户:
|
|
23
|
+
* · `run` —— 一条固定 shell 命令(适配器的起动形);
|
|
24
|
+
* · `write` —— 把整份字节写到一个绝对路径(父目录由调用方先建)。
|
|
25
|
+
*/
|
|
26
|
+
export interface WorkspaceChannel {
|
|
27
|
+
run(command: string): Promise<ChannelRun>;
|
|
28
|
+
write(absPath: string, bytes: Uint8Array): Promise<ChannelWrite>;
|
|
29
|
+
}
|
|
30
|
+
/** 恢复点交出的工作区从哪来:`minted` = 车道刚铸出的新工作区(没有可恢复的树 —— 含恢复点指的归档不在:`latest` 悬空、停驻快照的归档不在);`restored` = 恢复出来的(空间 `latest` / 停驻快照的树真回来了)。 */
|
|
31
|
+
export type WorkspaceOrigin = "minted" | "restored";
|
|
32
|
+
/** 一次填充的结局。`standing` = 这只执行环境此后每一次恢复点都会得到同一个答(适配器把它记成这只环境的拒,不再建机器)。 */
|
|
33
|
+
export type FillOutcome = {
|
|
34
|
+
readonly ok: true;
|
|
35
|
+
} | {
|
|
36
|
+
readonly ok: false;
|
|
37
|
+
readonly error: RemoteExecutionError;
|
|
38
|
+
readonly standing: boolean;
|
|
39
|
+
};
|
|
40
|
+
/** 恢复点的填充座(按 run 由工厂装配交给适配器;适配器只在恢复点调它、只说工作区从哪来,不判内容)。 */
|
|
41
|
+
export interface WorkspaceFill {
|
|
42
|
+
/** 往恢复点即将交出的工作区(根 = `root`,适配器的工作根;来处 = `origin`)里放内容。 */
|
|
43
|
+
fill(channel: WorkspaceChannel, root: string, origin: WorkspaceOrigin): Promise<FillOutcome>;
|
|
44
|
+
}
|
|
45
|
+
/** 工厂装配按 run 取填充座的那一只读法(`boot/execution-env.ts` 铸;隔离车道的三只工厂各收它一份)。`undefined` ⇒ 这条 run 不放任何东西。 */
|
|
46
|
+
export type WorkspaceFillOf = (ctx: ExecutionEnvFactoryContext) => WorkspaceFill | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* 一条 run 的填充座(活对象 ⇒ `create*`):会话 `sessionId` 的种子(`seeds` 为真且工作区是铸出的)与绑定的附件。
|
|
49
|
+
* 附件按物化路径(与 objective 告知同一份推导)写进 `<root>/attachments/`,每一次恢复点都整份重写(同路径同字节)。
|
|
50
|
+
*/
|
|
51
|
+
export declare function createWorkspaceFill(seat: {
|
|
52
|
+
readonly store: TaskAttachmentStore;
|
|
53
|
+
readonly sessionId: string;
|
|
54
|
+
/** 这条 run 是不是它工作区的空间属主(`placementRootSessionId === sessionId`):只有属主播种。 */
|
|
55
|
+
readonly seeds: boolean;
|
|
56
|
+
readonly logger?: Pick<Logger, "info">;
|
|
57
|
+
}): WorkspaceFill;
|
|
58
|
+
/** 播种的固定命令:建工作根(与附件的 `mkdir -p` 同姿)→ 查 `tar` → 查顶层名撞车 → 解(不还原属主)→ 不论结局都删两只临时档。 */
|
|
59
|
+
export declare function seedCommand(root: string, archive: string, names: string): string;
|
|
60
|
+
//# sourceMappingURL=workspace-fill.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { RemoteExecutionError } from "@sema-agent/core";
|
|
4
|
+
import { readProjectBundle } from "../project-bundle.js";
|
|
5
|
+
import { attachmentBytesOf, attachmentPlacementsOf, seedOfSession } from "./task-attachment-store.js";
|
|
6
|
+
import { shellQuote } from "./remote-shell.js";
|
|
7
|
+
const FILLED = { ok: true };
|
|
8
|
+
function refused(message, standing = false) {
|
|
9
|
+
return { ok: false, error: new RemoteExecutionError("connect_failed", message), standing };
|
|
10
|
+
}
|
|
11
|
+
function errText(e) {
|
|
12
|
+
return e instanceof Error ? e.message : String(e);
|
|
13
|
+
}
|
|
14
|
+
const SEED_NO_TAR_EXIT = 71;
|
|
15
|
+
const SEED_COLLISION_EXIT = 72;
|
|
16
|
+
const SEED_COLLISION_MARK = "SEMA_SEED_COLLISION ";
|
|
17
|
+
export function createWorkspaceFill(seat) {
|
|
18
|
+
return {
|
|
19
|
+
async fill(channel, root, origin) {
|
|
20
|
+
if (origin === "minted" && seat.seeds) {
|
|
21
|
+
const seeded = await seedInto(channel, root, seat);
|
|
22
|
+
if (!seeded.ok)
|
|
23
|
+
return seeded;
|
|
24
|
+
}
|
|
25
|
+
let placements;
|
|
26
|
+
try {
|
|
27
|
+
placements = await attachmentPlacementsOf(seat.store, seat.sessionId);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
return refused(`attachment materialization failed (listing the session's attachments): ${errText(e)}`);
|
|
31
|
+
}
|
|
32
|
+
if (placements.length === 0)
|
|
33
|
+
return FILLED;
|
|
34
|
+
const mk = await channel.run(`mkdir -p ${shellQuote(path.posix.join(root, "attachments"))}`);
|
|
35
|
+
if (!mk.ok || mk.exitCode !== 0)
|
|
36
|
+
return refused(`attachment materialization failed (createDir attachments/): ${mk.ok ? mk.stderr.trim() || `exit ${mk.exitCode}` : mk.error}`);
|
|
37
|
+
for (const p of placements) {
|
|
38
|
+
let bytes;
|
|
39
|
+
try {
|
|
40
|
+
bytes = await attachmentBytesOf(seat.store, p.record);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return refused(errText(e));
|
|
44
|
+
}
|
|
45
|
+
const w = await channel.write(path.posix.join(root, p.relPath), bytes);
|
|
46
|
+
if (!w.ok)
|
|
47
|
+
return refused(`attachment materialization failed (write ${p.relPath}): ${w.error}`);
|
|
48
|
+
}
|
|
49
|
+
seat.logger?.info("attachments_materialized", { sessionId: seat.sessionId, count: placements.length });
|
|
50
|
+
return FILLED;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function seedInto(channel, root, seat) {
|
|
55
|
+
let seed;
|
|
56
|
+
try {
|
|
57
|
+
seed = await seedOfSession(seat.store, seat.sessionId);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
return refused(`workspace seeding failed (reading the session's project bundle from the attachment store): ${errText(e)}`);
|
|
61
|
+
}
|
|
62
|
+
if (seed === undefined)
|
|
63
|
+
return FILLED;
|
|
64
|
+
if ("broken" in seed)
|
|
65
|
+
return refused(`workspace seeding failed: ${seed.broken}`, true);
|
|
66
|
+
const id = seed.record.id;
|
|
67
|
+
const reading = await readProjectBundle(seed.bytes, { canonical: true });
|
|
68
|
+
if (!reading.ok)
|
|
69
|
+
return refused(`workspace seeding failed: the session's project bundle ${id} does not read as a project bundle (${reading.reason}${reading.path !== undefined ? ` at '${reading.path}'` : ""}: ${reading.detail})`, true);
|
|
70
|
+
const stem = `/tmp/sema-seed-${randomUUID()}`;
|
|
71
|
+
const archive = `${stem}.tgz`;
|
|
72
|
+
const names = `${stem}.names`;
|
|
73
|
+
const wn = await channel.write(names, new TextEncoder().encode(reading.topLevel.map((n) => `${n}\n`).join("")));
|
|
74
|
+
if (!wn.ok)
|
|
75
|
+
return refused(`workspace seeding failed (writing the bundle's top-level names to ${names}): ${wn.error}`);
|
|
76
|
+
const wa = await channel.write(archive, reading.canonical);
|
|
77
|
+
if (!wa.ok) {
|
|
78
|
+
await channel.run(`rm -f ${shellQuote(names)}`);
|
|
79
|
+
return refused(`workspace seeding failed (writing the bundle to ${archive}): ${wa.error}`);
|
|
80
|
+
}
|
|
81
|
+
const r = await channel.run(seedCommand(root, archive, names));
|
|
82
|
+
if (!r.ok)
|
|
83
|
+
return refused(`workspace seeding failed (unpacking the project bundle ${id}): ${r.error}`);
|
|
84
|
+
if (r.exitCode === SEED_NO_TAR_EXIT) {
|
|
85
|
+
return refused(`workspace seeding failed: this isolated lane's image has no \`tar\` — seeding a project bundle needs \`tar\` with gzip support in the sandbox image (the same tool the k8s workspace snapshots use)`, true);
|
|
86
|
+
}
|
|
87
|
+
if (r.exitCode === SEED_COLLISION_EXIT) {
|
|
88
|
+
const hit = r.stdout.split("\n").find((l) => l.startsWith(SEED_COLLISION_MARK))?.slice(SEED_COLLISION_MARK.length) ?? "?";
|
|
89
|
+
return refused(`workspace seeding failed: '${hit}' of the project bundle ${id} already exists in the freshly minted workspace ${root} (the sandbox image presets it) — the seed neither overwrites nor merges; rename it in the project, or remove it from the image's working directory`, true);
|
|
90
|
+
}
|
|
91
|
+
if (r.exitCode !== 0)
|
|
92
|
+
return refused(`workspace seeding failed (unpacking the project bundle ${id} exited ${r.exitCode}): ${r.stderr.trim().slice(0, 300)}`);
|
|
93
|
+
seat.logger?.info("workspace_seeded", { sessionId: seat.sessionId, bundleId: id, entries: reading.entries });
|
|
94
|
+
return FILLED;
|
|
95
|
+
}
|
|
96
|
+
export function seedCommand(root, archive, names) {
|
|
97
|
+
const R = shellQuote(root);
|
|
98
|
+
const A = shellQuote(archive);
|
|
99
|
+
const N = shellQuote(names);
|
|
100
|
+
return (`r=0; mkdir -p ${R} || r=$?; ` +
|
|
101
|
+
`if [ "$r" = 0 ] && ! command -v tar >/dev/null 2>&1; then r=${SEED_NO_TAR_EXIT}; fi; ` +
|
|
102
|
+
`if [ "$r" = 0 ]; then while IFS= read -r p; do if [ -e ${R}/"$p" ] || [ -L ${R}/"$p" ]; then printf '${SEED_COLLISION_MARK}%s\\n' "$p"; r=${SEED_COLLISION_EXIT}; break; fi; done < ${N}; fi; ` +
|
|
103
|
+
`if [ "$r" = 0 ]; then tar -x -z -o -f ${A} -C ${R} || r=$?; fi; ` +
|
|
104
|
+
`rm -f ${A} ${N}; exit "$r"`);
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=workspace-fill.js.map
|