@sema-agent/server 7.131.0 → 7.132.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 +309 -0
- package/README.md +5 -1
- package/README.zh-CN.md +3 -1
- package/USAGE.md +163 -54
- package/dist/approval-card.d.ts +3 -1
- package/dist/approval-card.js +2 -3
- package/dist/audit.d.ts +7 -10
- package/dist/auth-bridge.d.ts +4 -0
- package/dist/auth-bridge.js +10 -11
- package/dist/boot/deployment-mods.d.ts +31 -0
- package/dist/boot/deployment-mods.js +26 -0
- package/dist/boot/execution-env.d.ts +1 -1
- package/dist/boot/execution-env.js +19 -6
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/read-deny-anchor-audit.d.ts +17 -0
- package/dist/boot/read-deny-anchor-audit.js +36 -0
- package/dist/boot/resolve-spec.d.ts +1 -5
- package/dist/boot/resolve-spec.js +19 -47
- package/dist/boot/runner-deps.d.ts +19 -8
- package/dist/boot/runner-deps.js +14 -2
- package/dist/boot/session-faces.d.ts +1 -7
- package/dist/boot/stage-03-store-backend.d.ts +1 -1
- package/dist/boot/stage-03-store-backend.js +2 -2
- package/dist/boot/stage-04-stores.d.ts +1 -1
- package/dist/boot/stage-05-execution-env.d.ts +4 -4
- package/dist/boot/stage-05-execution-env.js +7 -10
- package/dist/boot/stage-06-runners.d.ts +2 -2
- package/dist/boot/stage-06-runners.js +5 -4
- package/dist/boot/stage-07-capability-layer.d.ts +2 -2
- package/dist/boot/stage-07-capability-layer.js +3 -3
- package/dist/boot/stage-08-reapers.d.ts +2 -2
- package/dist/boot/stage-09-leader.d.ts +3 -9
- package/dist/boot/stage-10-http-server.d.ts +3 -9
- package/dist/boot/stage-10-http-server.js +6 -7
- package/dist/boot/stores.d.ts +1 -1
- package/dist/boot/stores.js +5 -6
- package/dist/boot/web-admin.d.ts +0 -3
- package/dist/boot/web-admin.js +35 -35
- package/dist/brain.js +17 -22
- package/dist/budget.js +5 -4
- package/dist/caller-text-echo.d.ts +20 -0
- package/dist/caller-text-echo.js +24 -0
- package/dist/capabilities/brand-identity.d.ts +26 -0
- package/dist/capabilities/brand-identity.js +27 -0
- package/dist/capabilities/center-plugins.js +1 -1
- package/dist/capabilities/center-prompts.d.ts +32 -59
- package/dist/capabilities/center-prompts.js +18 -32
- package/dist/capabilities/engine-capabilities.js +2 -1
- package/dist/capabilities/hands-lane.d.ts +9 -6
- package/dist/capabilities/hands-lane.js +1 -1
- package/dist/capabilities/mcp-probe.d.ts +14 -1
- package/dist/capabilities/mcp-probe.js +22 -133
- package/dist/capabilities/scenarios.d.ts +34 -40
- package/dist/capabilities/scenarios.js +16 -33
- package/dist/capabilities/skills.d.ts +19 -4
- package/dist/capabilities/skills.js +30 -10
- package/dist/config-catalog.js +6 -6
- package/dist/config-lkg.d.ts +9 -9
- package/dist/config-lkg.js +20 -4
- package/dist/config-provider.d.ts +3 -1
- package/dist/config-provider.js +12 -4
- package/dist/config-types.d.ts +7 -9
- package/dist/config.js +17 -4
- package/dist/device-ws-protocol.d.ts +1 -1
- package/dist/device-ws-protocol.js +1 -0
- package/dist/env-name-allowlist-knobs.d.ts +12 -2
- package/dist/env-name-allowlist-knobs.js +5 -4
- package/dist/exec-bounds.d.ts +15 -3
- package/dist/exec-bounds.js +4 -0
- package/dist/execution-lane-caps.d.ts +9 -2
- package/dist/execution-lane-caps.js +11 -0
- package/dist/hooks/classic-stations.d.ts +86 -0
- package/dist/hooks/classic-stations.js +27 -0
- package/dist/hooks/hook-llm.d.ts +8 -5
- package/dist/hooks/hook-llm.js +78 -35
- package/dist/hooks/hook-runner.d.ts +232 -80
- package/dist/hooks/hook-runner.js +218 -312
- package/dist/http/admission.d.ts +8 -2
- package/dist/http/admission.js +101 -57
- package/dist/http/approval-sweeps.js +57 -16
- package/dist/http/checkpoint-ctx.d.ts +25 -8
- package/dist/http/checkpoint-ctx.js +11 -7
- package/dist/http/claimed-run.d.ts +13 -0
- package/dist/http/claimed-run.js +15 -0
- package/dist/http/device-placement.js +5 -10
- package/dist/http/dispatch.d.ts +0 -4
- package/dist/http/dispatch.js +5 -9
- package/dist/http/durable-run-leg.d.ts +2 -1
- package/dist/http/durable-run-leg.js +5 -3
- package/dist/http/health-self-report.d.ts +7 -5
- package/dist/http/health-self-report.js +4 -18
- package/dist/http/principal-gate.d.ts +23 -6
- package/dist/http/principal-gate.js +24 -5
- package/dist/http/refusal-body.d.ts +21 -0
- package/dist/http/refusal-body.js +4 -0
- package/dist/http/request-guards.d.ts +27 -0
- package/dist/http/request-guards.js +24 -3
- package/dist/http/resume-legs.d.ts +25 -0
- package/dist/http/resume-legs.js +43 -22
- package/dist/http/routes/capabilities.js +1 -1
- package/dist/http/routes/diagnostics.js +1 -1
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +7 -7
- package/dist/http/routes/session-sync.js +4 -2
- package/dist/http/routes/sessions.js +1 -1
- package/dist/http/routes/shared-memory.js +2 -13
- package/dist/http/routes/side-query.js +2 -3
- package/dist/http/routes/tasks.js +19 -9
- package/dist/http/send.d.ts +9 -7
- package/dist/http/send.js +7 -2
- package/dist/http/server.d.ts +21 -11
- package/dist/http/server.js +28 -2
- package/dist/http/submit-ctx.d.ts +25 -0
- package/dist/http/submit-ctx.js +39 -0
- package/dist/http/wire-types.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lane-home-probe.d.ts +43 -0
- package/dist/lane-home-probe.js +35 -0
- package/dist/limit-refusal.d.ts +47 -0
- package/dist/limit-refusal.js +12 -0
- package/dist/main.js +1 -1
- package/dist/mcp-assertion-header.d.ts +1 -9
- package/dist/mcp-assertion-header.js +0 -6
- package/dist/mcp-tool-faces.d.ts +5 -0
- package/dist/mcp-tool-faces.js +5 -1
- package/dist/memory-scope.d.ts +1 -1
- package/dist/model-provider.d.ts +39 -12
- package/dist/model-provider.js +19 -2
- package/dist/model-route-endpoint.d.ts +1 -1
- package/dist/model-route-endpoint.js +5 -5
- package/dist/observability/cost-taxonomy.d.ts +7 -7
- package/dist/observability/fail-open.d.ts +42 -6
- package/dist/observability/fail-open.js +42 -6
- package/dist/observability/metrics.js +3 -1
- package/dist/observability/prompt-manifest.d.ts +47 -82
- package/dist/observability/prompt-manifest.js +119 -17
- package/dist/observability/run-terminal.js +14 -6
- package/dist/observability/secret-env-scrub.d.ts +13 -1
- package/dist/observability/secret-env-scrub.js +13 -1
- package/dist/observability/tool-trace.d.ts +16 -21
- package/dist/observability/tool-trace.js +51 -42
- package/dist/per-task-image.d.ts +7 -1
- package/dist/per-task-image.js +3 -3
- package/dist/plain-string-record.d.ts +25 -0
- package/dist/plain-string-record.js +32 -0
- package/dist/plugins/blob-backend.d.ts +68 -13
- package/dist/plugins/blob-backend.js +53 -10
- package/dist/plugins/checkpoint-store-sql.d.ts +14 -4
- package/dist/plugins/checkpoint-store-sql.js +39 -26
- package/dist/plugins/k8s-bg-scripts.d.ts +11 -0
- package/dist/plugins/k8s-bg-scripts.js +22 -0
- package/dist/plugins/local-checkpoint-store.js +5 -2
- package/dist/plugins/local-session-store.js +1 -1
- package/dist/plugins/one-time-migrations.d.ts +137 -90
- package/dist/plugins/one-time-migrations.js +356 -207
- package/dist/plugins/permission-rule-store-sql.d.ts +13 -7
- package/dist/plugins/permission-rule-store-sql.js +4 -18
- package/dist/plugins/pg-session-storage.d.ts +4 -5
- package/dist/plugins/pg-session-storage.js +7 -17
- package/dist/plugins/remote-env-adb.d.ts +6 -0
- package/dist/plugins/remote-env-adb.js +9 -4
- package/dist/plugins/remote-env-device.js +3 -0
- package/dist/plugins/remote-env-e2b.d.ts +1 -1
- package/dist/plugins/remote-env-e2b.js +21 -9
- package/dist/plugins/remote-env-host.d.ts +9 -9
- package/dist/plugins/remote-env-host.js +10 -32
- package/dist/plugins/remote-env-k8s.d.ts +95 -26
- package/dist/plugins/remote-env-k8s.js +265 -90
- package/dist/plugins/remote-env-local-docker.d.ts +20 -1
- package/dist/plugins/remote-env-local-docker.js +77 -9
- package/dist/plugins/remote-env-ssh.d.ts +15 -0
- package/dist/plugins/remote-env-ssh.js +109 -3
- package/dist/plugins/remote-shell.d.ts +0 -1
- package/dist/plugins/remote-shell.js +1 -2
- package/dist/plugins/retention-store-sql.js +6 -2
- package/dist/plugins/schema-consistency.d.ts +54 -0
- package/dist/plugins/schema-consistency.js +52 -0
- package/dist/plugins/session-epoch-anchor.d.ts +13 -0
- package/dist/plugins/session-epoch-anchor.js +23 -0
- package/dist/plugins/shared-memory-store-sql.d.ts +9 -3
- package/dist/plugins/shared-memory-store-sql.js +5 -16
- package/dist/plugins/sql-driver.d.ts +141 -0
- package/dist/plugins/sql-driver.js +115 -1
- package/dist/plugins/sql-errors.d.ts +41 -0
- package/dist/plugins/sql-errors.js +36 -0
- package/dist/plugins/store-backend.js +2 -1
- package/dist/plugins/tidb-session-storage.d.ts +7 -11
- package/dist/plugins/tidb-session-storage.js +6 -16
- package/dist/plugins/tidb-session-store.js +1 -1
- package/dist/prompts-domain-validate.d.ts +17 -61
- package/dist/prompts-domain-validate.js +9 -119
- package/dist/request-key-closure.d.ts +2 -2
- package/dist/request-key-closure.js +2 -4
- package/dist/rule-entry-list.d.ts +12 -9
- package/dist/rule-entry-list.js +37 -21
- package/dist/run-local.js +19 -19
- package/dist/runs.d.ts +6 -0
- package/dist/runs.js +16 -0
- package/dist/server-secret-env.d.ts +44 -18
- package/dist/server-secret-env.js +49 -8
- package/dist/session-incarnation.d.ts +15 -5
- package/dist/session-incarnation.js +16 -9
- package/dist/session-titler.js +12 -2
- package/dist/spec-fields.d.ts +28 -16
- package/dist/spec-fields.js +46 -53
- package/dist/sql-engine-posture.d.ts +29 -4
- package/dist/sql-engine-posture.js +36 -1
- package/dist/stored-request-tool-axes-m19.d.ts +19 -4
- package/dist/stored-request-tool-axes-m19.js +30 -2
- package/dist/task-a2a.d.ts +9 -1
- package/dist/task-a2a.js +73 -47
- package/dist/task-mcp.d.ts +46 -58
- package/dist/task-mcp.js +99 -65
- package/dist/task-settings.d.ts +7 -21
- package/dist/task-settings.js +7 -36
- package/dist/text-bidi.d.ts +3 -46
- package/dist/text-bidi.js +3 -3
- package/dist/tool-approval.d.ts +1 -1
- package/dist/trace/core-keyset-guard.d.ts +24 -4
- package/dist/trace/engine-notice-wire.d.ts +11 -3
- package/dist/trace/engine-notice-wire.js +8 -1
- package/dist/trace/ledger-events.d.ts +3 -1
- package/dist/trace/ledger-sink.js +4 -1
- package/dist/trace/model-segment.js +1 -1
- package/dist/trace/project.d.ts +19 -6
- package/dist/trace/project.js +57 -22
- package/dist/trace/wire-projection-faces.d.ts +79 -1
- package/dist/trace/wire-projection-faces.js +22 -0
- package/dist/web-admin-client/candidate-gate.d.ts +6 -6
- package/dist/web-admin-client/candidate-gate.js +3 -3
- package/dist/web-admin-client/http-client.js +0 -6
- package/dist/web-admin-client/read-warnings.d.ts +13 -0
- package/dist/web-admin-client/read-warnings.js +10 -0
- package/dist/web-admin-client/skills-mcp.d.ts +3 -0
- package/dist/web-admin-client/skills-mcp.js +9 -6
- package/dist/web-admin-client/types.d.ts +2 -3
- package/dist/workspace-cache.d.ts +35 -0
- package/dist/workspace-cache.js +36 -0
- package/dist/workspace-space.d.ts +3 -2
- package/dist/workspace-space.js +2 -0
- package/docs/DEVICE-EXECUTOR-PROTOCOL.md +8 -1
- package/package.json +4 -4
- package/scripts/drain-check-local.mjs +34 -1
- package/scripts/drain-check.mysql.sql +30 -2
- package/scripts/drain-check.pg.sql +29 -2
- package/dist/capabilities/memory-notice.d.ts +0 -138
- package/dist/capabilities/memory-notice.js +0 -64
- package/dist/capabilities/prompt.d.ts +0 -17
- package/dist/capabilities/prompt.js +0 -6
- package/dist/mcp-content-origin.d.ts +0 -34
- package/dist/mcp-content-origin.js +0 -10
- package/dist/tool-axis-words.d.ts +0 -27
- package/dist/tool-axis-words.js +0 -9
- package/dist/wire-name-bound.d.ts +0 -14
- package/dist/wire-name-bound.js +0 -13
|
@@ -1,39 +1,102 @@
|
|
|
1
|
-
import { SESSION_RULES_GRAMMAR, compileSessionRuleBucket, readSessionRulesShape } from "@sema-agent/core";
|
|
1
|
+
import { PAUSE_REGISTRY, SESSION_RULES_GRAMMAR, compileSessionRuleBucket, isGateKind, readSessionRulesShape, unreadableDenyEntries } from "@sema-agent/core";
|
|
2
|
+
import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
|
|
3
|
+
import { dialectProtocolJsonEncoder } from "./sql-driver.js";
|
|
2
4
|
import { SCHEMA_STATEMENTS, acquireEnsureSchemaLock, releaseEnsureSchemaLock } from "./tidb-pool.js";
|
|
3
5
|
import { PG_SCHEMA_STATEMENTS, acquirePgEnsureSchemaLock, releasePgEnsureSchemaLock } from "./pg-pool.js";
|
|
4
6
|
import { CENTRAL_SCHEMA_INDEXES } from "./schema-indexes.js";
|
|
5
7
|
import { ensureIndexes } from "./ensure-index.js";
|
|
6
|
-
import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn, readOpaqueJsonValueColumn } from "./sql-json-column.js";
|
|
8
|
+
import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, isJsonColumnBodyProblem, jsonColumnText, jsonColumnTextExpr, readNonNullJsonValueColumn, readOpaqueJsonValueColumn, } from "./sql-json-column.js";
|
|
7
9
|
import { SESSION_META_INCARNATION_COLUMN, mintSessionIncarnation } from "../session-incarnation.js";
|
|
8
|
-
import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR
|
|
10
|
+
import { RUN_HOLDER_DEATH_CODES, RUN_ORPHANED_ERROR, RUN_STALLED_ERROR } from "./store-contracts.js";
|
|
9
11
|
import { PERMISSION_RULE_TABLE, purgeUndurableScopeRows } from "./permission-rule-store-sql.js";
|
|
10
|
-
import { SCHEMA_MISMATCH_ERROR_CODE } from "./schema-consistency.js";
|
|
11
12
|
import { SESSION_RULES_COLUMN } from "./session-rules-column.js";
|
|
12
13
|
import { readJsonObjectColumn } from "./sql-json-column.js";
|
|
13
14
|
import { retireStoredCardWords } from "../retired-card-words.js";
|
|
14
15
|
import { redactSecrets } from "../trace/redact.js";
|
|
15
16
|
const RETIRED_SESSION_RULE_BUCKETS = ["commandAllow", "commandDeny"];
|
|
16
|
-
export function
|
|
17
|
-
return {
|
|
17
|
+
export function buildMigrationReceipts() {
|
|
18
|
+
return {
|
|
19
|
+
auditDetailJsonNullRows: 0,
|
|
20
|
+
sessionIncarnationsBackfilled: 0,
|
|
21
|
+
holderDeathCodeRowsBackfilled: 0,
|
|
22
|
+
allowRuleShadowRowsMigrated: 0,
|
|
23
|
+
allowRuleShadowRowsUnreadable: 0,
|
|
24
|
+
workflowParkIndexRowsCarried: 0,
|
|
25
|
+
undurableScopeRuleRows: [],
|
|
26
|
+
undurableScopeRuleBucketsDeferred: 0,
|
|
27
|
+
undurableScopeRuleBucketsUnreadable: [],
|
|
28
|
+
sessionRuleRowsWithRetiredBuckets: [],
|
|
29
|
+
sessionRuleRowsWithOldGrammarEntries: [],
|
|
30
|
+
sessionRuleRowsWithUnreadableDeny: [],
|
|
31
|
+
retiredCardWordAskRowsRewritten: 0,
|
|
32
|
+
retiredCardWordRowsUnreadable: 0,
|
|
33
|
+
parkedQuestionCardsWritten: 0,
|
|
34
|
+
parkedQuestionRowsLeftAsWritten: 0,
|
|
35
|
+
parkedRowGateKindsBackfilled: 0,
|
|
36
|
+
parkedRowsUnresumableExpired: 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function completedReport(r) {
|
|
40
|
+
const { sessionIncarnationColumn, workflowParkIndexKey, sessionRuleColumn } = r;
|
|
41
|
+
if (sessionIncarnationColumn === undefined || workflowParkIndexKey === undefined || sessionRuleColumn === undefined) {
|
|
42
|
+
throw new Error(`one-time migrations finished without settling a shape reading (M5 ${String(sessionIncarnationColumn)} / M9 ${String(workflowParkIndexKey)} / M17 ${String(sessionRuleColumn)}) — a step did not record it`);
|
|
43
|
+
}
|
|
44
|
+
return { ...r, sessionIncarnationColumn, workflowParkIndexKey, sessionRuleColumn };
|
|
45
|
+
}
|
|
46
|
+
function changedAnything(r) {
|
|
47
|
+
return (r.auditDetailJsonNullRows > 0 ||
|
|
48
|
+
r.sessionIncarnationColumn === "added" ||
|
|
49
|
+
r.sessionIncarnationsBackfilled > 0 ||
|
|
50
|
+
r.holderDeathCodeRowsBackfilled > 0 ||
|
|
51
|
+
r.allowRuleShadowRowsMigrated > 0 ||
|
|
52
|
+
r.allowRuleShadowRowsUnreadable > 0 ||
|
|
53
|
+
r.workflowParkIndexKey === "reshaped" ||
|
|
54
|
+
r.workflowParkIndexRowsCarried > 0 ||
|
|
55
|
+
r.undurableScopeRuleRows.length > 0 ||
|
|
56
|
+
r.undurableScopeRuleBucketsDeferred > 0 ||
|
|
57
|
+
r.undurableScopeRuleBucketsUnreadable.length > 0 ||
|
|
58
|
+
r.sessionRuleColumn === "renamed" ||
|
|
59
|
+
r.sessionRuleRowsWithRetiredBuckets.length > 0 ||
|
|
60
|
+
r.sessionRuleRowsWithOldGrammarEntries.length > 0 ||
|
|
61
|
+
r.sessionRuleRowsWithUnreadableDeny.length > 0 ||
|
|
62
|
+
r.retiredCardWordAskRowsRewritten > 0 ||
|
|
63
|
+
r.retiredCardWordRowsUnreadable > 0 ||
|
|
64
|
+
r.parkedQuestionCardsWritten > 0 ||
|
|
65
|
+
r.parkedQuestionRowsLeftAsWritten > 0 ||
|
|
66
|
+
r.parkedRowGateKindsBackfilled > 0 ||
|
|
67
|
+
r.parkedRowsUnresumableExpired > 0);
|
|
68
|
+
}
|
|
69
|
+
function readingsSnapshot(r) {
|
|
70
|
+
return {
|
|
71
|
+
...r,
|
|
72
|
+
undurableScopeRuleRows: [...r.undurableScopeRuleRows],
|
|
73
|
+
undurableScopeRuleBucketsUnreadable: [...r.undurableScopeRuleBucketsUnreadable],
|
|
74
|
+
sessionRuleRowsWithRetiredBuckets: [...r.sessionRuleRowsWithRetiredBuckets],
|
|
75
|
+
sessionRuleRowsWithOldGrammarEntries: [...r.sessionRuleRowsWithOldGrammarEntries],
|
|
76
|
+
sessionRuleRowsWithUnreadableDeny: [...r.sessionRuleRowsWithUnreadableDeny],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function receiptSentence(r) {
|
|
80
|
+
const counts = Object.entries(readingFields(r))
|
|
81
|
+
.filter(([, v]) => (typeof v === "number" ? v > 0 : v !== undefined))
|
|
82
|
+
.map(([k, v]) => `${k}=${String(v)}`);
|
|
83
|
+
const rows = [
|
|
84
|
+
...r.undurableScopeRuleRows.map((x) => `removed ${x.kind} in permission-rule bucket ${x.ownerKey} (${x.behavior} ${x.tool}, scope root ${JSON.stringify(x.scopeRoot)})`),
|
|
85
|
+
...r.sessionRuleRowsWithOldGrammarEntries.map((x) => `${x.policyKey} (session ${x.sessionId}): toolAllowDropped=${JSON.stringify(x.toolAllowDropped)}`),
|
|
86
|
+
...r.sessionRuleRowsWithUnreadableDeny.map((x) => `${x.policyKey} (session ${x.sessionId}): left as stored, toolDenyUnreadable=${JSON.stringify(x.toolDenyUnreadable)}${x.retiredBuckets !== undefined ? `, retiredBuckets=${JSON.stringify(x.retiredBuckets)}` : ""}`),
|
|
87
|
+
];
|
|
88
|
+
return [counts.join(", "), ...rows].filter((part) => part !== "").join("; ");
|
|
18
89
|
}
|
|
19
90
|
export class OneTimeMigrationInterruptedError extends Error {
|
|
20
91
|
receipts;
|
|
21
92
|
constructor(cause, receipts) {
|
|
22
|
-
super(`${cause instanceof Error ? cause.message : String(cause)} — one-time
|
|
23
|
-
"
|
|
24
|
-
receipts
|
|
93
|
+
super(`${cause instanceof Error ? cause.message : String(cause)} — the one-time migrations had already committed changes before this failure; a retried boot finds that work done ` +
|
|
94
|
+
"and cannot report it again — record these readings before retrying: " +
|
|
95
|
+
receiptSentence(receipts), { cause });
|
|
25
96
|
this.name = "OneTimeMigrationInterruptedError";
|
|
26
97
|
this.receipts = receipts;
|
|
27
98
|
}
|
|
28
99
|
}
|
|
29
|
-
function interruptedWith(cause, receipts) {
|
|
30
|
-
if (receipts.rows.length === 0)
|
|
31
|
-
return cause;
|
|
32
|
-
return new OneTimeMigrationInterruptedError(cause, {
|
|
33
|
-
...(receipts.renamed ? { sessionRuleColumn: "renamed" } : {}),
|
|
34
|
-
sessionRuleRowsWithOldGrammarEntries: [...receipts.rows],
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
100
|
export const ONE_TIME_MIGRATIONS = [
|
|
38
101
|
{
|
|
39
102
|
id: "M4",
|
|
@@ -132,7 +195,7 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
132
195
|
releasedOn: "2026-10-08",
|
|
133
196
|
removableAfter: "2026-11-07",
|
|
134
197
|
removal: "删 migrateOn 的 M11 步(`permission_rule` 在场探针 + `purgeUndurableScopeRows` 调用)与 `permission-rule-store-sql.ts` 的 `purgeUndurableScopeRows` / " +
|
|
135
|
-
"`
|
|
198
|
+
"`purgeOneBucket` / `contentFreeRefusal` / `UndurableScopeRuleRow` / `UnreadableRuleBucket` / `UndurableScopePurgeReadings` / " +
|
|
136
199
|
"`UndurableScopePurgeDb` / `UNDURABLE_PURGE_PAGE` / `UNDURABLE_PURGE_OCC_ATTEMPTS`、本文件的 `removedRuleRowNote`,以及 `BucketRefusedError` 的 `verdict` 位(若届时再无别的读者;" +
|
|
137
200
|
"判官 `decodeBucketRow` 与写法 `writeBucket` 是读腿 / 写臂本身,留);报告、日志行(含逐行的 `permission_rule_row_removed` 与逐桶的 " +
|
|
138
201
|
"`permission_rule_bucket_unreadable`)与 `NO_SQL_MIGRATIONS` 的 `undurableScopeRuleRows` / `undurableScopeRuleBucketsDeferred` / `undurableScopeRuleBucketsUnreadable`",
|
|
@@ -213,16 +276,15 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
213
276
|
removal: "删 migrateOn 的 M17 步(`migrateSessionRuleGrammar`:`session_policy` 在场探针、旧名 → 进行中的名 → 判读 → 终名三段、退役命令桶扫描与点名)与 " +
|
|
214
277
|
"`retiredSessionRuleBucketsOf` / `RetiredSessionRuleBucket` / `RETIRED_SESSION_RULE_BUCKETS` / `RETIRED_SESSION_RULES_COLUMN` / `OLD_GRAMMAR_SESSION_RULES_COLUMN` / `SESSION_RULES_SCAN_PAGE` / `SessionRuleRowRetiredBuckets` / " +
|
|
215
278
|
"`oldGrammarEntriesOf` / `OldGrammarEntries` / `judgeOldGrammarRows` / `renameSessionRulesColumnSql` / `scanSessionRuleRows` / `SessionRuleScanRow` / " +
|
|
216
|
-
"`RETIRED_BUCKET_PATTERNS` / `SessionRuleRowOldGrammarEntries` /
|
|
217
|
-
"`
|
|
218
|
-
"
|
|
219
|
-
"`sessionRuleRowsWithRetiredBuckets` / `sessionRuleRowsWithOldGrammarEntries`;" +
|
|
279
|
+
"`RETIRED_BUCKET_PATTERNS` / `SessionRuleRowOldGrammarEntries` / `SessionRuleRowUnreadableDeny` / `UnreadableDenyEntry`;`logReceiptRows` 里 M17 的三段与 `receiptSentence` 里 M17 的两段;" +
|
|
280
|
+
"报告、日志行(含逐行的 `session_rule_row_retired_bucket` · `session_rule_row_old_grammar_entries` · `session_rule_row_unreadable_deny`)与 `NO_SQL_MIGRATIONS` 的 `sessionRuleColumn` / " +
|
|
281
|
+
"`sessionRuleRowsWithRetiredBuckets` / `sessionRuleRowsWithOldGrammarEntries` / `sessionRuleRowsWithUnreadableDeny`;" +
|
|
220
282
|
"**列名本身留在中央 DDL 里**(`session-rules-column.ts`,它不是迁移的一部分)",
|
|
221
283
|
afterRemoval: "① 列还叫 `rules`、或停在进行中的名 `rule_entries_old_grammar` 的存量库(跳过了带本条的版本直接升 / 判读没走完就跳版):起服列册探针 `schema-consistency.ts`(S-547)拒启 " +
|
|
222
284
|
"`config.schema_mismatch`(缺 `rule_entries`、多那一列)—— " +
|
|
223
285
|
"出路是先在一只带本条的版本上起服一次;② 行里仍带退役命令桶:SQL 店读回照旧答 `corrupt`(引擎的形读法点名那只桶,与本条在场时同一个结局),只是起服日志不再逐行点名 —— " +
|
|
224
286
|
"运维在库里把那一行改写成引擎文法或删掉它(删会话即删规则);③ 跳过本条直接升的库同样没有改名那一拍的条目判读 —— 由 ① 的拒启接住(出路同 ①)",
|
|
225
|
-
reportKeys: ["sessionRuleColumn", "sessionRuleRowsWithRetiredBuckets", "sessionRuleRowsWithOldGrammarEntries"],
|
|
287
|
+
reportKeys: ["sessionRuleColumn", "sessionRuleRowsWithRetiredBuckets", "sessionRuleRowsWithOldGrammarEntries", "sessionRuleRowsWithUnreadableDeny"],
|
|
226
288
|
},
|
|
227
289
|
{
|
|
228
290
|
id: "M18",
|
|
@@ -246,16 +308,18 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
246
308
|
introducedIn: "7.130.0",
|
|
247
309
|
releasedOn: "2026-10-16",
|
|
248
310
|
removableAfter: "2026-11-15",
|
|
249
|
-
removal: "删 `src/stored-request-tool-axes-m19.ts` 整只文件(`storedToolAxesRereadM19` / `readToolAxesAsServer7129M19` / `StoredToolAxesRereadM19`);" +
|
|
250
|
-
"`boot/resolve-spec.ts`
|
|
311
|
+
removal: "删 `src/stored-request-tool-axes-m19.ts` 整只文件(`storedToolAxesRereadM19` / `readToolAxesAsServer7129M19` / `readA2aToolAxesAsServer7131M19` / `StoredToolAxesRereadM19`);" +
|
|
312
|
+
"`boot/resolve-spec.ts` 里两条请求腿共用的那一行(`const storedToolAxes = opts?.leg === \"fresh\" ? undefined : storedToolAxesRereadM19(…)`)、递给 `resolveRequestMcp` / `resolveRequestA2a` 的两处实参与它的 import;" +
|
|
251
313
|
"`task-mcp.ts` 里 `resolveRequestMcp` / `classifyRequestMcp` / `classifyHonored` / `validateRequestMcpEntries` / `normalizeServer` 的 `storedToolAxes` 形参、" +
|
|
252
|
-
"`toolAxesReread` 事实与运维行 `mcp_tool_axes_read_as_before_upgrade`,以及对本模块的型 import
|
|
314
|
+
"`toolAxesReread` 事实与运维行 `mcp_tool_axes_read_as_before_upgrade`,以及对本模块的型 import(升级前读法的词表与两只谓词住在本模块里,随它一起删);" +
|
|
315
|
+
"`task-a2a.ts` 里 `resolveRequestA2a` / `validateRequestA2aEntries` / `normalizeA2aPeer` 的 `storedToolAxes` 形参、`toolAxesReread` 事实与运维行 `a2a_tool_axes_read_as_before_upgrade`、对本模块的型 import;" +
|
|
253
316
|
"`test/mcp-tool-axes-m19.test.ts`(窗关那一格 —— 存量坏形整条不进、warn 与通告点名 `field: toolAxes` —— 改钉到 `test/task-mcp.test.ts`);" +
|
|
254
317
|
"本文件的 `migrationWindowOpen`(若再无别的读者)",
|
|
255
318
|
afterRemoval: "没有新的臂,接住它的是今天的读法(删码零行为变化:窗关那一刻起重读臂已经不生效):升级前存下的请求体里引擎判官判不过的 `toolAxes`,续跑重放时这一条 server 整条不进," +
|
|
256
319
|
"运维 warn `mcp_injection_dropped`(`reason: malformed_entry`、`field: toolAxes`)+ 用户面通告 `mcp.injection_dropped`,兄弟照挂;停驻在这台 server 工具审批上的任务" +
|
|
257
320
|
"批准后撞 `resume.tool_unavailable`(拒绝照常可出)—— 出路是拒绝那张卡,用改好的 `toolAxes` 重新提交。会撞上它的是活过窗的升级前停驻行:发布后才升级的部署、" +
|
|
258
|
-
"调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M19 段)"
|
|
321
|
+
"调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M19 段)。A2A 一面同:存量体里 `a2aPeers[].toolAxes` 判不过的对端续跑整条不挂," +
|
|
322
|
+
"运维 warn `a2a_injection_dropped` 点名,停驻在它 skill 上的审批批准后撞 `resume.tool_unavailable` —— 出路同上:拒绝那张卡,改好声明重新提交",
|
|
259
323
|
reportKeys: [],
|
|
260
324
|
},
|
|
261
325
|
{
|
|
@@ -273,6 +337,33 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
273
337
|
"出路是把这类停驻任务决掉、删掉那个未知字段 / `__proto__` 键后重新提交。会撞上它的是活过窗的升级前停驻行:发布后才升级的部署、调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M20 段)",
|
|
274
338
|
reportKeys: [],
|
|
275
339
|
},
|
|
340
|
+
{
|
|
341
|
+
id: "M21",
|
|
342
|
+
introducedIn: "7.132.0",
|
|
343
|
+
releasedOn: "2026-10-18",
|
|
344
|
+
removableAfter: "2026-11-17",
|
|
345
|
+
removal: "删 migrateOn 的 M21 步(`repairParkedRows`:pending 候选分页扫描、门型回填、续不了的置 expired、问答卡词的整只文档读 + CAS 回写)与 `parkedRowRepairOf` / `ParkedRowRepair` / " +
|
|
346
|
+
"`M21_GATE` / `M21_PENDING_ACTION` / `PARKED_ROW_REPAIR_PAGE` / `QUESTION_CARD` / `PARKED_APPROVAL_GATE_KINDS` / `readM21Subtree` / `readM21Document` / `m21Subtree`;" +
|
|
347
|
+
"报告、日志行与 `NO_SQL_MIGRATIONS` 的 `parkedQuestionCardsWritten` / `parkedQuestionRowsLeftAsWritten` / `parkedRowGateKindsBackfilled` / `parkedRowsUnresumableExpired`;" +
|
|
348
|
+
"`test/one-time-migrations.test.ts` 的 M21 组与 `test/one-time-migrations-db-integration.test.ts` 的 M21 格",
|
|
349
|
+
afterRemoval: "① 门上没有卡词的存量问答行(跳过了带本条的版本直接升的库、把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署、local 车道 —— 那一车道本条从来够不着):没有新的臂,回到本条之前的读法 —— " +
|
|
350
|
+
"队列行不带卡词(壳显示通用审批卡),到期走 SLA 拒扫腿(模型读到「拒」),决断口仍按工具名认问答(`approve` 必须带 `answer`,否则 400);出路 = 升级前答完或拒掉;" +
|
|
351
|
+
"② `gate_kind` 为 NULL 的停驻行:既有响亮臂 —— 运维待批队列(`listPending`)不列它、计 `server.approvals.queue-row-gate-kind-missing`(决断口按门型绑定取行,本来就不认它);" +
|
|
352
|
+
"出路是运维把那一行置 expired 或删掉会话(终局收割照旧在 `terminal_at_ms` 收它)",
|
|
353
|
+
reportKeys: ["parkedQuestionCardsWritten", "parkedQuestionRowsLeftAsWritten", "parkedRowGateKindsBackfilled", "parkedRowsUnresumableExpired"],
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
id: "M22",
|
|
357
|
+
introducedIn: "7.132.0",
|
|
358
|
+
releasedOn: "2026-10-18",
|
|
359
|
+
removableAfter: "2026-11-17",
|
|
360
|
+
removal: "删 `config-lkg.ts` 的 formatVersion 4 读回迁移:`migrateV4Lkg` 与其上的窗注;`loadLkg` 里调它的那一支(`formatVersion !== 5` 那一段)与版本判据里的 `4`" +
|
|
361
|
+
"(4 回到「unknown formatVersion」拒;若 M13 / M15 仍在窗内,先删到期的那几条再删本条,版本判据收成只认 5)、`LkgReadMigration.fromFormat` 的 `4`;" +
|
|
362
|
+
"文件头注 formatVersion 5 一段里「4 的载荷 …」那两句;`test/center-artifact-v3-lane.test.ts` 的两格 4 读回(5 原样读回那一格留)",
|
|
363
|
+
afterRemoval: "既有响亮臂 `config_lkg_unusable`(`loadLkg` 对 4 答「unknown formatVersion 4」):4 读回 ⇒ 不可用,中心可达照拉(整拉,新 LKG 按 5 覆盖)、断着则 env 兜底 —— " +
|
|
364
|
+
"与回滚到 7.131.x 读 5 同一种处置",
|
|
365
|
+
reportKeys: [],
|
|
366
|
+
},
|
|
276
367
|
];
|
|
277
368
|
export function migrationWindowOpen(window, today) {
|
|
278
369
|
return window.removableAfter === null || today <= window.removableAfter;
|
|
@@ -291,29 +382,16 @@ export const NO_SQL_MIGRATIONS = {
|
|
|
291
382
|
sessionRuleColumn: "file-store",
|
|
292
383
|
sessionRuleRowsWithRetiredBuckets: [],
|
|
293
384
|
sessionRuleRowsWithOldGrammarEntries: [],
|
|
385
|
+
sessionRuleRowsWithUnreadableDeny: [],
|
|
294
386
|
retiredCardWordAskRowsRewritten: 0,
|
|
295
387
|
retiredCardWordRowsUnreadable: 0,
|
|
388
|
+
parkedQuestionCardsWritten: 0,
|
|
389
|
+
parkedQuestionRowsLeftAsWritten: 0,
|
|
390
|
+
parkedRowGateKindsBackfilled: 0,
|
|
391
|
+
parkedRowsUnresumableExpired: 0,
|
|
296
392
|
};
|
|
297
393
|
const AUDIT_TABLE = "device_audit";
|
|
298
394
|
const AUDIT_DETAIL_COLUMN = "detail";
|
|
299
|
-
const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
|
|
300
|
-
const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
|
|
301
|
-
export class RetiredSchemaPresentError extends StoreBootRefusalError {
|
|
302
|
-
found;
|
|
303
|
-
code = SCHEMA_MISMATCH_ERROR_CODE;
|
|
304
|
-
constructor(found) {
|
|
305
|
-
super(`${SCHEMA_MISMATCH_ERROR_CODE}: this database still carries schema that the one-time migrations of server 7.74.0–7.125.x retired — refusing to start. ` +
|
|
306
|
-
`Found: ${found.join("; ")}. ` +
|
|
307
|
-
`Both come from builds that predate those migrations, so this database never booted a build that still carried them — which also means the third one never ran: ` +
|
|
308
|
-
`ask-origin gates stored under the retired spelling \`unresolvable\` (in task_event.data and in checkpoint.execution_outcome) were not rewritten to \`ancestor_marked\`. ` +
|
|
309
|
-
`This build carries none of the three and will not guess. ` +
|
|
310
|
-
`Way out: start a server 7.125.x build once against this database and let its one-time migrations finish (it drops the table and the column and rewrites the spelling; ` +
|
|
311
|
-
`while the column still holds a steering instruction that can be delivered it refuses and names the rows and its own ways out), then upgrade to this build. ` +
|
|
312
|
-
`The check behind this refusal is read-only and runs before this build creates any table or rewrites anything: it changed nothing in the database.`);
|
|
313
|
-
this.found = found;
|
|
314
|
-
this.name = "RetiredSchemaPresentError";
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
395
|
function mysqlDb(conn) {
|
|
318
396
|
return {
|
|
319
397
|
async rows(sql, params) {
|
|
@@ -358,34 +436,6 @@ async function columnExists(db, dialect, table, column) {
|
|
|
358
436
|
const sql = `SELECT 1 AS present FROM information_schema.columns WHERE table_schema = ${scope} AND table_name = '${table}' AND column_name = '${column}'`;
|
|
359
437
|
return (await db.rows(sql)).length > 0;
|
|
360
438
|
}
|
|
361
|
-
async function assertRetiredSchemaAbsent(db, dialect) {
|
|
362
|
-
const found = [];
|
|
363
|
-
if (await tableExists(db, dialect, RETIRED_SNAPSHOT_TABLE))
|
|
364
|
-
found.push(`table ${RETIRED_SNAPSHOT_TABLE} (the retired whole-tree snapshot index)`);
|
|
365
|
-
if (await columnExists(db, dialect, "checkpoint", RETIRED_STEER_SEAT_COLUMN))
|
|
366
|
-
found.push(`column checkpoint.${RETIRED_STEER_SEAT_COLUMN} (the retired single-seat steering column)`);
|
|
367
|
-
if (found.length > 0)
|
|
368
|
-
throw new RetiredSchemaPresentError(found);
|
|
369
|
-
}
|
|
370
|
-
export async function assertRetiredSchemaAbsentOnPool(target) {
|
|
371
|
-
if (target.dialect === "tidb") {
|
|
372
|
-
const conn = await target.pool.getConnection();
|
|
373
|
-
try {
|
|
374
|
-
await assertRetiredSchemaAbsent(mysqlDb(conn), "tidb");
|
|
375
|
-
}
|
|
376
|
-
finally {
|
|
377
|
-
conn.release();
|
|
378
|
-
}
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
const client = await target.pool.connect();
|
|
382
|
-
try {
|
|
383
|
-
await assertRetiredSchemaAbsent(pgDb(client), "pg");
|
|
384
|
-
}
|
|
385
|
-
finally {
|
|
386
|
-
client.release();
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
439
|
export async function runSqlOneTimeMigrations(dialect, pool) {
|
|
390
440
|
return dialect === "tidb" ? withMysqlLock(pool, (db) => migrateOn(db, "tidb")) : withPgLock(pool, (db) => migrateOn(db, "pg"));
|
|
391
441
|
}
|
|
@@ -418,44 +468,33 @@ async function withPgLock(pool, run) {
|
|
|
418
468
|
}
|
|
419
469
|
}
|
|
420
470
|
async function migrateOn(db, dialect) {
|
|
421
|
-
const receipts =
|
|
471
|
+
const receipts = buildMigrationReceipts();
|
|
422
472
|
try {
|
|
423
|
-
|
|
473
|
+
await migrateStepsOn(db, dialect, receipts);
|
|
424
474
|
}
|
|
425
475
|
catch (e) {
|
|
426
|
-
throw
|
|
476
|
+
throw changedAnything(receipts) ? new OneTimeMigrationInterruptedError(e, readingsSnapshot(receipts)) : e;
|
|
427
477
|
}
|
|
478
|
+
return completedReport(receipts);
|
|
428
479
|
}
|
|
429
480
|
async function migrateStepsOn(db, dialect, receipts) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
? 0
|
|
433
|
-
: await db.exec(`UPDATE ${AUDIT_TABLE} SET ${AUDIT_DETAIL_COLUMN} = NULL ` +
|
|
434
|
-
`WHERE ${AUDIT_DETAIL_COLUMN} IS NOT NULL AND ${jsonColumnTextExpr(dialect, AUDIT_DETAIL_COLUMN)} = 'null'`);
|
|
435
|
-
const { sessionIncarnationColumn, sessionIncarnationsBackfilled } = await migrateSessionIncarnations(db, dialect);
|
|
436
|
-
const holderDeathCodeRowsBackfilled = (await db.exec(dialect === "tidb"
|
|
437
|
-
? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND error = ?"
|
|
438
|
-
: "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND error = $2", [RUN_HOLDER_DEATH_CODES.stalled, RUN_STALLED_ERROR])) +
|
|
439
|
-
(await db.exec(dialect === "tidb"
|
|
440
|
-
? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
|
|
441
|
-
: "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND (error = $2 OR error LIKE $3)", [RUN_HOLDER_DEATH_CODES.orphaned, RUN_ORPHANED_ERROR, `${RUN_ORPHANED_ERROR} (%`]));
|
|
442
|
-
const { migrated: allowRuleShadowRowsMigrated, unreadable: allowRuleShadowRowsUnreadable } = await migrateShadowedRuleKey(db, dialect);
|
|
443
|
-
const { askRows: retiredCardWordAskRowsRewritten, unreadable: retiredCardWordRowsUnreadable } = await migrateRetiredCardWords(db, dialect);
|
|
444
|
-
const { key: workflowParkIndexKey, carried: workflowParkIndexRowsCarried } = await reshapeWorkflowParkIndex(db, dialect);
|
|
445
|
-
const { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets, sessionRuleRowsWithOldGrammarEntries } = await migrateSessionRuleGrammar(db, dialect, receipts);
|
|
446
|
-
let undurableScopeRuleRows = [];
|
|
447
|
-
let undurableScopeRuleBucketsDeferred = 0;
|
|
448
|
-
let undurableScopeRuleBucketsUnreadable = [];
|
|
449
|
-
if (await tableExists(db, dialect, PERMISSION_RULE_TABLE)) {
|
|
450
|
-
({ removed: undurableScopeRuleRows, bucketsDeferred: undurableScopeRuleBucketsDeferred, bucketsUnreadable: undurableScopeRuleBucketsUnreadable } = await purgeUndurableScopeRows(db, dialect));
|
|
481
|
+
if (await tableExists(db, dialect, AUDIT_TABLE)) {
|
|
482
|
+
receipts.auditDetailJsonNullRows += await db.exec(`UPDATE ${AUDIT_TABLE} SET ${AUDIT_DETAIL_COLUMN} = NULL ` + `WHERE ${AUDIT_DETAIL_COLUMN} IS NOT NULL AND ${jsonColumnTextExpr(dialect, AUDIT_DETAIL_COLUMN)} = 'null'`);
|
|
451
483
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
484
|
+
await migrateSessionIncarnations(db, dialect, receipts);
|
|
485
|
+
receipts.holderDeathCodeRowsBackfilled += await db.exec(dialect === "tidb"
|
|
486
|
+
? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND error = ?"
|
|
487
|
+
: "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND error = $2", [RUN_HOLDER_DEATH_CODES.stalled, RUN_STALLED_ERROR]);
|
|
488
|
+
receipts.holderDeathCodeRowsBackfilled += await db.exec(dialect === "tidb"
|
|
489
|
+
? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
|
|
490
|
+
: "UPDATE task_run SET error_code = $1 WHERE status = 'failed' AND error_code IS NULL AND (error = $2 OR error LIKE $3)", [RUN_HOLDER_DEATH_CODES.orphaned, RUN_ORPHANED_ERROR, `${RUN_ORPHANED_ERROR} (%`]);
|
|
491
|
+
await migrateShadowedRuleKey(db, dialect, receipts);
|
|
492
|
+
await migrateRetiredCardWords(db, dialect, receipts);
|
|
493
|
+
await reshapeWorkflowParkIndex(db, dialect, receipts);
|
|
494
|
+
await migrateSessionRuleGrammar(db, dialect, receipts);
|
|
495
|
+
await repairParkedRows(db, dialect, receipts);
|
|
496
|
+
if (await tableExists(db, dialect, PERMISSION_RULE_TABLE))
|
|
497
|
+
await purgeUndurableScopeRows(db, dialect, receipts);
|
|
459
498
|
}
|
|
460
499
|
const SESSION_POLICY_TABLE = "session_policy";
|
|
461
500
|
const RETIRED_SESSION_RULES_COLUMN = "rules";
|
|
@@ -474,9 +513,10 @@ export function oldGrammarEntriesOf(body, rev) {
|
|
|
474
513
|
const shaped = readSessionRulesShape({ ...buckets, rev });
|
|
475
514
|
if ("fault" in shaped)
|
|
476
515
|
return undefined;
|
|
516
|
+
const toolDenyUnreadable = unreadableDenyEntries(shaped.rules).map(({ entry, reason }) => ({ entry, reason }));
|
|
517
|
+
if (toolDenyUnreadable.length > 0)
|
|
518
|
+
return { toolAllowDropped: [], toolDenyUnreadable };
|
|
477
519
|
const compiled = compileSessionRuleBucket(shaped.rules, "toolAllow");
|
|
478
|
-
if (body.grammar === undefined && compiled.inert.some((i) => i.reason === "wildcard_all"))
|
|
479
|
-
return { toolAllowDropped: [], wildcardMasked: true };
|
|
480
520
|
const earlier = new Set(compiled.inert.filter((i) => i.reason === "earlier_grammar").map((i) => i.entry));
|
|
481
521
|
const toolAllow = shaped.rules.toolAllow?.filter((e) => !earlier.has(e));
|
|
482
522
|
const toolAllowDropped = (shaped.rules.toolAllow ?? []).filter((e) => earlier.has(e));
|
|
@@ -519,8 +559,14 @@ async function judgeOldGrammarRows(db, dialect, receipts) {
|
|
|
519
559
|
: `UPDATE ${SESSION_POLICY_TABLE} SET ${column} = $1::jsonb WHERE policy_key = $2 AND ${text} = $3`;
|
|
520
560
|
await scanSessionRuleRows(db, dialect, column, [], async (row) => {
|
|
521
561
|
const read = oldGrammarEntriesOf(row.body, row.rev);
|
|
522
|
-
if (read?.
|
|
523
|
-
|
|
562
|
+
if (read?.toolDenyUnreadable !== undefined) {
|
|
563
|
+
const retiredBuckets = retiredSessionRuleBucketsOf(row.body);
|
|
564
|
+
receipts.sessionRuleRowsWithUnreadableDeny.push({
|
|
565
|
+
policyKey: row.policyKey,
|
|
566
|
+
sessionId: row.sessionId,
|
|
567
|
+
toolDenyUnreadable: read.toolDenyUnreadable.map(({ entry, reason }) => ({ entry: redactSecrets(entry), reason })),
|
|
568
|
+
...(retiredBuckets.length > 0 ? { retiredBuckets } : {}),
|
|
569
|
+
});
|
|
524
570
|
return;
|
|
525
571
|
}
|
|
526
572
|
if (read?.rewritten === undefined)
|
|
@@ -529,32 +575,34 @@ async function judgeOldGrammarRows(db, dialect, receipts) {
|
|
|
529
575
|
throw new Error(`one-time migration M17: session_policy row ${row.policyKey} changed while its rule entries were being rewritten under the in-progress column ${column} — no writer can reach the ` +
|
|
530
576
|
"table at that moment (builds up to 7.128.x lost the column, this build's replicas wait on the boot lock), so the step refuses rather than guess");
|
|
531
577
|
}
|
|
532
|
-
if (read.toolAllowDropped.length > 0)
|
|
533
|
-
receipts.
|
|
578
|
+
if (read.toolAllowDropped.length > 0) {
|
|
579
|
+
receipts.sessionRuleRowsWithOldGrammarEntries.push({ policyKey: row.policyKey, sessionId: row.sessionId, toolAllowDropped: read.toolAllowDropped.map((e) => redactSecrets(e)) });
|
|
580
|
+
}
|
|
534
581
|
});
|
|
535
582
|
}
|
|
536
583
|
export async function migrateSessionRuleGrammar(db, dialect, receipts) {
|
|
537
|
-
if (!(await tableExists(db, dialect, SESSION_POLICY_TABLE)))
|
|
538
|
-
|
|
584
|
+
if (!(await tableExists(db, dialect, SESSION_POLICY_TABLE))) {
|
|
585
|
+
receipts.sessionRuleColumn = "no-table";
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
539
588
|
const col = SESSION_RULES_COLUMN;
|
|
540
589
|
const has = (column) => columnExists(db, dialect, SESSION_POLICY_TABLE, column);
|
|
541
590
|
if (await has(RETIRED_SESSION_RULES_COLUMN)) {
|
|
542
591
|
await execUntil(db, renameSessionRulesColumnSql(dialect, RETIRED_SESSION_RULES_COLUMN, OLD_GRAMMAR_SESSION_RULES_COLUMN), async () => !(await has(RETIRED_SESSION_RULES_COLUMN)));
|
|
543
592
|
}
|
|
544
|
-
let sessionRuleColumn = "current";
|
|
545
593
|
if (await has(OLD_GRAMMAR_SESSION_RULES_COLUMN)) {
|
|
546
594
|
await judgeOldGrammarRows(db, dialect, receipts);
|
|
547
595
|
await execUntil(db, renameSessionRulesColumnSql(dialect, OLD_GRAMMAR_SESSION_RULES_COLUMN, col.name), async () => !(await has(OLD_GRAMMAR_SESSION_RULES_COLUMN)) && (await has(col.name)));
|
|
548
|
-
receipts.
|
|
549
|
-
|
|
596
|
+
receipts.sessionRuleColumn = "renamed";
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
receipts.sessionRuleColumn = "current";
|
|
550
600
|
}
|
|
551
|
-
const named = [];
|
|
552
601
|
await scanSessionRuleRows(db, dialect, col.name, RETIRED_BUCKET_PATTERNS, async (row) => {
|
|
553
602
|
const buckets = retiredSessionRuleBucketsOf(row.body);
|
|
554
603
|
if (buckets.length > 0)
|
|
555
|
-
|
|
604
|
+
receipts.sessionRuleRowsWithRetiredBuckets.push({ policyKey: row.policyKey, sessionId: row.sessionId, buckets });
|
|
556
605
|
});
|
|
557
|
-
return { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets: named, sessionRuleRowsWithOldGrammarEntries: receipts.rows };
|
|
558
606
|
}
|
|
559
607
|
function renameSessionRulesColumnSql(dialect, from, to) {
|
|
560
608
|
const definition = SESSION_RULES_COLUMN.tidb.slice(SESSION_RULES_COLUMN.name.length);
|
|
@@ -571,7 +619,7 @@ async function primaryKeyWidth(db, dialect, table) {
|
|
|
571
619
|
}
|
|
572
620
|
const WORKFLOW_PARK_INDEX_STAGING = `${WORKFLOW_PARK_INDEX_TABLE}_m9`;
|
|
573
621
|
const WORKFLOW_PARK_INDEX_RETIRED = `${WORKFLOW_PARK_INDEX_TABLE}_m9_old`;
|
|
574
|
-
async function reshapeWorkflowParkIndex(db, dialect) {
|
|
622
|
+
async function reshapeWorkflowParkIndex(db, dialect, receipts) {
|
|
575
623
|
const T = WORKFLOW_PARK_INDEX_TABLE;
|
|
576
624
|
const S = WORKFLOW_PARK_INDEX_STAGING;
|
|
577
625
|
const O = WORKFLOW_PARK_INDEX_RETIRED;
|
|
@@ -582,11 +630,10 @@ async function reshapeWorkflowParkIndex(db, dialect) {
|
|
|
582
630
|
const copyPointers = (to, from) => db.exec(dialect === "tidb"
|
|
583
631
|
? `INSERT IGNORE INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from}`
|
|
584
632
|
: `INSERT INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from} ON CONFLICT DO NOTHING`);
|
|
585
|
-
let carried = 0;
|
|
586
633
|
const resumed = await exists(S);
|
|
587
634
|
if (resumed) {
|
|
588
635
|
await db.exec(ddl);
|
|
589
|
-
|
|
636
|
+
receipts.workflowParkIndexRowsCarried += await copyPointers(T, S);
|
|
590
637
|
await execUntil(db, `DROP TABLE ${S}`, async () => !(await exists(S)));
|
|
591
638
|
}
|
|
592
639
|
if (await exists(O))
|
|
@@ -594,10 +641,11 @@ async function reshapeWorkflowParkIndex(db, dialect) {
|
|
|
594
641
|
if ((await primaryKeyWidth(db, dialect, T)) !== 1) {
|
|
595
642
|
if (await exists(T))
|
|
596
643
|
await finishParkIndexShape(db, dialect);
|
|
597
|
-
|
|
644
|
+
receipts.workflowParkIndexKey = resumed ? "reshaped" : "current";
|
|
645
|
+
return;
|
|
598
646
|
}
|
|
599
647
|
await db.exec(ddl.replace(`CREATE TABLE IF NOT EXISTS ${T} (`, `CREATE TABLE IF NOT EXISTS ${S} (`));
|
|
600
|
-
|
|
648
|
+
receipts.workflowParkIndexRowsCarried += await copyPointers(S, T);
|
|
601
649
|
const swapped = async () => !(await exists(S)) && (await exists(T)) && (await primaryKeyWidth(db, dialect, T)) !== 1;
|
|
602
650
|
if (dialect === "tidb") {
|
|
603
651
|
await execUntil(db, `RENAME TABLE ${T} TO ${O}, ${S} TO ${T}`, swapped);
|
|
@@ -607,7 +655,7 @@ async function reshapeWorkflowParkIndex(db, dialect) {
|
|
|
607
655
|
await execAtomically(db, [`ALTER TABLE ${T} RENAME TO ${O}`, `ALTER TABLE ${S} RENAME TO ${T}`, `DROP TABLE ${O}`], async () => (await swapped()) && !(await exists(O)));
|
|
608
656
|
}
|
|
609
657
|
await finishParkIndexShape(db, dialect);
|
|
610
|
-
|
|
658
|
+
receipts.workflowParkIndexKey = "reshaped";
|
|
611
659
|
}
|
|
612
660
|
async function execAtomically(db, statements, postcondition) {
|
|
613
661
|
try {
|
|
@@ -658,10 +706,8 @@ export function migrateShadowedRuleDescriptor(text) {
|
|
|
658
706
|
return { kind: "rewrite", text: JSON.stringify(next) };
|
|
659
707
|
}
|
|
660
708
|
const SHADOWED_RULE_BATCH = 500;
|
|
661
|
-
async function migrateShadowedRuleKey(db, dialect) {
|
|
709
|
+
async function migrateShadowedRuleKey(db, dialect, receipts) {
|
|
662
710
|
const like = `%"${RETIRED_SHADOWED_RULE_KEY}"%`;
|
|
663
|
-
let migrated = 0;
|
|
664
|
-
let unreadable = 0;
|
|
665
711
|
let after = "";
|
|
666
712
|
for (;;) {
|
|
667
713
|
const rows = await db.rows(dialect === "tidb"
|
|
@@ -675,17 +721,17 @@ async function migrateShadowedRuleKey(db, dialect) {
|
|
|
675
721
|
continue;
|
|
676
722
|
const next = migrateShadowedRuleDescriptor(text);
|
|
677
723
|
if (next.kind === "unreadable") {
|
|
678
|
-
|
|
724
|
+
receipts.allowRuleShadowRowsUnreadable += 1;
|
|
679
725
|
continue;
|
|
680
726
|
}
|
|
681
727
|
if (next.kind === "untouched")
|
|
682
728
|
continue;
|
|
683
|
-
|
|
729
|
+
receipts.allowRuleShadowRowsMigrated += await db.exec(dialect === "tidb"
|
|
684
730
|
? `UPDATE checkpoint SET risk_descriptor = ? WHERE token = ? AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = ?`
|
|
685
731
|
: `UPDATE checkpoint SET risk_descriptor = $1 WHERE token = $2 AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = $3`, [next.text, token, text]);
|
|
686
732
|
}
|
|
687
733
|
if (rows.length < SHADOWED_RULE_BATCH)
|
|
688
|
-
return
|
|
734
|
+
return;
|
|
689
735
|
}
|
|
690
736
|
}
|
|
691
737
|
const RETIRED_CARD_WORD_PATTERNS = ['%"shadowed"%', '%"governance_unavailable"%'];
|
|
@@ -707,11 +753,9 @@ export function migrateRetiredCardWordsInCardJson(text) {
|
|
|
707
753
|
const next = retireStoredCardWords(decoded.value);
|
|
708
754
|
return next.seats.length === 0 ? { kind: "untouched" } : { kind: "rewrite", text: JSON.stringify(next.value), value: next.value };
|
|
709
755
|
}
|
|
710
|
-
async function migrateRetiredCardWords(db, dialect) {
|
|
711
|
-
let askRows = 0;
|
|
712
|
-
let unreadable = 0;
|
|
756
|
+
async function migrateRetiredCardWords(db, dialect, receipts) {
|
|
713
757
|
if (!(await tableExists(db, dialect, "approval_ask")))
|
|
714
|
-
return
|
|
758
|
+
return;
|
|
715
759
|
const cardText = jsonColumnTextExpr(dialect, "card_json");
|
|
716
760
|
let after = "";
|
|
717
761
|
for (;;) {
|
|
@@ -726,42 +770,129 @@ async function migrateRetiredCardWords(db, dialect) {
|
|
|
726
770
|
continue;
|
|
727
771
|
const next = migrateRetiredCardWordsInCardJson(text);
|
|
728
772
|
if (next.kind === "unreadable") {
|
|
729
|
-
|
|
773
|
+
receipts.retiredCardWordRowsUnreadable += 1;
|
|
730
774
|
continue;
|
|
731
775
|
}
|
|
732
776
|
if (next.kind === "untouched")
|
|
733
777
|
continue;
|
|
734
|
-
|
|
778
|
+
receipts.retiredCardWordAskRowsRewritten += await db.exec(dialect === "tidb"
|
|
735
779
|
? `UPDATE approval_ask SET card_json = ? WHERE ask_id = ? AND CAST(${cardText} AS BINARY) = CAST(? AS BINARY)`
|
|
736
780
|
: `UPDATE approval_ask SET card_json = $1 WHERE ask_id = $2 AND ${cardText} = $3`, [next.text, askId, text]);
|
|
737
781
|
}
|
|
738
782
|
if (rows.length < RETIRED_CARD_WORD_BATCH)
|
|
739
|
-
return
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const M21_GATE = { key: "gate", as: "m21_gate" };
|
|
787
|
+
const M21_PENDING_ACTION = { key: "pendingAction", as: "m21_pending_action" };
|
|
788
|
+
const PARKED_ROW_REPAIR_PAGE = 200;
|
|
789
|
+
const QUESTION_CARD = "question";
|
|
790
|
+
const PARKED_APPROVAL_GATE_KINDS = Object.keys(PAUSE_REGISTRY).filter((k) => PAUSE_REGISTRY[k].pending === "tool_approval");
|
|
791
|
+
const readM21Subtree = createObjectColumnReader();
|
|
792
|
+
const readM21Document = createObjectColumnReader();
|
|
793
|
+
export function parkedRowRepairOf(gateKindCell, gate, pendingAction) {
|
|
794
|
+
const gateObj = gate !== null && typeof gate === "object" && !Array.isArray(gate) ? gate : undefined;
|
|
795
|
+
const columnKind = typeof gateKindCell === "string" && isGateKind(gateKindCell) ? gateKindCell : undefined;
|
|
796
|
+
const documentKind = gateObj !== undefined && isGateKind(gateObj.kind) ? gateObj.kind : undefined;
|
|
797
|
+
const kind = columnKind ?? documentKind;
|
|
798
|
+
const pa = pendingAction !== null && typeof pendingAction === "object" && !Array.isArray(pendingAction) ? pendingAction : undefined;
|
|
799
|
+
const questionCard = kind !== undefined &&
|
|
800
|
+
PARKED_APPROVAL_GATE_KINDS.includes(kind) &&
|
|
801
|
+
gateObj !== undefined &&
|
|
802
|
+
!Object.hasOwn(gateObj, "approvalCard") &&
|
|
803
|
+
pa?.kind === "tool_approval" &&
|
|
804
|
+
pa.toolName === ASK_USER_QUESTION_TOOL_NAME;
|
|
805
|
+
const columnNull = gateKindCell === null || gateKindCell === undefined;
|
|
806
|
+
return { ...(columnNull && documentKind !== undefined ? { gateKind: documentKind } : {}), questionCard, unresumable: columnNull && documentKind === undefined };
|
|
807
|
+
}
|
|
808
|
+
function m21Subtree(cell, where) {
|
|
809
|
+
try {
|
|
810
|
+
return decodeNullableJsonColumn(cell, readM21Subtree, where);
|
|
811
|
+
}
|
|
812
|
+
catch (e) {
|
|
813
|
+
if (!isJsonColumnBodyProblem(e))
|
|
814
|
+
throw e;
|
|
815
|
+
return undefined;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
export async function repairParkedRows(db, dialect, receipts) {
|
|
819
|
+
const ph = (n) => (dialect === "tidb" ? "?" : `$${n}`);
|
|
820
|
+
const kinds = PARKED_APPROVAL_GATE_KINDS;
|
|
821
|
+
const kindSlots = kinds.map((_, i) => ph(i + 1)).join(", ");
|
|
822
|
+
const scan = `SELECT token, session_id, gate_kind, ${jsonColumnText(dialect, "checkpoint", M21_GATE)}, ${jsonColumnText(dialect, "checkpoint", M21_PENDING_ACTION)} FROM checkpoint ` +
|
|
823
|
+
`WHERE status = 'pending' AND (gate_kind IS NULL OR (gate_kind IN (${kindSlots}) AND tool_name = ${ph(kinds.length + 1)})) AND token > ${ph(kinds.length + 2)} ` +
|
|
824
|
+
`ORDER BY token LIMIT ${PARKED_ROW_REPAIR_PAGE}`;
|
|
825
|
+
const docText = jsonColumnTextExpr(dialect, "checkpoint");
|
|
826
|
+
const encode = dialectProtocolJsonEncoder(dialect);
|
|
827
|
+
let after = "";
|
|
828
|
+
for (;;) {
|
|
829
|
+
const page = await db.rows(scan, [...kinds, ASK_USER_QUESTION_TOOL_NAME, after]);
|
|
830
|
+
for (const r of page) {
|
|
831
|
+
const token = String(r.token);
|
|
832
|
+
after = token;
|
|
833
|
+
const plan = parkedRowRepairOf(r.gate_kind, m21Subtree(r[M21_GATE.as], "checkpoint.checkpoint $.gate (M21 parked-row repair)"), m21Subtree(r[M21_PENDING_ACTION.as], "checkpoint.checkpoint $.pendingAction (M21 parked-row repair)"));
|
|
834
|
+
if (plan.unresumable) {
|
|
835
|
+
receipts.parkedRowsUnresumableExpired += await db.exec(`UPDATE checkpoint SET status = 'expired', decided_at_ms = ${ph(1)} WHERE token = ${ph(2)} AND status = 'pending' AND gate_kind IS NULL`, [Date.now(), token]);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
if (plan.gateKind !== undefined) {
|
|
839
|
+
receipts.parkedRowGateKindsBackfilled += await db.exec(`UPDATE checkpoint SET gate_kind = ${ph(1)} WHERE token = ${ph(2)} AND status = 'pending' AND gate_kind IS NULL`, [plan.gateKind, token]);
|
|
840
|
+
}
|
|
841
|
+
if (!plan.questionCard)
|
|
842
|
+
continue;
|
|
843
|
+
const [full] = await db.rows(`SELECT ${jsonColumnText(dialect, "checkpoint")} FROM checkpoint WHERE token = ${ph(1)} AND status = 'pending'`, [token]);
|
|
844
|
+
if (full === undefined)
|
|
845
|
+
continue;
|
|
846
|
+
let doc;
|
|
847
|
+
try {
|
|
848
|
+
doc = decodeJsonColumn(full.checkpoint, readM21Document, "checkpoint.checkpoint (M21 question-card repair)");
|
|
849
|
+
}
|
|
850
|
+
catch (e) {
|
|
851
|
+
if (!isJsonColumnBodyProblem(e))
|
|
852
|
+
throw e;
|
|
853
|
+
receipts.parkedQuestionRowsLeftAsWritten += 1;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const gate = doc.gate;
|
|
857
|
+
if (Object.hasOwn(doc, "requirementsDigest") || gate === null || typeof gate !== "object" || Array.isArray(gate)) {
|
|
858
|
+
receipts.parkedQuestionRowsLeftAsWritten += 1;
|
|
859
|
+
continue;
|
|
860
|
+
}
|
|
861
|
+
const next = encode({ ...doc, gate: { ...gate, approvalCard: QUESTION_CARD } }, "checkpoint (M21 question-card repair)");
|
|
862
|
+
receipts.parkedQuestionCardsWritten += await db.exec(dialect === "tidb"
|
|
863
|
+
? `UPDATE checkpoint SET checkpoint = ? WHERE token = ? AND status = 'pending' AND CAST(${docText} AS BINARY) = CAST(? AS BINARY)`
|
|
864
|
+
: `UPDATE checkpoint SET checkpoint = $1::jsonb WHERE token = $2 AND status = 'pending' AND ${docText} = $3`, [next, token, String(full.checkpoint)]);
|
|
865
|
+
}
|
|
866
|
+
if (page.length < PARKED_ROW_REPAIR_PAGE)
|
|
867
|
+
return;
|
|
740
868
|
}
|
|
741
869
|
}
|
|
742
870
|
const INCARNATION_BACKFILL_BATCH = 500;
|
|
743
|
-
async function migrateSessionIncarnations(db, dialect) {
|
|
744
|
-
if (!(await tableExists(db, dialect, "session_meta")))
|
|
745
|
-
|
|
871
|
+
async function migrateSessionIncarnations(db, dialect, receipts) {
|
|
872
|
+
if (!(await tableExists(db, dialect, "session_meta"))) {
|
|
873
|
+
receipts.sessionIncarnationColumn = "no-table";
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
746
876
|
const col = SESSION_META_INCARNATION_COLUMN;
|
|
747
|
-
let
|
|
877
|
+
let column = "present";
|
|
748
878
|
if (!(await columnExists(db, dialect, "session_meta", col.name))) {
|
|
749
879
|
try {
|
|
750
880
|
await db.exec(`ALTER TABLE session_meta ADD COLUMN ${dialect === "tidb" ? col.tidb : col.pg}`);
|
|
751
|
-
|
|
881
|
+
column = "added";
|
|
752
882
|
}
|
|
753
883
|
catch (e) {
|
|
754
884
|
if (!(await columnExists(db, dialect, "session_meta", col.name)))
|
|
755
885
|
throw e;
|
|
756
886
|
}
|
|
757
887
|
}
|
|
758
|
-
|
|
888
|
+
receipts.sessionIncarnationColumn = column;
|
|
889
|
+
await backfillSessionIncarnations(db, dialect, receipts);
|
|
759
890
|
}
|
|
760
|
-
async function backfillSessionIncarnations(db, dialect) {
|
|
891
|
+
async function backfillSessionIncarnations(db, dialect, receipts) {
|
|
761
892
|
const remaining = async () => Number((await db.rows("SELECT COUNT(*) AS n FROM session_meta WHERE incarnation = ''"))[0]?.n ?? 0);
|
|
762
893
|
const initial = await remaining();
|
|
763
894
|
if (initial === 0)
|
|
764
|
-
return
|
|
895
|
+
return;
|
|
765
896
|
const ceiling = Math.ceil(initial / INCARNATION_BACKFILL_BATCH) * 2 + 2;
|
|
766
897
|
let written = 0;
|
|
767
898
|
let after;
|
|
@@ -771,7 +902,7 @@ async function backfillSessionIncarnations(db, dialect) {
|
|
|
771
902
|
: await db.rows(`SELECT session_id FROM session_meta WHERE incarnation = '' AND session_id > ${dialect === "pg" ? "$1" : "?"} ORDER BY session_id LIMIT ${INCARNATION_BACKFILL_BATCH}`, [after])).map((r) => String(r.session_id));
|
|
772
903
|
if (ids.length === 0) {
|
|
773
904
|
if (after === undefined)
|
|
774
|
-
return
|
|
905
|
+
return;
|
|
775
906
|
after = undefined;
|
|
776
907
|
continue;
|
|
777
908
|
}
|
|
@@ -782,7 +913,9 @@ async function backfillSessionIncarnations(db, dialect) {
|
|
|
782
913
|
};
|
|
783
914
|
const cases = ids.map((id) => `WHEN ${bind(id)} THEN ${bind(mintSessionIncarnation())}`).join(" ");
|
|
784
915
|
const inList = ids.map((id) => bind(id)).join(", ");
|
|
785
|
-
|
|
916
|
+
const landed = await db.exec(`UPDATE session_meta SET incarnation = CASE session_id ${cases} END WHERE incarnation = '' AND session_id IN (${inList})`, params);
|
|
917
|
+
written += landed;
|
|
918
|
+
receipts.sessionIncarnationsBackfilled += landed;
|
|
786
919
|
after = ids.length < INCARNATION_BACKFILL_BATCH ? undefined : ids[ids.length - 1];
|
|
787
920
|
}
|
|
788
921
|
const left = await remaining();
|
|
@@ -791,33 +924,36 @@ async function backfillSessionIncarnations(db, dialect) {
|
|
|
791
924
|
`(${written} written) — refusing to start rather than serve sessions whose occupant cannot be told apart. Retry the boot; ` +
|
|
792
925
|
`if it repeats, something keeps writing rows without an incarnation (an older replica still inserting?).`);
|
|
793
926
|
}
|
|
794
|
-
|
|
927
|
+
}
|
|
928
|
+
function readingFields(r) {
|
|
929
|
+
return {
|
|
930
|
+
auditDetailJsonNullRows: r.auditDetailJsonNullRows,
|
|
931
|
+
...(r.sessionIncarnationColumn !== undefined ? { sessionIncarnationColumn: r.sessionIncarnationColumn } : {}),
|
|
932
|
+
sessionIncarnationsBackfilled: r.sessionIncarnationsBackfilled,
|
|
933
|
+
holderDeathCodeRowsBackfilled: r.holderDeathCodeRowsBackfilled,
|
|
934
|
+
allowRuleShadowRowsMigrated: r.allowRuleShadowRowsMigrated,
|
|
935
|
+
allowRuleShadowRowsUnreadable: r.allowRuleShadowRowsUnreadable,
|
|
936
|
+
...(r.workflowParkIndexKey !== undefined ? { workflowParkIndexKey: r.workflowParkIndexKey } : {}),
|
|
937
|
+
workflowParkIndexRowsCarried: r.workflowParkIndexRowsCarried,
|
|
938
|
+
undurableScopeRuleRowsRemoved: r.undurableScopeRuleRows.length,
|
|
939
|
+
undurableScopeRuleBucketsDeferred: r.undurableScopeRuleBucketsDeferred,
|
|
940
|
+
undurableScopeRuleBucketsUnreadable: r.undurableScopeRuleBucketsUnreadable.length,
|
|
941
|
+
...(r.sessionRuleColumn !== undefined ? { sessionRuleColumn: r.sessionRuleColumn } : {}),
|
|
942
|
+
sessionRuleRowsWithRetiredBuckets: r.sessionRuleRowsWithRetiredBuckets.length,
|
|
943
|
+
sessionRuleRowsWithOldGrammarEntries: r.sessionRuleRowsWithOldGrammarEntries.length,
|
|
944
|
+
sessionRuleRowsWithUnreadableDeny: r.sessionRuleRowsWithUnreadableDeny.length,
|
|
945
|
+
retiredCardWordAskRowsRewritten: r.retiredCardWordAskRowsRewritten,
|
|
946
|
+
retiredCardWordRowsUnreadable: r.retiredCardWordRowsUnreadable,
|
|
947
|
+
parkedQuestionCardsWritten: r.parkedQuestionCardsWritten,
|
|
948
|
+
parkedQuestionRowsLeftAsWritten: r.parkedQuestionRowsLeftAsWritten,
|
|
949
|
+
parkedRowGateKindsBackfilled: r.parkedRowGateKindsBackfilled,
|
|
950
|
+
parkedRowsUnresumableExpired: r.parkedRowsUnresumableExpired,
|
|
951
|
+
};
|
|
795
952
|
}
|
|
796
953
|
export function logOneTimeMigrations(log, backend, report) {
|
|
797
|
-
|
|
798
|
-
report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0 || report.allowRuleShadowRowsMigrated > 0 || report.allowRuleShadowRowsUnreadable > 0 ||
|
|
799
|
-
report.workflowParkIndexKey === "reshaped" || report.undurableScopeRuleRows.length > 0 || report.undurableScopeRuleBucketsDeferred > 0 ||
|
|
800
|
-
report.undurableScopeRuleBucketsUnreadable.length > 0 ||
|
|
801
|
-
report.sessionRuleColumn === "renamed" || report.sessionRuleRowsWithRetiredBuckets.length > 0 || report.sessionRuleRowsWithOldGrammarEntries.length > 0 ||
|
|
802
|
-
report.retiredCardWordAskRowsRewritten > 0 || report.retiredCardWordRowsUnreadable > 0;
|
|
803
|
-
(changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
|
|
954
|
+
(changedAnything(report) ? log.warn : log.info).call(log, "one_time_migrations_applied", {
|
|
804
955
|
backend,
|
|
805
|
-
|
|
806
|
-
sessionIncarnationColumn: report.sessionIncarnationColumn,
|
|
807
|
-
sessionIncarnationsBackfilled: report.sessionIncarnationsBackfilled,
|
|
808
|
-
holderDeathCodeRowsBackfilled: report.holderDeathCodeRowsBackfilled,
|
|
809
|
-
allowRuleShadowRowsMigrated: report.allowRuleShadowRowsMigrated,
|
|
810
|
-
allowRuleShadowRowsUnreadable: report.allowRuleShadowRowsUnreadable,
|
|
811
|
-
workflowParkIndexKey: report.workflowParkIndexKey,
|
|
812
|
-
workflowParkIndexRowsCarried: report.workflowParkIndexRowsCarried,
|
|
813
|
-
undurableScopeRuleRowsRemoved: report.undurableScopeRuleRows.length,
|
|
814
|
-
undurableScopeRuleBucketsDeferred: report.undurableScopeRuleBucketsDeferred,
|
|
815
|
-
undurableScopeRuleBucketsUnreadable: report.undurableScopeRuleBucketsUnreadable.length,
|
|
816
|
-
sessionRuleColumn: report.sessionRuleColumn,
|
|
817
|
-
sessionRuleRowsWithRetiredBuckets: report.sessionRuleRowsWithRetiredBuckets.length,
|
|
818
|
-
sessionRuleRowsWithOldGrammarEntries: report.sessionRuleRowsWithOldGrammarEntries.length,
|
|
819
|
-
retiredCardWordAskRowsRewritten: report.retiredCardWordAskRowsRewritten,
|
|
820
|
-
retiredCardWordRowsUnreadable: report.retiredCardWordRowsUnreadable,
|
|
956
|
+
...readingFields(report),
|
|
821
957
|
removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
|
|
822
958
|
note: "one-time persisted-data migrations (M4: audit details written as the JSON " +
|
|
823
959
|
"text `null` by an earlier version folded to a SQL NULL, so \"no detail\" has exactly one on-disk spelling; M5: the " +
|
|
@@ -835,16 +971,23 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
835
971
|
"are read by the engine in the grammar the row carries (none = the grammar before the stamp) and rewritten as the same statement in the current grammar " +
|
|
836
972
|
"(the column carries the in-progress name rule_entries_old_grammar until every row is rewritten, so an interrupted boot resumes there) — toolAllow entries " +
|
|
837
973
|
"that the earlier grammar read as naming no tool are removed from the row, every row is stamped with the current grammar, one session_rule_row_old_grammar_entries " +
|
|
838
|
-
"line per row that lost entries
|
|
974
|
+
"line per row that lost entries — except a row whose toolDeny holds an entry the engine reads as no rule: that row is left whole and unstamped (the engine refuses every " +
|
|
975
|
+
"call of such a session until an operator rewrites its rules), one session_rule_row_unreadable_deny line per row; " +
|
|
839
976
|
"rows still holding a retired command bucket keep that bucket and are named, one session_rule_row_retired_bucket line per row on every boot; " +
|
|
840
977
|
"M18: stored approval cards written by an earlier engine with two words the current engine retired rewritten — ruleOffersAbsence shadowed removed, ask class " +
|
|
841
978
|
"governance_unavailable read as rule_unreadable; the engine's own parked-row documents are never rewritten (their row digest covers the ask class), they and " +
|
|
842
|
-
"the cards this step cannot reach are read the same way, counted under server.approval-card.retired-card-word-read
|
|
979
|
+
"the cards this step cannot reach are read the same way, counted under server.approval-card.retired-card-word-read; M21: suspended rows parked by an engine " +
|
|
980
|
+
"before the question card existed and still pending — a question approval whose gate carries no card — get the card word question (rows the engine sealed " +
|
|
981
|
+
"with its row digest are left as written and counted), and suspended rows with no gate kind on the row get it from the gate (a row whose gate names no " +
|
|
982
|
+
"registered kind cannot be resumed and is expired)). Each step " +
|
|
843
983
|
"runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
|
|
844
984
|
"removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
|
|
845
985
|
"version that still carries a step at least once before upgrading past its deletion.",
|
|
846
986
|
});
|
|
847
|
-
|
|
987
|
+
logReceiptRows(log, backend, report);
|
|
988
|
+
}
|
|
989
|
+
function logReceiptRows(log, backend, r) {
|
|
990
|
+
for (const row of r.undurableScopeRuleRows) {
|
|
848
991
|
log.warn("permission_rule_row_removed", {
|
|
849
992
|
backend,
|
|
850
993
|
ownerKey: row.ownerKey,
|
|
@@ -855,7 +998,7 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
855
998
|
note: removedRuleRowNote(row.kind),
|
|
856
999
|
});
|
|
857
1000
|
}
|
|
858
|
-
for (const bucket of
|
|
1001
|
+
for (const bucket of r.undurableScopeRuleBucketsUnreadable) {
|
|
859
1002
|
log.warn("permission_rule_bucket_unreadable", {
|
|
860
1003
|
backend,
|
|
861
1004
|
ownerKey: bucket.ownerKey,
|
|
@@ -865,8 +1008,37 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
865
1008
|
"keeps being refused, as before this release, until someone repairs or deletes that row by hand",
|
|
866
1009
|
});
|
|
867
1010
|
}
|
|
868
|
-
|
|
869
|
-
|
|
1011
|
+
for (const row of r.sessionRuleRowsWithOldGrammarEntries) {
|
|
1012
|
+
log.warn("session_rule_row_old_grammar_entries", {
|
|
1013
|
+
backend,
|
|
1014
|
+
policyKey: row.policyKey,
|
|
1015
|
+
sessionId: row.sessionId,
|
|
1016
|
+
toolAllowDropped: row.toolAllowDropped,
|
|
1017
|
+
note: "a session's stored rules were written before the rule-grammar stamp, when an entry was compared as a tool name as written; the engine reads such a row " +
|
|
1018
|
+
"in that earlier grammar, where these toolAllow entries named no tool and admitted nothing, while in the current grammar they would be allow rules. " +
|
|
1019
|
+
"The row was rewritten as the same statement in the current grammar: these entries were removed and the row stamped (an emptied list stays in place and allows " +
|
|
1020
|
+
"nothing); toolDeny entries were left as written (a row whose toolDeny holds an entry the engine reads as no rule is not rewritten at all — it is named by " +
|
|
1021
|
+
"session_rule_row_unreadable_deny instead). Every entry named on this line is named exactly once, by the boot that " +
|
|
1022
|
+
"removed it — no later boot lists it again — so keep the line. Write the session's rules afresh through the session policy endpoint if an entry was meant as an allow rule",
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
for (const row of r.sessionRuleRowsWithUnreadableDeny) {
|
|
1026
|
+
log.warn("session_rule_row_unreadable_deny", {
|
|
1027
|
+
backend,
|
|
1028
|
+
policyKey: row.policyKey,
|
|
1029
|
+
sessionId: row.sessionId,
|
|
1030
|
+
toolDenyUnreadable: row.toolDenyUnreadable,
|
|
1031
|
+
...(row.retiredBuckets !== undefined ? { retiredBuckets: row.retiredBuckets } : {}),
|
|
1032
|
+
note: "a session's stored rules hold toolDeny entries the engine's rule reader reads as no rule at all (the reason on each entry is the reader's own word). The engine never " +
|
|
1033
|
+
"reads rules it cannot read as fewer rules, so every tool call in this session is refused until the rules are rewritten. The row was not rewritten and not stamped " +
|
|
1034
|
+
"with the current rule grammar: the stamp would only silence the engine's notice about the earlier grammar (config.session_rules_earlier_grammar) and lift nothing. " +
|
|
1035
|
+
"Nothing was removed for you — a deny nobody can read is the operator's to rewrite. Write the session's rules afresh as an operator through the session policy " +
|
|
1036
|
+
"endpoint (PUT /v1/sessions/:id/policy, a new value without the stored row's rev, leaving these entries out or spelling each as a rule), or delete the session " +
|
|
1037
|
+
"(deleting the session deletes its rules). A line carrying retiredBuckets names a row that also holds a retired command bucket: the policy endpoint cannot read such a row " +
|
|
1038
|
+
"(not even for an operator), so rewrite the row in the database in the current grammar (keep rev and gen) or delete the session. Each row is named exactly once, by the boot that ran the judgement — no later boot lists it again — so keep the line",
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
for (const row of r.sessionRuleRowsWithRetiredBuckets) {
|
|
870
1042
|
log.warn("session_rule_row_retired_bucket", {
|
|
871
1043
|
backend,
|
|
872
1044
|
policyKey: row.policyKey,
|
|
@@ -880,40 +1052,17 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
880
1052
|
});
|
|
881
1053
|
}
|
|
882
1054
|
}
|
|
883
|
-
function logSessionRuleOldGrammarRows(log, backend, rows) {
|
|
884
|
-
for (const row of rows) {
|
|
885
|
-
log.warn("session_rule_row_old_grammar_entries", {
|
|
886
|
-
backend,
|
|
887
|
-
policyKey: row.policyKey,
|
|
888
|
-
sessionId: row.sessionId,
|
|
889
|
-
toolAllowDropped: row.toolAllowDropped,
|
|
890
|
-
...(row.wildcardMasked ? { wildcardMasked: true } : {}),
|
|
891
|
-
note: row.wildcardMasked
|
|
892
|
-
? "a session's stored rules were written before the rule-grammar stamp and its toolAllow list holds `*`: the engine reads that list as void in every grammar (every call " +
|
|
893
|
-
"refused), which hides what the other entries meant in the earlier grammar, so this row was NOT rewritten or stamped — it stays in its earlier grammar. To restore the other " +
|
|
894
|
-
"entries, an operator rewrites the row without `*` (entries spelled as rules, `Bash(rm:*)`, then become allow rules — review them first)."
|
|
895
|
-
:
|
|
896
|
-
"a session's stored rules were written before the rule-grammar stamp, when an entry was compared as a tool name as written; the engine reads such a row " +
|
|
897
|
-
"in that earlier grammar, where these toolAllow entries named no tool and admitted nothing, while in the current grammar they would be allow rules. " +
|
|
898
|
-
"The row was rewritten as the same statement in the current grammar: these entries were removed and the row stamped (an emptied list stays in place and allows " +
|
|
899
|
-
"nothing); toolDeny entries read the same in every grammar and were left as written. Every entry named on this line is named exactly once, by the boot that " +
|
|
900
|
-
"removed it — no later boot lists it again — so keep the line. Write the session's rules afresh through the session policy endpoint if an entry was meant as an allow rule",
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
1055
|
function logOneTimeMigrationsInterrupted(log, backend, error) {
|
|
905
|
-
const { receipts } = error;
|
|
906
1056
|
log.warn("one_time_migrations_interrupted", {
|
|
907
1057
|
backend,
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
"
|
|
912
|
-
"
|
|
913
|
-
"
|
|
914
|
-
"rewritten are left alone) and publishes the final name",
|
|
1058
|
+
...readingFields(error.receipts),
|
|
1059
|
+
note: "the one-time migrations failed after committing changes: the readings on this line and the lines that follow are what this boot already committed — a retried " +
|
|
1060
|
+
"boot finds that work done and does not report it again, so these lines are the only record of them, written once by this boot — save them. The failure itself follows " +
|
|
1061
|
+
"as this boot's refusal. A shape reading absent from this line (sessionIncarnationColumn / workflowParkIndexKey / sessionRuleColumn) belongs to a step that did not " +
|
|
1062
|
+
"finish; sessionRuleColumn absent after session_rule_row_old_grammar_entries lines means the column still carries its in-progress name — the next boot rewrites the " +
|
|
1063
|
+
"remaining rows (rows already rewritten are left alone) and publishes the final name",
|
|
915
1064
|
});
|
|
916
|
-
|
|
1065
|
+
logReceiptRows(log, backend, error.receipts);
|
|
917
1066
|
}
|
|
918
1067
|
export async function runAndLogOneTimeMigrations(log, backend) {
|
|
919
1068
|
let report;
|