@sema-agent/server 7.130.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 +396 -0
- package/README.md +13 -6
- package/README.zh-CN.md +9 -5
- package/USAGE.md +187 -50
- package/dist/approval-card.d.ts +3 -1
- package/dist/approval-card.js +2 -3
- package/dist/audit.d.ts +7 -10
- package/dist/audit.js +4 -1
- package/dist/auth-bridge.d.ts +4 -0
- package/dist/auth-bridge.js +10 -11
- package/dist/bench/s1/live-deps.js +2 -0
- 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 +56 -61
- 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-02-web-admin-pull.js +9 -1
- 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 +12 -10
- 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/builtin-tools.js +2 -1
- 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/composite-members.d.ts +1 -0
- package/dist/capabilities/composite-members.js +1 -0
- package/dist/capabilities/engine-capabilities.js +2 -1
- package/dist/capabilities/hands-lane.d.ts +9 -6
- package/dist/capabilities/hands-lane.js +3 -3
- package/dist/capabilities/host-tool-names.d.ts +17 -0
- package/dist/capabilities/host-tool-names.js +15 -0
- package/dist/capabilities/mcp-probe.d.ts +14 -1
- package/dist/capabilities/mcp-probe.js +22 -133
- package/dist/capabilities/repo-tools.js +4 -3
- package/dist/capabilities/scenarios.d.ts +42 -47
- package/dist/capabilities/scenarios.js +19 -35
- package/dist/capabilities/select-environment-tool.js +2 -1
- package/dist/capabilities/send-user-file-tool.d.ts +0 -1
- package/dist/capabilities/send-user-file-tool.js +2 -2
- package/dist/capabilities/skills.d.ts +19 -4
- package/dist/capabilities/skills.js +30 -10
- package/dist/capabilities/tool-defer.js +2 -1
- package/dist/config-catalog.js +22 -11
- package/dist/config-invariants.js +2 -2
- 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 +14 -6
- package/dist/config-types.d.ts +13 -9
- package/dist/config.d.ts +2 -2
- package/dist/config.js +34 -20
- package/dist/device-ws-protocol.d.ts +1 -1
- package/dist/device-ws-protocol.js +1 -0
- package/dist/elicitation.d.ts +7 -0
- package/dist/elicitation.js +20 -5
- 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 +80 -37
- package/dist/hooks/hook-runner.d.ts +273 -75
- package/dist/hooks/hook-runner.js +293 -317
- package/dist/http/admission.d.ts +8 -2
- package/dist/http/admission.js +103 -59
- package/dist/http/approval-sweeps.js +59 -18
- package/dist/http/checkpoint-ctx.d.ts +36 -18
- package/dist/http/checkpoint-ctx.js +14 -10
- 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 +6 -10
- 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 +48 -26
- package/dist/http/routes/approvals-assistant.js +1 -1
- package/dist/http/routes/capabilities.js +7 -17
- package/dist/http/routes/diagnostics.js +1 -1
- package/dist/http/routes/rules.js +2 -2
- package/dist/http/routes/runs.js +13 -17
- package/dist/http/routes/session-sync.js +4 -2
- package/dist/http/routes/sessions.js +23 -11
- package/dist/http/routes/shared-memory.js +2 -13
- package/dist/http/routes/side-query.js +2 -3
- package/dist/http/routes/tasks.js +34 -19
- package/dist/http/scenario-read-face.d.ts +14 -0
- package/dist/http/scenario-read-face.js +5 -0
- package/dist/http/send.d.ts +13 -1
- package/dist/http/send.js +12 -1
- package/dist/http/server.d.ts +37 -23
- package/dist/http/server.js +30 -4
- package/dist/http/stored-body-replay.d.ts +10 -3
- package/dist/http/stored-body-replay.js +27 -6
- package/dist/http/submit-ctx.d.ts +25 -0
- package/dist/http/submit-ctx.js +39 -0
- package/dist/http/wire-types.d.ts +18 -5
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- 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 +3 -11
- package/dist/mcp-assertion-header.js +3 -9
- 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-entry-refusal.d.ts +1 -7
- package/dist/model-provider.d.ts +39 -12
- package/dist/model-provider.js +19 -2
- package/dist/model-route-endpoint.d.ts +52 -12
- package/dist/model-route-endpoint.js +48 -26
- package/dist/model-route-gap.d.ts +5 -4
- package/dist/model-route-gap.js +1 -1
- package/dist/model-select.d.ts +58 -0
- package/dist/model-select.js +31 -0
- package/dist/namespaced-rule-name.d.ts +7 -4
- package/dist/namespaced-rule-name.js +4 -3
- package/dist/observability/cost-taxonomy.d.ts +7 -7
- package/dist/observability/fail-open.d.ts +53 -12
- package/dist/observability/fail-open.js +47 -7
- 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 +3 -3
- package/dist/plugins/one-time-migrations.d.ts +147 -91
- package/dist/plugins/one-time-migrations.js +372 -208
- 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/question.d.ts +18 -9
- package/dist/question.js +21 -13
- package/dist/request-key-closure.d.ts +4 -4
- package/dist/request-key-closure.js +18 -13
- package/dist/rule-entry-list.d.ts +134 -24
- package/dist/rule-entry-list.js +162 -30
- package/dist/run-local.d.ts +3 -3
- package/dist/run-local.js +26 -25
- package/dist/runs.d.ts +48 -2
- package/dist/runs.js +61 -2
- 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 +34 -18
- 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-hooks-m20.d.ts +23 -0
- package/dist/stored-request-hooks-m20.js +124 -0
- package/dist/stored-request-tool-axes-m19.d.ts +20 -5
- 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 +54 -102
- package/dist/task-mcp.js +194 -92
- package/dist/task-settings.d.ts +47 -25
- package/dist/task-settings.js +46 -61
- 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.d.ts +29 -0
- package/dist/trace/ledger-sink.js +15 -1
- package/dist/trace/model-segment.js +1 -1
- package/dist/trace/project.d.ts +25 -6
- package/dist/trace/project.js +68 -22
- package/dist/trace/wire-projection-faces.d.ts +80 -2
- package/dist/trace/wire-projection-faces.js +23 -1
- package/dist/user-authored-text.d.ts +7 -0
- package/dist/user-authored-text.js +3 -0
- package/dist/web-admin-client/apply-effective.js +10 -5
- 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 +3 -4
- 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 +37 -3
- 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`",
|
|
@@ -149,7 +212,7 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
149
212
|
removal: "删模式词读法记号的判据与拒臂:`http/stored-body-replay.ts` 的 `permissionModeReadingUnstated`;`http/checkpoint-ctx.ts` 的 `permissionModeReadingStated` / `permissionModeReadingRefusal` / `PERMISSION_MODE_READINGS`、" +
|
|
150
213
|
"`StoredBodyReplayRefusal` 的 `permission_mode_reading` 臂及其日志字段与答复臂、`buildCheckpointCtx` 与 `withPermissionModeRewritten` 两处记号写点" +
|
|
151
214
|
"(后者删后 `resume-legs.ts` `drivePlanReview` 里对它的那一处调用回到只换体的 `{ ...ctx, body }`)、`CheckpointCtx.permissionModeReading`、" +
|
|
152
|
-
"`http/stored-body-replay.ts` `
|
|
215
|
+
"`http/stored-body-replay.ts` 里 `StoredReplayDeployment` 的 `autonomy` 位与 `storedBodyReplayRefusal` 读 `deployment.autonomy` 的那一句(这一位只服务这一问;四个问点传 `deps.config` 整份,home 位还服务 `permissions` 一问,不动)",
|
|
153
216
|
afterRemoval: "没有响亮臂:删码之后仍停驻、在 7.121.x 及以前写下的行(把 `APPROVAL_TERMINAL_BACKSTOP_MS` 调长的部署,或跳过了带本条的版本直接升的库)在设了 " +
|
|
154
217
|
"`AUTONOMY=plan` 的部署上按体里的原词重放(运维的 plan 不再被按段重钳)—— 升级前把这些停驻的 run 决掉 / 取消,或升级窗内设 `AUTONOMY=read-only`",
|
|
155
218
|
reportKeys: [],
|
|
@@ -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,60 @@ 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` —— 出路同上:拒绝那张卡,改好声明重新提交",
|
|
323
|
+
reportKeys: [],
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: "M20",
|
|
327
|
+
introducedIn: "7.131.0",
|
|
328
|
+
releasedOn: "2026-10-16",
|
|
329
|
+
removableAfter: "2026-11-15",
|
|
330
|
+
removal: "删 `src/stored-request-hooks-m20.ts` 整只文件(`storedHooksRereadM20` / `rereadStoredHooksM20` / `StoredHooksRereadM20` / `ENTRY_FIELD_NAMES` / `MATCHER_FIELD_NAMES` / `nodeAt` / `cloneDoc` / `removeKeyAt`);" +
|
|
331
|
+
"`boot/resolve-spec.ts` 里递给 `parseTaskSettings` 的重读座实参(`opts?.leg === \"fresh\" || !taskHooksHonored(config) ? {} : { storedHooksReread: storedHooksRereadM20(…) }`)与它的 import、" +
|
|
332
|
+
"`hooks_read_as_before_upgrade` 那一段 warn;`task-settings.ts` 的 `ParseTaskSettingsSeats` / `ParseTaskSettingsResult.hooksReread` / `parseTaskSettings` 第二形参与 hooks 臂里的重读那一支;" +
|
|
333
|
+
"`hooks/hook-runner.ts` 的 `ParseHooksConfigResult.pieces` / `HooksRefusalPiece`(若再无别的读者)与 `hooksCoordinate` 的 export;" +
|
|
334
|
+
"`test/hooks-stored-reread-m20.test.ts`(窗关那一格 —— 整份不采纳 + `server.hooks.resume-reader-refused` 计一笔 —— 改钉到 `test/hooks-entry-admission.test.ts` 的续跑腿组)",
|
|
335
|
+
afterRemoval: "没有新的臂,接住它的是今天的读法(删码零行为变化:窗关那一刻起重读臂已经不生效):升级前存下的请求体里带这两形的 `settings.hooks`,续跑重放时整份不采纳(其中真在拦的 `PreToolUse` 钩子也不再拦)," +
|
|
336
|
+
"运维 warn `task_settings_deferred`(字段 `hooks`)+ fail-open 登记 `server.hooks.resume-reader-refused`(计数 + 探针 + 一次性 warn,census 第 140 行),任务照常续跑 —— " +
|
|
337
|
+
"出路是把这类停驻任务决掉、删掉那个未知字段 / `__proto__` 键后重新提交。会撞上它的是活过窗的升级前停驻行:发布后才升级的部署、调长了 `APPROVAL_TERMINAL_BACKSTOP_MS` 的部署、滚动升级里旧副本在窗内新写的行(头注 M20 段)",
|
|
338
|
+
reportKeys: [],
|
|
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 同一种处置",
|
|
259
365
|
reportKeys: [],
|
|
260
366
|
},
|
|
261
367
|
];
|
|
@@ -276,29 +382,16 @@ export const NO_SQL_MIGRATIONS = {
|
|
|
276
382
|
sessionRuleColumn: "file-store",
|
|
277
383
|
sessionRuleRowsWithRetiredBuckets: [],
|
|
278
384
|
sessionRuleRowsWithOldGrammarEntries: [],
|
|
385
|
+
sessionRuleRowsWithUnreadableDeny: [],
|
|
279
386
|
retiredCardWordAskRowsRewritten: 0,
|
|
280
387
|
retiredCardWordRowsUnreadable: 0,
|
|
388
|
+
parkedQuestionCardsWritten: 0,
|
|
389
|
+
parkedQuestionRowsLeftAsWritten: 0,
|
|
390
|
+
parkedRowGateKindsBackfilled: 0,
|
|
391
|
+
parkedRowsUnresumableExpired: 0,
|
|
281
392
|
};
|
|
282
393
|
const AUDIT_TABLE = "device_audit";
|
|
283
394
|
const AUDIT_DETAIL_COLUMN = "detail";
|
|
284
|
-
const RETIRED_SNAPSHOT_TABLE = "snapshot_manifest";
|
|
285
|
-
const RETIRED_STEER_SEAT_COLUMN = "pending_steer";
|
|
286
|
-
export class RetiredSchemaPresentError extends StoreBootRefusalError {
|
|
287
|
-
found;
|
|
288
|
-
code = SCHEMA_MISMATCH_ERROR_CODE;
|
|
289
|
-
constructor(found) {
|
|
290
|
-
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. ` +
|
|
291
|
-
`Found: ${found.join("; ")}. ` +
|
|
292
|
-
`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: ` +
|
|
293
|
-
`ask-origin gates stored under the retired spelling \`unresolvable\` (in task_event.data and in checkpoint.execution_outcome) were not rewritten to \`ancestor_marked\`. ` +
|
|
294
|
-
`This build carries none of the three and will not guess. ` +
|
|
295
|
-
`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; ` +
|
|
296
|
-
`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. ` +
|
|
297
|
-
`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.`);
|
|
298
|
-
this.found = found;
|
|
299
|
-
this.name = "RetiredSchemaPresentError";
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
395
|
function mysqlDb(conn) {
|
|
303
396
|
return {
|
|
304
397
|
async rows(sql, params) {
|
|
@@ -343,34 +436,6 @@ async function columnExists(db, dialect, table, column) {
|
|
|
343
436
|
const sql = `SELECT 1 AS present FROM information_schema.columns WHERE table_schema = ${scope} AND table_name = '${table}' AND column_name = '${column}'`;
|
|
344
437
|
return (await db.rows(sql)).length > 0;
|
|
345
438
|
}
|
|
346
|
-
async function assertRetiredSchemaAbsent(db, dialect) {
|
|
347
|
-
const found = [];
|
|
348
|
-
if (await tableExists(db, dialect, RETIRED_SNAPSHOT_TABLE))
|
|
349
|
-
found.push(`table ${RETIRED_SNAPSHOT_TABLE} (the retired whole-tree snapshot index)`);
|
|
350
|
-
if (await columnExists(db, dialect, "checkpoint", RETIRED_STEER_SEAT_COLUMN))
|
|
351
|
-
found.push(`column checkpoint.${RETIRED_STEER_SEAT_COLUMN} (the retired single-seat steering column)`);
|
|
352
|
-
if (found.length > 0)
|
|
353
|
-
throw new RetiredSchemaPresentError(found);
|
|
354
|
-
}
|
|
355
|
-
export async function assertRetiredSchemaAbsentOnPool(target) {
|
|
356
|
-
if (target.dialect === "tidb") {
|
|
357
|
-
const conn = await target.pool.getConnection();
|
|
358
|
-
try {
|
|
359
|
-
await assertRetiredSchemaAbsent(mysqlDb(conn), "tidb");
|
|
360
|
-
}
|
|
361
|
-
finally {
|
|
362
|
-
conn.release();
|
|
363
|
-
}
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
const client = await target.pool.connect();
|
|
367
|
-
try {
|
|
368
|
-
await assertRetiredSchemaAbsent(pgDb(client), "pg");
|
|
369
|
-
}
|
|
370
|
-
finally {
|
|
371
|
-
client.release();
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
439
|
export async function runSqlOneTimeMigrations(dialect, pool) {
|
|
375
440
|
return dialect === "tidb" ? withMysqlLock(pool, (db) => migrateOn(db, "tidb")) : withPgLock(pool, (db) => migrateOn(db, "pg"));
|
|
376
441
|
}
|
|
@@ -403,44 +468,33 @@ async function withPgLock(pool, run) {
|
|
|
403
468
|
}
|
|
404
469
|
}
|
|
405
470
|
async function migrateOn(db, dialect) {
|
|
406
|
-
const receipts =
|
|
471
|
+
const receipts = buildMigrationReceipts();
|
|
407
472
|
try {
|
|
408
|
-
|
|
473
|
+
await migrateStepsOn(db, dialect, receipts);
|
|
409
474
|
}
|
|
410
475
|
catch (e) {
|
|
411
|
-
throw
|
|
476
|
+
throw changedAnything(receipts) ? new OneTimeMigrationInterruptedError(e, readingsSnapshot(receipts)) : e;
|
|
412
477
|
}
|
|
478
|
+
return completedReport(receipts);
|
|
413
479
|
}
|
|
414
480
|
async function migrateStepsOn(db, dialect, receipts) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
? 0
|
|
418
|
-
: await db.exec(`UPDATE ${AUDIT_TABLE} SET ${AUDIT_DETAIL_COLUMN} = NULL ` +
|
|
419
|
-
`WHERE ${AUDIT_DETAIL_COLUMN} IS NOT NULL AND ${jsonColumnTextExpr(dialect, AUDIT_DETAIL_COLUMN)} = 'null'`);
|
|
420
|
-
const { sessionIncarnationColumn, sessionIncarnationsBackfilled } = await migrateSessionIncarnations(db, dialect);
|
|
421
|
-
const holderDeathCodeRowsBackfilled = (await db.exec(dialect === "tidb"
|
|
422
|
-
? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND error = ?"
|
|
423
|
-
: "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])) +
|
|
424
|
-
(await db.exec(dialect === "tidb"
|
|
425
|
-
? "UPDATE task_run SET error_code = ? WHERE status = 'failed' AND error_code IS NULL AND (error = ? OR error LIKE ?)"
|
|
426
|
-
: "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} (%`]));
|
|
427
|
-
const { migrated: allowRuleShadowRowsMigrated, unreadable: allowRuleShadowRowsUnreadable } = await migrateShadowedRuleKey(db, dialect);
|
|
428
|
-
const { askRows: retiredCardWordAskRowsRewritten, unreadable: retiredCardWordRowsUnreadable } = await migrateRetiredCardWords(db, dialect);
|
|
429
|
-
const { key: workflowParkIndexKey, carried: workflowParkIndexRowsCarried } = await reshapeWorkflowParkIndex(db, dialect);
|
|
430
|
-
const { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets, sessionRuleRowsWithOldGrammarEntries } = await migrateSessionRuleGrammar(db, dialect, receipts);
|
|
431
|
-
let undurableScopeRuleRows = [];
|
|
432
|
-
let undurableScopeRuleBucketsDeferred = 0;
|
|
433
|
-
let undurableScopeRuleBucketsUnreadable = [];
|
|
434
|
-
if (await tableExists(db, dialect, PERMISSION_RULE_TABLE)) {
|
|
435
|
-
({ 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'`);
|
|
436
483
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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);
|
|
444
498
|
}
|
|
445
499
|
const SESSION_POLICY_TABLE = "session_policy";
|
|
446
500
|
const RETIRED_SESSION_RULES_COLUMN = "rules";
|
|
@@ -459,9 +513,10 @@ export function oldGrammarEntriesOf(body, rev) {
|
|
|
459
513
|
const shaped = readSessionRulesShape({ ...buckets, rev });
|
|
460
514
|
if ("fault" in shaped)
|
|
461
515
|
return undefined;
|
|
516
|
+
const toolDenyUnreadable = unreadableDenyEntries(shaped.rules).map(({ entry, reason }) => ({ entry, reason }));
|
|
517
|
+
if (toolDenyUnreadable.length > 0)
|
|
518
|
+
return { toolAllowDropped: [], toolDenyUnreadable };
|
|
462
519
|
const compiled = compileSessionRuleBucket(shaped.rules, "toolAllow");
|
|
463
|
-
if (body.grammar === undefined && compiled.inert.some((i) => i.reason === "wildcard_all"))
|
|
464
|
-
return { toolAllowDropped: [], wildcardMasked: true };
|
|
465
520
|
const earlier = new Set(compiled.inert.filter((i) => i.reason === "earlier_grammar").map((i) => i.entry));
|
|
466
521
|
const toolAllow = shaped.rules.toolAllow?.filter((e) => !earlier.has(e));
|
|
467
522
|
const toolAllowDropped = (shaped.rules.toolAllow ?? []).filter((e) => earlier.has(e));
|
|
@@ -504,8 +559,14 @@ async function judgeOldGrammarRows(db, dialect, receipts) {
|
|
|
504
559
|
: `UPDATE ${SESSION_POLICY_TABLE} SET ${column} = $1::jsonb WHERE policy_key = $2 AND ${text} = $3`;
|
|
505
560
|
await scanSessionRuleRows(db, dialect, column, [], async (row) => {
|
|
506
561
|
const read = oldGrammarEntriesOf(row.body, row.rev);
|
|
507
|
-
if (read?.
|
|
508
|
-
|
|
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
|
+
});
|
|
509
570
|
return;
|
|
510
571
|
}
|
|
511
572
|
if (read?.rewritten === undefined)
|
|
@@ -514,32 +575,34 @@ async function judgeOldGrammarRows(db, dialect, receipts) {
|
|
|
514
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 ` +
|
|
515
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");
|
|
516
577
|
}
|
|
517
|
-
if (read.toolAllowDropped.length > 0)
|
|
518
|
-
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
|
+
}
|
|
519
581
|
});
|
|
520
582
|
}
|
|
521
583
|
export async function migrateSessionRuleGrammar(db, dialect, receipts) {
|
|
522
|
-
if (!(await tableExists(db, dialect, SESSION_POLICY_TABLE)))
|
|
523
|
-
|
|
584
|
+
if (!(await tableExists(db, dialect, SESSION_POLICY_TABLE))) {
|
|
585
|
+
receipts.sessionRuleColumn = "no-table";
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
524
588
|
const col = SESSION_RULES_COLUMN;
|
|
525
589
|
const has = (column) => columnExists(db, dialect, SESSION_POLICY_TABLE, column);
|
|
526
590
|
if (await has(RETIRED_SESSION_RULES_COLUMN)) {
|
|
527
591
|
await execUntil(db, renameSessionRulesColumnSql(dialect, RETIRED_SESSION_RULES_COLUMN, OLD_GRAMMAR_SESSION_RULES_COLUMN), async () => !(await has(RETIRED_SESSION_RULES_COLUMN)));
|
|
528
592
|
}
|
|
529
|
-
let sessionRuleColumn = "current";
|
|
530
593
|
if (await has(OLD_GRAMMAR_SESSION_RULES_COLUMN)) {
|
|
531
594
|
await judgeOldGrammarRows(db, dialect, receipts);
|
|
532
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)));
|
|
533
|
-
receipts.
|
|
534
|
-
|
|
596
|
+
receipts.sessionRuleColumn = "renamed";
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
receipts.sessionRuleColumn = "current";
|
|
535
600
|
}
|
|
536
|
-
const named = [];
|
|
537
601
|
await scanSessionRuleRows(db, dialect, col.name, RETIRED_BUCKET_PATTERNS, async (row) => {
|
|
538
602
|
const buckets = retiredSessionRuleBucketsOf(row.body);
|
|
539
603
|
if (buckets.length > 0)
|
|
540
|
-
|
|
604
|
+
receipts.sessionRuleRowsWithRetiredBuckets.push({ policyKey: row.policyKey, sessionId: row.sessionId, buckets });
|
|
541
605
|
});
|
|
542
|
-
return { sessionRuleColumn, sessionRuleRowsWithRetiredBuckets: named, sessionRuleRowsWithOldGrammarEntries: receipts.rows };
|
|
543
606
|
}
|
|
544
607
|
function renameSessionRulesColumnSql(dialect, from, to) {
|
|
545
608
|
const definition = SESSION_RULES_COLUMN.tidb.slice(SESSION_RULES_COLUMN.name.length);
|
|
@@ -556,7 +619,7 @@ async function primaryKeyWidth(db, dialect, table) {
|
|
|
556
619
|
}
|
|
557
620
|
const WORKFLOW_PARK_INDEX_STAGING = `${WORKFLOW_PARK_INDEX_TABLE}_m9`;
|
|
558
621
|
const WORKFLOW_PARK_INDEX_RETIRED = `${WORKFLOW_PARK_INDEX_TABLE}_m9_old`;
|
|
559
|
-
async function reshapeWorkflowParkIndex(db, dialect) {
|
|
622
|
+
async function reshapeWorkflowParkIndex(db, dialect, receipts) {
|
|
560
623
|
const T = WORKFLOW_PARK_INDEX_TABLE;
|
|
561
624
|
const S = WORKFLOW_PARK_INDEX_STAGING;
|
|
562
625
|
const O = WORKFLOW_PARK_INDEX_RETIRED;
|
|
@@ -567,11 +630,10 @@ async function reshapeWorkflowParkIndex(db, dialect) {
|
|
|
567
630
|
const copyPointers = (to, from) => db.exec(dialect === "tidb"
|
|
568
631
|
? `INSERT IGNORE INTO ${to} (session_id, run_id, agent_key, scope) SELECT session_id, run_id, agent_key, scope FROM ${from}`
|
|
569
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`);
|
|
570
|
-
let carried = 0;
|
|
571
633
|
const resumed = await exists(S);
|
|
572
634
|
if (resumed) {
|
|
573
635
|
await db.exec(ddl);
|
|
574
|
-
|
|
636
|
+
receipts.workflowParkIndexRowsCarried += await copyPointers(T, S);
|
|
575
637
|
await execUntil(db, `DROP TABLE ${S}`, async () => !(await exists(S)));
|
|
576
638
|
}
|
|
577
639
|
if (await exists(O))
|
|
@@ -579,10 +641,11 @@ async function reshapeWorkflowParkIndex(db, dialect) {
|
|
|
579
641
|
if ((await primaryKeyWidth(db, dialect, T)) !== 1) {
|
|
580
642
|
if (await exists(T))
|
|
581
643
|
await finishParkIndexShape(db, dialect);
|
|
582
|
-
|
|
644
|
+
receipts.workflowParkIndexKey = resumed ? "reshaped" : "current";
|
|
645
|
+
return;
|
|
583
646
|
}
|
|
584
647
|
await db.exec(ddl.replace(`CREATE TABLE IF NOT EXISTS ${T} (`, `CREATE TABLE IF NOT EXISTS ${S} (`));
|
|
585
|
-
|
|
648
|
+
receipts.workflowParkIndexRowsCarried += await copyPointers(S, T);
|
|
586
649
|
const swapped = async () => !(await exists(S)) && (await exists(T)) && (await primaryKeyWidth(db, dialect, T)) !== 1;
|
|
587
650
|
if (dialect === "tidb") {
|
|
588
651
|
await execUntil(db, `RENAME TABLE ${T} TO ${O}, ${S} TO ${T}`, swapped);
|
|
@@ -592,7 +655,7 @@ async function reshapeWorkflowParkIndex(db, dialect) {
|
|
|
592
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)));
|
|
593
656
|
}
|
|
594
657
|
await finishParkIndexShape(db, dialect);
|
|
595
|
-
|
|
658
|
+
receipts.workflowParkIndexKey = "reshaped";
|
|
596
659
|
}
|
|
597
660
|
async function execAtomically(db, statements, postcondition) {
|
|
598
661
|
try {
|
|
@@ -643,10 +706,8 @@ export function migrateShadowedRuleDescriptor(text) {
|
|
|
643
706
|
return { kind: "rewrite", text: JSON.stringify(next) };
|
|
644
707
|
}
|
|
645
708
|
const SHADOWED_RULE_BATCH = 500;
|
|
646
|
-
async function migrateShadowedRuleKey(db, dialect) {
|
|
709
|
+
async function migrateShadowedRuleKey(db, dialect, receipts) {
|
|
647
710
|
const like = `%"${RETIRED_SHADOWED_RULE_KEY}"%`;
|
|
648
|
-
let migrated = 0;
|
|
649
|
-
let unreadable = 0;
|
|
650
711
|
let after = "";
|
|
651
712
|
for (;;) {
|
|
652
713
|
const rows = await db.rows(dialect === "tidb"
|
|
@@ -660,17 +721,17 @@ async function migrateShadowedRuleKey(db, dialect) {
|
|
|
660
721
|
continue;
|
|
661
722
|
const next = migrateShadowedRuleDescriptor(text);
|
|
662
723
|
if (next.kind === "unreadable") {
|
|
663
|
-
|
|
724
|
+
receipts.allowRuleShadowRowsUnreadable += 1;
|
|
664
725
|
continue;
|
|
665
726
|
}
|
|
666
727
|
if (next.kind === "untouched")
|
|
667
728
|
continue;
|
|
668
|
-
|
|
729
|
+
receipts.allowRuleShadowRowsMigrated += await db.exec(dialect === "tidb"
|
|
669
730
|
? `UPDATE checkpoint SET risk_descriptor = ? WHERE token = ? AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = ?`
|
|
670
731
|
: `UPDATE checkpoint SET risk_descriptor = $1 WHERE token = $2 AND ${jsonColumnTextExpr(dialect, "risk_descriptor")} = $3`, [next.text, token, text]);
|
|
671
732
|
}
|
|
672
733
|
if (rows.length < SHADOWED_RULE_BATCH)
|
|
673
|
-
return
|
|
734
|
+
return;
|
|
674
735
|
}
|
|
675
736
|
}
|
|
676
737
|
const RETIRED_CARD_WORD_PATTERNS = ['%"shadowed"%', '%"governance_unavailable"%'];
|
|
@@ -692,11 +753,9 @@ export function migrateRetiredCardWordsInCardJson(text) {
|
|
|
692
753
|
const next = retireStoredCardWords(decoded.value);
|
|
693
754
|
return next.seats.length === 0 ? { kind: "untouched" } : { kind: "rewrite", text: JSON.stringify(next.value), value: next.value };
|
|
694
755
|
}
|
|
695
|
-
async function migrateRetiredCardWords(db, dialect) {
|
|
696
|
-
let askRows = 0;
|
|
697
|
-
let unreadable = 0;
|
|
756
|
+
async function migrateRetiredCardWords(db, dialect, receipts) {
|
|
698
757
|
if (!(await tableExists(db, dialect, "approval_ask")))
|
|
699
|
-
return
|
|
758
|
+
return;
|
|
700
759
|
const cardText = jsonColumnTextExpr(dialect, "card_json");
|
|
701
760
|
let after = "";
|
|
702
761
|
for (;;) {
|
|
@@ -711,42 +770,129 @@ async function migrateRetiredCardWords(db, dialect) {
|
|
|
711
770
|
continue;
|
|
712
771
|
const next = migrateRetiredCardWordsInCardJson(text);
|
|
713
772
|
if (next.kind === "unreadable") {
|
|
714
|
-
|
|
773
|
+
receipts.retiredCardWordRowsUnreadable += 1;
|
|
715
774
|
continue;
|
|
716
775
|
}
|
|
717
776
|
if (next.kind === "untouched")
|
|
718
777
|
continue;
|
|
719
|
-
|
|
778
|
+
receipts.retiredCardWordAskRowsRewritten += await db.exec(dialect === "tidb"
|
|
720
779
|
? `UPDATE approval_ask SET card_json = ? WHERE ask_id = ? AND CAST(${cardText} AS BINARY) = CAST(? AS BINARY)`
|
|
721
780
|
: `UPDATE approval_ask SET card_json = $1 WHERE ask_id = $2 AND ${cardText} = $3`, [next.text, askId, text]);
|
|
722
781
|
}
|
|
723
782
|
if (rows.length < RETIRED_CARD_WORD_BATCH)
|
|
724
|
-
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;
|
|
725
868
|
}
|
|
726
869
|
}
|
|
727
870
|
const INCARNATION_BACKFILL_BATCH = 500;
|
|
728
|
-
async function migrateSessionIncarnations(db, dialect) {
|
|
729
|
-
if (!(await tableExists(db, dialect, "session_meta")))
|
|
730
|
-
|
|
871
|
+
async function migrateSessionIncarnations(db, dialect, receipts) {
|
|
872
|
+
if (!(await tableExists(db, dialect, "session_meta"))) {
|
|
873
|
+
receipts.sessionIncarnationColumn = "no-table";
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
731
876
|
const col = SESSION_META_INCARNATION_COLUMN;
|
|
732
|
-
let
|
|
877
|
+
let column = "present";
|
|
733
878
|
if (!(await columnExists(db, dialect, "session_meta", col.name))) {
|
|
734
879
|
try {
|
|
735
880
|
await db.exec(`ALTER TABLE session_meta ADD COLUMN ${dialect === "tidb" ? col.tidb : col.pg}`);
|
|
736
|
-
|
|
881
|
+
column = "added";
|
|
737
882
|
}
|
|
738
883
|
catch (e) {
|
|
739
884
|
if (!(await columnExists(db, dialect, "session_meta", col.name)))
|
|
740
885
|
throw e;
|
|
741
886
|
}
|
|
742
887
|
}
|
|
743
|
-
|
|
888
|
+
receipts.sessionIncarnationColumn = column;
|
|
889
|
+
await backfillSessionIncarnations(db, dialect, receipts);
|
|
744
890
|
}
|
|
745
|
-
async function backfillSessionIncarnations(db, dialect) {
|
|
891
|
+
async function backfillSessionIncarnations(db, dialect, receipts) {
|
|
746
892
|
const remaining = async () => Number((await db.rows("SELECT COUNT(*) AS n FROM session_meta WHERE incarnation = ''"))[0]?.n ?? 0);
|
|
747
893
|
const initial = await remaining();
|
|
748
894
|
if (initial === 0)
|
|
749
|
-
return
|
|
895
|
+
return;
|
|
750
896
|
const ceiling = Math.ceil(initial / INCARNATION_BACKFILL_BATCH) * 2 + 2;
|
|
751
897
|
let written = 0;
|
|
752
898
|
let after;
|
|
@@ -756,7 +902,7 @@ async function backfillSessionIncarnations(db, dialect) {
|
|
|
756
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));
|
|
757
903
|
if (ids.length === 0) {
|
|
758
904
|
if (after === undefined)
|
|
759
|
-
return
|
|
905
|
+
return;
|
|
760
906
|
after = undefined;
|
|
761
907
|
continue;
|
|
762
908
|
}
|
|
@@ -767,7 +913,9 @@ async function backfillSessionIncarnations(db, dialect) {
|
|
|
767
913
|
};
|
|
768
914
|
const cases = ids.map((id) => `WHEN ${bind(id)} THEN ${bind(mintSessionIncarnation())}`).join(" ");
|
|
769
915
|
const inList = ids.map((id) => bind(id)).join(", ");
|
|
770
|
-
|
|
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;
|
|
771
919
|
after = ids.length < INCARNATION_BACKFILL_BATCH ? undefined : ids[ids.length - 1];
|
|
772
920
|
}
|
|
773
921
|
const left = await remaining();
|
|
@@ -776,33 +924,36 @@ async function backfillSessionIncarnations(db, dialect) {
|
|
|
776
924
|
`(${written} written) — refusing to start rather than serve sessions whose occupant cannot be told apart. Retry the boot; ` +
|
|
777
925
|
`if it repeats, something keeps writing rows without an incarnation (an older replica still inserting?).`);
|
|
778
926
|
}
|
|
779
|
-
|
|
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
|
+
};
|
|
780
952
|
}
|
|
781
953
|
export function logOneTimeMigrations(log, backend, report) {
|
|
782
|
-
|
|
783
|
-
report.sessionIncarnationColumn === "added" || report.sessionIncarnationsBackfilled > 0 || report.holderDeathCodeRowsBackfilled > 0 || report.allowRuleShadowRowsMigrated > 0 || report.allowRuleShadowRowsUnreadable > 0 ||
|
|
784
|
-
report.workflowParkIndexKey === "reshaped" || report.undurableScopeRuleRows.length > 0 || report.undurableScopeRuleBucketsDeferred > 0 ||
|
|
785
|
-
report.undurableScopeRuleBucketsUnreadable.length > 0 ||
|
|
786
|
-
report.sessionRuleColumn === "renamed" || report.sessionRuleRowsWithRetiredBuckets.length > 0 || report.sessionRuleRowsWithOldGrammarEntries.length > 0 ||
|
|
787
|
-
report.retiredCardWordAskRowsRewritten > 0 || report.retiredCardWordRowsUnreadable > 0;
|
|
788
|
-
(changed ? log.warn : log.info).call(log, "one_time_migrations_applied", {
|
|
954
|
+
(changedAnything(report) ? log.warn : log.info).call(log, "one_time_migrations_applied", {
|
|
789
955
|
backend,
|
|
790
|
-
|
|
791
|
-
sessionIncarnationColumn: report.sessionIncarnationColumn,
|
|
792
|
-
sessionIncarnationsBackfilled: report.sessionIncarnationsBackfilled,
|
|
793
|
-
holderDeathCodeRowsBackfilled: report.holderDeathCodeRowsBackfilled,
|
|
794
|
-
allowRuleShadowRowsMigrated: report.allowRuleShadowRowsMigrated,
|
|
795
|
-
allowRuleShadowRowsUnreadable: report.allowRuleShadowRowsUnreadable,
|
|
796
|
-
workflowParkIndexKey: report.workflowParkIndexKey,
|
|
797
|
-
workflowParkIndexRowsCarried: report.workflowParkIndexRowsCarried,
|
|
798
|
-
undurableScopeRuleRowsRemoved: report.undurableScopeRuleRows.length,
|
|
799
|
-
undurableScopeRuleBucketsDeferred: report.undurableScopeRuleBucketsDeferred,
|
|
800
|
-
undurableScopeRuleBucketsUnreadable: report.undurableScopeRuleBucketsUnreadable.length,
|
|
801
|
-
sessionRuleColumn: report.sessionRuleColumn,
|
|
802
|
-
sessionRuleRowsWithRetiredBuckets: report.sessionRuleRowsWithRetiredBuckets.length,
|
|
803
|
-
sessionRuleRowsWithOldGrammarEntries: report.sessionRuleRowsWithOldGrammarEntries.length,
|
|
804
|
-
retiredCardWordAskRowsRewritten: report.retiredCardWordAskRowsRewritten,
|
|
805
|
-
retiredCardWordRowsUnreadable: report.retiredCardWordRowsUnreadable,
|
|
956
|
+
...readingFields(report),
|
|
806
957
|
removableAfter: Object.fromEntries(ONE_TIME_MIGRATIONS.map((w) => [w.id, w.removableAfter])),
|
|
807
958
|
note: "one-time persisted-data migrations (M4: audit details written as the JSON " +
|
|
808
959
|
"text `null` by an earlier version folded to a SQL NULL, so \"no detail\" has exactly one on-disk spelling; M5: the " +
|
|
@@ -820,16 +971,23 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
820
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 " +
|
|
821
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 " +
|
|
822
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 " +
|
|
823
|
-
"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; " +
|
|
824
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; " +
|
|
825
977
|
"M18: stored approval cards written by an earlier engine with two words the current engine retired rewritten — ruleOffersAbsence shadowed removed, ask class " +
|
|
826
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 " +
|
|
827
|
-
"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 " +
|
|
828
983
|
"runs on every boot until its window closes (the release that introduced it + 30 days; the per-step date is in " +
|
|
829
984
|
"removableAfter, null while that release is not published yet) and is deleted in the first minor after that — start a " +
|
|
830
985
|
"version that still carries a step at least once before upgrading past its deletion.",
|
|
831
986
|
});
|
|
832
|
-
|
|
987
|
+
logReceiptRows(log, backend, report);
|
|
988
|
+
}
|
|
989
|
+
function logReceiptRows(log, backend, r) {
|
|
990
|
+
for (const row of r.undurableScopeRuleRows) {
|
|
833
991
|
log.warn("permission_rule_row_removed", {
|
|
834
992
|
backend,
|
|
835
993
|
ownerKey: row.ownerKey,
|
|
@@ -840,7 +998,7 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
840
998
|
note: removedRuleRowNote(row.kind),
|
|
841
999
|
});
|
|
842
1000
|
}
|
|
843
|
-
for (const bucket of
|
|
1001
|
+
for (const bucket of r.undurableScopeRuleBucketsUnreadable) {
|
|
844
1002
|
log.warn("permission_rule_bucket_unreadable", {
|
|
845
1003
|
backend,
|
|
846
1004
|
ownerKey: bucket.ownerKey,
|
|
@@ -850,8 +1008,37 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
850
1008
|
"keeps being refused, as before this release, until someone repairs or deletes that row by hand",
|
|
851
1009
|
});
|
|
852
1010
|
}
|
|
853
|
-
|
|
854
|
-
|
|
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) {
|
|
855
1042
|
log.warn("session_rule_row_retired_bucket", {
|
|
856
1043
|
backend,
|
|
857
1044
|
policyKey: row.policyKey,
|
|
@@ -865,40 +1052,17 @@ export function logOneTimeMigrations(log, backend, report) {
|
|
|
865
1052
|
});
|
|
866
1053
|
}
|
|
867
1054
|
}
|
|
868
|
-
function logSessionRuleOldGrammarRows(log, backend, rows) {
|
|
869
|
-
for (const row of rows) {
|
|
870
|
-
log.warn("session_rule_row_old_grammar_entries", {
|
|
871
|
-
backend,
|
|
872
|
-
policyKey: row.policyKey,
|
|
873
|
-
sessionId: row.sessionId,
|
|
874
|
-
toolAllowDropped: row.toolAllowDropped,
|
|
875
|
-
...(row.wildcardMasked ? { wildcardMasked: true } : {}),
|
|
876
|
-
note: row.wildcardMasked
|
|
877
|
-
? "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 " +
|
|
878
|
-
"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 " +
|
|
879
|
-
"entries, an operator rewrites the row without `*` (entries spelled as rules, `Bash(rm:*)`, then become allow rules — review them first)."
|
|
880
|
-
:
|
|
881
|
-
"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 " +
|
|
882
|
-
"in that earlier grammar, where these toolAllow entries named no tool and admitted nothing, while in the current grammar they would be allow rules. " +
|
|
883
|
-
"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 " +
|
|
884
|
-
"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 " +
|
|
885
|
-
"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",
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
1055
|
function logOneTimeMigrationsInterrupted(log, backend, error) {
|
|
890
|
-
const { receipts } = error;
|
|
891
1056
|
log.warn("one_time_migrations_interrupted", {
|
|
892
1057
|
backend,
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
"
|
|
897
|
-
"
|
|
898
|
-
"
|
|
899
|
-
"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",
|
|
900
1064
|
});
|
|
901
|
-
|
|
1065
|
+
logReceiptRows(log, backend, error.receipts);
|
|
902
1066
|
}
|
|
903
1067
|
export async function runAndLogOneTimeMigrations(log, backend) {
|
|
904
1068
|
let report;
|