@shawnstack/quickforge 1.7.7 → 1.7.8
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 +1 -0
- package/dist/assets/ChatPanelHost-cd80S4EX.js +48 -0
- package/dist/assets/CloudAccountSettingsPage-BJMrfgy9.js +1 -0
- package/dist/assets/{PluginsPage-CPE5tbbf.js → PluginsPage-mJkTmVVV.js} +1 -1
- package/dist/assets/ScheduledTasksPage-DJj_NLm9.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-BE2T4d3P.js → SettingsWorkspacePage-7lQ0Wl63.js} +389 -389
- package/dist/assets/ShareLinksSettingsPage-DjFq_Lnb.js +1 -0
- package/dist/assets/SharedConversationPage-BbuNpcpP.js +1 -0
- package/dist/assets/TerminalDock-CczcCJJE.js +2 -0
- package/dist/assets/WorkspaceInspector-0i6x-diB.js +36 -0
- package/dist/assets/{icons-2D18YbSI.js → icons-DM_qJ7VJ.js} +1 -1
- package/dist/assets/{index-BiCTgUlY.css → index-D-J_8Smf.css} +1 -1
- package/dist/assets/index-DFASc15l.js +66 -0
- package/dist/assets/local-tools-Dn1Y9aPe.js +270 -0
- package/dist/assets/{model-display-label-C6-0eDxF.js → logger-8REuLPGK.js} +1 -1
- package/dist/assets/{mcp-servers-dialog-GY7sdfIr.js → mcp-servers-dialog-BG4b67ea.js} +2 -2
- package/dist/assets/{model-reference-o41fJ6jQ.js → model-display-label-CURwH0xM.js} +1 -1
- package/dist/assets/{monaco-DvXErxp2.js → monaco-bDr860xx.js} +1 -1
- package/dist/assets/{react-vendor-3UaA-C9W.js → react-vendor-BpWW_Nvp.js} +1 -1
- package/dist/assets/{skills-dialog-C1fJ1Jl7.js → skills-dialog-DIzZvQqW.js} +3 -3
- package/dist/index.html +8 -8
- package/package.json +1 -1
- package/server/acp/server.mjs +17 -1
- package/server/agent-manager.mjs +251 -31
- package/server/ai-http-logger.mjs +162 -19
- package/server/ai-provider-options.mjs +4 -1
- package/server/auto-archive.mjs +42 -29
- package/server/cloud/auto-approval.mjs +155 -0
- package/server/cloud/client.mjs +5 -0
- package/server/cloud/qf-agent-process.mjs +163 -3
- package/server/cloud/service-config.mjs +1 -1
- package/server/index.mjs +69 -13
- package/server/lan-access-backup.mjs +256 -0
- package/server/lan-access-cutover.mjs +306 -0
- package/server/lan-access-json-file.mjs +75 -0
- package/server/lan-access-service.mjs +155 -0
- package/server/lan-access-store.mjs +110 -9
- package/server/maintenance/downgrade-lan-access-v1.mjs +101 -0
- package/server/maintenance/downgrade-session-state-v1.mjs +130 -0
- package/server/maintenance/downgrade-share-v1.mjs +101 -0
- package/server/maintenance/export-lan-access-v1.mjs +80 -0
- package/server/maintenance/export-scheduled-runs-v1.mjs +45 -0
- package/server/maintenance/export-session-state-v1.mjs +67 -0
- package/server/maintenance/export-share-v1.mjs +77 -0
- package/server/routes/agent.mjs +37 -4
- package/server/routes/backup.mjs +221 -39
- package/server/routes/cloud.mjs +28 -1
- package/server/routes/scheduled-tasks.mjs +347 -134
- package/server/routes/shares.mjs +4 -2
- package/server/routes/storage.mjs +138 -8
- package/server/routes/workspace.mjs +265 -4
- package/server/scheduled-runs-backup.mjs +235 -0
- package/server/scheduled-runs-cutover.mjs +497 -0
- package/server/scheduled-task-runs-service.mjs +279 -0
- package/server/session-index-service.mjs +495 -0
- package/server/session-state-backup.mjs +277 -0
- package/server/session-state-cutover.mjs +370 -0
- package/server/session-state-service.mjs +709 -0
- package/server/share-backup.mjs +255 -0
- package/server/share-cutover.mjs +296 -0
- package/server/share-json-file.mjs +60 -0
- package/server/share-service.mjs +163 -0
- package/server/share-store.mjs +196 -19
- package/server/skills.mjs +0 -1
- package/server/sqlite/database.mjs +233 -0
- package/server/sqlite/lan-access-repository.mjs +652 -0
- package/server/sqlite/migrations.mjs +577 -0
- package/server/sqlite/scheduled-task-runs-repository.mjs +464 -0
- package/server/sqlite/session-index-repository.mjs +297 -0
- package/server/sqlite/session-state-repository.mjs +786 -0
- package/server/sqlite/share-repository.mjs +854 -0
- package/server/storage.mjs +317 -1
- package/dist/assets/AgentProfilesPage-BfA_sQ3A.js +0 -1
- package/dist/assets/ChatPanelHost-Dul1ahKj.js +0 -307
- package/dist/assets/CloudAccountSettingsPage-qZh0lTUN.js +0 -1
- package/dist/assets/ScheduledTasksPage-PTxL_PPg.js +0 -2
- package/dist/assets/ShareLinksSettingsPage-Bl8fP62X.js +0 -1
- package/dist/assets/SharedConversationPage-BK9XJnEe.js +0 -1
- package/dist/assets/TerminalDock-CL7HU9DI.js +0 -2
- package/dist/assets/WorkspaceInspector-MNBLIKMn.js +0 -36
- package/dist/assets/index-BTPzyq7d.js +0 -65
- package/dist/assets/mermaid-renderer-jEbqFaTe.js +0 -2
- /package/dist/assets/{plugin-api-Uu2xAB4d.js → plugin-api-BrdQeM4B.js} +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { getSqliteStorage } from './sqlite/database.mjs'
|
|
5
|
+
import { createScheduledTaskRunsRepository } from './sqlite/scheduled-task-runs-repository.mjs'
|
|
6
|
+
import {
|
|
7
|
+
currentScheduledRunsMaintenanceContext,
|
|
8
|
+
getScheduledRunsPhase,
|
|
9
|
+
isScheduledRunsAuthoritative,
|
|
10
|
+
runScheduledRunsMaintenance,
|
|
11
|
+
splitScheduledTasksRuns,
|
|
12
|
+
verifyScheduledRunsRepository,
|
|
13
|
+
} from './scheduled-runs-cutover.mjs'
|
|
14
|
+
import { readStore, storageDir, writeStore } from './storage.mjs'
|
|
15
|
+
|
|
16
|
+
const RESTORE_PLAN_FILE = path.join(storageDir, 'scheduled-runs-restore-plan.json')
|
|
17
|
+
const ROLL_FORWARD_STATUSES = new Set(['prepared', 'applying', 'target_applied'])
|
|
18
|
+
const ROLL_BACK_STATUSES = new Set(['compensating', 'compensation_failed'])
|
|
19
|
+
|
|
20
|
+
function repositoryRequired() {
|
|
21
|
+
try {
|
|
22
|
+
return createScheduledTaskRunsRepository(getSqliteStorage())
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error('SQLite storage is required for authoritative scheduled runs', { cause: error })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function groupRuns(repository, taskIds) {
|
|
29
|
+
const grouped = new Map(taskIds.map((taskId) => [taskId, []]))
|
|
30
|
+
if (taskIds.length === 0) return grouped
|
|
31
|
+
let page = 1
|
|
32
|
+
let total = null
|
|
33
|
+
let count = 0
|
|
34
|
+
while (true) {
|
|
35
|
+
const result = repository.list({ taskIds, page, pageSize: 100 })
|
|
36
|
+
if (!result || !Array.isArray(result.runs) || !Number.isInteger(result.total) || result.total < 0) {
|
|
37
|
+
throw new Error('Scheduled runs repository returned an invalid backup page')
|
|
38
|
+
}
|
|
39
|
+
if (total === null) total = result.total
|
|
40
|
+
else if (total !== result.total) throw new Error('Scheduled runs repository changed during backup export')
|
|
41
|
+
for (const run of result.runs) {
|
|
42
|
+
if (!taskIds.includes(run.taskId)) throw new Error('Scheduled runs repository returned an orphan backup row')
|
|
43
|
+
const { taskId, source: _source, ...value } = run
|
|
44
|
+
grouped.get(taskId).push(value)
|
|
45
|
+
count += 1
|
|
46
|
+
}
|
|
47
|
+
if (count >= total) break
|
|
48
|
+
if (result.runs.length === 0) throw new Error('Scheduled runs repository backup pagination was incomplete')
|
|
49
|
+
page += 1
|
|
50
|
+
}
|
|
51
|
+
if (count !== total) throw new Error('Scheduled runs repository backup count verification failed')
|
|
52
|
+
return grouped
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function readScheduledTasksForBackup({
|
|
56
|
+
readTasks = () => readStore('scheduled-tasks'),
|
|
57
|
+
repository: repositoryOverride,
|
|
58
|
+
maintenance = true,
|
|
59
|
+
} = {}) {
|
|
60
|
+
const operation = async () => {
|
|
61
|
+
const tasks = await readTasks()
|
|
62
|
+
if (!isScheduledRunsAuthoritative()) return tasks
|
|
63
|
+
const repository = repositoryOverride || repositoryRequired()
|
|
64
|
+
const taskIds = Object.keys(tasks)
|
|
65
|
+
const grouped = groupRuns(repository, taskIds)
|
|
66
|
+
const logical = Object.fromEntries(Object.entries(tasks).map(([taskId, task]) => [taskId, {
|
|
67
|
+
...task,
|
|
68
|
+
runs: grouped.get(taskId) || [],
|
|
69
|
+
}]))
|
|
70
|
+
const snapshot = splitScheduledTasksRuns(logical)
|
|
71
|
+
const repositoryCount = repository.count({ taskIds })
|
|
72
|
+
if (snapshot.count !== repositoryCount) {
|
|
73
|
+
throw new Error('Scheduled runs logical backup count/digest verification failed')
|
|
74
|
+
}
|
|
75
|
+
verifyScheduledRunsRepository(repository, snapshot)
|
|
76
|
+
return logical
|
|
77
|
+
}
|
|
78
|
+
if (!maintenance || !isScheduledRunsAuthoritative() || currentScheduledRunsMaintenanceContext()) return operation()
|
|
79
|
+
return runScheduledRunsMaintenance(operation, { operation: 'scheduled-runs-backup-export', pauseRuntime: false })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function writePlan(plan, planFile = RESTORE_PLAN_FILE) {
|
|
83
|
+
await fs.mkdir(path.dirname(planFile), { recursive: true })
|
|
84
|
+
const temporary = `${planFile}.${process.pid}.${randomUUID()}.tmp`
|
|
85
|
+
try {
|
|
86
|
+
await fs.writeFile(temporary, `${JSON.stringify(plan, null, 2)}\n`, 'utf8')
|
|
87
|
+
await fs.rename(temporary, planFile)
|
|
88
|
+
} catch (error) {
|
|
89
|
+
await fs.rm(temporary, { force: true }).catch(() => {})
|
|
90
|
+
throw error
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function planSnapshot(value) {
|
|
95
|
+
const snapshot = splitScheduledTasksRuns(value)
|
|
96
|
+
return { count: snapshot.count, digest: snapshot.digest }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validatePlan(plan) {
|
|
100
|
+
if (!plan || plan.version !== 1 || plan.operation !== 'scheduled_tasks_restore' || !isPlainPlanValue(plan.before) || !isPlainPlanValue(plan.target)) {
|
|
101
|
+
throw new Error('Scheduled runs restore plan is invalid')
|
|
102
|
+
}
|
|
103
|
+
if (!ROLL_FORWARD_STATUSES.has(plan.status) && !ROLL_BACK_STATUSES.has(plan.status)) {
|
|
104
|
+
throw new Error('Scheduled runs restore plan status is invalid')
|
|
105
|
+
}
|
|
106
|
+
const before = planSnapshot(plan.before)
|
|
107
|
+
const target = planSnapshot(plan.target)
|
|
108
|
+
if (plan.beforeCount !== before.count || plan.beforeDigest !== before.digest || plan.targetCount !== target.count || plan.targetDigest !== target.digest) {
|
|
109
|
+
throw new Error('Scheduled runs restore plan digest is invalid')
|
|
110
|
+
}
|
|
111
|
+
return plan
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isPlainPlanValue(value) {
|
|
115
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function applyLogicalTasks(tasks, repository, writeTasks = (value) => writeStore('scheduled-tasks', value)) {
|
|
119
|
+
const snapshot = splitScheduledTasksRuns(tasks)
|
|
120
|
+
repository.replaceAll(snapshot.entries, { source: 'restore' })
|
|
121
|
+
verifyScheduledRunsRepository(repository, snapshot)
|
|
122
|
+
await writeTasks(snapshot.metadata)
|
|
123
|
+
const verifiedMetadata = splitScheduledTasksRuns(snapshot.metadata)
|
|
124
|
+
if (verifiedMetadata.count !== 0) throw new Error('Scheduled runs restore metadata verification failed')
|
|
125
|
+
return snapshot
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function hasActiveRuns(tasks, repository) {
|
|
129
|
+
for (const task of Object.values(tasks || {})) {
|
|
130
|
+
if (task?.currentRunId || (Array.isArray(task?.currentRunIds) && task.currentRunIds.some(Boolean))) return true
|
|
131
|
+
if (Array.isArray(task?.runs) && task.runs.some((run) => run?.status === 'running')) return true
|
|
132
|
+
}
|
|
133
|
+
return repository.count({ status: 'running' }) > 0
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function activeRestoreError() {
|
|
137
|
+
const error = new Error('Cannot restore scheduled tasks while a run is active')
|
|
138
|
+
error.statusCode = 409
|
|
139
|
+
error.errorCode = 'scheduled_runs_active'
|
|
140
|
+
return error
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function restoreScheduledTasks(tasks, {
|
|
144
|
+
mode = 'replace',
|
|
145
|
+
repository: repositoryOverride,
|
|
146
|
+
readCurrent,
|
|
147
|
+
writeTasks,
|
|
148
|
+
planFile = RESTORE_PLAN_FILE,
|
|
149
|
+
storage,
|
|
150
|
+
} = {}) {
|
|
151
|
+
if (!isScheduledRunsAuthoritative()) {
|
|
152
|
+
const current = readCurrent ? await readCurrent() : await readStore('scheduled-tasks')
|
|
153
|
+
const target = mode === 'merge' ? { ...current, ...tasks } : tasks
|
|
154
|
+
if (hasActiveRuns(current, { count: () => 0 })) throw activeRestoreError()
|
|
155
|
+
await writeStore('scheduled-tasks', target)
|
|
156
|
+
return target
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const repository = repositoryOverride || repositoryRequired()
|
|
160
|
+
return runScheduledRunsMaintenance(async () => {
|
|
161
|
+
const current = readCurrent
|
|
162
|
+
? await readCurrent()
|
|
163
|
+
: await readScheduledTasksForBackup({ repository, maintenance: false })
|
|
164
|
+
if (hasActiveRuns(current, repository)) throw activeRestoreError()
|
|
165
|
+
const target = mode === 'merge' ? { ...current, ...tasks } : tasks
|
|
166
|
+
const beforeSnapshot = planSnapshot(current)
|
|
167
|
+
const targetSnapshot = planSnapshot(target)
|
|
168
|
+
let plan = {
|
|
169
|
+
version: 1,
|
|
170
|
+
operation: 'scheduled_tasks_restore',
|
|
171
|
+
status: 'prepared',
|
|
172
|
+
createdAt: new Date().toISOString(),
|
|
173
|
+
before: current,
|
|
174
|
+
target,
|
|
175
|
+
beforeCount: beforeSnapshot.count,
|
|
176
|
+
beforeDigest: beforeSnapshot.digest,
|
|
177
|
+
targetCount: targetSnapshot.count,
|
|
178
|
+
targetDigest: targetSnapshot.digest,
|
|
179
|
+
}
|
|
180
|
+
await writePlan(plan, planFile)
|
|
181
|
+
try {
|
|
182
|
+
plan = { ...plan, status: 'applying' }
|
|
183
|
+
await writePlan(plan, planFile)
|
|
184
|
+
await applyLogicalTasks(target, repository, writeTasks)
|
|
185
|
+
plan = { ...plan, status: 'target_applied' }
|
|
186
|
+
await writePlan(plan, planFile)
|
|
187
|
+
await fs.rm(planFile, { force: true })
|
|
188
|
+
return target
|
|
189
|
+
} catch (error) {
|
|
190
|
+
try {
|
|
191
|
+
plan = { ...plan, status: 'compensating', failedAt: new Date().toISOString() }
|
|
192
|
+
await writePlan(plan, planFile)
|
|
193
|
+
await applyLogicalTasks(current, repository, writeTasks)
|
|
194
|
+
await fs.rm(planFile, { force: true })
|
|
195
|
+
} catch (compensationError) {
|
|
196
|
+
await writePlan({
|
|
197
|
+
...plan,
|
|
198
|
+
status: 'compensation_failed',
|
|
199
|
+
compensationFailedAt: new Date().toISOString(),
|
|
200
|
+
errorName: error?.name || 'Error',
|
|
201
|
+
compensationErrorName: compensationError?.name || 'Error',
|
|
202
|
+
}, planFile).catch(() => {})
|
|
203
|
+
const retained = new Error('Scheduled task restore failed and compensation failed', { cause: compensationError })
|
|
204
|
+
retained.retainScheduledRunsMaintenance = true
|
|
205
|
+
throw retained
|
|
206
|
+
}
|
|
207
|
+
throw error
|
|
208
|
+
}
|
|
209
|
+
}, { storage, operation: 'scheduled-runs-restore' })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function recoverScheduledRunsRestorePlan({
|
|
213
|
+
repository: repositoryOverride,
|
|
214
|
+
writeTasks,
|
|
215
|
+
planFile = RESTORE_PLAN_FILE,
|
|
216
|
+
storage,
|
|
217
|
+
} = {}) {
|
|
218
|
+
let plan
|
|
219
|
+
try {
|
|
220
|
+
plan = validatePlan(JSON.parse(await fs.readFile(planFile, 'utf8')))
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (error?.code === 'ENOENT') return false
|
|
223
|
+
throw new Error(`Scheduled runs restore plan is unreadable or invalid: ${error?.message || error}`, { cause: error })
|
|
224
|
+
}
|
|
225
|
+
if (!['sqlite_authoritative_json_pending', 'authoritative'].includes(getScheduledRunsPhase())) {
|
|
226
|
+
throw new Error('Scheduled runs restore plan exists outside authoritative mode')
|
|
227
|
+
}
|
|
228
|
+
const repository = repositoryOverride || repositoryRequired()
|
|
229
|
+
return runScheduledRunsMaintenance(async () => {
|
|
230
|
+
const value = ROLL_BACK_STATUSES.has(plan.status) ? plan.before : plan.target
|
|
231
|
+
await applyLogicalTasks(value, repository, writeTasks)
|
|
232
|
+
await fs.rm(planFile, { force: true })
|
|
233
|
+
return true
|
|
234
|
+
}, { storage, operation: 'scheduled-runs-restore-recovery', pauseRuntime: false })
|
|
235
|
+
}
|