@shawnstack/quickforge 1.7.9 → 1.7.11
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-BSM5w3bn.js → AgentProfilesPage-Cvb1KTz5.js} +1 -1
- package/dist/assets/ChatPanelHost-C7zV-xE_.js +48 -0
- package/dist/assets/{CloudAccountSettingsPage-BJMrfgy9.js → CloudAccountSettingsPage-mAs_nbvw.js} +1 -1
- package/dist/assets/{PluginsPage-mJkTmVVV.js → PluginsPage-mLvwvTmL.js} +1 -1
- package/dist/assets/{ScheduledTasksPage-DJj_NLm9.js → ScheduledTasksPage-CuUW_9lq.js} +1 -1
- package/dist/assets/{SettingsWorkspacePage-7lQ0Wl63.js → SettingsWorkspacePage-0OINfEqK.js} +390 -390
- package/dist/assets/{ShareLinksSettingsPage-DjFq_Lnb.js → ShareLinksSettingsPage-DqeyJYoD.js} +1 -1
- package/dist/assets/{SharedConversationPage-BbuNpcpP.js → SharedConversationPage-C7CDEoX9.js} +1 -1
- package/dist/assets/TerminalDock-CZXcuMpq.js +2 -0
- package/dist/assets/{WorkspaceInspector-0i6x-diB.js → WorkspaceInspector-CNSBOEvP.js} +3 -3
- package/dist/assets/index-BgMvQW1b.js +66 -0
- package/dist/assets/index-CB1g2D0f.css +3 -0
- package/dist/assets/local-tools-Dopjg9xq.js +270 -0
- package/dist/assets/{mcp-servers-dialog-BG4b67ea.js → mcp-servers-dialog-B2lALtTE.js} +1 -1
- package/dist/assets/{skills-dialog-DIzZvQqW.js → skills-dialog-DYp35JM3.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/server/acp/server.mjs +3 -6
- package/server/agent-manager.mjs +125 -43
- package/server/auto-archive.mjs +55 -8
- package/server/auto-compaction.mjs +0 -2
- package/server/context-usage.mjs +9 -7
- package/server/index.mjs +172 -51
- package/server/lan-access-cutover.mjs +21 -4
- package/server/maintenance/downgrade-session-state-v1.mjs +50 -87
- package/server/maintenance/export-session-state-v1.mjs +5 -13
- package/server/routes/backup.mjs +8 -1
- package/server/routes/storage.mjs +85 -48
- package/server/scheduled-runs-cutover.mjs +15 -1
- package/server/session-index-service.mjs +57 -391
- package/server/session-persistence-lock.mjs +17 -6
- package/server/session-state-backup.mjs +60 -7
- package/server/session-state-import.mjs +157 -0
- package/server/session-state-maintenance.mjs +126 -0
- package/server/session-state-service.mjs +133 -194
- 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 +359 -289
- package/server/sqlite/share-repository.mjs +56 -45
- package/server/startup-state.mjs +107 -0
- package/server/storage.mjs +240 -448
- package/server/utils/logger.mjs +15 -4
- package/server/utils/process-tree.mjs +14 -2
- package/dist/assets/ChatPanelHost-cd80S4EX.js +0 -48
- package/dist/assets/TerminalDock-CczcCJJE.js +0 -2
- package/dist/assets/index-D-J_8Smf.css +0 -3
- package/dist/assets/index-DFASc15l.js +0 -66
- package/dist/assets/local-tools-Dn1Y9aPe.js +0 -270
- package/server/session-state-cutover.mjs +0 -370
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
2
2
|
import { readStore, writeStore, atomicUpdate, getComparable, getStoreRevision, readSessionStoreScoped, readSessionValue, writeSessionValueWithMetadata, deleteSessionWithMetadata, applySessionBatch, ensureStorage, dataDir, configDir, storageDir, cacheDir, logsDir } from '../storage.mjs'
|
|
3
3
|
import { AUTO_ARCHIVE_SETTINGS_KEY, archiveInactiveSessions, normalizeAutoArchiveSettings } from '../auto-archive.mjs'
|
|
4
|
+
import { refreshAllSessionModels, destroyAgent } from '../agent-manager.mjs'
|
|
5
|
+
import { logger } from '../utils/logger.mjs'
|
|
4
6
|
import { directorySize } from '../utils/workspace.mjs'
|
|
5
7
|
import { isAuthenticatedAppClient } from '../access-policy.mjs'
|
|
6
|
-
import {
|
|
7
|
-
import { isSessionStateMaintenanceActive } from '../session-state-
|
|
8
|
+
import { querySessionIndexPage } from '../session-index-service.mjs'
|
|
9
|
+
import { isSessionStateMaintenanceActive } from '../session-state-maintenance.mjs'
|
|
10
|
+
import { isSessionStateAuthoritative } from '../session-state-service.mjs'
|
|
11
|
+
import { verifySessionStateIntegrityForMaintenance } from '../session-state-backup.mjs'
|
|
8
12
|
|
|
9
13
|
const metadataIndexCache = new Map()
|
|
10
14
|
const MAX_METADATA_INDEX_CACHE_ENTRIES = 50
|
|
11
15
|
const METADATA_INDEX_CACHE_TTL_MS = 1000
|
|
12
16
|
const SESSION_QUERY_INDEXES = new Set(['createdAt', 'lastModified', 'pinnedAt'])
|
|
13
|
-
const shadowGenerationByShape = new Map()
|
|
14
|
-
let shadowSampler = () => Math.random() < 0.01
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
function canonicalJson(value) {
|
|
23
|
-
if (Array.isArray(value)) return value.map(canonicalJson)
|
|
24
|
-
if (!value || typeof value !== 'object') return value
|
|
25
|
-
return Object.fromEntries(Object.keys(value).sort().filter((key) => value[key] !== undefined).map((key) => [key, canonicalJson(value[key])]))
|
|
26
|
-
}
|
|
18
|
+
// In-memory agent disposal used when sessions are deleted from storage.
|
|
19
|
+
// destroyAgent performs a final persist, so deletions must destroy the agent
|
|
20
|
+
// BEFORE removing persisted state — otherwise the final persist resurrects
|
|
21
|
+
// the deleted row. Tests inject a spy through configureStorageSessionAgentDisposal.
|
|
22
|
+
let destroyAgentForSession = destroyAgent
|
|
27
23
|
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
if (sqlPage.total !== jsonValues.length || sqlPage.values.length !== expected.length) return false
|
|
31
|
-
return JSON.stringify(sqlPage.values.map(canonicalJson)) === JSON.stringify(expected.map(canonicalJson))
|
|
24
|
+
export function configureStorageSessionAgentDisposal({ destroy } = {}) {
|
|
25
|
+
destroyAgentForSession = destroy ?? destroyAgent
|
|
32
26
|
}
|
|
33
27
|
|
|
34
28
|
function parseSqlPagination(limitParam, offsetParam) {
|
|
@@ -65,19 +59,6 @@ function sqlSessionQueryOptions({ store, indexName, directionParam, scope, proje
|
|
|
65
59
|
}
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
function shadowShape(options) {
|
|
69
|
-
return JSON.stringify({
|
|
70
|
-
scopeMode: options.scopeMode,
|
|
71
|
-
hasProjectId: options.scopeMode === 'project',
|
|
72
|
-
archive: options.archive,
|
|
73
|
-
pinnedOnly: options.pinnedOnly,
|
|
74
|
-
sort: options.sort,
|
|
75
|
-
direction: options.direction,
|
|
76
|
-
limit: options.limit,
|
|
77
|
-
offset: options.offset,
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
62
|
function metadataIndexCacheKey({ scope, projectId, indexName, direction, archived, pinned }) {
|
|
82
63
|
return JSON.stringify({ scope: scope || '', projectId: projectId || '', indexName, direction, archived: archived || '', pinned: pinned || '' })
|
|
83
64
|
}
|
|
@@ -213,6 +194,17 @@ export async function handleStorageApi(req, res, url, context = { isLocalRequest
|
|
|
213
194
|
throw error
|
|
214
195
|
}
|
|
215
196
|
try {
|
|
197
|
+
// Destroy in-memory agents for deleted sessions BEFORE removing the
|
|
198
|
+
// persisted state: destroyAgent's final persist would otherwise
|
|
199
|
+
// resurrect the deleted row right after the batch commit.
|
|
200
|
+
for (const operation of operations) {
|
|
201
|
+
if (operation?.type !== 'delete' || operation?.store !== 'sessions') continue
|
|
202
|
+
try {
|
|
203
|
+
await destroyAgentForSession(operation.key)
|
|
204
|
+
} catch (error) {
|
|
205
|
+
logger.warn(`Failed to destroy in-memory agent for deleted session ${operation.key}:`, error?.message || error)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
216
208
|
const result = await applySessionBatch(operations)
|
|
217
209
|
sendJson(res, 200, { ok: true, saved: result?.saved ?? 0, deleted: result?.deleted ?? 0 })
|
|
218
210
|
} catch (error) {
|
|
@@ -230,6 +222,34 @@ export async function handleStorageApi(req, res, url, context = { isLocalRequest
|
|
|
230
222
|
return
|
|
231
223
|
}
|
|
232
224
|
|
|
225
|
+
// Manual session state integrity verification (design review suggestion
|
|
226
|
+
// 9): startup only runs the lightweight SQL-level check, so per-row digest
|
|
227
|
+
// rot would stay invisible until an offline full verification. POST with
|
|
228
|
+
// { full: true } runs the complete per-row digest recomputation under the
|
|
229
|
+
// session state maintenance lock (same pattern as the authoritative backup
|
|
230
|
+
// export); the response carries only summary counters and the elapsed
|
|
231
|
+
// time, never row payloads.
|
|
232
|
+
if (req.method === 'POST' && parts.length === 4 && store === 'maintenance' && parts[3] === 'verify-session-integrity') {
|
|
233
|
+
if (!isSessionStateAuthoritative()) {
|
|
234
|
+
const error = new Error('Session state integrity verification requires authoritative SQLite storage')
|
|
235
|
+
error.statusCode = 409
|
|
236
|
+
error.errorCode = 'SESSION_STATE_NOT_AUTHORITATIVE'
|
|
237
|
+
throw error
|
|
238
|
+
}
|
|
239
|
+
if (isSessionStateMaintenanceActive()) {
|
|
240
|
+
const error = new Error('Session storage maintenance is in progress')
|
|
241
|
+
error.statusCode = 423
|
|
242
|
+
error.errorCode = 'SESSION_STORAGE_MAINTENANCE'
|
|
243
|
+
throw error
|
|
244
|
+
}
|
|
245
|
+
const body = await readJsonBody(req)
|
|
246
|
+
// forceQuickCheck: the manual endpoint always runs a real quick_check scan
|
|
247
|
+
// (never the startup process-cache/marker shortcut).
|
|
248
|
+
const result = await verifySessionStateIntegrityForMaintenance({ full: body?.full === true, forceQuickCheck: true, maintenance: true })
|
|
249
|
+
sendJson(res, 200, result)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
233
253
|
if (req.method === 'GET' && parts[3] === 'keys') {
|
|
234
254
|
const prefix = url.searchParams.get('prefix') || ''
|
|
235
255
|
const data = await readStore(store)
|
|
@@ -254,25 +274,14 @@ export async function handleStorageApi(req, res, url, context = { isLocalRequest
|
|
|
254
274
|
const sqlOptions = sqlSessionQueryOptions({
|
|
255
275
|
store, indexName, directionParam, scope, projectId, archived, pinned, limitParam, offsetParam,
|
|
256
276
|
})
|
|
277
|
+
// Storage v2: sessions-metadata queries eligible for SQL LIMIT/OFFSET are
|
|
278
|
+
// served straight from the authoritative sessions table (no JSON shadow —
|
|
279
|
+
// the SQLite projection IS the store). Everything else keeps the legacy
|
|
280
|
+
// read+sort path (which also reads from SQLite through the facade).
|
|
257
281
|
if (sqlOptions) {
|
|
258
282
|
const sqlResult = await querySessionIndexPage(sqlOptions)
|
|
259
283
|
if (sqlResult.ok) {
|
|
260
|
-
|
|
261
|
-
const shape = shadowShape(sqlOptions)
|
|
262
|
-
const lastGeneration = shadowGenerationByShape.get(shape)
|
|
263
|
-
const forced = lastGeneration !== diagnostics.rebuildGeneration
|
|
264
|
-
if (!forced && !shadowSampler({ shape, generation: diagnostics.rebuildGeneration })) {
|
|
265
|
-
sendJson(res, 200, { values: sqlResult.page.values, total: sqlResult.page.total })
|
|
266
|
-
return
|
|
267
|
-
}
|
|
268
|
-
const jsonValues = await readIndexedValues(store, indexName, direction, scope, projectId, archived, pinned)
|
|
269
|
-
shadowGenerationByShape.set(shape, diagnostics.rebuildGeneration)
|
|
270
|
-
if (sameShadowPage(sqlResult.page, jsonValues, sqlOptions.limit, sqlOptions.offset)) {
|
|
271
|
-
sendJson(res, 200, { values: sqlResult.page.values, total: sqlResult.page.total })
|
|
272
|
-
return
|
|
273
|
-
}
|
|
274
|
-
markSessionIndexQueryFailure('shadow_mismatch')
|
|
275
|
-
sendJson(res, 200, { values: jsonValues.slice(sqlOptions.offset, sqlOptions.offset + sqlOptions.limit), total: jsonValues.length })
|
|
284
|
+
sendJson(res, 200, { values: sqlResult.page.values, total: sqlResult.page.total })
|
|
276
285
|
return
|
|
277
286
|
}
|
|
278
287
|
}
|
|
@@ -293,6 +302,13 @@ export async function handleStorageApi(req, res, url, context = { isLocalRequest
|
|
|
293
302
|
|
|
294
303
|
if (req.method === 'DELETE' && parts.length === 3) {
|
|
295
304
|
await writeStore(store, {})
|
|
305
|
+
if (store === 'custom-providers') {
|
|
306
|
+
try {
|
|
307
|
+
await refreshAllSessionModels()
|
|
308
|
+
} catch (error) {
|
|
309
|
+
logger.error('Failed to refresh session models after clearing custom-providers:', error)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
296
312
|
sendJson(res, 200, { ok: true })
|
|
297
313
|
return
|
|
298
314
|
}
|
|
@@ -340,12 +356,26 @@ export async function handleStorageApi(req, res, url, context = { isLocalRequest
|
|
|
340
356
|
if (store === 'settings' && key === AUTO_ARCHIVE_SETTINGS_KEY && normalizeAutoArchiveSettings(body?.value).enabled) {
|
|
341
357
|
await archiveInactiveSessions()
|
|
342
358
|
}
|
|
359
|
+
if (store === 'custom-providers') {
|
|
360
|
+
try {
|
|
361
|
+
await refreshAllSessionModels()
|
|
362
|
+
} catch (error) {
|
|
363
|
+
logger.error('Failed to refresh session models after custom-providers update:', error)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
343
366
|
sendJson(res, 200, { ok: true })
|
|
344
367
|
return
|
|
345
368
|
}
|
|
346
369
|
|
|
347
370
|
if (req.method === 'DELETE') {
|
|
348
371
|
if (store === 'sessions') {
|
|
372
|
+
// Destroy the in-memory agent first: its final persist would
|
|
373
|
+
// otherwise resurrect the row right after the delete.
|
|
374
|
+
try {
|
|
375
|
+
await destroyAgentForSession(key)
|
|
376
|
+
} catch (error) {
|
|
377
|
+
logger.warn(`Failed to destroy in-memory agent for deleted session ${key}:`, error?.message || error)
|
|
378
|
+
}
|
|
349
379
|
await deleteSessionWithMetadata(key)
|
|
350
380
|
sendJson(res, 200, { ok: true })
|
|
351
381
|
return
|
|
@@ -355,6 +385,13 @@ export async function handleStorageApi(req, res, url, context = { isLocalRequest
|
|
|
355
385
|
delete data[key]
|
|
356
386
|
return data
|
|
357
387
|
})
|
|
388
|
+
if (store === 'custom-providers') {
|
|
389
|
+
try {
|
|
390
|
+
await refreshAllSessionModels()
|
|
391
|
+
} catch (error) {
|
|
392
|
+
logger.error('Failed to refresh session models after custom-providers key deletion:', error)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
358
395
|
sendJson(res, 200, { ok: true })
|
|
359
396
|
return
|
|
360
397
|
}
|
|
@@ -241,8 +241,9 @@ export function acquireScheduledRunsMaintenanceLock(storage, {
|
|
|
241
241
|
return { owner: identity.id, ownerPid: identity.pid, fencing, expiresAt }
|
|
242
242
|
}
|
|
243
243
|
if (current) {
|
|
244
|
+
const expired = Date.parse(current.expires_at) <= nowMs
|
|
244
245
|
const stalePid = ownerPidFromRow(current)
|
|
245
|
-
if (stalePid === null || pidAlive(stalePid)) return null
|
|
246
|
+
if (!expired || stalePid === null || pidAlive(stalePid)) return null
|
|
246
247
|
}
|
|
247
248
|
const fencing = Number(current?.fencing || 0) + 1
|
|
248
249
|
database.prepare(`
|
|
@@ -341,6 +342,10 @@ export async function runScheduledRunsMaintenance(operation, options = {}) {
|
|
|
341
342
|
throw error
|
|
342
343
|
} finally {
|
|
343
344
|
if (retainedMaintenance?.lease !== lease) {
|
|
345
|
+
// Reset any stale retained flag from an earlier retain: this branch
|
|
346
|
+
// releases the lock normally, so retainedMaintenance must only stay set
|
|
347
|
+
// while a retained lease is genuinely still held.
|
|
348
|
+
retainedMaintenance = null
|
|
344
349
|
clearInterval(timer)
|
|
345
350
|
releaseScheduledRunsMaintenanceLock(storage, lease)
|
|
346
351
|
localMaintenanceCount = Math.max(0, localMaintenanceCount - 1)
|
|
@@ -424,9 +429,18 @@ export async function initializeScheduledRunsCutover(options = {}) {
|
|
|
424
429
|
}
|
|
425
430
|
|
|
426
431
|
if (state.phase === SCHEDULED_RUNS_PHASES.AUTHORITATIVE) {
|
|
432
|
+
// The JSON file is only a non-authoritative mirror in this phase: a
|
|
433
|
+
// corrupt or unslimmable mirror degrades (diagnostic + warn) without
|
|
434
|
+
// blocking startup, while the authoritative SQLite health check stays
|
|
435
|
+
// fail-closed.
|
|
427
436
|
try {
|
|
428
437
|
const snapshot = splitScheduledTasksRuns(await readTasks())
|
|
429
438
|
if (snapshot.count > 0) await slimJson(readTasks, writeTasks, snapshot.metadata)
|
|
439
|
+
} catch (error) {
|
|
440
|
+
recordScheduledRunsDiagnostic('authoritative_validation', error, {}, storage)
|
|
441
|
+
log.warn('Scheduled runs authoritative JSON mirror is invalid; continuing startup', { errorName: error?.name || 'Error' })
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
430
444
|
storage.health({ quickCheck: true })
|
|
431
445
|
} catch (error) {
|
|
432
446
|
recordScheduledRunsDiagnostic('authoritative_validation', error, {}, storage)
|