@sema-agent/server 7.148.0 → 7.150.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 +551 -9
- package/README.md +17 -2
- package/README.zh-CN.md +5 -1
- package/USAGE.md +126 -68
- package/dist/adoption/wire.d.ts +3 -3
- package/dist/approval-card.d.ts +33 -33
- package/dist/approval-gate-currency.d.ts +1 -1
- package/dist/approval-leg-origin.d.ts +33 -0
- package/dist/approval-leg-origin.js +4 -0
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +2 -2
- package/dist/auth-bridge.d.ts +9 -1
- package/dist/auth-bridge.js +4 -4
- package/dist/auth-keys.d.ts +10 -1
- package/dist/auth-keys.js +8 -0
- package/dist/background-session-reap.d.ts +1 -1
- package/dist/boot/deferred-sandbox-path-env.d.ts +5 -3
- package/dist/boot/deferred-sandbox-path-env.js +1 -2
- package/dist/boot/execution-env.js +10 -11
- package/dist/boot/execution-lane.d.ts +6 -4
- package/dist/boot/execution-lane.js +3 -1
- package/dist/boot/org-delivery.d.ts +3 -2
- package/dist/boot/parked-revive-gate.d.ts +5 -5
- package/dist/boot/parked-revive-gate.js +2 -3
- package/dist/boot/resolve-spec.d.ts +4 -5
- package/dist/boot/resolve-spec.js +66 -28
- package/dist/boot/retention-lane.d.ts +1 -1
- package/dist/boot/runner-deps.js +3 -1
- package/dist/boot/session-faces.js +21 -21
- package/dist/boot/stage-01-config.d.ts +1 -0
- package/dist/boot/stage-01-config.js +4 -3
- package/dist/boot/stage-02-web-admin-pull.d.ts +1 -0
- package/dist/boot/stage-02-web-admin-pull.js +10 -2
- package/dist/boot/stage-03-store-backend.d.ts +1 -0
- package/dist/boot/stage-03-store-backend.js +4 -4
- package/dist/boot/stage-04-stores.d.ts +1 -0
- package/dist/boot/stage-05-execution-env.d.ts +1 -0
- package/dist/boot/stage-06-runners.d.ts +2 -1
- package/dist/boot/stage-06-runners.js +1 -2
- package/dist/boot/stage-07-capability-layer.d.ts +2 -1
- package/dist/boot/stage-07-capability-layer.js +1 -1
- package/dist/boot/stage-08-reapers.d.ts +2 -1
- package/dist/boot/stage-09-leader.d.ts +2 -1
- package/dist/boot/stage-10-http-server.d.ts +35 -2
- package/dist/boot/stage-10-http-server.js +9 -4
- package/dist/boot/stores.d.ts +3 -0
- package/dist/boot/stores.js +22 -19
- package/dist/boot/web-admin.d.ts +1 -1
- package/dist/capabilities/tool-defer.d.ts +1 -1
- package/dist/capability-facts.d.ts +70 -60
- package/dist/capability-facts.js +45 -49
- package/dist/card-settle-stored-scope.d.ts +56 -0
- package/dist/card-settle-stored-scope.js +27 -0
- package/dist/card-void.d.ts +7 -12
- package/dist/card-void.js +2 -7
- package/dist/config-catalog.d.ts +12 -0
- package/dist/config-catalog.js +37 -33
- package/dist/config-invariants.d.ts +5 -4
- package/dist/config-invariants.js +4 -4
- package/dist/config-types.d.ts +49 -16
- package/dist/config.d.ts +6 -5
- package/dist/config.js +35 -24
- package/dist/context-window-notice.d.ts +31 -0
- package/dist/context-window-notice.js +28 -0
- package/dist/deployment-governance.d.ts +7 -28
- package/dist/deployment-governance.js +1 -14
- package/dist/deployment-shape.d.ts +20 -11
- package/dist/deployment-shape.js +1 -0
- package/dist/device-ws-hub.d.ts +1 -0
- package/dist/device-ws-hub.js +64 -28
- package/dist/device-ws-protocol.d.ts +20 -6
- package/dist/device-ws-protocol.js +9 -2
- package/dist/engine-identity.d.ts +7 -1
- package/dist/engine-identity.js +5 -0
- package/dist/env-name-allowlist-knobs.d.ts +6 -5
- package/dist/env-name-allowlist-knobs.js +5 -0
- package/dist/execution-lane-caps.d.ts +20 -5
- package/dist/execution-lane-caps.js +18 -4
- package/dist/field-conflict.d.ts +2 -2
- package/dist/fleet/engine-signal.d.ts +14 -1
- package/dist/fleet/engine-signal.js +5 -1
- package/dist/fleet/fleet-reconciler.d.ts +1 -1
- package/dist/fleet/serving-lease.d.ts +18 -3
- package/dist/fleet/serving-lease.js +81 -35
- package/dist/fleet-client.d.ts +13 -11
- package/dist/fleet-client.js +53 -33
- package/dist/history-anchor.js +1 -1
- package/dist/hitl-delivery.d.ts +1 -3
- package/dist/hitl-delivery.js +1 -1
- package/dist/hooks/command-spawn.d.ts +7 -2
- package/dist/hooks/command-spawn.js +13 -6
- package/dist/hooks/hook-runner.d.ts +5 -0
- package/dist/hooks/hook-runner.js +11 -7
- package/dist/http/active-run-conflict.d.ts +3 -7
- package/dist/http/active-run-conflict.js +2 -2
- package/dist/http/admission.js +5 -5
- package/dist/http/approval-sweeps.js +5 -3
- package/dist/http/caller-trust.d.ts +2 -2
- package/dist/http/caller-trust.js +3 -3
- package/dist/http/checkpoint-ctx.d.ts +18 -63
- package/dist/http/checkpoint-ctx.js +1 -24
- package/dist/http/claim-heartbeat.d.ts +6 -6
- package/dist/http/claim-heartbeat.js +6 -6
- package/dist/http/claimed-run.d.ts +16 -9
- package/dist/http/claimed-run.js +8 -0
- package/dist/http/dispatch.js +7 -5
- package/dist/http/durable-run-leg.d.ts +8 -1
- package/dist/http/durable-run-leg.js +34 -15
- package/dist/http/execution-context.d.ts +115 -14
- package/dist/http/execution-context.js +130 -40
- package/dist/http/owner-access.d.ts +246 -21
- package/dist/http/owner-access.js +186 -8
- package/dist/http/principal-gate.d.ts +18 -11
- package/dist/http/principal-gate.js +3 -1
- package/dist/http/readiness.d.ts +4 -2
- package/dist/http/resume-ctx-read.d.ts +14 -3
- package/dist/http/resume-ctx-read.js +24 -3
- package/dist/http/resume-legs.d.ts +35 -3
- package/dist/http/resume-legs.js +262 -106
- package/dist/http/route-ctx.d.ts +29 -11
- package/dist/http/routes/a2a-serve.js +17 -14
- package/dist/http/routes/approvals-assistant.d.ts +3 -2
- package/dist/http/routes/approvals-assistant.js +11 -11
- package/dist/http/routes/capabilities.js +5 -4
- package/dist/http/routes/config-catalog.d.ts +1 -1
- package/dist/http/routes/diagnostics.d.ts +3 -1
- package/dist/http/routes/diagnostics.js +30 -5
- package/dist/http/routes/mcp-reconnect.js +2 -2
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/notify-wake.js +2 -3
- package/dist/http/routes/principal-inbox.d.ts +3 -1
- package/dist/http/routes/principal-inbox.js +3 -2
- package/dist/http/routes/projects.d.ts +1 -0
- package/dist/http/routes/projects.js +5 -3
- package/dist/http/routes/retention-ops.d.ts +1 -1
- package/dist/http/routes/run-permission-mode.d.ts +1 -1
- package/dist/http/routes/run-permission-mode.js +2 -2
- package/dist/http/routes/runs.d.ts +1 -1
- package/dist/http/routes/runs.js +33 -37
- package/dist/http/routes/session-memory-sync.js +4 -5
- package/dist/http/routes/session-sync.d.ts +24 -5
- package/dist/http/routes/session-sync.js +45 -37
- package/dist/http/routes/sessions-list.d.ts +1 -1
- package/dist/http/routes/sessions.d.ts +1 -1
- package/dist/http/routes/sessions.js +18 -44
- package/dist/http/routes/tasks.js +136 -101
- package/dist/http/routes/trace-usage.js +14 -14
- package/dist/http/routes/workflows.d.ts +1 -0
- package/dist/http/routes/workflows.js +20 -38
- package/dist/http/server.d.ts +12 -9
- package/dist/http/server.js +35 -6
- package/dist/http/stored-body-replay.d.ts +3 -24
- package/dist/http/stored-body-replay.js +2 -12
- package/dist/http/submit-ctx.d.ts +2 -2
- package/dist/http/submit-ctx.js +5 -5
- package/dist/http/submit-replay.d.ts +15 -2
- package/dist/http/submit-replay.js +10 -4
- package/dist/http/wire-types.d.ts +6 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/leader/endpoint.js +9 -8
- package/dist/leg-facts.d.ts +13 -2
- package/dist/leg-facts.js +2 -0
- package/dist/local-store-declaration.d.ts +54 -0
- package/dist/local-store-declaration.js +59 -0
- package/dist/mcp-assertion-header.d.ts +1 -1
- package/dist/mcp-assertion-header.js +1 -1
- package/dist/mcp-authorization-face.d.ts +18 -14
- package/dist/mcp-authorization-face.js +3 -2
- package/dist/memory-project-layer.js +1 -1
- package/dist/memory-scope.d.ts +4 -4
- package/dist/memory-scope.js +4 -4
- package/dist/memory-store-refusal.d.ts +1 -1
- package/dist/memory-sync-client.js +2 -2
- package/dist/model-context-window.d.ts +50 -11
- package/dist/model-context-window.js +20 -1
- package/dist/model-entry-refusal.d.ts +5 -4
- package/dist/model-entry-refusal.js +17 -3
- package/dist/model-reach-scope.d.ts +2 -1
- package/dist/observability/corrupt-read-seat.d.ts +1 -1
- package/dist/observability/fail-open.d.ts +19 -7
- package/dist/observability/fail-open.js +19 -7
- package/dist/observability/logger.d.ts +7 -0
- package/dist/observability/logger.js +3 -1
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal.d.ts +1 -1
- package/dist/observability/run-terminal.js +1 -0
- package/dist/observability/secret-env-scrub.d.ts +1 -1
- package/dist/observability/secret-env-scrub.js +1 -1
- package/dist/org-delivery-gate.d.ts +31 -6
- package/dist/org-delivery-gate.js +7 -4
- package/dist/owner-faces.d.ts +30 -5
- package/dist/owner-faces.js +2 -0
- package/dist/park-row-commit.d.ts +67 -0
- package/dist/park-row-commit.js +76 -0
- package/dist/parked-decide.d.ts +9 -7
- package/dist/parked-decide.js +25 -6
- package/dist/peer-directory.d.ts +1 -1
- package/dist/peer-seat.d.ts +1 -1
- package/dist/peer-seat.js +52 -59
- package/dist/plugins/adoption-log-sql.d.ts +3 -3
- package/dist/plugins/adoption-log-sql.js +19 -19
- package/dist/plugins/approval-ask-store-sql.d.ts +1 -1
- package/dist/plugins/approval-ask-store-sql.js +3 -5
- package/dist/plugins/approval-exemption-skill-cutoff.d.ts +64 -0
- package/dist/plugins/approval-exemption-skill-cutoff.js +122 -0
- package/dist/plugins/approval-exemption-skill-m46.d.ts +0 -6
- package/dist/plugins/approval-exemption-skill-m46.js +6 -10
- package/dist/plugins/approval-exemption-store.d.ts +26 -1
- package/dist/plugins/approval-exemption-store.js +28 -19
- package/dist/plugins/ensure-schema-lock-policy.d.ts +36 -3
- package/dist/plugins/ensure-schema-lock-policy.js +75 -9
- package/dist/plugins/exec-path-law.d.ts +34 -0
- package/dist/plugins/exec-path-law.js +13 -0
- package/dist/plugins/file-run-store.d.ts +22 -9
- package/dist/plugins/file-run-store.js +51 -23
- package/dist/plugins/local-checkpoint-store.d.ts +8 -1
- package/dist/plugins/local-checkpoint-store.js +5 -1
- package/dist/plugins/local-data-root-admission.d.ts +52 -0
- package/dist/plugins/local-data-root-admission.js +100 -0
- package/dist/plugins/local-engine-high-water.d.ts +28 -7
- package/dist/plugins/local-engine-high-water.js +27 -22
- package/dist/plugins/local-session-store.d.ts +134 -49
- package/dist/plugins/local-session-store.js +149 -108
- package/dist/plugins/memory-run-store.d.ts +14 -9
- package/dist/plugins/memory-run-store.js +48 -22
- package/dist/plugins/one-time-migration-marker.d.ts +19 -3
- package/dist/plugins/one-time-migration-marker.js +7 -1
- package/dist/plugins/one-time-migrations.d.ts +110 -59
- package/dist/plugins/one-time-migrations.js +224 -136
- package/dist/plugins/org-delivery-quota-store.d.ts +34 -11
- package/dist/plugins/org-delivery-quota-store.js +29 -14
- package/dist/plugins/permission-rule-store-sql.d.ts +1 -1
- package/dist/plugins/permission-rule-store-sql.js +5 -9
- package/dist/plugins/pg-pool.d.ts +1 -1
- package/dist/plugins/pg-pool.js +14 -12
- package/dist/plugins/posix-shell-fs.js +4 -2
- package/dist/plugins/remote-env-adb.js +3 -2
- package/dist/plugins/remote-env-device.d.ts +3 -2
- package/dist/plugins/remote-env-device.js +8 -6
- package/dist/plugins/remote-env-e2b.d.ts +4 -8
- package/dist/plugins/remote-env-e2b.js +21 -15
- package/dist/plugins/remote-env-host.d.ts +51 -10
- package/dist/plugins/remote-env-host.js +97 -31
- package/dist/plugins/remote-env-k8s.js +7 -4
- package/dist/plugins/remote-env-local-docker.d.ts +6 -3
- package/dist/plugins/remote-env-local-docker.js +11 -25
- package/dist/plugins/remote-env-ssh.js +4 -3
- package/dist/plugins/remote-shell.d.ts +3 -1
- package/dist/plugins/remote-shell.js +3 -3
- package/dist/plugins/replica-registration.d.ts +35 -0
- package/dist/plugins/replica-registration.js +48 -0
- package/dist/plugins/retention-lane-store-sql.d.ts +1 -1
- package/dist/plugins/run-store-sql.d.ts +27 -8
- package/dist/plugins/run-store-sql.js +46 -18
- package/dist/plugins/scenario-prefix-switch-m42.d.ts +8 -12
- package/dist/plugins/scenario-prefix-switch-m42.js +9 -6
- package/dist/plugins/schema-consistency.d.ts +91 -5
- package/dist/plugins/schema-consistency.js +189 -7
- package/dist/plugins/schema-drift-sentence.d.ts +6 -5
- package/dist/plugins/schema-drift-sentence.js +13 -5
- package/dist/plugins/serving-requirement.d.ts +49 -0
- package/dist/plugins/serving-requirement.js +53 -0
- package/dist/plugins/session-placement.d.ts +5 -4
- package/dist/plugins/shell-sentinel-owner-m48.d.ts +22 -0
- package/dist/plugins/shell-sentinel-owner-m48.js +6 -0
- package/dist/plugins/stop-then-start-sentence.d.ts +55 -0
- package/dist/plugins/stop-then-start-sentence.js +51 -0
- package/dist/plugins/store-backend.d.ts +23 -9
- package/dist/plugins/store-backend.js +58 -24
- package/dist/plugins/store-contracts.d.ts +45 -5
- package/dist/plugins/store-contracts.js +19 -0
- package/dist/plugins/tidb-pool.d.ts +11 -6
- package/dist/plugins/tidb-pool.js +22 -12
- package/dist/plugins/workspace-custody.d.ts +44 -0
- package/dist/plugins/workspace-custody.js +47 -0
- package/dist/process-instance.d.ts +3 -0
- package/dist/process-instance.js +7 -0
- package/dist/project-context.d.ts +66 -0
- package/dist/project-context.js +70 -0
- package/dist/project-identity.d.ts +8 -7
- package/dist/project-identity.js +40 -17
- package/dist/project-layer-key.d.ts +13 -2
- package/dist/project-layer-key.js +9 -1
- package/dist/question.d.ts +9 -21
- package/dist/question.js +28 -19
- package/dist/read-deny-floor-notice.d.ts +36 -0
- package/dist/read-deny-floor-notice.js +40 -0
- package/dist/{plain-string-record.d.ts → request-declaration-disposition.d.ts} +9 -23
- package/dist/request-declaration-disposition.js +39 -0
- package/dist/run-cancel-card.d.ts +28 -0
- package/dist/run-cancel-card.js +20 -0
- package/dist/run-cancel-context.d.ts +10 -0
- package/dist/run-cancel-context.js +1 -0
- package/dist/run-local.js +28 -13
- package/dist/runs.d.ts +31 -7
- package/dist/runs.js +34 -16
- package/dist/security.d.ts +3 -3
- package/dist/server-secret-env.d.ts +34 -20
- package/dist/server-secret-env.js +61 -18
- package/dist/session-agent.d.ts +4 -12
- package/dist/session-agent.js +0 -6
- package/dist/session-children-retire.d.ts +6 -3
- package/dist/session-children-retire.js +2 -5
- package/dist/session-incarnation.d.ts +18 -4
- package/dist/session-incarnation.js +4 -1
- package/dist/session-notify-door.d.ts +4 -4
- package/dist/session-notify-door.js +5 -6
- package/dist/session-rules-seat.d.ts +4 -3
- package/dist/session-rules-seat.js +3 -11
- package/dist/session-sync.js +3 -3
- package/dist/session-workspace.d.ts +3 -3
- package/dist/store-admission.d.ts +33 -0
- package/dist/store-admission.js +49 -0
- package/dist/store-posture.d.ts +5 -0
- package/dist/store-posture.js +18 -1
- package/dist/stores-sql/checkpoint-store-sql.d.ts +22 -3
- package/dist/stores-sql/checkpoint-store-sql.js +109 -23
- package/dist/stores-sql/file-history-store-sql.d.ts +1 -1
- package/dist/stores-sql/file-history-store-sql.js +3 -5
- package/dist/stores-sql/retention-store-sql.d.ts +1 -1
- package/dist/stores-sql/tool-result-store-sql.d.ts +1 -1
- package/dist/stores-sql/tool-result-store-sql.js +4 -6
- package/dist/task-a2a.d.ts +3 -8
- package/dist/task-a2a.js +25 -27
- package/dist/task-cwd.d.ts +27 -10
- package/dist/task-cwd.js +3 -0
- package/dist/task-mcp.d.ts +3 -3
- package/dist/task-mcp.js +1 -1
- package/dist/task-settings.d.ts +13 -0
- package/dist/task-settings.js +14 -7
- package/dist/terminal.d.ts +2 -0
- package/dist/terminal.js +21 -6
- package/dist/tool-approval.d.ts +50 -41
- package/dist/tool-approval.js +96 -73
- package/dist/trace/engine-notice-wire.d.ts +38 -21
- package/dist/trace/engine-notice-wire.js +25 -4
- package/dist/trace/ledger-sink.d.ts +3 -1
- package/dist/trace/project.d.ts +8 -5
- package/dist/trace/project.js +11 -9
- package/dist/trace/projection-table.d.ts +33 -2
- package/dist/trace/redact.d.ts +12 -0
- package/dist/trace/redact.js +5 -1
- package/dist/trace/self-minted-wire.d.ts +268 -56
- package/dist/trace/self-minted-wire.js +110 -40
- package/dist/trace/tool-card.d.ts +3 -0
- package/dist/trace/tool-card.js +24 -0
- package/dist/trace/tool-output-media.d.ts +2 -1
- package/dist/unattended-permission-mode.d.ts +1 -1
- package/dist/web-admin-client/apply-effective.d.ts +1 -1
- package/dist/web-admin-client/apply-effective.js +22 -9
- package/dist/web-admin-client/effective-keys.d.ts +1 -1
- package/dist/web-admin-client/skills-mcp.d.ts +1 -1
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +57 -29
- package/docs/error-text-lexicon.json +317 -0
- package/package.json +4 -2
- package/scripts/embed-minimal.mjs +183 -0
- package/wire-changes/server@7.149.0.json +372 -0
- package/wire-changes/server@7.150.0.json +335 -0
- package/dist/plain-string-record.js +0 -70
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { SKILL_TOOL_NAME } from "@sema-agent/core";
|
|
4
|
+
import { APPROVAL_EXEMPTION_DIR, EXEMPTION_LOG_SUFFIX, readExemptionLog } from "./approval-exemption-store.js";
|
|
5
|
+
import { MIGRATION_MARKER_TABLE, SKILL_EXEMPTION_CUTOFF_ID } from "./one-time-migration-marker.js";
|
|
6
|
+
import { atomicWriteFileSync } from "./owner-only-file-mode.js";
|
|
7
|
+
import { readLiveColumns } from "./schema-live-columns.js";
|
|
8
|
+
import { SCHEMA_MISMATCH_ERROR_CODE, StoreBootRefusalError } from "./store-contracts.js";
|
|
9
|
+
export const SKILL_EXEMPTION_TOOL_NAME = SKILL_TOOL_NAME;
|
|
10
|
+
export const SKILL_EXEMPTION_CUTOFF_SINCE = "7.148.0";
|
|
11
|
+
export const LOCAL_SKILL_EXEMPTION_CUTOFF_FILE = "one-time-migration-M46.marker";
|
|
12
|
+
export const PRE_CUTOFF_SKILL_EXEMPTION_CONSEQUENCE = `Such a row was written when approving a \`${SKILL_TOOL_NAME}\` call meant activating a skill (and pre-approving the tools that skill lists); ` +
|
|
13
|
+
`this build would read it as a standing answer for any later call of a tool named \`${SKILL_TOOL_NAME}\` in that session — a wider grant than the one given.`;
|
|
14
|
+
function cutoffMsOf(raw) {
|
|
15
|
+
const n = typeof raw === "number" ? raw : typeof raw === "bigint" ? Number(raw) : typeof raw === "string" && /^-?\d+$/.test(raw.trim()) ? Number(raw.trim()) : Number.NaN;
|
|
16
|
+
return Number.isSafeInteger(n) && !Number.isNaN(new Date(n).getTime()) ? n : undefined;
|
|
17
|
+
}
|
|
18
|
+
export async function readSqlSkillExemptionCutoff(query, dialect) {
|
|
19
|
+
const [row] = await query(`SELECT marked_at_ms FROM ${MIGRATION_MARKER_TABLE} WHERE migration_id = ${dialect === "mysql" ? "?" : "$1"}`, [SKILL_EXEMPTION_CUTOFF_ID]);
|
|
20
|
+
if (row === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
const ms = cutoffMsOf(row["marked_at_ms"]);
|
|
23
|
+
if (ms !== undefined)
|
|
24
|
+
return ms;
|
|
25
|
+
throw new StoreBootRefusalError(`${SCHEMA_MISMATCH_ERROR_CODE}: the Skill exemption cutoff row (${MIGRATION_MARKER_TABLE}, migration_id '${SKILL_EXEMPTION_CUTOFF_ID}') holds ${JSON.stringify(String(row["marked_at_ms"]))}, ` +
|
|
26
|
+
"which is not a time this build can read — refusing to start rather than guess which stored Skill session approval exemption rows predate it. " +
|
|
27
|
+
`Way out: DELETE FROM ${MIGRATION_MARKER_TABLE} WHERE migration_id = '${SKILL_EXEMPTION_CUTOFF_ID}'; then restart — the start records a new cutoff once the stored \`${SKILL_TOOL_NAME}\` rows are dealt with ` +
|
|
28
|
+
"(a release that still carries the one-time migration deletes them; a later release refuses until they are gone). This check only reads that row and changes nothing.");
|
|
29
|
+
}
|
|
30
|
+
export class LegacySkillExemptionsPresentError extends StoreBootRefusalError {
|
|
31
|
+
rows;
|
|
32
|
+
cutoff;
|
|
33
|
+
code = SCHEMA_MISMATCH_ERROR_CODE;
|
|
34
|
+
constructor(rows, cutoff) {
|
|
35
|
+
super(`${SCHEMA_MISMATCH_ERROR_CODE}: this database holds ${rows} \`${SKILL_TOOL_NAME}\` session approval exemption row(s) first granted before its Skill exemption cutoff ` +
|
|
36
|
+
`(${cutoff ?? "none recorded: no release that records it has started against this database"}) — refusing to start. ` +
|
|
37
|
+
`${PRE_CUTOFF_SKILL_EXEMPTION_CONSEQUENCE} ` +
|
|
38
|
+
`This build no longer carries the one-time migration (introduced in ${SKILL_EXEMPTION_CUTOFF_SINCE}) that deletes such rows. ` +
|
|
39
|
+
`Ways out: (1) start a server release at or above ${SKILL_EXEMPTION_CUTOFF_SINCE} that still carries that migration against this database once — it records the cutoff and deletes those rows — then start this build; ` +
|
|
40
|
+
`(2) or delete them yourself: DELETE FROM approval_exemption WHERE tool_name = '${SKILL_TOOL_NAME}'; — each session that held one is asked once more on its next \`${SKILL_TOOL_NAME}\` call. ` +
|
|
41
|
+
"The check behind this refusal is read-only and runs before this build creates any table: it changed nothing in the database.");
|
|
42
|
+
this.rows = rows;
|
|
43
|
+
this.cutoff = cutoff;
|
|
44
|
+
this.name = "LegacySkillExemptionsPresentError";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function judgeSqlSkillExemptionCutoff(query, dialect, carried) {
|
|
48
|
+
const live = await readLiveColumns((sql) => query(sql), dialect);
|
|
49
|
+
const cutoffMs = live.has(MIGRATION_MARKER_TABLE) ? await readSqlSkillExemptionCutoff(query, dialect) : undefined;
|
|
50
|
+
if (carried || !live.has("approval_exemption"))
|
|
51
|
+
return;
|
|
52
|
+
const ph = (i) => (dialect === "mysql" ? "?" : `$${i}`);
|
|
53
|
+
const [counted] = cutoffMs === undefined
|
|
54
|
+
? await query(`SELECT COUNT(*) AS n FROM approval_exemption WHERE tool_name = ${ph(1)}`, [SKILL_EXEMPTION_TOOL_NAME])
|
|
55
|
+
: await query(`SELECT COUNT(*) AS n FROM approval_exemption WHERE tool_name = ${ph(1)} AND created_at < ${ph(2)}`, [SKILL_EXEMPTION_TOOL_NAME, new Date(cutoffMs)]);
|
|
56
|
+
const rows = Number(counted?.["n"] ?? 0);
|
|
57
|
+
if (rows > 0)
|
|
58
|
+
throw new LegacySkillExemptionsPresentError(rows, cutoffMs === undefined ? null : new Date(cutoffMs).toISOString());
|
|
59
|
+
}
|
|
60
|
+
export async function assertSkillExemptionCutoffHeldOnPool(target, carried) {
|
|
61
|
+
if (target.dialect === "mysql") {
|
|
62
|
+
const conn = await target.pool.getConnection();
|
|
63
|
+
try {
|
|
64
|
+
await judgeSqlSkillExemptionCutoff(async (sql, params) => (await conn.query(sql, params === undefined ? [] : [...params]))[0], "mysql", carried);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
conn.release();
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const client = await target.pool.connect();
|
|
72
|
+
try {
|
|
73
|
+
await judgeSqlSkillExemptionCutoff(async (sql, params) => (await client.query(sql, params === undefined ? [] : [...params])).rows, "pg", carried);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
client.release();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function skillExemptionCutoffOfText(text) {
|
|
80
|
+
const t = Date.parse(text.trim());
|
|
81
|
+
return Number.isFinite(t) ? t : undefined;
|
|
82
|
+
}
|
|
83
|
+
export function writeLocalSkillExemptionCutoff(path, cutoffMs) {
|
|
84
|
+
atomicWriteFileSync(path, `${new Date(cutoffMs).toISOString()}\n`, { durability: "required" });
|
|
85
|
+
}
|
|
86
|
+
function exemptionLogNames(root) {
|
|
87
|
+
try {
|
|
88
|
+
return readdirSync(join(root, APPROVAL_EXEMPTION_DIR)).filter((name) => name.endsWith(EXEMPTION_LOG_SUFFIX)).sort();
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
if (e?.code === "ENOENT")
|
|
92
|
+
return [];
|
|
93
|
+
throw e;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function localSkillExemptionSessionsBefore(root, cutoffMs, warn) {
|
|
97
|
+
let sessions = 0;
|
|
98
|
+
for (const name of exemptionLogNames(root)) {
|
|
99
|
+
const { rows, gate } = readExemptionLog(join(root, APPROVAL_EXEMPTION_DIR, name), warn);
|
|
100
|
+
if (gate.tripped)
|
|
101
|
+
continue;
|
|
102
|
+
const row = rows.get(SKILL_EXEMPTION_TOOL_NAME);
|
|
103
|
+
if (row !== undefined && (cutoffMs === undefined || row.createdAt < cutoffMs))
|
|
104
|
+
sessions += 1;
|
|
105
|
+
}
|
|
106
|
+
return sessions;
|
|
107
|
+
}
|
|
108
|
+
export function exemptionLogFingerprints(root) {
|
|
109
|
+
return exemptionLogNames(root).map((name) => {
|
|
110
|
+
const st = statSync(join(root, APPROVAL_EXEMPTION_DIR, name));
|
|
111
|
+
return `${name}\u0000${st.size}\u0000${st.mtimeMs}`;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export function settleLocalSkillExemptionCutoff(physicalRoot, action, now = Date.now) {
|
|
115
|
+
switch (action) {
|
|
116
|
+
case "none":
|
|
117
|
+
return;
|
|
118
|
+
case "lay":
|
|
119
|
+
return writeLocalSkillExemptionCutoff(join(physicalRoot, LOCAL_SKILL_EXEMPTION_CUTOFF_FILE), now());
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=approval-exemption-skill-cutoff.js.map
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import type { FileApprovalExemptionStore } from "./approval-exemption-store.js";
|
|
2
|
-
/** 本条在标记表(`one-time-migration-marker.ts`)里的那一行的键。 */
|
|
3
|
-
export declare const LEGACY_SKILL_EXEMPTION_MARKER_ID = "M46";
|
|
4
|
-
/** local 车道的标记文件(数据根下,文件名即本条;正文 = T0 的 ISO 时刻一行,人可直接读)。 */
|
|
5
|
-
export declare const LOCAL_LEGACY_SKILL_EXEMPTION_MARKER_FILE = "one-time-migration-M46.marker";
|
|
6
|
-
/** 被删行的 wire 工具名 —— 引擎 `Skill` 工具的名字,core 导出(core 改名 ⇒ 谓词跟着走,不会静默指向空气)。 */
|
|
7
|
-
export declare const LEGACY_SKILL_EXEMPTION_TOOL_NAME: string;
|
|
8
2
|
/** SQL 腿一页的会话数(键集分批:`session_id` 升序,一页一次读、一句删)。 */
|
|
9
3
|
export declare const LEGACY_SKILL_EXEMPTION_PAGE = 500;
|
|
10
4
|
/**
|
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { SKILL_TOOL_NAME } from "@sema-agent/core";
|
|
3
|
-
import { atomicWriteFileSync } from "./owner-only-file-mode.js";
|
|
4
2
|
import { StoreBootRefusalError } from "./store-contracts.js";
|
|
5
|
-
|
|
6
|
-
export const LOCAL_LEGACY_SKILL_EXEMPTION_MARKER_FILE = "one-time-migration-M46.marker";
|
|
7
|
-
export const LEGACY_SKILL_EXEMPTION_TOOL_NAME = SKILL_TOOL_NAME;
|
|
3
|
+
import { SKILL_EXEMPTION_TOOL_NAME, skillExemptionCutoffOfText, writeLocalSkillExemptionCutoff } from "./approval-exemption-skill-cutoff.js";
|
|
8
4
|
export const LEGACY_SKILL_EXEMPTION_PAGE = 500;
|
|
9
5
|
export function legacySkillExemptionCutoffLocal(markerPath, now = Date.now) {
|
|
10
6
|
if (existsSync(markerPath)) {
|
|
11
|
-
const marked =
|
|
12
|
-
if (
|
|
13
|
-
throw new StoreBootRefusalError(`one-time migration
|
|
7
|
+
const marked = skillExemptionCutoffOfText(readFileSync(markerPath, "utf8"));
|
|
8
|
+
if (marked === undefined) {
|
|
9
|
+
throw new StoreBootRefusalError(`one-time migration M46: ${markerPath} exists but does not hold a readable cutoff time (expected one ISO time, e.g. 2026-11-06T09:00:00.000Z); ` +
|
|
14
10
|
`refusing to start rather than guess which stored Skill exemption rows predate the upgrade. Delete the file and restart: the step then records a new cutoff (now) and deletes the stored Skill exemption rows older than it.`);
|
|
15
11
|
}
|
|
16
12
|
return marked;
|
|
17
13
|
}
|
|
18
14
|
const t0 = now();
|
|
19
|
-
|
|
15
|
+
writeLocalSkillExemptionCutoff(markerPath, t0);
|
|
20
16
|
return t0;
|
|
21
17
|
}
|
|
22
18
|
export function dropLocalLegacySkillExemptionsM46(markerPath, store) {
|
|
23
|
-
return store.dropToolRowsCreatedBeforeM46(
|
|
19
|
+
return store.dropToolRowsCreatedBeforeM46(SKILL_EXEMPTION_TOOL_NAME, legacySkillExemptionCutoffLocal(markerPath));
|
|
24
20
|
}
|
|
25
21
|
//# sourceMappingURL=approval-exemption-skill-m46.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Pool as MysqlPool } from "mysql2/promise";
|
|
2
2
|
import type { Pool as PgPool } from "pg";
|
|
3
|
+
import { type JsonlStoreGate } from "../observability/corrupt-read-seat.js";
|
|
3
4
|
import type { Logger } from "../observability/logger.js";
|
|
4
5
|
/** The server's own audit event for a question a row answered without asking anyone — one name for both reading positions
|
|
5
6
|
* (the list gate: field `seat: "deployment_list"`; the approval seat: `seat: "approval_seat"`). Not a settlement record: nobody
|
|
@@ -48,6 +49,30 @@ export declare class MemoryApprovalExemptionStore implements ApprovalExemptionSt
|
|
|
48
49
|
revoke(sessionId: string, toolName: string): Promise<boolean>;
|
|
49
50
|
deleteBySession(sessionId: string): Promise<number>;
|
|
50
51
|
}
|
|
52
|
+
/** The File twin's directory under the data root (one JSONL log per session) and the log suffix — the store and the `Skill` cutoff's skip-version arm read the same layout. */
|
|
53
|
+
export declare const APPROVAL_EXEMPTION_DIR = "approval-exemptions";
|
|
54
|
+
export declare const EXEMPTION_LOG_SUFFIX = ".jsonl";
|
|
55
|
+
/**
|
|
56
|
+
* A record whose first-grant time cannot be read (no `ts`, or a `ts` that is not a time — only a damaged or hand-edited log; the writer always stamps it)
|
|
57
|
+
* reads as the EARLIEST time, never as "now": every reader that asks "granted before cutoff X?" (the `Skill` exemption cutoff arm, one-time migration M46)
|
|
58
|
+
* then answers yes — the fail-closed direction. ("Now" changed with every restart and, read after a cutoff was recorded, silently kept such a row.)
|
|
59
|
+
*/
|
|
60
|
+
export declare const UNDATED_FIRST_GRANT_MS = 0;
|
|
61
|
+
/** One session log's replayed rows: per wire tool name, the row in force (`createdAt` = its FIRST grant since the last revoke; unreadable ⇒ {@link UNDATED_FIRST_GRANT_MS}). */
|
|
62
|
+
export type ExemptionLogRows = ReadonlyMap<string, {
|
|
63
|
+
readonly grantedBy: string | null;
|
|
64
|
+
readonly createdAt: number;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* Replay ONE session log (the only reader of a log's bytes: the store's hydrate and the `Skill` cutoff's skip-version arm both call it —
|
|
68
|
+
* `approval-exemption-skill-cutoff.ts`). Torn-tail-safe, last-wins with revoke tombstones; a record in the middle that cannot be read closes
|
|
69
|
+
* that file's gate (`gate.tripped`) — the store then refuses every read of that session (ask anyway), so its rows answer nothing.
|
|
70
|
+
* A record without a parseable `ts` reads as granted now (the replay's own moment).
|
|
71
|
+
*/
|
|
72
|
+
export declare function readExemptionLog(path: string, warn?: Logger["warn"]): {
|
|
73
|
+
readonly rows: ExemptionLogRows;
|
|
74
|
+
readonly gate: JsonlStoreGate;
|
|
75
|
+
};
|
|
51
76
|
/**
|
|
52
77
|
* 🔴 [ref] —— **fail-closed per session file.** A record in the MIDDLE of a session's log that cannot be read (bit rot,
|
|
53
78
|
* a hand edit, a line `42`, an object missing `toolName`) used to be skipped silently. When that line was a REVOKE
|
|
@@ -73,7 +98,7 @@ export declare class FileApprovalExemptionStore implements ApprovalExemptionStor
|
|
|
73
98
|
/** [ref]: first line of every judging / appending method (see the class note). */
|
|
74
99
|
private assertUsable;
|
|
75
100
|
private pathFor;
|
|
76
|
-
/** Replay every session log (torn-tail-safe; last-wins with revoke tombstones). Index keyed by the SANITIZED id. */
|
|
101
|
+
/** Replay every session log (torn-tail-safe; last-wins with revoke tombstones). Index keyed by the SANITIZED id. One reader per file: {@link readExemptionLog}. */
|
|
77
102
|
private hydrate;
|
|
78
103
|
private logFor;
|
|
79
104
|
grant(sessionId: string, toolName: string, grantedBy: string | null): Promise<void>;
|
|
@@ -115,6 +115,27 @@ const EXEMPTION_RECORD_SHAPE = {
|
|
|
115
115
|
return { toolName, grantedBy: grantedBy ?? null, ts: ts ?? "", ...(revoked !== undefined ? { revoked } : {}) };
|
|
116
116
|
},
|
|
117
117
|
};
|
|
118
|
+
export const APPROVAL_EXEMPTION_DIR = "approval-exemptions";
|
|
119
|
+
export const EXEMPTION_LOG_SUFFIX = ".jsonl";
|
|
120
|
+
export const UNDATED_FIRST_GRANT_MS = 0;
|
|
121
|
+
const firstGrantMsOf = (ts) => {
|
|
122
|
+
const t = Date.parse(ts);
|
|
123
|
+
return Number.isNaN(t) ? UNDATED_FIRST_GRANT_MS : t;
|
|
124
|
+
};
|
|
125
|
+
export function readExemptionLog(path, warn) {
|
|
126
|
+
const { records, gate } = replayJsonlGuarded(path, "approval-exemption", EXEMPTION_RECORD_SHAPE, warn);
|
|
127
|
+
const rows = new Map();
|
|
128
|
+
for (const r of records) {
|
|
129
|
+
if (r.revoked) {
|
|
130
|
+
rows.delete(r.toolName);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const prior = rows.get(r.toolName);
|
|
134
|
+
rows.set(r.toolName, { grantedBy: r.grantedBy ?? null, createdAt: prior?.createdAt ?? firstGrantMsOf(r.ts) });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { rows, gate };
|
|
138
|
+
}
|
|
118
139
|
export class FileApprovalExemptionStore {
|
|
119
140
|
dir;
|
|
120
141
|
bySession = new Map();
|
|
@@ -122,7 +143,7 @@ export class FileApprovalExemptionStore {
|
|
|
122
143
|
corruptGates = new Map();
|
|
123
144
|
warn;
|
|
124
145
|
constructor(root, opts) {
|
|
125
|
-
this.dir = join(root,
|
|
146
|
+
this.dir = join(root, APPROVAL_EXEMPTION_DIR);
|
|
126
147
|
this.warn = opts?.warn;
|
|
127
148
|
ensureDir(this.dir);
|
|
128
149
|
this.hydrate();
|
|
@@ -131,7 +152,7 @@ export class FileApprovalExemptionStore {
|
|
|
131
152
|
this.corruptGates.get(key)?.assertUsable();
|
|
132
153
|
}
|
|
133
154
|
pathFor(sessionId) {
|
|
134
|
-
return join(this.dir, `${sanitizePathComponent(sessionId)}
|
|
155
|
+
return join(this.dir, `${sanitizePathComponent(sessionId)}${EXEMPTION_LOG_SUFFIX}`);
|
|
135
156
|
}
|
|
136
157
|
hydrate() {
|
|
137
158
|
let names;
|
|
@@ -142,26 +163,14 @@ export class FileApprovalExemptionStore {
|
|
|
142
163
|
return;
|
|
143
164
|
}
|
|
144
165
|
for (const name of names) {
|
|
145
|
-
if (!name.endsWith(
|
|
166
|
+
if (!name.endsWith(EXEMPTION_LOG_SUFFIX))
|
|
146
167
|
continue;
|
|
147
|
-
const key = name.slice(0, -
|
|
148
|
-
const {
|
|
168
|
+
const key = name.slice(0, -EXEMPTION_LOG_SUFFIX.length);
|
|
169
|
+
const { rows, gate } = readExemptionLog(join(this.dir, name), this.warn);
|
|
149
170
|
if (gate.tripped)
|
|
150
171
|
this.corruptGates.set(key, gate);
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
const m = this.bySession.get(key) ?? new Map();
|
|
154
|
-
for (const r of records) {
|
|
155
|
-
if (r.revoked) {
|
|
156
|
-
m.delete(r.toolName);
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
const prior = m.get(r.toolName);
|
|
160
|
-
m.set(r.toolName, { grantedBy: r.grantedBy ?? null, createdAt: prior?.createdAt ?? (Date.parse(r.ts) || Date.now()) });
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (m.size)
|
|
164
|
-
this.bySession.set(key, m);
|
|
172
|
+
if (rows.size)
|
|
173
|
+
this.bySession.set(key, new Map(rows));
|
|
165
174
|
else
|
|
166
175
|
this.bySession.delete(key);
|
|
167
176
|
}
|
|
@@ -1,14 +1,47 @@
|
|
|
1
1
|
export declare const LOCK_PROBE_INITIAL_JITTER_MS = 400;
|
|
2
2
|
export declare const LOCK_PROBE_WARN_EVERY = 40;
|
|
3
3
|
export declare const MAX_CONSECUTIVE_LOCK_PROBE_ERRORS = 5;
|
|
4
|
-
/** 抖动退避(~0.1s..0.4s + 至多 0.15s 抖动)——
|
|
4
|
+
/** 抖动退避(~0.1s..0.4s + 至多 0.15s 抖动)—— 启动锁的曲线。 */
|
|
5
5
|
export declare function lockProbeBackoffMs(attempt: number): number;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* 一把库内具名锁的身份。两方言的「名字」不同形(引擎事实,不是口味):MySQL 协议一个字符串(服务器实例**全局**,上限 64 字符),
|
|
8
|
+
* PG 一个 bigint 键或一对 int4 键(**每库**;单键与双键是两个互不相交的键空间)。身份由属主模块各自铸(启动锁:`tidb-pool.ts` / `pg-pool.ts`;弧锁:`adoption-log-sql.ts`),
|
|
9
|
+
* 本模块只负责怎么取、怎么放。
|
|
10
|
+
*/
|
|
11
|
+
export type AdvisoryLockId = {
|
|
12
|
+
readonly dialect: "mysql";
|
|
13
|
+
readonly name: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly dialect: "pg";
|
|
16
|
+
readonly keys: readonly [number] | readonly [number, number];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* 取锁要的最小连接面:发一句带参 SQL、读回行。锁是**连接级**的 —— 调用方必须把取、放与锁罩住的全部语句跑在**同一条**连接上。
|
|
20
|
+
* `SqlTxConn`(`{ rows, affected }`)与 node-pg 的 `PoolClient`(`{ rows }`)天然满足;mysql2 的 `[rows, fields]` 由调用方一行适配。
|
|
21
|
+
*/
|
|
22
|
+
export interface LockConn {
|
|
23
|
+
query(sql: string, params: unknown[]): Promise<{
|
|
24
|
+
readonly rows: ReadonlyArray<Record<string, unknown>>;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 轮到拿到为止(启动锁的预算)。`tryOnce` = 方言自己的非阻塞探针:`true` 拿到、`false` 别人持着(继续轮)、抛 = 探针本身失败。
|
|
29
|
+
* 取锁调用方走 {@link acquireAdvisoryLock};本函数留给只想验循环律、不带连接的单测。
|
|
9
30
|
*/
|
|
10
31
|
export declare function pollUntilLocked(tryOnce: () => Promise<boolean>, opts: {
|
|
11
32
|
tag: string;
|
|
12
33
|
label: string;
|
|
13
34
|
}): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* 在 `conn` 上取 `id`,**轮到拿到为止**(永不无锁跑;一条探针连续抛的连接上抛成起服失败)。
|
|
37
|
+
* 锁是连接级的:调用方必须把锁罩住的全部语句跑在这条 `conn` 上,并用 {@link releaseAdvisoryLock} 放。
|
|
38
|
+
*/
|
|
39
|
+
export declare function acquireAdvisoryLock(conn: LockConn, id: AdvisoryLockId, tag: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* 在 `conn` 上取 `id`,**最多探 `attempts` 次、每次没拿到睡 `sleepMs`**(无开跑抖动:常路零额外延迟)。
|
|
42
|
+
* `true` = 拿到(调用方负责放);`false` = 用尽次数仍是「别人持着」(调用方按「在飞」应答)。其余同 {@link acquireAdvisoryLock}。
|
|
43
|
+
*/
|
|
44
|
+
export declare function acquireAdvisoryLockWithin(conn: LockConn, id: AdvisoryLockId, tag: string, attempts: number, sleepMs: number): Promise<boolean>;
|
|
45
|
+
/** {@link acquireAdvisoryLock} / {@link acquireAdvisoryLockWithin} 的释放(尽力而为 —— 锁随连接关闭自动释放;失败留一行,不盖掉真正的结果)。 */
|
|
46
|
+
export declare function releaseAdvisoryLock(conn: LockConn, id: AdvisoryLockId, tag: string): Promise<void>;
|
|
14
47
|
//# sourceMappingURL=ensure-schema-lock-policy.d.ts.map
|
|
@@ -6,27 +6,93 @@ export function lockProbeBackoffMs(attempt) {
|
|
|
6
6
|
return 100 + Math.min(attempt, 3) * 100 + Math.floor(Math.random() * 150);
|
|
7
7
|
}
|
|
8
8
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const TIDB_PESSIMISTIC_RETRY_LIMIT = "pessimistic lock retry limit reached";
|
|
10
|
+
function isTidbPessimisticRetryLimit(err) {
|
|
11
|
+
return err?.message === TIDB_PESSIMISTIC_RETRY_LIMIT;
|
|
12
|
+
}
|
|
13
|
+
function lockStatements(id) {
|
|
14
|
+
switch (id.dialect) {
|
|
15
|
+
case "mysql":
|
|
16
|
+
return {
|
|
17
|
+
probe: "SELECT GET_LOCK(?, 0) AS got",
|
|
18
|
+
release: "SELECT RELEASE_LOCK(?)",
|
|
19
|
+
params: [id.name],
|
|
20
|
+
label: `GET_LOCK('${id.name}')`,
|
|
21
|
+
held: (row) => Number(row?.got) === 1,
|
|
22
|
+
contended: isTidbPessimisticRetryLimit,
|
|
23
|
+
};
|
|
24
|
+
case "pg":
|
|
25
|
+
return id.keys.length === 1
|
|
26
|
+
? { probe: "SELECT pg_try_advisory_lock($1) AS got", release: "SELECT pg_advisory_unlock($1)", params: [...id.keys], label: `pg advisory lock ${id.keys[0]}`, held: (row) => row?.got === true, contended: () => false }
|
|
27
|
+
: { probe: "SELECT pg_try_advisory_lock($1, $2) AS got", release: "SELECT pg_advisory_unlock($1, $2)", params: [...id.keys], label: `pg advisory lock ${id.keys[0]}/${id.keys[1]}`, held: (row) => row?.got === true, contended: () => false };
|
|
28
|
+
default: {
|
|
29
|
+
const unreachable = id;
|
|
30
|
+
throw new Error(`unreachable advisory-lock dialect: ${JSON.stringify(unreachable)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const UNTIL_HELD = { initialJitterMs: LOCK_PROBE_INITIAL_JITTER_MS, maxAttempts: Number.POSITIVE_INFINITY, backoffMs: lockProbeBackoffMs };
|
|
35
|
+
async function pollLock(tryOnce, budget, opts) {
|
|
36
|
+
if (budget.initialJitterMs > 0)
|
|
37
|
+
await sleep(Math.floor(Math.random() * budget.initialJitterMs));
|
|
11
38
|
let consecutiveErrors = 0;
|
|
12
|
-
|
|
39
|
+
let lastMsg = "";
|
|
40
|
+
let lastErr;
|
|
41
|
+
for (let attempt = 0; attempt < budget.maxAttempts; attempt++) {
|
|
13
42
|
try {
|
|
14
43
|
if (await tryOnce())
|
|
15
|
-
return;
|
|
44
|
+
return true;
|
|
16
45
|
consecutiveErrors = 0;
|
|
17
46
|
}
|
|
18
47
|
catch (err) {
|
|
19
48
|
consecutiveErrors += 1;
|
|
20
|
-
|
|
21
|
-
|
|
49
|
+
lastErr = err;
|
|
50
|
+
lastMsg = err?.message ?? String(err);
|
|
51
|
+
writeLogText("stderr", `[${opts.tag}] ${opts.label} probe attempt ${attempt + 1} threw (${consecutiveErrors}/${MAX_CONSECUTIVE_LOCK_PROBE_ERRORS}):`, lastMsg);
|
|
22
52
|
if (consecutiveErrors >= MAX_CONSECUTIVE_LOCK_PROBE_ERRORS) {
|
|
23
|
-
throw new Error(`[${opts.tag}] ${opts.label}: lock probe threw ${MAX_CONSECUTIVE_LOCK_PROBE_ERRORS} times in a row — the connection itself is unusable, refusing to poll forever (last: ${
|
|
53
|
+
throw new Error(`[${opts.tag}] ${opts.label}: lock probe threw ${MAX_CONSECUTIVE_LOCK_PROBE_ERRORS} times in a row — the connection itself is unusable, refusing to poll forever (last: ${lastMsg})`, { cause: err });
|
|
24
54
|
}
|
|
25
55
|
}
|
|
26
56
|
if (attempt > 0 && attempt % LOCK_PROBE_WARN_EVERY === 0) {
|
|
27
|
-
writeLogText("stderr", `[${opts.tag}] still waiting for ${opts.label} (another
|
|
57
|
+
writeLogText("stderr", `[${opts.tag}] still waiting for ${opts.label} (another session holds it; attempt ${attempt})`);
|
|
28
58
|
}
|
|
29
|
-
|
|
59
|
+
if (attempt + 1 < budget.maxAttempts)
|
|
60
|
+
await sleep(budget.backoffMs(attempt));
|
|
61
|
+
}
|
|
62
|
+
if (consecutiveErrors > 0) {
|
|
63
|
+
throw new Error(`[${opts.tag}] ${opts.label}: the attempt budget (${budget.maxAttempts}) ran out while the lock probe was still throwing (${consecutiveErrors} in a row) — not reading that as "held by another" (last: ${lastMsg})`, { cause: lastErr });
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
export async function pollUntilLocked(tryOnce, opts) {
|
|
68
|
+
await pollLock(tryOnce, UNTIL_HELD, opts);
|
|
69
|
+
}
|
|
70
|
+
async function tryAdvisoryLock(conn, stmts) {
|
|
71
|
+
try {
|
|
72
|
+
const res = await conn.query(stmts.probe, stmts.params);
|
|
73
|
+
return stmts.held(res.rows[0]);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (stmts.contended(err))
|
|
77
|
+
return false;
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export async function acquireAdvisoryLock(conn, id, tag) {
|
|
82
|
+
const stmts = lockStatements(id);
|
|
83
|
+
await pollLock(() => tryAdvisoryLock(conn, stmts), UNTIL_HELD, { tag, label: stmts.label });
|
|
84
|
+
}
|
|
85
|
+
export async function acquireAdvisoryLockWithin(conn, id, tag, attempts, sleepMs) {
|
|
86
|
+
const stmts = lockStatements(id);
|
|
87
|
+
return pollLock(() => tryAdvisoryLock(conn, stmts), { initialJitterMs: 0, maxAttempts: attempts, backoffMs: () => sleepMs }, { tag, label: stmts.label });
|
|
88
|
+
}
|
|
89
|
+
export async function releaseAdvisoryLock(conn, id, tag) {
|
|
90
|
+
const stmts = lockStatements(id);
|
|
91
|
+
try {
|
|
92
|
+
await conn.query(stmts.release, stmts.params);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
writeLogText("stderr", `[${tag}] releasing ${stmts.label} failed (the lock auto-frees when the connection closes):`, err);
|
|
30
96
|
}
|
|
31
97
|
}
|
|
32
98
|
//# sourceMappingURL=ensure-schema-lock-policy.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The execution seam's path law — ONE reading for every execution environment this server mints (the seven lanes: adb / device / e2b /
|
|
3
|
+
* host / k8s / local-docker / ssh) and for the sandbox write gate's stand-in environment (`boot/deferred-sandbox-path-env.ts`).
|
|
4
|
+
*
|
|
5
|
+
* The law (the engine's, `ExecutionEnv` contract; the engine's own `NodeExecutionEnv` reads every path this way): a path a method is
|
|
6
|
+
* handed is ABSOLUTE AS WRITTEN, or relative and ANCHORED at the environment's working directory with exactly one separator. Nothing here
|
|
7
|
+
* folds a `..` or a `.` and nothing resolves a link: the filesystem the path lands on resolves them, components first — so a `..` that
|
|
8
|
+
* follows a symbolic link goes where the link leads (`<base>/bridge/../next` with `bridge -> elsewhere/sub` names `<base>/elsewhere/next`).
|
|
9
|
+
* A lexical fold would name `<base>/next` instead: the write gate (which asks the real filesystem what the spelling names) and the tool
|
|
10
|
+
* (which opened the folded name) would then judge and touch two different files.
|
|
11
|
+
*
|
|
12
|
+
* Three faces, one anchor ({@link anchorUnder}): the path a method reads ({@link anchoredPath}), `joinPath` ({@link joinedPath}) and the
|
|
13
|
+
* paths a listing reports (each entry anchored under the path the listing was handed). The remote lanes' shell scripts (the link-chain walk
|
|
14
|
+
* and the staged write, `remote-shell.ts`) already anchor this way in shell; this module is the same law for the TypeScript faces.
|
|
15
|
+
*
|
|
16
|
+
* Pure leaf: string arithmetic only, no I/O.
|
|
17
|
+
*/
|
|
18
|
+
import type { PlatformPath } from "node:path";
|
|
19
|
+
/**
|
|
20
|
+
* The two facts of a path family the law reads: its separator and its absolute-path judge. The remote lanes read POSIX whatever the
|
|
21
|
+
* worker runs on (`path.posix`; the device lane passes its one absolute-path judge, `isDeviceAbsolutePath`); the host lane reads the
|
|
22
|
+
* worker's own (`path`).
|
|
23
|
+
*/
|
|
24
|
+
export type SeamPathFamily = Pick<PlatformPath, "sep" | "isAbsolute">;
|
|
25
|
+
/** `name` placed under `dir` with exactly one separator — never folded, never re-rooted. */
|
|
26
|
+
export declare function anchorUnder(family: SeamPathFamily, dir: string, name: string): string;
|
|
27
|
+
/** The path a method is handed, as the environment reads it: absolute ⇒ as written; relative ⇒ anchored at `cwd`. */
|
|
28
|
+
export declare function anchoredPath(family: SeamPathFamily, cwd: string, p: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* `joinPath`: the first part as written; every later part is a segment, never a root (its leading separators are dropped); empty parts
|
|
31
|
+
* are dropped; the rest anchored in turn. No parts ⇒ `"."`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function joinedPath(family: SeamPathFamily, parts: readonly string[]): string;
|
|
34
|
+
//# sourceMappingURL=exec-path-law.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function anchorUnder(family, dir, name) {
|
|
2
|
+
const endsInSeparator = dir.endsWith(family.sep) || (family.sep === "\\" && dir.endsWith("/"));
|
|
3
|
+
return endsInSeparator ? `${dir}${name}` : `${dir}${family.sep}${name}`;
|
|
4
|
+
}
|
|
5
|
+
export function anchoredPath(family, cwd, p) {
|
|
6
|
+
return family.isAbsolute(p) ? p : anchorUnder(family, cwd, p);
|
|
7
|
+
}
|
|
8
|
+
export function joinedPath(family, parts) {
|
|
9
|
+
const leading = family.sep === "\\" ? /^[\\/]+/ : /^\/+/;
|
|
10
|
+
const named = parts.map((part, i) => (i === 0 ? part : part.replace(leading, ""))).filter((part) => part !== "");
|
|
11
|
+
return named.length === 0 ? "." : named.reduce((at, part) => anchorUnder(family, at, part));
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=exec-path-law.js.map
|
|
@@ -204,15 +204,16 @@ export declare class FileRunStore implements InProcessClaimFence {
|
|
|
204
204
|
/** The terminal write's one synchronous section (SQL twin: one transaction): the k8s space's pending commit record (when
|
|
205
205
|
* offered) → the terminal row → the active-run claim release. Returns whether the pending record is now this run's. */
|
|
206
206
|
private writeTerminal;
|
|
207
|
-
/** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS
|
|
208
|
-
* [ref]:同时落 `result` —— 封口脱敏后的投影(`sealRunRowResult`,凭据不落;`null` = 没观察到 park 结果),SQL 孪生同注。
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
207
|
+
/** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS = 行仍归 `legId` ∧ running / suspended ∧ 没挂取消请求(三件的理由见 SQL 孪生)。
|
|
208
|
+
* [ref]:同时落 `result` —— 封口脱敏后的投影(`sealRunRowResult`,凭据不落;`null` = 没观察到 park 结果),SQL 孪生同注。
|
|
209
|
+
* 交回 CAS 的答(内存 / SQL 孪生同义):`true` ⇔ 这一写之后行停在这个词上;`false` ⇔ 三件有一件不成立(或行不在)。判与落盘同一个同步段。 */
|
|
210
|
+
setSuspended(taskId: string, result: PersistedTaskResult | null, legId: string): Promise<boolean>;
|
|
211
|
+
/** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS = {@link setSuspended} 的三件(词换成 needs_review)。
|
|
212
|
+
* [ref]:同 {@link setSuspended} 落封口脱敏后的 `result`;CAS 的答同 {@link setSuspended}。 */
|
|
213
|
+
setNeedsReview(taskId: string, result: PersistedTaskResult | null, legId: string): Promise<boolean>;
|
|
214
|
+
/** Durable F4 resume: flip suspended/needs_review → running (CAS);认领 = 换持有腿(铸新的 `legId` 交回)+ 两面请求旗随旧腿清零(SQL 孪生同注)。
|
|
215
|
+
* [ref](codex R1 [medium]):认领 = 接管 `instanceId`(= 当前持有副本;SQL 孪生同注)。CAS 输不改、交回 `undefined`。 */
|
|
216
|
+
markResuming(taskId: string, instanceId: string): Promise<string | undefined>;
|
|
216
217
|
getActiveTaskId(sessionId: string): Promise<string | undefined>;
|
|
217
218
|
/** 认领当写栅栏用(契约 = `store-contracts.ts` 的 `InProcessClaimFence`):判、刷新活性(同步落盘)与 `write` 是同一个同步段。 */
|
|
218
219
|
whileHoldingClaim(sessionId: string, taskId: string, write: (run: FencedRun) => FenceLanded): boolean;
|
|
@@ -222,6 +223,13 @@ export declare class FileRunStore implements InProcessClaimFence {
|
|
|
222
223
|
* the error answer invites a retry, and a retry with the same key must run, not replay the refusal. The row stays (it
|
|
223
224
|
* is the refusal's record); only the binding is freed. */
|
|
224
225
|
releaseIdemKey(taskId: string): Promise<void>;
|
|
226
|
+
/** 放一条行上的提交幂等键绑定(盘先、索引后;`releaseIdemKey` 与 `withdrawMintedRun` 的非在跑臂同读)。同步段。 */
|
|
227
|
+
private dropIdemBinding;
|
|
228
|
+
/**
|
|
229
|
+
* 撤回本请求铸的这一条 run(SQL 孪生同判同序,律见那里的头注):行上的持有腿不是 `legId` ⇒ 不是本腿的行,一个字节不动;是 ⇒ 行仍在跑 ⇒ 严格删行({@link dropRun}:盘先、索引后;删不掉 ⇒ 抛,认领与行都没动)、
|
|
230
|
+
* 再放本行持有的那一枚认领;已终局 ⇒ 行零删(收割 / 终局先到,那一份就是这条行的结局),只放它上面的提交幂等键;停驻 ⇒ 一个字节不动。同步段,不让出事件循环。交回删没删。
|
|
231
|
+
*/
|
|
232
|
+
withdrawMintedRun(taskId: string, legId: string): Promise<boolean>;
|
|
225
233
|
/** The body fingerprint this row's binding of `idem.key` holds — binding `idem.bodyFingerprint` first when it holds none (first
|
|
226
234
|
* arrival binds and is persisted, everyone after reads; the file twin of the SQL conditional UPDATE — one writer per data root,
|
|
227
235
|
* single-threaded between the check and the write). `null` = no such row, or the row is not bound to this key (never written). */
|
|
@@ -422,8 +430,13 @@ export declare class FileRunStore implements InProcessClaimFence {
|
|
|
422
430
|
/**
|
|
423
431
|
* 一次性迁移 **M48 的 local 腿**(run 行半场;窗、删法与删后谁接住:窗口见 `plugins/one-time-migrations.ts` 登记表):属主恰为 cli 哨兵(`shell-sentinel-owner-m48.ts`)的 run 行
|
|
424
432
|
* 改写成无主。盘上与内存同改(副本写律)。起服时调(`LocalBackend.runOneTimeMigrations`,任何读者之前);数据根独占 ⇒ 没有混跑窗。幂等:第二跑零命中零写盘。
|
|
433
|
+
* 命中的行由 {@link shellSentinelRunsM48} 说(与 {@link countShellSentinelOwnersM48} 同一只)。
|
|
425
434
|
*/
|
|
426
435
|
clearShellSentinelOwnersM48(): number;
|
|
436
|
+
/** 一次性迁移 **M48 的 local 腿**(有身份边界的部署:只数不写):属主恰为 cli 哨兵的 run 行数 —— 命中判据与 {@link clearShellSentinelOwnersM48} 同一只({@link shellSentinelRunsM48})。读内存索引,零写。 */
|
|
437
|
+
countShellSentinelOwnersM48(): number;
|
|
438
|
+
/** M48(local 腿)命中的 run 行:属主恰为 cli 哨兵({@link isShellSentinelOwnerM48})—— 改写与留存计数共用的**唯一**判据。先取快照再改(改写会 `persist` 替换索引里的行)。随 M48 删。 */
|
|
439
|
+
private shellSentinelRunsM48;
|
|
427
440
|
/**
|
|
428
441
|
* 一次性迁移 **M32 的 local 腿**(执行放置回填;窗、删法与删后谁接住:窗口见 `plugins/one-time-migrations.ts` 登记表):有 run 行的会话 id(去重)。
|
|
429
442
|
* 起服时调(`LocalBackend.runOneTimeMigrations`,任何读者之前);读内存索引(构造时已从盘上 hydrate),零写。
|