@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,3 +1,4 @@
|
|
|
1
|
+
import { readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
|
|
1
2
|
import { mkdir } from 'node:fs/promises'
|
|
2
3
|
import os from 'node:os'
|
|
3
4
|
import path from 'node:path'
|
|
@@ -7,10 +8,28 @@ import { applySqliteMigrations, inspectSqliteMigrationState, SQLITE_MIGRATIONS }
|
|
|
7
8
|
|
|
8
9
|
export const SQLITE_BUSY_TIMEOUT_MS = 5_000
|
|
9
10
|
export const SQLITE_JOURNAL_MODE = 'wal'
|
|
10
|
-
export const SQLITE_SYNCHRONOUS =
|
|
11
|
+
export const SQLITE_SYNCHRONOUS = 2
|
|
12
|
+
|
|
13
|
+
// Startup quick_check tax optimization. All four storage domains share one
|
|
14
|
+
// quickforge.sqlite3 file, and `PRAGMA quick_check` scans the whole file, so
|
|
15
|
+
// four per-domain startup checks used to pay the full-scan cost four times
|
|
16
|
+
// (~30s on a 3GB production library). One gated scan per database per process
|
|
17
|
+
// (sharedQuickCheckCache) plus a cross-restart marker file reduce it to at
|
|
18
|
+
// most one real scan per database per 7 days.
|
|
19
|
+
//
|
|
20
|
+
// Safety argument: WAL + synchronous=FULL make committed transactions immune
|
|
21
|
+
// to torn writes (a crash either replays or rolls back the WAL). quick_check
|
|
22
|
+
// therefore only guards against bit-rot / filesystem-level corruption, which
|
|
23
|
+
// evolves on disk timescales, not per startup; the 7-day cadence plus the
|
|
24
|
+
// manual force entry points (maintenance verify-session-integrity endpoint,
|
|
25
|
+
// QUICKFORGE_SQLITE_QUICK_CHECK=force) keep the blind window bounded. When a
|
|
26
|
+
// quick_check is skipped, all other SQL-level count/join checks still run.
|
|
27
|
+
export const SQLITE_QUICK_CHECK_MARKER_FILENAME = 'quickforge-quick-check.marker.json'
|
|
28
|
+
export const SQLITE_QUICK_CHECK_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000
|
|
11
29
|
|
|
12
30
|
const sqliteLogger = logger.child({ component: 'sqlite' })
|
|
13
31
|
const TRANSACTION_MODES = new Set(['deferred', 'immediate', 'exclusive'])
|
|
32
|
+
const sharedQuickCheckCache = new Map()
|
|
14
33
|
let initializationPromise = null
|
|
15
34
|
let openState = null
|
|
16
35
|
let savepointCounter = 0
|
|
@@ -65,22 +84,138 @@ function verifyPragmas(database) {
|
|
|
65
84
|
|
|
66
85
|
function configurePragmas(database) {
|
|
67
86
|
database.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`)
|
|
87
|
+
// Session-domain deletes reclaim disk pages through incremental_vacuum (see
|
|
88
|
+
// the session-state repository), which requires auto_vacuum = INCREMENTAL.
|
|
89
|
+
// The flag only takes effect on a freshly created (empty) database; an
|
|
90
|
+
// existing library keeps its current layout until a full VACUUM rebuilds
|
|
91
|
+
// it, so applying it here is best-effort for old databases. PRAGMA cannot
|
|
92
|
+
// run inside a transaction — this executes before applySqliteMigrations
|
|
93
|
+
// opens its BEGIN IMMEDIATE — and it must come AFTER busy_timeout: during
|
|
94
|
+
// a multi-process first initialization another worker can already hold the
|
|
95
|
+
// fresh database inside its migration transaction, and without the busy
|
|
96
|
+
// timeout this pragma would fail immediately with SQLITE_BUSY.
|
|
97
|
+
database.exec('PRAGMA auto_vacuum = INCREMENTAL')
|
|
68
98
|
database.exec('PRAGMA foreign_keys = ON')
|
|
69
99
|
database.exec('PRAGMA journal_mode = WAL')
|
|
70
|
-
|
|
100
|
+
// FULL (not NORMAL): decision finalized in docs/architecture/sqlite-storage-foundation.zh-CN.md §3.1
|
|
101
|
+
// — measured cost is ~0.46 ms/op on the save hot path, in exchange for zero committed-transaction
|
|
102
|
+
// loss on OS crash/power loss.
|
|
103
|
+
database.exec('PRAGMA synchronous = FULL')
|
|
71
104
|
return verifyPragmas(database)
|
|
72
105
|
}
|
|
73
106
|
|
|
107
|
+
function quickCheckMarkerPath(databasePath) {
|
|
108
|
+
return path.join(path.dirname(databasePath), SQLITE_QUICK_CHECK_MARKER_FILENAME)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Resolve the main database file path from any storage surface (raw
|
|
112
|
+
// DatabaseSync, the controlled handle, or test duck-typed handles). Returns
|
|
113
|
+
// null for in-memory databases, where no marker is possible and every call
|
|
114
|
+
// runs a real scan.
|
|
115
|
+
function resolveQuickCheckDatabasePath(database) {
|
|
116
|
+
try {
|
|
117
|
+
const rows = database.prepare('PRAGMA database_list').all()
|
|
118
|
+
const main = Array.isArray(rows) ? rows.find((row) => row && row.name === 'main') : null
|
|
119
|
+
const file = typeof main?.file === 'string' ? main.file.trim() : ''
|
|
120
|
+
return file ? path.resolve(file) : null
|
|
121
|
+
} catch {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Marker read failures (missing, unreadable, malformed) are treated as "no
|
|
127
|
+
// marker": the caller falls through to a real scan.
|
|
128
|
+
function readQuickCheckMarker(databasePath) {
|
|
129
|
+
try {
|
|
130
|
+
const marker = JSON.parse(readFileSync(quickCheckMarkerPath(databasePath), 'utf8'))
|
|
131
|
+
const lastOkAt = Number(marker?.lastOkAt)
|
|
132
|
+
return Number.isFinite(lastOkAt) ? { ...marker, lastOkAt } : null
|
|
133
|
+
} catch {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Best-effort atomic marker write: failures only warn, never affect the
|
|
139
|
+
// quick_check verdict.
|
|
140
|
+
function writeQuickCheckMarker(databasePath, okAt, database) {
|
|
141
|
+
try {
|
|
142
|
+
const markerPath = quickCheckMarkerPath(databasePath)
|
|
143
|
+
const temporaryPath = `${markerPath}.${process.pid}.tmp`
|
|
144
|
+
let sqliteVersion = null
|
|
145
|
+
try {
|
|
146
|
+
sqliteVersion = String(database.prepare('SELECT sqlite_version() AS version').get().version)
|
|
147
|
+
} catch { /* Optional diagnostic field. */ }
|
|
148
|
+
let databaseBytes = null
|
|
149
|
+
try {
|
|
150
|
+
databaseBytes = statSync(databasePath).size
|
|
151
|
+
} catch { /* Optional diagnostic field. */ }
|
|
152
|
+
writeFileSync(temporaryPath, `${JSON.stringify({ app: 'quickforge', lastOkAt: okAt, sqliteVersion, databaseBytes })}\n`, 'utf8')
|
|
153
|
+
renameSync(temporaryPath, markerPath)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
sqliteLogger.warn('SQLite quick_check marker write failed', {
|
|
156
|
+
error: error instanceof Error ? error.message : String(error),
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Shared startup `PRAGMA quick_check` gate (see the block comment above for
|
|
163
|
+
* the rationale and safety argument). Semantics:
|
|
164
|
+
* - In-process: one successful scan per database file path is reused for the
|
|
165
|
+
* process lifetime (unless `maxAgeMs` elapses or `resetSharedQuickCheckCache`
|
|
166
|
+
* is called); `force` skips this reuse but still refreshes it after success.
|
|
167
|
+
* - Cross-restart: a marker file next to the database (`lastOkAt`) skips the
|
|
168
|
+
* real scan while younger than `maxAgeMs` (default 7 days). Missing or
|
|
169
|
+
* unreadable markers are treated as absent.
|
|
170
|
+
* - On a real scan success the marker is rewritten (atomic, best-effort).
|
|
171
|
+
* - On failure the quick_check error is re-thrown as-is: no marker is written
|
|
172
|
+
* and the process cache never caches failures, so the next call scans again.
|
|
173
|
+
* - QUICKFORGE_SQLITE_QUICK_CHECK=force forces a real scan every call.
|
|
174
|
+
* Returns `{ ok: true, skipped: null | 'process' | 'marker', lastOkAt? }`.
|
|
175
|
+
*/
|
|
176
|
+
export function runSharedSqliteQuickCheck(database, {
|
|
177
|
+
force = false, maxAgeMs = SQLITE_QUICK_CHECK_MAX_AGE_MS, now = Date.now, writeMarker = true, databasePath = null,
|
|
178
|
+
} = {}) {
|
|
179
|
+
const forced = force === true || process.env.QUICKFORGE_SQLITE_QUICK_CHECK === 'force'
|
|
180
|
+
const resolvedPath = databasePath ? path.resolve(databasePath) : resolveQuickCheckDatabasePath(database)
|
|
181
|
+
if (!forced && resolvedPath) {
|
|
182
|
+
const cached = sharedQuickCheckCache.get(resolvedPath)
|
|
183
|
+
if (cached && now() - cached.okAt < maxAgeMs) {
|
|
184
|
+
return { ok: true, skipped: 'process', lastOkAt: cached.okAt }
|
|
185
|
+
}
|
|
186
|
+
const marker = readQuickCheckMarker(resolvedPath)
|
|
187
|
+
if (marker && now() - marker.lastOkAt < maxAgeMs) {
|
|
188
|
+
sharedQuickCheckCache.set(resolvedPath, { okAt: marker.lastOkAt })
|
|
189
|
+
return { ok: true, skipped: 'marker', lastOkAt: marker.lastOkAt }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const result = database.prepare('PRAGMA quick_check').all().map((row) => row.quick_check)
|
|
193
|
+
if (result.length !== 1 || result[0] !== 'ok') {
|
|
194
|
+
throw new Error(`SQLite quick_check failed: ${result.join(', ')}`)
|
|
195
|
+
}
|
|
196
|
+
const okAt = now()
|
|
197
|
+
if (resolvedPath) sharedQuickCheckCache.set(resolvedPath, { okAt })
|
|
198
|
+
if (writeMarker !== false && resolvedPath) {
|
|
199
|
+
writeQuickCheckMarker(resolvedPath, okAt, database)
|
|
200
|
+
}
|
|
201
|
+
return { ok: true, skipped: null, lastOkAt: okAt }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Test helper: clears the in-process quick_check cache, marker files stay. */
|
|
205
|
+
export function resetSharedQuickCheckCache() {
|
|
206
|
+
sharedQuickCheckCache.clear()
|
|
207
|
+
}
|
|
208
|
+
|
|
74
209
|
function publicHealth(state, { quickCheck = false } = {}) {
|
|
75
210
|
const alive = Number(state.database.prepare('SELECT 1 AS ok').get().ok) === 1
|
|
76
211
|
const pragmas = verifyPragmas(state.database)
|
|
77
212
|
const migration = inspectSqliteMigrationState(state.database, { migrations: state.migrations })
|
|
78
213
|
let quickCheckResult = null
|
|
79
214
|
if (quickCheck) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
215
|
+
// Shared gate: process-level dedupe + marker-based cadence; throws on
|
|
216
|
+
// failure exactly like the previous inline PRAGMA quick_check did.
|
|
217
|
+
runSharedSqliteQuickCheck(state.database, { databasePath: state.databasePath })
|
|
218
|
+
quickCheckResult = ['ok']
|
|
84
219
|
}
|
|
85
220
|
return {
|
|
86
221
|
ok: alive && migration.consistent,
|
|
@@ -91,7 +226,7 @@ function publicHealth(state, { quickCheck = false } = {}) {
|
|
|
91
226
|
journalMode: pragmas.journalMode,
|
|
92
227
|
busyTimeout: pragmas.busyTimeout,
|
|
93
228
|
foreignKeys: pragmas.foreignKeys,
|
|
94
|
-
synchronous: '
|
|
229
|
+
synchronous: 'full',
|
|
95
230
|
...(quickCheck ? { quickCheck: quickCheckResult[0] } : {}),
|
|
96
231
|
}
|
|
97
232
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
|
-
import { getSqliteStorage } from './database.mjs'
|
|
2
|
+
import { getSqliteStorage, runSharedSqliteQuickCheck } from './database.mjs'
|
|
3
3
|
import { createRandomToken, safeHashEqual, sha256Base64Url } from '../utils/password-auth.mjs'
|
|
4
4
|
|
|
5
5
|
// F11 LAN access storage: `lan_access_state` (single config row with a fixed
|
|
@@ -544,10 +544,12 @@ export function createLanAccessRepository(storageHandle, { now = () => new Date(
|
|
|
544
544
|
return exportSnapshot().digest
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
-
function verifyIntegrity({ quickCheck = false } = {}) {
|
|
547
|
+
function verifyIntegrity({ quickCheck = false, forceQuickCheck = false } = {}) {
|
|
548
548
|
if (quickCheck) {
|
|
549
|
-
|
|
550
|
-
|
|
549
|
+
// Shared quick_check gate (process cache + marker cadence) — see
|
|
550
|
+
// runSharedSqliteQuickCheck in database.mjs; `forceQuickCheck` is the
|
|
551
|
+
// manual-maintenance escape hatch that always runs a real scan.
|
|
552
|
+
runSharedSqliteQuickCheck(storage, { force: forceQuickCheck === true })
|
|
551
553
|
}
|
|
552
554
|
const row = currentConfigRow(storage)
|
|
553
555
|
let invalidRecords = 0
|
|
@@ -460,6 +460,115 @@ export const SQLITE_MIGRATIONS = Object.freeze([
|
|
|
460
460
|
`)
|
|
461
461
|
},
|
|
462
462
|
}),
|
|
463
|
+
Object.freeze({
|
|
464
|
+
// Root cause: session_states is WITHOUT ROWID, so the PK b-tree IS the
|
|
465
|
+
// table and stores the multi-MB state_json payload inline. metadata_json /
|
|
466
|
+
// metadata_digest sit AFTER state_json in the record, so every metadata
|
|
467
|
+
// read must walk the preceding state_json overflow-page chain — an
|
|
468
|
+
// implicit full-database read (2.93GB cold DB: ~45s for a metadata-only
|
|
469
|
+
// projection). This covering index carries the metadata columns next to
|
|
470
|
+
// the PK prefix, making metadata reads index-only (KB-sized entries); the
|
|
471
|
+
// planner picks it up automatically with zero query changes.
|
|
472
|
+
// Note: CREATE INDEX scans the table once — on first upgrade of a large
|
|
473
|
+
// existing database this migration is a one-time cost paid inside
|
|
474
|
+
// initializeSqliteStorage, before the server starts listening.
|
|
475
|
+
version: 10,
|
|
476
|
+
name: 'session_states_metadata_covering_index',
|
|
477
|
+
up(database) {
|
|
478
|
+
database.exec(`
|
|
479
|
+
CREATE INDEX session_states_metadata_cover_idx
|
|
480
|
+
ON session_states (scope, project_id, session_id, metadata_json, metadata_digest);
|
|
481
|
+
`)
|
|
482
|
+
},
|
|
483
|
+
}),
|
|
484
|
+
Object.freeze({
|
|
485
|
+
// v11 session-domain rebuild (session storage v2): one small `sessions`
|
|
486
|
+
// row per session (no giant state_json column — the multi-GB bloat root
|
|
487
|
+
// cause), messages always stored row-per-message in `session_messages`
|
|
488
|
+
// (append-only, no inline/split dual mode), deletes tombstone into
|
|
489
|
+
// `session_tombstones` and space is reclaimed through incremental_vacuum
|
|
490
|
+
// (auto_vacuum=INCREMENTAL, set in database.mjs). The JSON mirror outbox
|
|
491
|
+
// chain, the session_index derived table and the session_storage_state
|
|
492
|
+
// phase machine are retired with this schema. Old session-domain tables
|
|
493
|
+
// are RENAMED (not dropped) as a rollback safety net; the maintenance
|
|
494
|
+
// lock and the share/lan-access/scheduled-runs domains are untouched.
|
|
495
|
+
// A fresh database runs v1..v11 sequentially, so every renamed table
|
|
496
|
+
// necessarily exists by the time v11 executes. Renamed tables keep their
|
|
497
|
+
// old index names (index names are schema-global), so the new tables use
|
|
498
|
+
// the idx_* naming convention to avoid collisions.
|
|
499
|
+
version: 11,
|
|
500
|
+
name: 'session_state_v2_storage',
|
|
501
|
+
up(database) {
|
|
502
|
+
database.exec(`
|
|
503
|
+
ALTER TABLE session_states RENAME TO session_states_v10_backup;
|
|
504
|
+
ALTER TABLE session_messages RENAME TO session_messages_v10_backup;
|
|
505
|
+
ALTER TABLE session_index RENAME TO session_index_v10_backup;
|
|
506
|
+
ALTER TABLE session_state_tombstones RENAME TO session_state_tombstones_v10_backup;
|
|
507
|
+
ALTER TABLE session_json_mirror_queue RENAME TO session_json_mirror_queue_v10_backup;
|
|
508
|
+
ALTER TABLE session_storage_state RENAME TO session_storage_state_v10_backup;
|
|
509
|
+
|
|
510
|
+
CREATE TABLE sessions (
|
|
511
|
+
scope TEXT NOT NULL CHECK (scope IN ('global', 'project')),
|
|
512
|
+
project_id TEXT NOT NULL,
|
|
513
|
+
session_id TEXT NOT NULL CHECK (length(trim(session_id)) > 0),
|
|
514
|
+
title TEXT NOT NULL DEFAULT '',
|
|
515
|
+
title_source TEXT,
|
|
516
|
+
created_at TEXT NOT NULL CHECK (length(trim(created_at)) > 0),
|
|
517
|
+
updated_at TEXT NOT NULL CHECK (length(trim(updated_at)) > 0),
|
|
518
|
+
message_count INTEGER NOT NULL DEFAULT 0 CHECK (typeof(message_count) = 'integer' AND message_count >= 0),
|
|
519
|
+
state_version INTEGER NOT NULL DEFAULT 0 CHECK (typeof(state_version) = 'integer' AND state_version >= 0),
|
|
520
|
+
harness TEXT,
|
|
521
|
+
task_status TEXT,
|
|
522
|
+
archived_at TEXT,
|
|
523
|
+
pinned_at TEXT,
|
|
524
|
+
body_json TEXT NOT NULL CHECK (json_valid(body_json) AND json_type(body_json) = 'object'),
|
|
525
|
+
meta_json TEXT NOT NULL CHECK (json_valid(meta_json) AND json_type(meta_json) = 'object'),
|
|
526
|
+
revision INTEGER NOT NULL CHECK (typeof(revision) = 'integer' AND revision > 0),
|
|
527
|
+
updated_at_ms INTEGER NOT NULL CHECK (typeof(updated_at_ms) = 'integer' AND updated_at_ms >= 0),
|
|
528
|
+
PRIMARY KEY (scope, project_id, session_id),
|
|
529
|
+
CHECK (
|
|
530
|
+
(scope = 'global' AND project_id = '') OR
|
|
531
|
+
(scope = 'project' AND length(trim(project_id)) > 0)
|
|
532
|
+
)
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
CREATE INDEX idx_sessions_list ON sessions (scope, project_id, archived_at, updated_at DESC);
|
|
536
|
+
CREATE INDEX idx_sessions_pinned ON sessions (scope, project_id, updated_at DESC) WHERE pinned_at IS NOT NULL;
|
|
537
|
+
CREATE INDEX idx_sessions_session_id ON sessions (session_id);
|
|
538
|
+
|
|
539
|
+
CREATE TABLE session_messages (
|
|
540
|
+
scope TEXT NOT NULL CHECK (scope IN ('global', 'project')),
|
|
541
|
+
project_id TEXT NOT NULL,
|
|
542
|
+
session_id TEXT NOT NULL CHECK (length(trim(session_id)) > 0),
|
|
543
|
+
seq INTEGER NOT NULL CHECK (typeof(seq) = 'integer' AND seq >= 0),
|
|
544
|
+
message_id TEXT CHECK (message_id IS NULL OR length(trim(message_id)) > 0),
|
|
545
|
+
message_json TEXT NOT NULL CHECK (json_valid(message_json) AND json_type(message_json) = 'object'),
|
|
546
|
+
message_digest TEXT NOT NULL CHECK (length(message_digest) = 64 AND message_digest NOT GLOB '*[^0-9a-f]*'),
|
|
547
|
+
PRIMARY KEY (scope, project_id, session_id, seq),
|
|
548
|
+
UNIQUE (scope, project_id, session_id, message_id),
|
|
549
|
+
FOREIGN KEY (scope, project_id, session_id) REFERENCES sessions (scope, project_id, session_id) ON DELETE CASCADE,
|
|
550
|
+
CHECK (
|
|
551
|
+
(scope = 'global' AND project_id = '') OR
|
|
552
|
+
(scope = 'project' AND length(trim(project_id)) > 0)
|
|
553
|
+
)
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
CREATE INDEX idx_session_messages_session_id ON session_messages (session_id);
|
|
557
|
+
|
|
558
|
+
CREATE TABLE session_tombstones (
|
|
559
|
+
scope TEXT NOT NULL CHECK (scope IN ('global', 'project')),
|
|
560
|
+
project_id TEXT NOT NULL,
|
|
561
|
+
session_id TEXT NOT NULL CHECK (length(trim(session_id)) > 0),
|
|
562
|
+
deleted_at INTEGER NOT NULL CHECK (typeof(deleted_at) = 'integer' AND deleted_at >= 0),
|
|
563
|
+
PRIMARY KEY (scope, project_id, session_id),
|
|
564
|
+
CHECK (
|
|
565
|
+
(scope = 'global' AND project_id = '') OR
|
|
566
|
+
(scope = 'project' AND length(trim(project_id)) > 0)
|
|
567
|
+
)
|
|
568
|
+
) WITHOUT ROWID;
|
|
569
|
+
`)
|
|
570
|
+
},
|
|
571
|
+
}),
|
|
463
572
|
])
|
|
464
573
|
|
|
465
574
|
function readUserVersion(database) {
|
|
@@ -314,8 +314,10 @@ export function createScheduledTaskRunsRepository(storageHandle, options = {}) {
|
|
|
314
314
|
return database.prepare('SELECT * FROM scheduled_task_runs WHERE task_id = ? AND id = ?').get(taskId, runId)
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
// The optional statement lets the cutover replaceAll reuse one precompiled insert.
|
|
318
|
+
function insert(database, value, statement = null) {
|
|
319
|
+
const insertStatement = statement ?? database.prepare(`INSERT INTO scheduled_task_runs (${INSERT_COLUMNS}) VALUES (${INSERT_PLACEHOLDERS})`)
|
|
320
|
+
insertStatement.run(...insertParameters(value))
|
|
319
321
|
}
|
|
320
322
|
|
|
321
323
|
function upsert(database, value) {
|
|
@@ -392,7 +394,8 @@ export function createScheduledTaskRunsRepository(storageHandle, options = {}) {
|
|
|
392
394
|
}
|
|
393
395
|
return storage.transaction((database) => {
|
|
394
396
|
database.prepare('DELETE FROM scheduled_task_runs').run()
|
|
395
|
-
|
|
397
|
+
const insertStatement = database.prepare(`INSERT INTO scheduled_task_runs (${INSERT_COLUMNS}) VALUES (${INSERT_PLACEHOLDERS})`)
|
|
398
|
+
for (const value of values) insert(database, value, insertStatement)
|
|
396
399
|
return values.length
|
|
397
400
|
})
|
|
398
401
|
},
|