@shawnstack/quickforge 1.7.9 → 1.7.10
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-Dw9orPNt.js} +1 -1
- package/dist/assets/{ChatPanelHost-cd80S4EX.js → ChatPanelHost-CW3-MQyL.js} +2 -2
- package/dist/assets/{CloudAccountSettingsPage-BJMrfgy9.js → CloudAccountSettingsPage-CDkKN1Sz.js} +1 -1
- package/dist/assets/{PluginsPage-mJkTmVVV.js → PluginsPage-Dj1bmsEf.js} +1 -1
- package/dist/assets/{ScheduledTasksPage-DJj_NLm9.js → ScheduledTasksPage-C9lOTi75.js} +1 -1
- package/dist/assets/{SettingsWorkspacePage-7lQ0Wl63.js → SettingsWorkspacePage-CFuvSATi.js} +3 -3
- package/dist/assets/{ShareLinksSettingsPage-DjFq_Lnb.js → ShareLinksSettingsPage-CH_t33A3.js} +1 -1
- package/dist/assets/{SharedConversationPage-BbuNpcpP.js → SharedConversationPage-DLo05j7c.js} +1 -1
- package/dist/assets/{TerminalDock-CczcCJJE.js → TerminalDock-CSOVub-m.js} +1 -1
- package/dist/assets/{WorkspaceInspector-0i6x-diB.js → WorkspaceInspector-DPXgLIum.js} +1 -1
- package/dist/assets/index-DQBOm9gt.js +66 -0
- package/dist/assets/index-ps85guTs.css +3 -0
- package/dist/assets/{local-tools-Dn1Y9aPe.js → local-tools-BxfQeIUQ.js} +1 -1
- package/dist/assets/{mcp-servers-dialog-BG4b67ea.js → mcp-servers-dialog-CwHp0K4q.js} +1 -1
- package/dist/assets/{skills-dialog-DIzZvQqW.js → skills-dialog-k9ycekIU.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/server/agent-manager.mjs +68 -2
- package/server/auto-compaction.mjs +0 -2
- package/server/context-usage.mjs +9 -7
- package/server/maintenance/downgrade-session-state-v1.mjs +4 -2
- package/server/maintenance/export-session-state-v1.mjs +4 -2
- package/server/routes/backup.mjs +7 -0
- package/server/routes/storage.mjs +23 -0
- package/server/session-persistence-lock.mjs +17 -6
- package/server/session-state-backup.mjs +4 -2
- package/server/session-state-cutover.mjs +284 -91
- package/server/session-state-service.mjs +38 -17
- package/server/sqlite/session-state-repository.mjs +145 -25
- package/server/storage.mjs +28 -0
- package/server/utils/process-tree.mjs +14 -2
- package/dist/assets/index-D-J_8Smf.css +0 -3
- package/dist/assets/index-DFASc15l.js +0 -66
|
@@ -294,9 +294,12 @@ function digestRows(database) {
|
|
|
294
294
|
}))
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
function digestFromLines(lines) {
|
|
298
|
+
return createHash('sha256').update([...lines].sort().join('\n')).digest('hex')
|
|
299
|
+
}
|
|
300
|
+
|
|
297
301
|
function verificationDigest(rows) {
|
|
298
|
-
|
|
299
|
-
return createHash('sha256').update(values.join('\n')).digest('hex')
|
|
302
|
+
return digestFromLines(rows.map((row) => snapshotDigestLine(row.scope, row.project_id, row.session_id, row.state_digest, row.metadata_digest, row.messages_digest)))
|
|
300
303
|
}
|
|
301
304
|
|
|
302
305
|
export function sessionStateSnapshotDigest(records) {
|
|
@@ -582,8 +585,9 @@ export function createSessionStateRepository(storageHandle, { now = () => new Da
|
|
|
582
585
|
}, options).saved
|
|
583
586
|
}
|
|
584
587
|
|
|
585
|
-
function replaceAll(inputs, { beforeCommit, storageState, expectedCount, expectedDigest } = {}) {
|
|
588
|
+
function replaceAll(inputs, { beforeCommit, storageState, expectedCount, expectedDigest, mirrorDeletes = [] } = {}) {
|
|
586
589
|
if (!Array.isArray(inputs)) throw new TypeError('records must be an array')
|
|
590
|
+
if (!Array.isArray(mirrorDeletes)) throw new TypeError('mirrorDeletes must be an array')
|
|
587
591
|
const timestamp = now()
|
|
588
592
|
const records = inputs.map((input) => {
|
|
589
593
|
let messages = input.messages
|
|
@@ -597,6 +601,13 @@ export function createSessionStateRepository(storageHandle, { now = () => new Da
|
|
|
597
601
|
if (ids.has(record.sessionId)) throw new TypeError(`Duplicate session id: ${record.sessionId}`)
|
|
598
602
|
ids.add(record.sessionId)
|
|
599
603
|
}
|
|
604
|
+
const deletes = mirrorDeletes.map((input) => ({ ...bucket(input.scope, input.projectId), sessionId: nonEmptyString(input.sessionId, 'sessionId') }))
|
|
605
|
+
const recordKeys = new Set(records.map((record) => JSON.stringify([record.scope, record.projectId, record.sessionId])))
|
|
606
|
+
for (const entry of deletes) {
|
|
607
|
+
if (recordKeys.has(JSON.stringify([entry.scope, entry.projectId, entry.sessionId]))) {
|
|
608
|
+
throw new TypeError(`mirrorDeletes entry duplicates a record key: ${entry.sessionId}`)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
600
611
|
return storage.transaction((database) => {
|
|
601
612
|
database.exec('DELETE FROM session_states; DELETE FROM session_state_tombstones; DELETE FROM session_index; DELETE FROM session_json_mirror_queue; DELETE FROM session_messages;')
|
|
602
613
|
for (const record of records) {
|
|
@@ -606,6 +617,12 @@ export function createSessionStateRepository(storageHandle, { now = () => new Da
|
|
|
606
617
|
upsertIndex(database, record)
|
|
607
618
|
enqueueMirror(database, record, 'upsert', 1)
|
|
608
619
|
}
|
|
620
|
+
// Orphan deletes intentionally have no session_states/session_index rows
|
|
621
|
+
// (they were dropped as stale residue); enqueue mirror deletes so the
|
|
622
|
+
// drain physically clears leftover JSON files (idempotent when absent).
|
|
623
|
+
for (const entry of deletes) {
|
|
624
|
+
enqueueMirror(database, { ...entry, stateJson: null, metadataJson: null, now: timestamp }, 'delete', 1)
|
|
625
|
+
}
|
|
609
626
|
const digest = verificationDigest(digestRows(database))
|
|
610
627
|
if (expectedCount !== undefined && records.length !== expectedCount) throw new Error('Session state replace count verification failed')
|
|
611
628
|
if (expectedDigest !== undefined && digest !== expectedDigest) throw new Error('Session state replace digest verification failed')
|
|
@@ -615,6 +632,72 @@ export function createSessionStateRepository(storageHandle, { now = () => new Da
|
|
|
615
632
|
}, { mode: 'immediate' })
|
|
616
633
|
}
|
|
617
634
|
|
|
635
|
+
// Streaming equivalent of replaceAll for sources that cannot be materialized
|
|
636
|
+
// in memory (large JSON cutover imports). Consumes a sync or async iterable
|
|
637
|
+
// record-by-record inside one immediate transaction; only the per-record
|
|
638
|
+
// digest lines accumulate. `storage.transaction` requires a synchronous
|
|
639
|
+
// callback, so the transaction is managed manually: any error — including
|
|
640
|
+
// one thrown by the iterator mid-stream — rolls everything back, preserving
|
|
641
|
+
// replaceAll's all-or-nothing semantics. Callers must hold the session
|
|
642
|
+
// state maintenance lock (a concurrent writer hitting the open transaction
|
|
643
|
+
// fails loudly instead of interleaving).
|
|
644
|
+
async function replaceAllStream(recordIterable, { beforeCommit, storageState, expectedCount, expectedDigest, mirrorDeletes = [] } = {}) {
|
|
645
|
+
if (!recordIterable || (typeof recordIterable[Symbol.asyncIterator] !== 'function' && typeof recordIterable[Symbol.iterator] !== 'function')) {
|
|
646
|
+
throw new TypeError('records must be a sync or async iterable')
|
|
647
|
+
}
|
|
648
|
+
if (!Array.isArray(mirrorDeletes)) throw new TypeError('mirrorDeletes must be an array')
|
|
649
|
+
const timestamp = now()
|
|
650
|
+
const deletes = mirrorDeletes.map((input) => ({ ...bucket(input.scope, input.projectId), sessionId: nonEmptyString(input.sessionId, 'sessionId') }))
|
|
651
|
+
// Record keys arrive incrementally while streaming, so the mirrorDeletes
|
|
652
|
+
// conflict check runs per record against the precomputed delete keys (the
|
|
653
|
+
// mirror image of replaceAll's post-hoc set comparison).
|
|
654
|
+
const deleteKeys = new Set(deletes.map((entry) => JSON.stringify([entry.scope, entry.projectId, entry.sessionId])))
|
|
655
|
+
storage.exec('BEGIN IMMEDIATE')
|
|
656
|
+
let count = 0
|
|
657
|
+
const digestLines = []
|
|
658
|
+
try {
|
|
659
|
+
storage.exec('DELETE FROM session_states; DELETE FROM session_state_tombstones; DELETE FROM session_index; DELETE FROM session_json_mirror_queue; DELETE FROM session_messages;')
|
|
660
|
+
const insert = storage.prepare(`INSERT INTO session_states (${STATE_COLUMNS}) VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)`)
|
|
661
|
+
const ids = new Set()
|
|
662
|
+
for await (const input of recordIterable) {
|
|
663
|
+
let messages = input.messages
|
|
664
|
+
if (messages === undefined && input.state?.messageStorage === 'split') {
|
|
665
|
+
messages = Array.isArray(input.state.messages) ? input.state.messages : []
|
|
666
|
+
}
|
|
667
|
+
const record = normalizeRecord(messages === undefined ? input : { ...input, messages }, timestamp)
|
|
668
|
+
if (ids.has(record.sessionId)) throw new TypeError(`Duplicate session id: ${record.sessionId}`)
|
|
669
|
+
ids.add(record.sessionId)
|
|
670
|
+
if (deleteKeys.has(JSON.stringify([record.scope, record.projectId, record.sessionId]))) {
|
|
671
|
+
throw new TypeError(`mirrorDeletes entry duplicates a record key: ${record.sessionId}`)
|
|
672
|
+
}
|
|
673
|
+
insert.run(record.scope, record.projectId, record.sessionId, record.stateVersion, record.stateJson, record.stateDigest, record.metadataJson, record.metadataDigest, timestamp, timestamp)
|
|
674
|
+
if (record.messages !== undefined) writeMessages(storage, record, 'replace', timestamp)
|
|
675
|
+
upsertIndex(storage, record)
|
|
676
|
+
enqueueMirror(storage, record, 'upsert', 1)
|
|
677
|
+
// Streamed imports are non-split (messages inline, none in
|
|
678
|
+
// session_messages), so the empty messages digest matches what
|
|
679
|
+
// digestRows computes for freshly imported non-split rows.
|
|
680
|
+
digestLines.push(snapshotDigestLine(record.scope, record.projectId, record.sessionId, record.stateDigest, record.metadataDigest, ''))
|
|
681
|
+
count += 1
|
|
682
|
+
}
|
|
683
|
+
// Orphan deletes intentionally have no session_states/session_index rows;
|
|
684
|
+
// see replaceAll for the mirror-drain rationale.
|
|
685
|
+
for (const entry of deletes) {
|
|
686
|
+
enqueueMirror(storage, { ...entry, stateJson: null, metadataJson: null, now: timestamp }, 'delete', 1)
|
|
687
|
+
}
|
|
688
|
+
const digest = digestFromLines(digestLines)
|
|
689
|
+
if (expectedCount !== undefined && count !== expectedCount) throw new Error('Session state replace count verification failed')
|
|
690
|
+
if (expectedDigest !== undefined && digest !== expectedDigest) throw new Error('Session state replace digest verification failed')
|
|
691
|
+
updateStorageState(storage, storageState ? { ...storageState, stateCount: count, digest, updatedAt: timestamp } : null)
|
|
692
|
+
beforeCommit?.(storage)
|
|
693
|
+
storage.exec('COMMIT')
|
|
694
|
+
return count
|
|
695
|
+
} catch (error) {
|
|
696
|
+
try { storage.exec('ROLLBACK') } catch { /* transaction already closed */ }
|
|
697
|
+
throw error
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
618
701
|
function exportSnapshot() {
|
|
619
702
|
return storage.transaction((database) => {
|
|
620
703
|
const rows = database.prepare(`SELECT ${STATE_COLUMNS} FROM session_states ORDER BY scope, project_id, session_id`).all()
|
|
@@ -644,10 +727,55 @@ export function createSessionStateRepository(storageHandle, { now = () => new Da
|
|
|
644
727
|
return verificationDigest(digestRows(storage))
|
|
645
728
|
}
|
|
646
729
|
|
|
730
|
+
// SQL-only integrity counts shared by both verification modes (no row
|
|
731
|
+
// bodies are loaded; joins and aggregates run inside SQLite).
|
|
732
|
+
function sqlIntegrityCounts() {
|
|
733
|
+
return {
|
|
734
|
+
missingIndex: Number(storage.prepare(`
|
|
735
|
+
SELECT COUNT(*) AS count FROM session_states s
|
|
736
|
+
LEFT JOIN session_index i ON i.scope = s.scope AND i.project_id = s.project_id AND i.session_id = s.session_id
|
|
737
|
+
WHERE i.session_id IS NULL OR i.metadata_digest <> s.metadata_digest OR i.state_version <> s.state_version
|
|
738
|
+
`).get().count),
|
|
739
|
+
orphanIndex: Number(storage.prepare(`
|
|
740
|
+
SELECT COUNT(*) AS count FROM session_index i
|
|
741
|
+
LEFT JOIN session_states s ON s.scope = i.scope AND s.project_id = i.project_id AND s.session_id = i.session_id
|
|
742
|
+
WHERE s.session_id IS NULL
|
|
743
|
+
`).get().count),
|
|
744
|
+
duplicateIds: Number(storage.prepare('SELECT COUNT(*) AS count FROM (SELECT session_id FROM session_states GROUP BY session_id HAVING COUNT(*) > 1)').get().count),
|
|
745
|
+
activeTombstones: Number(storage.prepare(`SELECT COUNT(*) AS count FROM session_state_tombstones t
|
|
746
|
+
JOIN session_states s ON s.scope = t.scope AND s.project_id = t.project_id AND s.session_id = t.session_id`).get().count),
|
|
747
|
+
staleTombstones: Number(storage.prepare(`SELECT COUNT(*) AS count FROM session_state_tombstones t
|
|
748
|
+
JOIN session_states s ON s.session_id = t.session_id AND NOT (s.scope = t.scope AND s.project_id = t.project_id)`).get().count),
|
|
749
|
+
orphanMessages: Number(storage.prepare(`
|
|
750
|
+
SELECT COUNT(*) AS count FROM session_messages m
|
|
751
|
+
LEFT JOIN session_states s ON s.scope = m.scope AND s.project_id = m.project_id AND s.session_id = m.session_id
|
|
752
|
+
WHERE s.session_id IS NULL
|
|
753
|
+
`).get().count),
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
647
757
|
function verifyIntegrity({ quickCheck = false } = {}) {
|
|
758
|
+
const counts = sqlIntegrityCounts()
|
|
648
759
|
if (quickCheck) {
|
|
649
760
|
const result = storage.prepare('PRAGMA quick_check').all().map((row) => row.quick_check)
|
|
650
761
|
if (result.length !== 1 || result[0] !== 'ok') throw new Error(`SQLite quick_check failed: ${result.join(', ')}`)
|
|
762
|
+
// Lightweight mode: SQL-level checks only. Per-row JSON.parse and digest
|
|
763
|
+
// recomputation (invalidRecords/invalidDigests/invalidIndexDigests/
|
|
764
|
+
// invalidMessageDigests/invalidMessageRepresentations) and the snapshot
|
|
765
|
+
// digest are left to a full verification (maintenance entry points).
|
|
766
|
+
return {
|
|
767
|
+
ok: counts.missingIndex === 0 && counts.orphanIndex === 0 && counts.duplicateIds === 0
|
|
768
|
+
&& counts.activeTombstones === 0 && counts.staleTombstones === 0 && counts.orphanMessages === 0,
|
|
769
|
+
count: Number(storage.prepare('SELECT COUNT(*) AS count FROM session_states').get().count),
|
|
770
|
+
digest: null,
|
|
771
|
+
...counts,
|
|
772
|
+
invalidRecords: 0,
|
|
773
|
+
invalidDigests: 0,
|
|
774
|
+
invalidIndexDigests: 0,
|
|
775
|
+
invalidMessageDigests: 0,
|
|
776
|
+
invalidMessageRepresentations: 0,
|
|
777
|
+
lightweight: true,
|
|
778
|
+
}
|
|
651
779
|
}
|
|
652
780
|
const rows = storage.prepare(`SELECT ${STATE_COLUMNS} FROM session_states`).all()
|
|
653
781
|
let invalidRecords = 0
|
|
@@ -675,11 +803,6 @@ export function createSessionStateRepository(storageHandle, { now = () => new Da
|
|
|
675
803
|
invalidMessageDigests += 1
|
|
676
804
|
}
|
|
677
805
|
}
|
|
678
|
-
const orphanMessages = Number(storage.prepare(`
|
|
679
|
-
SELECT COUNT(*) AS count FROM session_messages m
|
|
680
|
-
LEFT JOIN session_states s ON s.scope = m.scope AND s.project_id = m.project_id AND s.session_id = m.session_id
|
|
681
|
-
WHERE s.session_id IS NULL
|
|
682
|
-
`).get().count)
|
|
683
806
|
const indexRows = storage.prepare('SELECT metadata_json, metadata_digest FROM session_index').all()
|
|
684
807
|
let invalidIndexDigests = 0
|
|
685
808
|
for (const row of indexRows) {
|
|
@@ -689,21 +812,7 @@ export function createSessionStateRepository(storageHandle, { now = () => new Da
|
|
|
689
812
|
invalidIndexDigests += 1
|
|
690
813
|
}
|
|
691
814
|
}
|
|
692
|
-
const missingIndex =
|
|
693
|
-
SELECT COUNT(*) AS count FROM session_states s
|
|
694
|
-
LEFT JOIN session_index i ON i.scope = s.scope AND i.project_id = s.project_id AND i.session_id = s.session_id
|
|
695
|
-
WHERE i.session_id IS NULL OR i.metadata_digest <> s.metadata_digest OR i.state_version <> s.state_version
|
|
696
|
-
`).get().count)
|
|
697
|
-
const orphanIndex = Number(storage.prepare(`
|
|
698
|
-
SELECT COUNT(*) AS count FROM session_index i
|
|
699
|
-
LEFT JOIN session_states s ON s.scope = i.scope AND s.project_id = i.project_id AND s.session_id = i.session_id
|
|
700
|
-
WHERE s.session_id IS NULL
|
|
701
|
-
`).get().count)
|
|
702
|
-
const duplicateIds = Number(storage.prepare('SELECT COUNT(*) AS count FROM (SELECT session_id FROM session_states GROUP BY session_id HAVING COUNT(*) > 1)').get().count)
|
|
703
|
-
const activeTombstones = Number(storage.prepare(`SELECT COUNT(*) AS count FROM session_state_tombstones t
|
|
704
|
-
JOIN session_states s ON s.scope = t.scope AND s.project_id = t.project_id AND s.session_id = t.session_id`).get().count)
|
|
705
|
-
const staleTombstones = Number(storage.prepare(`SELECT COUNT(*) AS count FROM session_state_tombstones t
|
|
706
|
-
JOIN session_states s ON s.session_id = t.session_id AND NOT (s.scope = t.scope AND s.project_id = t.project_id)`).get().count)
|
|
815
|
+
const { missingIndex, orphanIndex, duplicateIds, activeTombstones, staleTombstones, orphanMessages } = counts
|
|
707
816
|
return {
|
|
708
817
|
ok: missingIndex === 0 && orphanIndex === 0 && duplicateIds === 0 && activeTombstones === 0 && staleTombstones === 0
|
|
709
818
|
&& invalidRecords === 0 && invalidDigests === 0 && invalidIndexDigests === 0
|
|
@@ -735,8 +844,13 @@ export function createSessionStateRepository(storageHandle, { now = () => new Da
|
|
|
735
844
|
}, { mode: 'immediate' })
|
|
736
845
|
}
|
|
737
846
|
|
|
738
|
-
function listMirrorQueue() {
|
|
739
|
-
|
|
847
|
+
function listMirrorQueue({ limit } = {}) {
|
|
848
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) throw new TypeError('limit must be a positive integer')
|
|
849
|
+
// The queue rows carry full state_json payloads, so callers draining large
|
|
850
|
+
// queues page through with `limit` instead of loading every row at once.
|
|
851
|
+
const rows = storage.prepare(`SELECT * FROM session_json_mirror_queue ORDER BY updated_at, scope, project_id, session_id${limit !== undefined ? ' LIMIT ?' : ''}`)
|
|
852
|
+
.all(...(limit !== undefined ? [limit] : []))
|
|
853
|
+
return rows.map((row) => ({
|
|
740
854
|
scope: row.scope,
|
|
741
855
|
projectId: row.scope === 'project' ? row.project_id : null,
|
|
742
856
|
sessionId: row.session_id,
|
|
@@ -750,6 +864,10 @@ export function createSessionStateRepository(storageHandle, { now = () => new Da
|
|
|
750
864
|
}))
|
|
751
865
|
}
|
|
752
866
|
|
|
867
|
+
function countMirrorQueue() {
|
|
868
|
+
return Number(storage.prepare('SELECT COUNT(*) AS count FROM session_json_mirror_queue').get().count)
|
|
869
|
+
}
|
|
870
|
+
|
|
753
871
|
function acknowledgeMirror(entry) {
|
|
754
872
|
return Number(storage.prepare('DELETE FROM session_json_mirror_queue WHERE scope = ? AND project_id = ? AND session_id = ? AND revision = ?')
|
|
755
873
|
.run(entry.scope, entry.scope === 'project' ? entry.projectId : '', entry.sessionId, entry.revision).changes) === 1
|
|
@@ -774,12 +892,14 @@ export function createSessionStateRepository(storageHandle, { now = () => new Da
|
|
|
774
892
|
delete: deleteRecord,
|
|
775
893
|
deleteBySessionId,
|
|
776
894
|
replaceAll,
|
|
895
|
+
replaceAllStream,
|
|
777
896
|
exportSnapshot,
|
|
778
897
|
verifyIntegrity,
|
|
779
898
|
count,
|
|
780
899
|
digest,
|
|
781
900
|
rebuildIndex,
|
|
782
901
|
listMirrorQueue,
|
|
902
|
+
countMirrorQueue,
|
|
783
903
|
acknowledgeMirror,
|
|
784
904
|
failMirror,
|
|
785
905
|
})
|
package/server/storage.mjs
CHANGED
|
@@ -463,6 +463,34 @@ export async function readPhysicalSessionStateBuckets() {
|
|
|
463
463
|
return result
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
+
// Streaming filesystem adapter for the session-state JSON cutover: unlike
|
|
467
|
+
// readPhysicalSessionStateBuckets (which materializes every bucket into
|
|
468
|
+
// memory at once), the cutover consumes sessions one file at a time so a
|
|
469
|
+
// multi-GB library never needs to be fully resident. It reuses the same
|
|
470
|
+
// physical helpers, so the layout and read semantics stay identical:
|
|
471
|
+
// - listBuckets(): global bucket + one bucket per project directory
|
|
472
|
+
// - listSessionFiles(bucket): session ids from sessions/<id>.json file names
|
|
473
|
+
// - readSessionState(bucket, sessionId): parsed session body (null if absent)
|
|
474
|
+
// - readMetadataBucket(bucket): parsed sessions-metadata bucket (default {})
|
|
475
|
+
export function createPhysicalSessionStateFsAdapter() {
|
|
476
|
+
return {
|
|
477
|
+
async *listBuckets() {
|
|
478
|
+
await ensureStorage()
|
|
479
|
+
yield { scope: 'global', projectId: null }
|
|
480
|
+
for (const projectId of await listProjectIds()) yield { scope: 'project', projectId }
|
|
481
|
+
},
|
|
482
|
+
async *listSessionFiles(bucket) {
|
|
483
|
+
for (const file of await listSessionDataFiles(bucket)) yield path.basename(file, '.json')
|
|
484
|
+
},
|
|
485
|
+
readSessionState(bucket, sessionId) {
|
|
486
|
+
return readJsonFile(sessionDataFile(sessionId, bucket), null)
|
|
487
|
+
},
|
|
488
|
+
readMetadataBucket(bucket) {
|
|
489
|
+
return readJsonFile(sessionStoreFile('sessions-metadata', bucket), {})
|
|
490
|
+
},
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
466
494
|
async function sessionStateFacade() {
|
|
467
495
|
const service = await import('./session-state-service.mjs')
|
|
468
496
|
return service.isSessionStateAuthoritative() ? service : null
|
|
@@ -3,6 +3,7 @@ import { once } from 'node:events'
|
|
|
3
3
|
|
|
4
4
|
const DEFAULT_GRACE_MS = 1000
|
|
5
5
|
const DEFAULT_FORCE_WAIT_MS = 1000
|
|
6
|
+
const TASKKILL_TIMEOUT_MS = 10_000
|
|
6
7
|
|
|
7
8
|
function isRunning(child) {
|
|
8
9
|
return Boolean(child?.pid) && child.exitCode == null && child.signalCode == null
|
|
@@ -36,8 +37,19 @@ function runTaskkill(pid, force, spawnImpl = spawn) {
|
|
|
36
37
|
resolve(false)
|
|
37
38
|
return
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
let settled = false
|
|
41
|
+
const finish = (ok) => {
|
|
42
|
+
if (settled) return
|
|
43
|
+
settled = true
|
|
44
|
+
clearTimeout(timer)
|
|
45
|
+
resolve(ok)
|
|
46
|
+
}
|
|
47
|
+
// taskkill normally exits immediately, but a wedged taskkill.exe must not
|
|
48
|
+
// hang dispose()/destroyAgent forever on a dangling child process.
|
|
49
|
+
const timer = setTimeout(() => finish(false), TASKKILL_TIMEOUT_MS)
|
|
50
|
+
timer.unref?.()
|
|
51
|
+
command.once?.('error', () => finish(false))
|
|
52
|
+
command.once?.('exit', (code) => finish(code === 0))
|
|
41
53
|
})
|
|
42
54
|
}
|
|
43
55
|
|