@shawnstack/quickforge 1.7.10 → 1.7.12
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/dist/assets/{AgentProfilesPage-Dw9orPNt.js → AgentProfilesPage-CApCpsDd.js} +1 -1
- package/dist/assets/ChatPanelHost-COTO7swS.js +48 -0
- package/dist/assets/CloudAccountSettingsPage-Be5r3dtv.js +1 -0
- package/dist/assets/{PluginsPage-Dj1bmsEf.js → PluginsPage-BIbIDfWY.js} +1 -1
- package/dist/assets/ScheduledTasksPage-BOqmBNCx.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-CFuvSATi.js → SettingsWorkspacePage-BCRtycRN.js} +304 -304
- package/dist/assets/ShareLinksSettingsPage-BrxQacJn.js +1 -0
- package/dist/assets/{SharedConversationPage-DLo05j7c.js → SharedConversationPage-DJa0M0El.js} +1 -1
- package/dist/assets/TerminalDock-Cqz_9mTC.js +2 -0
- package/dist/assets/WorkspaceInspector-C-GOK81D.js +36 -0
- package/dist/assets/{abnfDiagram-VRR7QNED-CMhVoYUh.js → abnfDiagram-VRR7QNED-DGklo3PO.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-C52qL6Hg.js → architectureDiagram-ZJ3FMSHR-CioWLVTp.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-CM3XbLol.js → blockDiagram-677ZJIJ3-BJWBq5e9.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-D7Bh1ui1.js → c4Diagram-LMCZKHZV-BacRvqFK.js} +1 -1
- package/dist/assets/channel-CeXY-wZo.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-xgrtbT8e.js → chunk-32BRIVSS-B53BNE2C.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-CDbaHw80.js → chunk-52WLFC77-CYq4hT4f.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-ZMLUvfLy.js → chunk-C7G6YPKG-KFaGpP_Z.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-BcQhaqt7.js → chunk-EX3LRPZG-DqVd_ihG.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-DrnrdrSu.js → chunk-FWX5IMBZ-BxRjZkOP.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-tdc-Z9eW.js → chunk-HOUHSVGY-C9QO8omv.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-Cd7igVGt.js → chunk-ICXQ74PX-ClbGPgLV.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CLTjV5ov.js → chunk-MOJQB5TN-Ba4wlF1J.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-TgvkXkFX.js → chunk-OGEWGWER-DczhQmcx.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-C5GMVYup.js → chunk-PUDLZKDR-CPWpcKUF.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-fo5-HsqY.js → chunk-Q4XR5HBZ-CiF6T--X.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-CEDazV6w.js → chunk-V7JOEXUC-Dxv0zwmw.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-CWYjg8zE.js → chunk-VAUOI2AC-D3J5bw1P.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-C_-eEj-9.js → chunk-VR4S4FIN-BXQSwwII.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-NF-MsJJR.js → chunk-WYO6CB5R-C0azl-fk.js} +2 -2
- package/dist/assets/{chunk-ZGVPDNZ5-CRrAYi4g.js → chunk-ZGVPDNZ5-D_8F4Akx.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-B8M6sf49.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-BY7fzBDH.js +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-D4sZ7LYI.js → cynefinDiagram-TSTJHNR4-BFlsWz0B.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-BlsnSh-Q.js → dagre-VKFMJZFB-DXhpjlEs.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-DW1yQiRO.js → diagram-FQU43EPY-DRVVOD9k.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-BTUAsFjb.js → diagram-G47NLZAW-BV9Gw1tL.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-BcaomqSd.js → diagram-NH7WQ7WH-CrUkXfRY.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-DDLX7Xwr.js → diagram-OA4YK3LP-CuILjn7v.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-CIave0An.js → diagram-WEI45ONY-nlNN3tWB.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-Cr1Y6lPu.js → ebnfDiagram-CCIWWBDH-BuDgZt9n.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-D3EgiVVH.js → erDiagram-Q63AITRT-MEwxjmtP.js} +1 -1
- package/dist/assets/flowDiagram-23GEKE2U-BiUP-Scq.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-B0oyDIqK.js → ganttDiagram-NO4QXBWP-DNw5qghE.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-uQtr6IqK.js → gitGraphDiagram-IHSO6WYX-CaTcakAt.js} +1 -1
- package/dist/assets/index-BWf6y3Kh.css +3 -0
- package/dist/assets/index-D3GWkXJR.js +66 -0
- package/dist/assets/{infoDiagram-FWYZ7A6U-B_GzlLIM.js → infoDiagram-FWYZ7A6U-DwfyP7dU.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CKQIsSdU.js → ishikawaDiagram-FXEZZL3T-CmrzbOCV.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-DrrvzZL1.js → journeyDiagram-5HDEW3XC-CYqe3sgr.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-CJQRTdXz.js → kanban-definition-HUTT4EX6-BMDO_mv0.js} +1 -1
- package/dist/assets/{line-B4DxX7tC.js → line-SbxJR8ZT.js} +1 -1
- package/dist/assets/lit-vendor-CinchGmI.js +2 -0
- package/dist/assets/local-tools-DY0yEU3T.js +350 -0
- package/dist/assets/{mcp-servers-dialog-CwHp0K4q.js → mcp-servers-dialog-7Xwu2RVW.js} +2 -2
- package/dist/assets/{mermaid.core-DWpShXCs.js → mermaid.core-CwXTzYjs.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-CEVwZD78.js → mindmap-definition-LN4V7U3C-CLyJ78mO.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-CBLkBdnQ.js → pegDiagram-2B236MQR-4qFznrSw.js} +1 -1
- package/dist/assets/{pi-web-ui-DV5jKjSg.js → pi-web-ui-BNuvqKDT.js} +289 -289
- package/dist/assets/{pieDiagram-ENE6RG2P-8tCnwtWS.js → pieDiagram-ENE6RG2P-141sGxVe.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-CWd28cHF.js → quadrantDiagram-ABIIQ3AL-SUI44pAX.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-CI4ouOBs.js → railroadDiagram-RFXS5EU6-1oiZuYBB.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-DtFz7sw4.js → requirementDiagram-TGXJPOKE-CfVishLi.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB--VeOwwk9.js → sankeyDiagram-HTMAVEWB-DVu7J_Z9.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-B8AHk5Oe.js → sequenceDiagram-DBY2YBRQ-D-Ip2yiS.js} +1 -1
- package/dist/assets/{skills-dialog-k9ycekIU.js → skills-dialog-C_JvWbAa.js} +3 -3
- package/dist/assets/{stateDiagram-2N3HPSRC-etXnJ8X8.js → stateDiagram-2N3HPSRC-Cg0hWwrp.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DhmzNQd9.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-pabIKAmn.js → swimlanes-5IMT3BWC-Co5D-cGU.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-BEvomQNa.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-C55MKm3L.js → timeline-definition-FHXFAJF6-CBk4c_Ua.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-CgNf1Q9W.js → vennDiagram-L72KCM5P-BII7wdRx.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-Drt5ytec.js → wardleyDiagram-EHGQE667-CkyJJIXP.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-hhoEo_Ar.js → xychartDiagram-FW5EYKEG-B1A66Ks0.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +1 -1
- package/server/acp/server.mjs +3 -6
- package/server/agent-manager.mjs +192 -48
- package/server/approval-store.mjs +1 -0
- package/server/ask-store.mjs +78 -0
- package/server/auto-archive.mjs +55 -8
- package/server/index.mjs +172 -51
- package/server/lan-access-cutover.mjs +21 -4
- package/server/maintenance/downgrade-session-state-v1.mjs +46 -85
- package/server/maintenance/export-session-state-v1.mjs +1 -11
- package/server/routes/agent.mjs +9 -0
- package/server/routes/backup.mjs +1 -1
- package/server/routes/storage.mjs +63 -49
- package/server/scheduled-runs-cutover.mjs +15 -1
- package/server/session-index-service.mjs +57 -391
- package/server/session-state-backup.mjs +56 -5
- package/server/session-state-import.mjs +174 -0
- package/server/session-state-maintenance.mjs +126 -0
- package/server/session-state-service.mjs +128 -210
- package/server/share-cutover.mjs +21 -4
- package/server/sqlite/database.mjs +142 -7
- package/server/sqlite/lan-access-repository.mjs +6 -4
- package/server/sqlite/migrations.mjs +109 -0
- package/server/sqlite/scheduled-task-runs-repository.mjs +6 -3
- package/server/sqlite/session-index-repository.mjs +59 -208
- package/server/sqlite/session-state-repository.mjs +337 -387
- package/server/sqlite/share-repository.mjs +56 -45
- package/server/startup-state.mjs +107 -0
- package/server/storage.mjs +218 -454
- package/server/tools/definitions.mjs +18 -0
- package/server/tools/index.mjs +14 -2
- package/server/utils/logger.mjs +15 -4
- package/dist/assets/ChatPanelHost-CW3-MQyL.js +0 -48
- package/dist/assets/CloudAccountSettingsPage-CDkKN1Sz.js +0 -1
- package/dist/assets/ScheduledTasksPage-C9lOTi75.js +0 -2
- package/dist/assets/ShareLinksSettingsPage-CH_t33A3.js +0 -1
- package/dist/assets/TerminalDock-CSOVub-m.js +0 -2
- package/dist/assets/WorkspaceInspector-DPXgLIum.js +0 -36
- package/dist/assets/channel-Duu1CuH9.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-Cc63fLWP.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-CoXU0qT6.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-BS7KXcqn.js +0 -1
- package/dist/assets/index-DQBOm9gt.js +0 -66
- package/dist/assets/index-ps85guTs.css +0 -3
- package/dist/assets/lit-vendor-DvaQX_UQ.js +0 -2
- package/dist/assets/local-tools-BxfQeIUQ.js +0 -270
- package/dist/assets/stateDiagram-v2-6OUMAXLB-Dms3pICj.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-B14aWjv6.js +0 -8
- package/server/session-state-cutover.mjs +0 -563
|
@@ -1,168 +1,18 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto'
|
|
2
1
|
import { createSessionIndexRepository } from './sqlite/session-index-repository.mjs'
|
|
3
2
|
import { getSqliteStorage } from './sqlite/database.mjs'
|
|
4
3
|
import { logger } from './utils/logger.mjs'
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
// Storage v2 session index service. The JSON-metadata convergence machinery
|
|
6
|
+
// (shadow verification, verify TTL, scheduled rebuilds, incremental
|
|
7
|
+
// syncMetadataCommit) is retired together with the session_index projection
|
|
8
|
+
// table: queries read the authoritative `sessions` table directly, so there is
|
|
9
|
+
// no second data source that could drift. The service keeps the route-facing
|
|
10
|
+
// boundary (readiness/analysis/queryPage) and the diagnostics shape the
|
|
11
|
+
// /api/system status surface renders.
|
|
8
12
|
|
|
9
|
-
let defaultReader = null
|
|
10
13
|
let defaultRepository = null
|
|
11
14
|
let activeService = null
|
|
12
15
|
|
|
13
|
-
function isPlainObject(value) {
|
|
14
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function canonicalValue(value) {
|
|
18
|
-
if (Array.isArray(value)) return value.map(canonicalValue)
|
|
19
|
-
if (!isPlainObject(value)) return value
|
|
20
|
-
const result = {}
|
|
21
|
-
for (const key of Object.keys(value).sort()) {
|
|
22
|
-
if (value[key] !== undefined) result[key] = canonicalValue(value[key])
|
|
23
|
-
}
|
|
24
|
-
return result
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function canonicalSessionMetadata(metadata, { scope, projectId, sessionId }) {
|
|
28
|
-
if (!isPlainObject(metadata)) throw new TypeError('Session metadata must be a plain object')
|
|
29
|
-
const canonical = canonicalValue(metadata)
|
|
30
|
-
canonical.id = sessionId
|
|
31
|
-
canonical.scope = scope
|
|
32
|
-
if (scope === 'project') canonical.projectId = projectId
|
|
33
|
-
else delete canonical.projectId
|
|
34
|
-
return canonicalValue(canonical)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function sha256(value) {
|
|
38
|
-
return createHash('sha256').update(value).digest('hex')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function sessionMetadataDigest(metadata) {
|
|
42
|
-
return sha256(JSON.stringify(canonicalValue(metadata)))
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function sessionIndexAggregateDigest(entries) {
|
|
46
|
-
const normalized = [...entries]
|
|
47
|
-
.map((entry) => `${entry.scope}\0${entry.projectId || ''}\0${entry.sessionId}\0${entry.metadataDigest}`)
|
|
48
|
-
.sort()
|
|
49
|
-
return sha256(normalized.join('\n'))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function normalizeBucket(bucket) {
|
|
53
|
-
if (!bucket || typeof bucket !== 'object' || Array.isArray(bucket)) throw new TypeError('Session metadata bucket must be an object')
|
|
54
|
-
if (bucket.scope === 'global') return { scope: 'global', projectId: null }
|
|
55
|
-
if (bucket.scope !== 'project' || typeof bucket.projectId !== 'string' || !bucket.projectId.trim()) {
|
|
56
|
-
throw new TypeError('Session metadata bucket scope/projectId is invalid')
|
|
57
|
-
}
|
|
58
|
-
return { scope: 'project', projectId: bucket.projectId }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function nullableString(value) {
|
|
62
|
-
return typeof value === 'string' ? value : null
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function nullableInteger(value) {
|
|
66
|
-
return Number.isInteger(value) && value >= 0 ? value : null
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function isCanonicalIso(value) {
|
|
70
|
-
if (typeof value !== 'string' || !value) return false
|
|
71
|
-
const timestamp = Date.parse(value)
|
|
72
|
-
return Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function fieldCompatibility(metadata, field, { iso = false } = {}) {
|
|
76
|
-
if (metadata[field] === undefined) return true
|
|
77
|
-
if (typeof metadata[field] !== 'string') return false
|
|
78
|
-
return !iso || isCanonicalIso(metadata[field])
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function metadataCompatibility(bucket, sessionId, metadata, canonical) {
|
|
82
|
-
const issues = {
|
|
83
|
-
keyIdMismatch: metadata.id !== undefined && metadata.id !== sessionId,
|
|
84
|
-
scopeConflict: (metadata.scope !== undefined && metadata.scope !== bucket.scope)
|
|
85
|
-
|| (bucket.scope === 'project'
|
|
86
|
-
? metadata.projectId !== undefined && metadata.projectId !== bucket.projectId
|
|
87
|
-
: Object.prototype.hasOwnProperty.call(metadata, 'projectId')),
|
|
88
|
-
invalidCreatedAt: !fieldCompatibility(metadata, 'createdAt', { iso: true }),
|
|
89
|
-
invalidLastModified: !fieldCompatibility(metadata, 'lastModified', { iso: true }),
|
|
90
|
-
invalidPinnedAt: !fieldCompatibility(metadata, 'pinnedAt', { iso: true }),
|
|
91
|
-
invalidArchivedAt: !fieldCompatibility(metadata, 'archivedAt'),
|
|
92
|
-
canonicalMismatch: JSON.stringify(canonicalValue(metadata)) !== JSON.stringify(canonical),
|
|
93
|
-
}
|
|
94
|
-
return { compatible: !Object.values(issues).some(Boolean), issues }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function buildRow(bucket, sessionId, metadata, indexedAt) {
|
|
98
|
-
const canonical = canonicalSessionMetadata(metadata, { ...bucket, sessionId })
|
|
99
|
-
return {
|
|
100
|
-
...bucket,
|
|
101
|
-
sessionId,
|
|
102
|
-
createdAt: nullableString(canonical.createdAt),
|
|
103
|
-
lastModified: nullableString(canonical.lastModified),
|
|
104
|
-
messageCount: nullableInteger(canonical.messageCount),
|
|
105
|
-
pinnedAt: nullableString(canonical.pinnedAt),
|
|
106
|
-
archivedAt: nullableString(canonical.archivedAt),
|
|
107
|
-
stateVersion: nullableInteger(canonical.stateVersion),
|
|
108
|
-
metadata: canonical,
|
|
109
|
-
metadataDigest: sessionMetadataDigest(canonical),
|
|
110
|
-
indexedAt,
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function snapshotFromBuckets(buckets, indexedAt) {
|
|
115
|
-
if (!Array.isArray(buckets)) throw new TypeError('Session metadata reader must return an array')
|
|
116
|
-
const rows = []
|
|
117
|
-
const keys = new Set()
|
|
118
|
-
const sessionBuckets = new Map()
|
|
119
|
-
const compatibilityIssues = {
|
|
120
|
-
keyIdMismatch: 0,
|
|
121
|
-
scopeConflict: 0,
|
|
122
|
-
invalidCreatedAt: 0,
|
|
123
|
-
invalidLastModified: 0,
|
|
124
|
-
invalidPinnedAt: 0,
|
|
125
|
-
invalidArchivedAt: 0,
|
|
126
|
-
canonicalMismatch: 0,
|
|
127
|
-
}
|
|
128
|
-
let duplicateSessionIdCount = 0
|
|
129
|
-
|
|
130
|
-
for (const rawBucket of buckets) {
|
|
131
|
-
const bucket = normalizeBucket(rawBucket)
|
|
132
|
-
if (!isPlainObject(rawBucket.metadata)) throw new TypeError('Session metadata bucket content must be a plain object')
|
|
133
|
-
for (const [sessionId, metadata] of Object.entries(rawBucket.metadata)) {
|
|
134
|
-
if (!sessionId || !isPlainObject(metadata)) throw new TypeError('Session metadata entries must be keyed plain objects')
|
|
135
|
-
const compositeKey = `${bucket.scope}\0${bucket.projectId || ''}\0${sessionId}`
|
|
136
|
-
if (keys.has(compositeKey)) throw new TypeError('Duplicate session metadata bucket composite key')
|
|
137
|
-
keys.add(compositeKey)
|
|
138
|
-
const seenBuckets = sessionBuckets.get(sessionId) || 0
|
|
139
|
-
if (seenBuckets === 1) duplicateSessionIdCount += 1
|
|
140
|
-
sessionBuckets.set(sessionId, seenBuckets + 1)
|
|
141
|
-
const row = buildRow(bucket, sessionId, metadata, indexedAt)
|
|
142
|
-
const compatibility = metadataCompatibility(bucket, sessionId, metadata, row.metadata)
|
|
143
|
-
for (const [issue, present] of Object.entries(compatibility.issues)) {
|
|
144
|
-
if (present) compatibilityIssues[issue] += 1
|
|
145
|
-
}
|
|
146
|
-
rows.push(row)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
rows.sort((left, right) => {
|
|
151
|
-
const leftKey = `${left.scope}\0${left.projectId || ''}\0${left.sessionId}`
|
|
152
|
-
const rightKey = `${right.scope}\0${right.projectId || ''}\0${right.sessionId}`
|
|
153
|
-
return leftKey.localeCompare(rightKey)
|
|
154
|
-
})
|
|
155
|
-
return {
|
|
156
|
-
rows,
|
|
157
|
-
count: rows.length,
|
|
158
|
-
digest: sessionIndexAggregateDigest(rows),
|
|
159
|
-
keyIdMismatchCount: compatibilityIssues.keyIdMismatch,
|
|
160
|
-
duplicateSessionIdCount,
|
|
161
|
-
compatibilityIssues,
|
|
162
|
-
queryCompatible: Object.values(compatibilityIssues).every((count) => count === 0),
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
16
|
function safeError(error) {
|
|
167
17
|
return {
|
|
168
18
|
name: error instanceof Error ? error.name : 'Error',
|
|
@@ -170,148 +20,54 @@ function safeError(error) {
|
|
|
170
20
|
}
|
|
171
21
|
}
|
|
172
22
|
|
|
173
|
-
export function createSessionIndexService({
|
|
174
|
-
repository
|
|
175
|
-
readBuckets,
|
|
176
|
-
now = () => new Date().toISOString(),
|
|
177
|
-
nowMs = () => Date.now(),
|
|
178
|
-
verifyTtlMs = DEFAULT_VERIFY_TTL_MS,
|
|
179
|
-
maxRebuildAttempts = DEFAULT_MAX_REBUILD_ATTEMPTS,
|
|
180
|
-
log = logger,
|
|
181
|
-
} = {}) {
|
|
182
|
-
if (!repository || typeof repository.replaceAll !== 'function' || typeof repository.applyChanges !== 'function') {
|
|
23
|
+
export function createSessionIndexService({ repository, log = logger } = {}) {
|
|
24
|
+
if (!repository || typeof repository.listPage !== 'function' || typeof repository.analyzeQuery !== 'function') {
|
|
183
25
|
throw new TypeError('Session index service requires a repository')
|
|
184
26
|
}
|
|
185
|
-
if (typeof readBuckets !== 'function') throw new TypeError('Session index service requires a bucket reader')
|
|
186
|
-
|
|
187
|
-
let rebuildPromise = null
|
|
188
|
-
const emptyDigest = sessionIndexAggregateDigest([])
|
|
189
27
|
const diagnostics = {
|
|
190
28
|
status: 'uninitialized',
|
|
191
29
|
initialized: false,
|
|
192
|
-
degraded:
|
|
193
|
-
dirty:
|
|
30
|
+
degraded: false,
|
|
31
|
+
dirty: false,
|
|
194
32
|
sourceCount: 0,
|
|
195
|
-
sourceDigest: emptyDigest,
|
|
196
33
|
indexCount: 0,
|
|
197
|
-
indexDigest: emptyDigest,
|
|
198
34
|
count: 0,
|
|
199
|
-
digest: emptyDigest,
|
|
200
35
|
lastVerifiedAt: null,
|
|
201
36
|
rebuildCount: 0,
|
|
202
37
|
rebuildGeneration: 0,
|
|
203
|
-
keyIdMismatchCount: 0,
|
|
204
38
|
duplicateSessionIdCount: 0,
|
|
205
|
-
queryCompatible:
|
|
206
|
-
compatibilityIssues: null,
|
|
39
|
+
queryCompatible: true,
|
|
207
40
|
lastFailure: null,
|
|
208
41
|
}
|
|
209
42
|
|
|
210
|
-
|
|
211
|
-
return
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function indexedVerification() {
|
|
215
|
-
const entries = repository.listVerification()
|
|
216
|
-
return { count: entries.length, digest: sessionIndexAggregateDigest(entries) }
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function projectionIntegrity() {
|
|
220
|
-
if (typeof repository.listIntegrity !== 'function') return true
|
|
221
|
-
return repository.listIntegrity().every((row) => {
|
|
222
|
-
const metadata = row.metadata
|
|
223
|
-
if (!isPlainObject(metadata)) return false
|
|
224
|
-
if (row.isArchived !== Boolean(metadata.archivedAt)) return false
|
|
225
|
-
if (row.isPinned !== Boolean(metadata.pinnedAt)) return false
|
|
226
|
-
if (row.createdAt !== nullableString(metadata.createdAt)) return false
|
|
227
|
-
if (row.lastModified !== nullableString(metadata.lastModified)) return false
|
|
228
|
-
if (row.pinnedAt !== nullableString(metadata.pinnedAt)) return false
|
|
229
|
-
if (row.archivedAt !== nullableString(metadata.archivedAt)) return false
|
|
230
|
-
if (row.messageCount !== nullableInteger(metadata.messageCount)) return false
|
|
231
|
-
return true
|
|
232
|
-
})
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function setVerification(snapshot, verification, { dirty = false } = {}) {
|
|
236
|
-
diagnostics.sourceCount = snapshot.count
|
|
237
|
-
diagnostics.sourceDigest = snapshot.digest
|
|
238
|
-
diagnostics.indexCount = verification.count
|
|
239
|
-
diagnostics.indexDigest = verification.digest
|
|
240
|
-
diagnostics.count = snapshot.count
|
|
241
|
-
diagnostics.digest = snapshot.digest
|
|
242
|
-
diagnostics.keyIdMismatchCount = snapshot.keyIdMismatchCount
|
|
243
|
-
diagnostics.duplicateSessionIdCount = snapshot.duplicateSessionIdCount
|
|
244
|
-
diagnostics.queryCompatible = snapshot.queryCompatible
|
|
245
|
-
diagnostics.compatibilityIssues = { ...snapshot.compatibilityIssues }
|
|
246
|
-
diagnostics.dirty = dirty
|
|
247
|
-
diagnostics.degraded = dirty || !snapshot.queryCompatible
|
|
248
|
-
diagnostics.status = diagnostics.degraded ? 'degraded' : 'ready'
|
|
249
|
-
diagnostics.lastVerifiedAt = now()
|
|
250
|
-
if (!diagnostics.degraded) diagnostics.lastFailure = null
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function rebuildInternal(initialSnapshot = null) {
|
|
254
|
-
let snapshot = initialSnapshot
|
|
255
|
-
for (let attempt = 1; attempt <= maxRebuildAttempts; attempt += 1) {
|
|
256
|
-
snapshot ||= await readSnapshot()
|
|
257
|
-
repository.replaceAll(snapshot.rows)
|
|
258
|
-
const afterSource = await readSnapshot()
|
|
259
|
-
if (afterSource.count !== snapshot.count || afterSource.digest !== snapshot.digest) {
|
|
260
|
-
snapshot = afterSource
|
|
261
|
-
continue
|
|
262
|
-
}
|
|
263
|
-
const verification = indexedVerification()
|
|
264
|
-
if (verification.count !== snapshot.count || verification.digest !== snapshot.digest) {
|
|
265
|
-
snapshot = afterSource
|
|
266
|
-
continue
|
|
267
|
-
}
|
|
268
|
-
diagnostics.rebuildCount += 1
|
|
269
|
-
diagnostics.rebuildGeneration += 1
|
|
270
|
-
setVerification(snapshot, verification)
|
|
271
|
-
return { ok: true, rebuilt: true, attempts: attempt, ...getDiagnostics() }
|
|
272
|
-
}
|
|
273
|
-
throw new Error(`Session index source changed during ${maxRebuildAttempts} rebuild attempts`)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function rebuild(initialSnapshot = null) {
|
|
277
|
-
if (rebuildPromise) return rebuildPromise
|
|
278
|
-
rebuildPromise = rebuildInternal(initialSnapshot).catch((error) => {
|
|
279
|
-
diagnostics.dirty = true
|
|
280
|
-
diagnostics.degraded = true
|
|
281
|
-
diagnostics.status = 'degraded'
|
|
282
|
-
diagnostics.lastFailure = safeError(error)
|
|
283
|
-
log.warn('Session index rebuild failed; JSON metadata remains authoritative.', {
|
|
284
|
-
operation: 'session_index_rebuild',
|
|
285
|
-
error: diagnostics.lastFailure,
|
|
286
|
-
})
|
|
287
|
-
throw error
|
|
288
|
-
}).finally(() => { rebuildPromise = null })
|
|
289
|
-
return rebuildPromise
|
|
43
|
+
function getDiagnostics() {
|
|
44
|
+
return { ...diagnostics }
|
|
290
45
|
}
|
|
291
46
|
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
47
|
+
function readiness() {
|
|
48
|
+
return {
|
|
49
|
+
ready: diagnostics.initialized && !diagnostics.degraded,
|
|
50
|
+
reason: diagnostics.initialized ? null : 'uninitialized',
|
|
51
|
+
...getDiagnostics(),
|
|
297
52
|
}
|
|
298
53
|
}
|
|
299
54
|
|
|
300
55
|
async function initialize() {
|
|
301
56
|
try {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
else setVerification(snapshot, verification)
|
|
57
|
+
diagnostics.count = repository.count()
|
|
58
|
+
diagnostics.sourceCount = diagnostics.count
|
|
59
|
+
diagnostics.indexCount = diagnostics.count
|
|
306
60
|
diagnostics.initialized = true
|
|
307
|
-
|
|
61
|
+
diagnostics.status = 'ready'
|
|
62
|
+
diagnostics.lastVerifiedAt = new Date().toISOString()
|
|
63
|
+
return { ok: true, rebuilt: false, ...getDiagnostics() }
|
|
308
64
|
} catch (error) {
|
|
309
65
|
diagnostics.initialized = true
|
|
310
|
-
diagnostics.dirty = true
|
|
311
66
|
diagnostics.degraded = true
|
|
67
|
+
diagnostics.dirty = true
|
|
312
68
|
diagnostics.status = 'degraded'
|
|
313
69
|
diagnostics.lastFailure = safeError(error)
|
|
314
|
-
log.warn('Session index initialization
|
|
70
|
+
log.warn('Session index initialization failed; queries will fall back.', {
|
|
315
71
|
operation: 'session_index_initialize',
|
|
316
72
|
error: diagnostics.lastFailure,
|
|
317
73
|
})
|
|
@@ -319,147 +75,62 @@ export function createSessionIndexService({
|
|
|
319
75
|
}
|
|
320
76
|
}
|
|
321
77
|
|
|
322
|
-
async function verifyIntegrity(
|
|
78
|
+
async function verifyIntegrity() {
|
|
323
79
|
if (!diagnostics.initialized) return { ready: false, reason: 'uninitialized', ...getDiagnostics() }
|
|
324
|
-
|
|
325
|
-
if (!force && Number.isFinite(lastVerifiedMs) && nowMs() - lastVerifiedMs < verifyTtlMs) {
|
|
326
|
-
return { ready: !diagnostics.dirty && !diagnostics.degraded && diagnostics.queryCompatible, ...getDiagnostics() }
|
|
327
|
-
}
|
|
328
|
-
try {
|
|
329
|
-
const snapshot = await readSnapshot()
|
|
330
|
-
const verification = indexedVerification()
|
|
331
|
-
const projectionValid = projectionIntegrity()
|
|
332
|
-
const matches = verification.count === snapshot.count && verification.digest === snapshot.digest && projectionValid
|
|
333
|
-
setVerification(snapshot, verification, { dirty: !matches })
|
|
334
|
-
if (!matches) scheduleRebuild()
|
|
335
|
-
return {
|
|
336
|
-
ready: matches && snapshot.queryCompatible,
|
|
337
|
-
reason: !projectionValid ? 'projection_mismatch' : !matches ? 'digest_mismatch' : snapshot.queryCompatible ? null : 'source_incompatible',
|
|
338
|
-
...getDiagnostics(),
|
|
339
|
-
}
|
|
340
|
-
} catch (error) {
|
|
341
|
-
diagnostics.dirty = true
|
|
342
|
-
diagnostics.degraded = true
|
|
343
|
-
diagnostics.status = 'degraded'
|
|
344
|
-
diagnostics.lastFailure = safeError(error)
|
|
345
|
-
scheduleRebuild()
|
|
346
|
-
return { ready: false, reason: 'verify_failed', ...getDiagnostics() }
|
|
347
|
-
}
|
|
80
|
+
return readiness()
|
|
348
81
|
}
|
|
349
82
|
|
|
350
|
-
async function
|
|
351
|
-
|
|
352
|
-
if (!isPlainObject(previous) || !isPlainObject(next)) throw new TypeError('previous and next metadata buckets must be plain objects')
|
|
353
|
-
if (!diagnostics.initialized) return { ok: true, skipped: true }
|
|
354
|
-
diagnostics.lastVerifiedAt = null
|
|
83
|
+
async function queryPage(options) {
|
|
84
|
+
if (!diagnostics.initialized) return { ok: false, reason: 'uninitialized', readiness: getDiagnostics() }
|
|
355
85
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const nextMetadata = next[sessionId]
|
|
363
|
-
if (nextMetadata === undefined) {
|
|
364
|
-
deletes.push({ ...bucket, sessionId })
|
|
365
|
-
continue
|
|
366
|
-
}
|
|
367
|
-
const nextRow = buildRow(bucket, sessionId, nextMetadata, indexedAt)
|
|
368
|
-
if (previousMetadata !== undefined) {
|
|
369
|
-
const previousRow = buildRow(bucket, sessionId, previousMetadata, indexedAt)
|
|
370
|
-
if (previousRow.metadataDigest === nextRow.metadataDigest) continue
|
|
371
|
-
}
|
|
372
|
-
upserts.push(nextRow)
|
|
373
|
-
}
|
|
374
|
-
repository.applyChanges({ upserts, deletes })
|
|
375
|
-
const verification = indexedVerification()
|
|
376
|
-
diagnostics.indexCount = verification.count
|
|
377
|
-
diagnostics.indexDigest = verification.digest
|
|
378
|
-
diagnostics.count = verification.count
|
|
379
|
-
diagnostics.digest = verification.digest
|
|
380
|
-
diagnostics.sourceCount = verification.count
|
|
381
|
-
diagnostics.sourceDigest = verification.digest
|
|
382
|
-
if (!diagnostics.dirty) diagnostics.lastFailure = null
|
|
383
|
-
return { ok: true, upserted: upserts.length, deleted: deletes.length, dirty: diagnostics.dirty }
|
|
86
|
+
// Duplicate session ids and complete sort-key ties make LIMIT/OFFSET
|
|
87
|
+
// pagination unstable; the route falls back to its legacy sorted read.
|
|
88
|
+
const analysis = repository.analyzeQuery(options)
|
|
89
|
+
if (analysis.duplicateSessionIdCount > 0) return { ok: false, reason: 'duplicate_session_id', analysis, readiness: readiness() }
|
|
90
|
+
if (analysis.fullSortKeyTieCount > 0) return { ok: false, reason: 'sort_key_tie', analysis, readiness: readiness() }
|
|
91
|
+
return { ok: true, page: repository.listPage(options), analysis, readiness: readiness() }
|
|
384
92
|
} catch (error) {
|
|
385
93
|
diagnostics.dirty = true
|
|
386
94
|
diagnostics.degraded = true
|
|
387
95
|
diagnostics.status = 'degraded'
|
|
388
96
|
diagnostics.lastFailure = safeError(error)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
operation: 'session_index_sync',
|
|
392
|
-
scope: bucket.scope,
|
|
393
|
-
hasProjectId: bucket.scope === 'project',
|
|
97
|
+
log.warn('Session index query failed; route falls back.', {
|
|
98
|
+
operation: 'session_index_query',
|
|
394
99
|
error: diagnostics.lastFailure,
|
|
395
100
|
})
|
|
396
|
-
return { ok: false,
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function markQueryFailure(reason, error = null) {
|
|
401
|
-
diagnostics.dirty = true
|
|
402
|
-
diagnostics.degraded = true
|
|
403
|
-
diagnostics.status = 'degraded'
|
|
404
|
-
diagnostics.lastFailure = error ? safeError(error) : { name: 'SessionIndexQueryMismatch', code: reason }
|
|
405
|
-
log.warn('Session index query degraded; JSON metadata remains authoritative.', {
|
|
406
|
-
operation: 'session_index_query',
|
|
407
|
-
reason,
|
|
408
|
-
error: error ? diagnostics.lastFailure : null,
|
|
409
|
-
})
|
|
410
|
-
scheduleRebuild()
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
async function queryPage(options) {
|
|
414
|
-
const readiness = await verifyIntegrity()
|
|
415
|
-
if (!readiness.ready) {
|
|
416
|
-
return {
|
|
417
|
-
ok: false,
|
|
418
|
-
reason: readiness.reason || (readiness.queryCompatible === false ? 'source_incompatible' : 'not_ready'),
|
|
419
|
-
readiness,
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
try {
|
|
423
|
-
const analysis = repository.analyzeQuery(options)
|
|
424
|
-
if (analysis.duplicateSessionIdCount > 0) return { ok: false, reason: 'duplicate_session_id', analysis, readiness }
|
|
425
|
-
if (analysis.fullSortKeyTieCount > 0) return { ok: false, reason: 'sort_key_tie', analysis, readiness }
|
|
426
|
-
return { ok: true, page: repository.listPage(options), analysis, readiness }
|
|
427
|
-
} catch (error) {
|
|
428
|
-
markQueryFailure('repository_error', error)
|
|
429
|
-
return { ok: false, reason: 'repository_error', error: safeError(error), readiness: getDiagnostics() }
|
|
101
|
+
return { ok: false, reason: 'repository_error', error: diagnostics.lastFailure, readiness: getDiagnostics() }
|
|
430
102
|
}
|
|
431
103
|
}
|
|
432
104
|
|
|
433
|
-
|
|
434
|
-
|
|
105
|
+
// Storage v2: the index IS the sessions table, so metadata commits have
|
|
106
|
+
// nothing to mirror. Kept for the storage.mjs commit-hook wiring contract.
|
|
107
|
+
async function syncMetadataCommit() {
|
|
108
|
+
return { ok: true, skipped: true }
|
|
435
109
|
}
|
|
436
110
|
|
|
437
|
-
function
|
|
438
|
-
|
|
111
|
+
function markQueryFailure() {
|
|
112
|
+
// No-op: there is no separate projection to mark dirty.
|
|
439
113
|
}
|
|
440
114
|
|
|
441
115
|
return Object.freeze({
|
|
442
|
-
|
|
443
|
-
syncMetadataCommit, getDiagnostics,
|
|
116
|
+
initialize, verifyIntegrity, queryPage, syncMetadataCommit, markQueryFailure, getDiagnostics,
|
|
444
117
|
})
|
|
445
118
|
}
|
|
446
119
|
|
|
447
|
-
export function configureSessionIndex({
|
|
448
|
-
if (
|
|
449
|
-
defaultReader = readBuckets ?? defaultReader
|
|
120
|
+
export function configureSessionIndex({ repository } = {}) {
|
|
121
|
+
if (repository !== undefined && repository !== null && typeof repository !== 'object') throw new TypeError('repository must be an object')
|
|
450
122
|
defaultRepository = repository ?? defaultRepository
|
|
451
123
|
}
|
|
452
124
|
|
|
453
125
|
export async function initializeSessionIndex(options = {}) {
|
|
454
|
-
const readBuckets = options.readBuckets ?? defaultReader
|
|
455
126
|
const repository = options.repository ?? defaultRepository ?? createSessionIndexRepository(getSqliteStorage())
|
|
456
|
-
activeService = createSessionIndexService({ ...options, repository
|
|
127
|
+
activeService = createSessionIndexService({ ...options, repository })
|
|
457
128
|
return activeService.initialize()
|
|
458
129
|
}
|
|
459
130
|
|
|
460
|
-
export async function syncSessionMetadataCommit(
|
|
131
|
+
export async function syncSessionMetadataCommit() {
|
|
461
132
|
if (!activeService) return { ok: true, skipped: true }
|
|
462
|
-
return activeService.syncMetadataCommit(
|
|
133
|
+
return activeService.syncMetadataCommit()
|
|
463
134
|
}
|
|
464
135
|
|
|
465
136
|
export async function querySessionIndexPage(options) {
|
|
@@ -467,29 +138,24 @@ export async function querySessionIndexPage(options) {
|
|
|
467
138
|
return activeService.queryPage(options)
|
|
468
139
|
}
|
|
469
140
|
|
|
470
|
-
export function markSessionIndexQueryFailure(
|
|
471
|
-
activeService?.markQueryFailure(
|
|
141
|
+
export function markSessionIndexQueryFailure() {
|
|
142
|
+
activeService?.markQueryFailure()
|
|
472
143
|
}
|
|
473
144
|
|
|
474
145
|
export function getSessionIndexDiagnostics() {
|
|
475
146
|
return activeService?.getDiagnostics() ?? {
|
|
476
147
|
status: 'uninitialized',
|
|
477
148
|
initialized: false,
|
|
478
|
-
degraded:
|
|
479
|
-
dirty:
|
|
149
|
+
degraded: false,
|
|
150
|
+
dirty: false,
|
|
480
151
|
sourceCount: 0,
|
|
481
|
-
sourceDigest: sessionIndexAggregateDigest([]),
|
|
482
152
|
indexCount: 0,
|
|
483
|
-
indexDigest: sessionIndexAggregateDigest([]),
|
|
484
153
|
count: 0,
|
|
485
|
-
digest: sessionIndexAggregateDigest([]),
|
|
486
154
|
lastVerifiedAt: null,
|
|
487
155
|
rebuildCount: 0,
|
|
488
156
|
rebuildGeneration: 0,
|
|
489
|
-
keyIdMismatchCount: 0,
|
|
490
157
|
duplicateSessionIdCount: 0,
|
|
491
|
-
queryCompatible:
|
|
492
|
-
compatibilityIssues: null,
|
|
158
|
+
queryCompatible: true,
|
|
493
159
|
lastFailure: null,
|
|
494
160
|
}
|
|
495
161
|
}
|
|
@@ -6,14 +6,15 @@ import { createSessionStateRepository, messagesDigestFromValues, snapshotDigestL
|
|
|
6
6
|
import {
|
|
7
7
|
currentSessionStateMaintenanceContext,
|
|
8
8
|
runSessionStateMaintenance,
|
|
9
|
-
} from './session-state-
|
|
9
|
+
} from './session-state-maintenance.mjs'
|
|
10
10
|
import {
|
|
11
|
-
getSessionStoragePhase,
|
|
12
11
|
isSessionStateAuthoritative,
|
|
13
12
|
normalizeSessionSnapshotValues,
|
|
13
|
+
readSessionStorageState,
|
|
14
14
|
replaceSessionStateSnapshot,
|
|
15
15
|
} from './session-state-service.mjs'
|
|
16
16
|
import { storageDir } from './storage.mjs'
|
|
17
|
+
import { logger } from './utils/logger.mjs'
|
|
17
18
|
|
|
18
19
|
const RESTORE_PLAN_FILE = path.join(storageDir, 'session-state-restore-plan.json')
|
|
19
20
|
const ROLL_FORWARD_STATUSES = new Set(['prepared', 'applying', 'target_applied'])
|
|
@@ -95,13 +96,49 @@ export async function exportSessionStateForBackup(options = {}) {
|
|
|
95
96
|
...snapshotValues(snapshot),
|
|
96
97
|
count: snapshot.count,
|
|
97
98
|
digest: snapshot.digest,
|
|
98
|
-
phase:
|
|
99
|
+
phase: readSessionStorageState().phase,
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
if (!options.maintenance || currentSessionStateMaintenanceContext()) return operation()
|
|
102
103
|
return runSessionStateMaintenance(operation, { storage: options.storage, operation: 'session-state-backup-export' })
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Manual integrity verification entry point for the maintenance route
|
|
108
|
+
* (design review suggestion 9): the startup chain only runs the lightweight
|
|
109
|
+
* SQL-level check, so silent per-row digest rot would stay invisible until
|
|
110
|
+
* an offline full verification. `{ full: true }` recomputes every stored
|
|
111
|
+
* digest (quickCheck: false); the default is the lightweight check. Runs
|
|
112
|
+
* under the session state maintenance lock (same pattern as the backup
|
|
113
|
+
* export above) so the scan serializes with cutover/restore, and returns
|
|
114
|
+
* only summary counters plus the elapsed time — never row payloads.
|
|
115
|
+
*/
|
|
116
|
+
export async function verifySessionStateIntegrityForMaintenance(options = {}) {
|
|
117
|
+
const operation = async () => {
|
|
118
|
+
const repository = options.repository || repositoryRequired()
|
|
119
|
+
const startedAt = Date.now()
|
|
120
|
+
// forceQuickCheck: a manual maintenance trigger must run a real quick_check
|
|
121
|
+
// scan, never the process-cache/marker shortcut.
|
|
122
|
+
const integrity = repository.verifyIntegrity({ quickCheck: options.full !== true, forceQuickCheck: options.forceQuickCheck === true })
|
|
123
|
+
return { ...integrity, full: options.full === true, durationMs: Date.now() - startedAt }
|
|
124
|
+
}
|
|
125
|
+
if (!options.maintenance || currentSessionStateMaintenanceContext()) return operation()
|
|
126
|
+
return runSessionStateMaintenance(operation, { storage: options.storage, operation: 'session-state-verify-integrity' })
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// A restore rewrites every session row inside one transaction, so the WAL
|
|
130
|
+
// grows by roughly the library size before converging; truncating right
|
|
131
|
+
// after success reclaims that space (same rationale as the post-promote
|
|
132
|
+
// checkpoint in the cutover). Best-effort: a busy or failed checkpoint
|
|
133
|
+
// leaves the WAL to a later auto-checkpoint and must never fail the restore.
|
|
134
|
+
function checkpointWalAfterRestore(repository, operation) {
|
|
135
|
+
try {
|
|
136
|
+
repository.checkpointWal()
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger.warn('Session state WAL checkpoint failed after restore', { domain: 'session-state', operation, errorName: error?.name || 'Error', errorMessage: error?.message })
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
105
142
|
async function writePlan(plan, planFile = RESTORE_PLAN_FILE) {
|
|
106
143
|
await fs.mkdir(path.dirname(planFile), { recursive: true })
|
|
107
144
|
const temporary = `${planFile}.${process.pid}.${randomUUID()}.tmp`
|
|
@@ -214,6 +251,7 @@ export async function restoreSessionStateSnapshot(values, options = {}) {
|
|
|
214
251
|
plan = { ...plan, status: 'target_applied' }
|
|
215
252
|
await writePlan(plan, planFile)
|
|
216
253
|
await fs.rm(planFile, { force: true })
|
|
254
|
+
checkpointWalAfterRestore(repository, 'session-state-restore')
|
|
217
255
|
return { sessions: targetCount, sessionsMetadata: targetCount }
|
|
218
256
|
} catch (error) {
|
|
219
257
|
try {
|
|
@@ -239,7 +277,12 @@ export async function restoreSessionStateSnapshot(values, options = {}) {
|
|
|
239
277
|
}
|
|
240
278
|
throw error
|
|
241
279
|
}
|
|
242
|
-
}, {
|
|
280
|
+
}, {
|
|
281
|
+
storage: options.storage,
|
|
282
|
+
operation: 'session-state-restore',
|
|
283
|
+
waitTimeoutMs: options.waitTimeoutMs,
|
|
284
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
285
|
+
})
|
|
243
286
|
}
|
|
244
287
|
|
|
245
288
|
/**
|
|
@@ -251,6 +294,8 @@ export async function recoverSessionStateRestorePlan({
|
|
|
251
294
|
repository: repositoryOverride,
|
|
252
295
|
planFile = RESTORE_PLAN_FILE,
|
|
253
296
|
storage,
|
|
297
|
+
waitTimeoutMs,
|
|
298
|
+
pollIntervalMs,
|
|
254
299
|
} = {}) {
|
|
255
300
|
let plan
|
|
256
301
|
try {
|
|
@@ -274,6 +319,12 @@ export async function recoverSessionStateRestorePlan({
|
|
|
274
319
|
const integrity = repository.verifyIntegrity()
|
|
275
320
|
if (!integrity.ok) throw new Error('Session state restore plan recovery integrity verification failed')
|
|
276
321
|
await fs.rm(planFile, { force: true })
|
|
322
|
+
checkpointWalAfterRestore(repository, 'session-state-restore-recovery')
|
|
277
323
|
return true
|
|
278
|
-
}, {
|
|
324
|
+
}, {
|
|
325
|
+
storage,
|
|
326
|
+
operation: 'session-state-restore-recovery',
|
|
327
|
+
waitTimeoutMs,
|
|
328
|
+
pollIntervalMs,
|
|
329
|
+
})
|
|
279
330
|
}
|