@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,464 @@
|
|
|
1
|
+
import { getSqliteStorage } from './database.mjs'
|
|
2
|
+
|
|
3
|
+
export const MAX_SCHEDULED_TASK_RUNS_PER_TASK = 200
|
|
4
|
+
|
|
5
|
+
const RUN_STATUSES = new Set(['running', 'success', 'failed'])
|
|
6
|
+
const RUN_TRIGGERS = new Set(['schedule', 'manual'])
|
|
7
|
+
const TEXT_FIELDS = Object.freeze({
|
|
8
|
+
inputContent: 'input_content',
|
|
9
|
+
aiResult: 'ai_result',
|
|
10
|
+
result: 'result',
|
|
11
|
+
errorMessage: 'error_message',
|
|
12
|
+
warning: 'warning',
|
|
13
|
+
sessionId: 'session_id',
|
|
14
|
+
finishedAt: 'finished_at',
|
|
15
|
+
agentId: 'agent_id',
|
|
16
|
+
agentLabel: 'agent_label',
|
|
17
|
+
})
|
|
18
|
+
const OPTIONAL_COLUMNS = Object.freeze({
|
|
19
|
+
trigger: 'trigger',
|
|
20
|
+
inputContent: 'input_content',
|
|
21
|
+
aiResult: 'ai_result',
|
|
22
|
+
result: 'result',
|
|
23
|
+
errorMessage: 'error_message',
|
|
24
|
+
warning: 'warning',
|
|
25
|
+
sessionId: 'session_id',
|
|
26
|
+
scheduledAt: 'scheduled_at',
|
|
27
|
+
finishedAt: 'finished_at',
|
|
28
|
+
durationMs: 'duration_ms',
|
|
29
|
+
})
|
|
30
|
+
const STORED_FIELDS = new Set([
|
|
31
|
+
'id', 'taskId', 'status', 'trigger', 'inputContent', 'aiResult', 'result', 'errorMessage',
|
|
32
|
+
'warning', 'sessionId', 'scheduledAt', 'startedAt', 'finishedAt', 'durationMs', 'agentId',
|
|
33
|
+
'agentLabel', 'agentSnapshot', 'legacy', 'source', 'updatedAt',
|
|
34
|
+
])
|
|
35
|
+
const FILTER_FIELDS = new Set([
|
|
36
|
+
'taskId', 'taskIds', 'status', 'trigger', 'startedFrom', 'startedTo', 'keyword',
|
|
37
|
+
'keywordTaskIds', 'page', 'pageSize',
|
|
38
|
+
])
|
|
39
|
+
const IMMUTABLE_UPDATE_FIELDS = new Set(['id', 'taskId', 'startedAt', 'trigger', 'scheduledAt', 'source'])
|
|
40
|
+
const UPDATE_FIELDS = new Set(['status', ...Object.keys(TEXT_FIELDS), 'durationMs', 'agentSnapshot', 'legacy'])
|
|
41
|
+
|
|
42
|
+
function assertStorage(storage) {
|
|
43
|
+
if (!storage || typeof storage.prepare !== 'function' || typeof storage.transaction !== 'function') {
|
|
44
|
+
throw new TypeError('Scheduled task runs repository requires a SQLite storage handle')
|
|
45
|
+
}
|
|
46
|
+
return storage
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertNonEmptyString(value, field) {
|
|
50
|
+
if (typeof value !== 'string' || value.trim().length === 0) throw new TypeError(`${field} must be a non-empty string`)
|
|
51
|
+
return value
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function assertNullableString(value, field) {
|
|
55
|
+
if (value !== null && typeof value !== 'string') throw new TypeError(`${field} must be a string or null`)
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function assertStatus(value) {
|
|
60
|
+
if (typeof value !== 'string' || !RUN_STATUSES.has(value)) throw new TypeError('status must be running, success, or failed')
|
|
61
|
+
return value
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function assertTrigger(value, { nullable = true } = {}) {
|
|
65
|
+
if (nullable && value === null) return null
|
|
66
|
+
if (typeof value !== 'string' || !RUN_TRIGGERS.has(value)) throw new TypeError('trigger must be schedule, manual, or null')
|
|
67
|
+
return value
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertDuration(value) {
|
|
71
|
+
if (value !== null && (!Number.isInteger(value) || value < 0)) throw new TypeError('durationMs must be a non-negative integer or null')
|
|
72
|
+
return value
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function assertNullablePlainObject(value, field) {
|
|
76
|
+
if (value !== null && (typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)) {
|
|
77
|
+
throw new TypeError(`${field} must be a plain object or null`)
|
|
78
|
+
}
|
|
79
|
+
return value
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function serializeJsonObject(value, field, { nullable = true } = {}) {
|
|
83
|
+
if (value === null && nullable) return null
|
|
84
|
+
assertNullablePlainObject(value, field)
|
|
85
|
+
try {
|
|
86
|
+
const serialized = JSON.stringify(value)
|
|
87
|
+
if (typeof serialized !== 'string') throw new TypeError('not serializable')
|
|
88
|
+
return serialized
|
|
89
|
+
} catch {
|
|
90
|
+
throw new TypeError(`${field} must be JSON-serializable`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cloneJsonValue(value, field) {
|
|
95
|
+
try {
|
|
96
|
+
const serialized = JSON.stringify(value)
|
|
97
|
+
if (serialized === undefined) throw new TypeError('not serializable')
|
|
98
|
+
return JSON.parse(serialized)
|
|
99
|
+
} catch {
|
|
100
|
+
throw new TypeError(`${field} must be JSON-serializable`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseJsonObject(value, fallback) {
|
|
105
|
+
return value === null ? fallback : JSON.parse(value)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extraFields(run) {
|
|
109
|
+
const extra = {}
|
|
110
|
+
for (const [field, value] of Object.entries(run)) {
|
|
111
|
+
if (!STORED_FIELDS.has(field) && value !== undefined) extra[field] = cloneJsonValue(value, field)
|
|
112
|
+
}
|
|
113
|
+
return extra
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function mapRow(row, { includeTaskId = false } = {}) {
|
|
117
|
+
if (!row) return null
|
|
118
|
+
const extra = parseJsonObject(row.extra_json, {})
|
|
119
|
+
const run = {
|
|
120
|
+
...extra,
|
|
121
|
+
id: row.id,
|
|
122
|
+
status: row.status,
|
|
123
|
+
startedAt: row.started_at,
|
|
124
|
+
source: row.source,
|
|
125
|
+
}
|
|
126
|
+
if (includeTaskId) run.taskId = row.task_id
|
|
127
|
+
for (const [field, column] of Object.entries(OPTIONAL_COLUMNS)) {
|
|
128
|
+
if (row[column] !== null) run[field] = row[column]
|
|
129
|
+
}
|
|
130
|
+
run.agentId = row.agent_id
|
|
131
|
+
run.agentLabel = row.agent_label
|
|
132
|
+
run.agentSnapshot = parseJsonObject(row.agent_snapshot_json, null)
|
|
133
|
+
const legacy = parseJsonObject(row.legacy_json, null)
|
|
134
|
+
if (legacy !== null) run.legacy = legacy
|
|
135
|
+
return run
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeFullRun(taskId, run, { source, now = () => new Date().toISOString() } = {}) {
|
|
139
|
+
assertNonEmptyString(taskId, 'taskId')
|
|
140
|
+
if (!run || typeof run !== 'object' || Array.isArray(run)) throw new TypeError('run must be an object')
|
|
141
|
+
const normalizedSource = assertNonEmptyString(source ?? run.source ?? 'runtime', 'source')
|
|
142
|
+
const values = {
|
|
143
|
+
taskId,
|
|
144
|
+
id: assertNonEmptyString(run.id, 'id'),
|
|
145
|
+
status: assertStatus(run.status),
|
|
146
|
+
trigger: run.trigger === undefined ? null : assertTrigger(run.trigger),
|
|
147
|
+
startedAt: assertNonEmptyString(run.startedAt, 'startedAt'),
|
|
148
|
+
durationMs: run.durationMs === undefined ? null : assertDuration(run.durationMs),
|
|
149
|
+
agentSnapshotJson: run.agentSnapshot === undefined ? null : serializeJsonObject(run.agentSnapshot, 'agentSnapshot'),
|
|
150
|
+
legacyJson: run.legacy === undefined ? null : serializeJsonObject(run.legacy, 'legacy'),
|
|
151
|
+
extraJson: serializeJsonObject(extraFields(run), 'extra', { nullable: false }),
|
|
152
|
+
source: normalizedSource,
|
|
153
|
+
updatedAt: assertNonEmptyString(run.updatedAt ?? now(), 'updatedAt'),
|
|
154
|
+
}
|
|
155
|
+
for (const field of Object.keys(TEXT_FIELDS)) values[field] = run[field] === undefined ? null : assertNullableString(run[field], field)
|
|
156
|
+
values.scheduledAt = run.scheduledAt === undefined ? null : assertNullableString(run.scheduledAt, 'scheduledAt')
|
|
157
|
+
return values
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalizePatch(patch) {
|
|
161
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) throw new TypeError('patch must be an object')
|
|
162
|
+
const entries = []
|
|
163
|
+
for (const [field, value] of Object.entries(patch)) {
|
|
164
|
+
if (IMMUTABLE_UPDATE_FIELDS.has(field)) throw new TypeError(`${field} cannot be updated`)
|
|
165
|
+
if (!UPDATE_FIELDS.has(field)) throw new TypeError(`Unknown scheduled task run patch field: ${field}`)
|
|
166
|
+
if (value === undefined) continue
|
|
167
|
+
if (field === 'status') entries.push(['status', assertStatus(value)])
|
|
168
|
+
else if (field === 'durationMs') entries.push(['duration_ms', assertDuration(value)])
|
|
169
|
+
else if (field === 'agentSnapshot') entries.push(['agent_snapshot_json', serializeJsonObject(value, 'agentSnapshot')])
|
|
170
|
+
else if (field === 'legacy') entries.push(['legacy_json', serializeJsonObject(value, 'legacy')])
|
|
171
|
+
else entries.push([TEXT_FIELDS[field], assertNullableString(value, field)])
|
|
172
|
+
}
|
|
173
|
+
return entries
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parsePositiveInteger(value, fallback, max) {
|
|
177
|
+
const parsed = Number(value)
|
|
178
|
+
if (!Number.isInteger(parsed) || parsed <= 0) return fallback
|
|
179
|
+
return Math.min(parsed, max)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function parseListByTaskLimit(value) {
|
|
183
|
+
const parsed = Number(value)
|
|
184
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > MAX_SCHEDULED_TASK_RUNS_PER_TASK) {
|
|
185
|
+
throw new TypeError(`limit must be an integer between 1 and ${MAX_SCHEDULED_TASK_RUNS_PER_TASK}`)
|
|
186
|
+
}
|
|
187
|
+
return parsed
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parsePruneLimit(value) {
|
|
191
|
+
const parsed = Number(value)
|
|
192
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > MAX_SCHEDULED_TASK_RUNS_PER_TASK) {
|
|
193
|
+
throw new TypeError(`limit must be an integer between 0 and ${MAX_SCHEDULED_TASK_RUNS_PER_TASK}`)
|
|
194
|
+
}
|
|
195
|
+
return parsed
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function parseOffset(value) {
|
|
199
|
+
if (value === undefined) return 0
|
|
200
|
+
const parsed = Number(value)
|
|
201
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new TypeError('offset must be a non-negative integer')
|
|
202
|
+
return parsed
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizeStringIds(value, field) {
|
|
206
|
+
if (value === undefined || value === null) return undefined
|
|
207
|
+
if (!Array.isArray(value)) throw new TypeError(`${field} must be an array`)
|
|
208
|
+
const result = [...new Set(value.map((id) => assertNonEmptyString(id, field)))]
|
|
209
|
+
return result
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function escapeLike(value) {
|
|
213
|
+
return value.replaceAll('\\', '\\\\').replaceAll('%', '\\%').replaceAll('_', '\\_')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeFilters(filters = {}) {
|
|
217
|
+
if (!filters || typeof filters !== 'object' || Array.isArray(filters)) throw new TypeError('filters must be an object')
|
|
218
|
+
for (const field of Object.keys(filters)) if (!FILTER_FIELDS.has(field)) throw new TypeError(`Unknown scheduled task run filter: ${field}`)
|
|
219
|
+
const normalized = {
|
|
220
|
+
page: parsePositiveInteger(filters.page, 1, 100_000),
|
|
221
|
+
pageSize: parsePositiveInteger(filters.pageSize, 10, 100),
|
|
222
|
+
}
|
|
223
|
+
if (filters.taskId !== undefined && filters.taskId !== null && filters.taskId !== '') normalized.taskId = assertNonEmptyString(filters.taskId, 'taskId')
|
|
224
|
+
normalized.taskIds = normalizeStringIds(filters.taskIds, 'taskIds')
|
|
225
|
+
normalized.keywordTaskIds = normalizeStringIds(filters.keywordTaskIds, 'keywordTaskIds')
|
|
226
|
+
if (filters.status !== undefined && filters.status !== null && filters.status !== '') normalized.status = assertStatus(filters.status)
|
|
227
|
+
if (filters.trigger !== undefined && filters.trigger !== null && filters.trigger !== '') normalized.trigger = assertTrigger(filters.trigger, { nullable: false })
|
|
228
|
+
if (filters.startedFrom !== undefined && filters.startedFrom !== null && filters.startedFrom !== '') normalized.startedFrom = assertNonEmptyString(filters.startedFrom, 'startedFrom')
|
|
229
|
+
if (filters.startedTo !== undefined && filters.startedTo !== null && filters.startedTo !== '') normalized.startedTo = assertNonEmptyString(filters.startedTo, 'startedTo')
|
|
230
|
+
if (filters.keyword !== undefined && filters.keyword !== null) {
|
|
231
|
+
if (typeof filters.keyword !== 'string') throw new TypeError('keyword must be a string')
|
|
232
|
+
const keyword = filters.keyword.trim()
|
|
233
|
+
if (keyword) normalized.keyword = keyword
|
|
234
|
+
}
|
|
235
|
+
return normalized
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function inClause(column, values, clauses, parameters) {
|
|
239
|
+
if (values === undefined) return
|
|
240
|
+
if (values.length === 0) {
|
|
241
|
+
clauses.push('0 = 1')
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
clauses.push(`${column} IN (${values.map(() => '?').join(', ')})`)
|
|
245
|
+
parameters.push(...values)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildWhere(filters) {
|
|
249
|
+
const clauses = []
|
|
250
|
+
const parameters = []
|
|
251
|
+
if (filters.taskId) {
|
|
252
|
+
clauses.push('task_id = ?')
|
|
253
|
+
parameters.push(filters.taskId)
|
|
254
|
+
}
|
|
255
|
+
inClause('task_id', filters.taskIds, clauses, parameters)
|
|
256
|
+
if (filters.status) {
|
|
257
|
+
clauses.push('status = ?')
|
|
258
|
+
parameters.push(filters.status)
|
|
259
|
+
}
|
|
260
|
+
if (filters.trigger) {
|
|
261
|
+
clauses.push('trigger = ?')
|
|
262
|
+
parameters.push(filters.trigger)
|
|
263
|
+
}
|
|
264
|
+
if (filters.startedFrom) {
|
|
265
|
+
clauses.push('started_at >= ?')
|
|
266
|
+
parameters.push(filters.startedFrom)
|
|
267
|
+
}
|
|
268
|
+
if (filters.startedTo) {
|
|
269
|
+
clauses.push('started_at <= ?')
|
|
270
|
+
parameters.push(filters.startedTo)
|
|
271
|
+
}
|
|
272
|
+
if (filters.keyword) {
|
|
273
|
+
const pattern = `%${escapeLike(filters.keyword)}%`
|
|
274
|
+
const keywordClauses = [
|
|
275
|
+
"input_content LIKE ? ESCAPE '\\' COLLATE NOCASE",
|
|
276
|
+
"ai_result LIKE ? ESCAPE '\\' COLLATE NOCASE",
|
|
277
|
+
"result LIKE ? ESCAPE '\\' COLLATE NOCASE",
|
|
278
|
+
"error_message LIKE ? ESCAPE '\\' COLLATE NOCASE",
|
|
279
|
+
]
|
|
280
|
+
const keywordParameters = [pattern, pattern, pattern, pattern]
|
|
281
|
+
if (filters.keywordTaskIds?.length) {
|
|
282
|
+
keywordClauses.unshift(`task_id IN (${filters.keywordTaskIds.map(() => '?').join(', ')})`)
|
|
283
|
+
keywordParameters.unshift(...filters.keywordTaskIds)
|
|
284
|
+
}
|
|
285
|
+
clauses.push(`(${keywordClauses.join(' OR ')})`)
|
|
286
|
+
parameters.push(...keywordParameters)
|
|
287
|
+
}
|
|
288
|
+
return { sql: clauses.length > 0 ? ` WHERE ${clauses.join(' AND ')}` : '', parameters }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const INSERT_COLUMNS = `
|
|
292
|
+
task_id, id, status, trigger, input_content, ai_result, result, error_message,
|
|
293
|
+
warning, session_id, scheduled_at, started_at, finished_at, duration_ms,
|
|
294
|
+
agent_id, agent_label, agent_snapshot_json, extra_json, legacy_json, source, updated_at
|
|
295
|
+
`
|
|
296
|
+
const INSERT_PLACEHOLDERS = new Array(21).fill('?').join(', ')
|
|
297
|
+
|
|
298
|
+
function insertParameters(value) {
|
|
299
|
+
return [
|
|
300
|
+
value.taskId, value.id, value.status, value.trigger, value.inputContent, value.aiResult,
|
|
301
|
+
value.result, value.errorMessage, value.warning, value.sessionId, value.scheduledAt,
|
|
302
|
+
value.startedAt, value.finishedAt, value.durationMs, value.agentId, value.agentLabel,
|
|
303
|
+
value.agentSnapshotJson, value.extraJson, value.legacyJson, value.source, value.updatedAt,
|
|
304
|
+
]
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function createScheduledTaskRunsRepository(storageHandle, options = {}) {
|
|
308
|
+
const storage = assertStorage(storageHandle ?? getSqliteStorage())
|
|
309
|
+
const now = typeof options.now === 'function' ? options.now : () => new Date().toISOString()
|
|
310
|
+
|
|
311
|
+
function getRow(taskId, runId, database = storage) {
|
|
312
|
+
assertNonEmptyString(taskId, 'taskId')
|
|
313
|
+
assertNonEmptyString(runId, 'runId')
|
|
314
|
+
return database.prepare('SELECT * FROM scheduled_task_runs WHERE task_id = ? AND id = ?').get(taskId, runId)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function insert(database, value) {
|
|
318
|
+
database.prepare(`INSERT INTO scheduled_task_runs (${INSERT_COLUMNS}) VALUES (${INSERT_PLACEHOLDERS})`).run(...insertParameters(value))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function upsert(database, value) {
|
|
322
|
+
database.prepare(`
|
|
323
|
+
INSERT INTO scheduled_task_runs (${INSERT_COLUMNS}) VALUES (${INSERT_PLACEHOLDERS})
|
|
324
|
+
ON CONFLICT(task_id, id) DO UPDATE SET
|
|
325
|
+
status = excluded.status,
|
|
326
|
+
trigger = excluded.trigger,
|
|
327
|
+
input_content = excluded.input_content,
|
|
328
|
+
ai_result = excluded.ai_result,
|
|
329
|
+
result = excluded.result,
|
|
330
|
+
error_message = excluded.error_message,
|
|
331
|
+
warning = excluded.warning,
|
|
332
|
+
session_id = excluded.session_id,
|
|
333
|
+
scheduled_at = excluded.scheduled_at,
|
|
334
|
+
started_at = excluded.started_at,
|
|
335
|
+
finished_at = excluded.finished_at,
|
|
336
|
+
duration_ms = excluded.duration_ms,
|
|
337
|
+
agent_id = excluded.agent_id,
|
|
338
|
+
agent_label = excluded.agent_label,
|
|
339
|
+
agent_snapshot_json = excluded.agent_snapshot_json,
|
|
340
|
+
extra_json = excluded.extra_json,
|
|
341
|
+
legacy_json = excluded.legacy_json,
|
|
342
|
+
source = excluded.source,
|
|
343
|
+
updated_at = excluded.updated_at
|
|
344
|
+
`).run(...insertParameters(value))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function pruneByTask(taskId, limit = MAX_SCHEDULED_TASK_RUNS_PER_TASK, database = storage) {
|
|
348
|
+
assertNonEmptyString(taskId, 'taskId')
|
|
349
|
+
const normalizedLimit = parsePruneLimit(limit)
|
|
350
|
+
return Number(database.prepare(`
|
|
351
|
+
DELETE FROM scheduled_task_runs
|
|
352
|
+
WHERE task_id = ? AND id IN (
|
|
353
|
+
SELECT id FROM scheduled_task_runs
|
|
354
|
+
WHERE task_id = ?
|
|
355
|
+
ORDER BY started_at DESC, id DESC
|
|
356
|
+
LIMIT -1 OFFSET ?
|
|
357
|
+
)
|
|
358
|
+
`).run(taskId, taskId, normalizedLimit).changes)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return Object.freeze({
|
|
362
|
+
create(taskId, run, createOptions = {}) {
|
|
363
|
+
const value = normalizeFullRun(taskId, run, { source: createOptions.source, now })
|
|
364
|
+
return storage.transaction((database) => {
|
|
365
|
+
insert(database, value)
|
|
366
|
+
const inserted = getRow(value.taskId, value.id, database)
|
|
367
|
+
pruneByTask(value.taskId, MAX_SCHEDULED_TASK_RUNS_PER_TASK, database)
|
|
368
|
+
return mapRow(inserted)
|
|
369
|
+
})
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
upsert(taskId, run, upsertOptions = {}) {
|
|
373
|
+
const value = normalizeFullRun(taskId, run, { source: upsertOptions.source, now })
|
|
374
|
+
return storage.transaction((database) => {
|
|
375
|
+
upsert(database, value)
|
|
376
|
+
pruneByTask(value.taskId, MAX_SCHEDULED_TASK_RUNS_PER_TASK, database)
|
|
377
|
+
return mapRow(getRow(value.taskId, value.id, database))
|
|
378
|
+
})
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
replaceAll(entries, replaceOptions = {}) {
|
|
382
|
+
if (!Array.isArray(entries)) throw new TypeError('entries must be an array')
|
|
383
|
+
const values = entries.map((entry) => {
|
|
384
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) throw new TypeError('entry must be an object')
|
|
385
|
+
return normalizeFullRun(entry.taskId, entry.run, { source: entry.source ?? replaceOptions.source, now })
|
|
386
|
+
})
|
|
387
|
+
const seen = new Set()
|
|
388
|
+
for (const value of values) {
|
|
389
|
+
const key = JSON.stringify([value.taskId, value.id])
|
|
390
|
+
if (seen.has(key)) throw new TypeError(`Duplicate scheduled task run: ${value.taskId}/${value.id}`)
|
|
391
|
+
seen.add(key)
|
|
392
|
+
}
|
|
393
|
+
return storage.transaction((database) => {
|
|
394
|
+
database.prepare('DELETE FROM scheduled_task_runs').run()
|
|
395
|
+
for (const value of values) insert(database, value)
|
|
396
|
+
return values.length
|
|
397
|
+
})
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
update(taskId, runId, patch) {
|
|
401
|
+
assertNonEmptyString(taskId, 'taskId')
|
|
402
|
+
assertNonEmptyString(runId, 'runId')
|
|
403
|
+
const entries = normalizePatch(patch)
|
|
404
|
+
if (entries.length === 0) return mapRow(getRow(taskId, runId))
|
|
405
|
+
entries.push(['updated_at', now()])
|
|
406
|
+
const assignments = entries.map(([column]) => `${column} = ?`).join(', ')
|
|
407
|
+
const values = entries.map(([, value]) => value)
|
|
408
|
+
const result = storage.prepare(`UPDATE scheduled_task_runs SET ${assignments} WHERE task_id = ? AND id = ?`).run(...values, taskId, runId)
|
|
409
|
+
return Number(result.changes) === 0 ? null : mapRow(getRow(taskId, runId))
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
get(taskId, runId) {
|
|
413
|
+
return mapRow(getRow(taskId, runId))
|
|
414
|
+
},
|
|
415
|
+
|
|
416
|
+
listByTask(taskId, { limit = MAX_SCHEDULED_TASK_RUNS_PER_TASK, offset = 0 } = {}) {
|
|
417
|
+
assertNonEmptyString(taskId, 'taskId')
|
|
418
|
+
const normalizedLimit = parseListByTaskLimit(limit)
|
|
419
|
+
const normalizedOffset = parseOffset(offset)
|
|
420
|
+
return storage.prepare(`
|
|
421
|
+
SELECT * FROM scheduled_task_runs
|
|
422
|
+
WHERE task_id = ?
|
|
423
|
+
ORDER BY started_at DESC, id DESC
|
|
424
|
+
LIMIT ? OFFSET ?
|
|
425
|
+
`).all(taskId, normalizedLimit, normalizedOffset).map((row) => mapRow(row))
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
list(filters = {}) {
|
|
429
|
+
const normalized = normalizeFilters(filters)
|
|
430
|
+
const where = buildWhere(normalized)
|
|
431
|
+
const offset = (normalized.page - 1) * normalized.pageSize
|
|
432
|
+
return storage.transaction((database) => {
|
|
433
|
+
const total = Number(database.prepare(`SELECT COUNT(*) AS count FROM scheduled_task_runs${where.sql}`).get(...where.parameters).count)
|
|
434
|
+
const runs = database.prepare(`
|
|
435
|
+
SELECT * FROM scheduled_task_runs${where.sql}
|
|
436
|
+
ORDER BY started_at DESC, id DESC, task_id DESC
|
|
437
|
+
LIMIT ? OFFSET ?
|
|
438
|
+
`).all(...where.parameters, normalized.pageSize, offset).map((row) => mapRow(row, { includeTaskId: true }))
|
|
439
|
+
return { runs, total, page: normalized.page, pageSize: normalized.pageSize }
|
|
440
|
+
}, { mode: 'deferred' })
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
count(filters = {}) {
|
|
444
|
+
const normalized = normalizeFilters(filters)
|
|
445
|
+
const where = buildWhere(normalized)
|
|
446
|
+
return Number(storage.prepare(`SELECT COUNT(*) AS count FROM scheduled_task_runs${where.sql}`).get(...where.parameters).count)
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
delete(taskId, runId) {
|
|
450
|
+
assertNonEmptyString(taskId, 'taskId')
|
|
451
|
+
assertNonEmptyString(runId, 'runId')
|
|
452
|
+
return Number(storage.prepare('DELETE FROM scheduled_task_runs WHERE task_id = ? AND id = ?').run(taskId, runId).changes) > 0
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
deleteByTask(taskId) {
|
|
456
|
+
assertNonEmptyString(taskId, 'taskId')
|
|
457
|
+
return Number(storage.prepare('DELETE FROM scheduled_task_runs WHERE task_id = ?').run(taskId).changes)
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
prune(taskId, limit = MAX_SCHEDULED_TASK_RUNS_PER_TASK) {
|
|
461
|
+
return storage.transaction((database) => pruneByTask(taskId, limit, database))
|
|
462
|
+
},
|
|
463
|
+
})
|
|
464
|
+
}
|