@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
package/server/agent-manager.mjs
CHANGED
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
validateModelReference,
|
|
29
29
|
} from './agent-profile-schema.mjs'
|
|
30
30
|
import { projectContextFromId, defaultGlobalWorkspaceContext, readProjectConfig } from './project-config.mjs'
|
|
31
|
-
import { ensureStorage, readStore, atomicUpdate,
|
|
32
|
-
import {
|
|
31
|
+
import { ensureStorage, readStore, atomicUpdate, readSessionValue, tempAgentsDir } from './storage.mjs'
|
|
32
|
+
import { readSessionStateRecord, saveSessionStatePair, deleteSessionState, storedMessagesState, sessionMessagesTailDigest } from './session-state-service.mjs'
|
|
33
33
|
import { logger } from './utils/logger.mjs'
|
|
34
34
|
import { publishChannelSessionChanged } from './channels/event-relay.mjs'
|
|
35
35
|
import { withSessionPersistenceLock } from './session-persistence-lock.mjs'
|
|
@@ -68,6 +68,15 @@ import {
|
|
|
68
68
|
commandToolPermissionError,
|
|
69
69
|
createCommandToolPermissions,
|
|
70
70
|
} from './approval-store.mjs'
|
|
71
|
+
import {
|
|
72
|
+
ASK_TIMEOUT_MS,
|
|
73
|
+
pendingAsks,
|
|
74
|
+
getPendingAskForSession,
|
|
75
|
+
normalizeAskQuestions,
|
|
76
|
+
formatAskResult,
|
|
77
|
+
} from './ask-store.mjs'
|
|
78
|
+
|
|
79
|
+
export { getPendingAskForSession, normalizeAskQuestions } from './ask-store.mjs'
|
|
71
80
|
|
|
72
81
|
// ---------------------------------------------------------------------------
|
|
73
82
|
// Tool definitions (server-side, no REST roundtrip)
|
|
@@ -88,8 +97,25 @@ function wrapSubagentToolDefinition(definition, parentSessionId) {
|
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
|
|
100
|
+
function wrapAskUserToolDefinition(definition, parentSessionId) {
|
|
101
|
+
return {
|
|
102
|
+
...definition,
|
|
103
|
+
execute: async (toolCallId, params) => {
|
|
104
|
+
const session = agentSessions.get(parentSessionId)
|
|
105
|
+
if (!session) {
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: 'text', text: 'No active session for ask_user.' }],
|
|
108
|
+
details: { askId: null, skipped: true },
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return createAskUserPromise(session, toolCallId, params || {})
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
91
116
|
function wrapWorkspaceToolDefinition(definition, context, toolPermissions, options = {}) {
|
|
92
117
|
if (definition.name === 'run_subagent') return wrapSubagentToolDefinition(definition, options.parentSessionId)
|
|
118
|
+
if (definition.name === 'ask_user') return wrapAskUserToolDefinition(definition, options.parentSessionId)
|
|
93
119
|
return wrapToolDefinition(definition, context, toolPermissions)
|
|
94
120
|
}
|
|
95
121
|
|
|
@@ -304,8 +330,83 @@ function createApprovalPromise(session, toolCallId, toolName, args, source) {
|
|
|
304
330
|
})
|
|
305
331
|
}
|
|
306
332
|
|
|
307
|
-
|
|
308
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Create a Promise that resolves when the user answers (or skips) an ask_user
|
|
335
|
+
* tool call. The tool's execute blocks on this promise, pausing the agent loop
|
|
336
|
+
* until the user responds.
|
|
337
|
+
*/
|
|
338
|
+
function createAskUserPromise(session, toolCallId, params) {
|
|
339
|
+
const questions = normalizeAskQuestions(params)
|
|
340
|
+
if (!questions.length) {
|
|
341
|
+
return Promise.resolve({
|
|
342
|
+
content: [{ type: 'text', text: formatAskResult(questions, null, true, 'no-questions') }],
|
|
343
|
+
details: { askId: null, skipped: true },
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
const askId = randomUUID()
|
|
347
|
+
return new Promise((resolve) => {
|
|
348
|
+
let settled = false
|
|
349
|
+
const requestedAt = Date.now()
|
|
350
|
+
const expiresAt = requestedAt + ASK_TIMEOUT_MS
|
|
351
|
+
|
|
352
|
+
const timeout = setTimeout(() => {
|
|
353
|
+
if (settled) return
|
|
354
|
+
finish({ skipped: true, reason: 'timeout' })
|
|
355
|
+
}, ASK_TIMEOUT_MS)
|
|
356
|
+
|
|
357
|
+
let onAbort = null
|
|
358
|
+
|
|
359
|
+
const finish = (payload) => {
|
|
360
|
+
if (settled) return
|
|
361
|
+
settled = true
|
|
362
|
+
clearTimeout(timeout)
|
|
363
|
+
if (onAbort && session.agent.signal) session.agent.signal.removeEventListener('abort', onAbort)
|
|
364
|
+
pendingAsks.delete(askId)
|
|
365
|
+
const answers = payload.skipped ? null : payload.answers
|
|
366
|
+
resolve({
|
|
367
|
+
content: [{ type: 'text', text: formatAskResult(questions, answers, payload.skipped, payload.reason) }],
|
|
368
|
+
details: { askId, questions, answers, skipped: !!payload.skipped, ...(payload.reason ? { skipReason: payload.reason } : {}) },
|
|
369
|
+
})
|
|
370
|
+
emitSessionEvent(session, {
|
|
371
|
+
type: 'ask_user_answered',
|
|
372
|
+
sessionId: session.sessionId,
|
|
373
|
+
askId,
|
|
374
|
+
toolCallId,
|
|
375
|
+
skipped: !!payload.skipped,
|
|
376
|
+
answers: answers || [],
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const signal = session.agent.signal
|
|
381
|
+
if (signal) {
|
|
382
|
+
if (signal.aborted) {
|
|
383
|
+
finish({ skipped: true, reason: 'aborted' })
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
onAbort = () => finish({ skipped: true, reason: 'aborted' })
|
|
387
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
pendingAsks.set(askId, {
|
|
391
|
+
finish: (payload) => finish(payload),
|
|
392
|
+
sessionId: session.sessionId,
|
|
393
|
+
toolCallId,
|
|
394
|
+
questions,
|
|
395
|
+
requestedAt,
|
|
396
|
+
expiresAt,
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
emitSessionEvent(session, {
|
|
400
|
+
type: 'ask_user_required',
|
|
401
|
+
sessionId: session.sessionId,
|
|
402
|
+
askId,
|
|
403
|
+
toolCallId,
|
|
404
|
+
questions,
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function createAcpApprovalPromise(session, request) { return new Promise((resolve, reject) => {
|
|
309
410
|
let settled = false
|
|
310
411
|
const requestedAt = Date.now()
|
|
311
412
|
const expiresAt = requestedAt + APPROVAL_TIMEOUT_MS
|
|
@@ -1880,6 +1981,8 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1880
1981
|
if (commandPermissionError) return { block: true, reason: commandPermissionError }
|
|
1881
1982
|
const isSkillTool = toolName === 'activate_skill' || toolName === 'read_skill_resource'
|
|
1882
1983
|
if (isSkillTool) return undefined
|
|
1984
|
+
// ask_user only waits for the user's answer; it changes nothing and needs no approval.
|
|
1985
|
+
if (toolName === 'ask_user') return undefined
|
|
1883
1986
|
if (profileToolNames && !profileToolNames.includes(toolName)) return { block: true, reason: `Agent profile ${agentProfile.name} is not allowed to use ${toolName}.` }
|
|
1884
1987
|
if (toolName === 'manage_global_memory') return undefined
|
|
1885
1988
|
if (toolName === 'run_subagent') {
|
|
@@ -1967,6 +2070,10 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1967
2070
|
persistedMessageCount: Number.isInteger(config.persistedMessageCount) && config.persistedMessageCount >= 0 ? config.persistedMessageCount : null,
|
|
1968
2071
|
persistedTailDigest: typeof config.persistedTailDigest === 'string' ? config.persistedTailDigest : null,
|
|
1969
2072
|
persistConflictCount: 0,
|
|
2073
|
+
// Set when an authoritative persist is skipped after CAS conflicts; cleared
|
|
2074
|
+
// on the next successful persist. Surfaced via getSessionState/getSessionStatus
|
|
2075
|
+
// so the UI can warn the user instead of silently losing messages.
|
|
2076
|
+
persistDegraded: null,
|
|
1970
2077
|
lastAutoCompactAt: null,
|
|
1971
2078
|
lastAutoCompactRejected: null,
|
|
1972
2079
|
memoryEnabled: initialMemoryEnabled,
|
|
@@ -2153,6 +2260,26 @@ function canonicalStateJson(value) {
|
|
|
2153
2260
|
return JSON.stringify(value)
|
|
2154
2261
|
}
|
|
2155
2262
|
|
|
2263
|
+
/**
|
|
2264
|
+
* Mark the session as persist-degraded (an authoritative persist was skipped
|
|
2265
|
+
* after CAS conflicts) and notify connected clients so the UI can surface a
|
|
2266
|
+
* non-blocking warning instead of silently losing messages.
|
|
2267
|
+
*/
|
|
2268
|
+
function markPersistDegraded(session, attempts) {
|
|
2269
|
+
session.persistDegraded = { at: new Date().toISOString(), attempts }
|
|
2270
|
+
emitSessionEvent(session, { type: 'persist_degraded', persistDegraded: true })
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
/**
|
|
2274
|
+
* Clear the degraded marker after a successful persist and notify clients so
|
|
2275
|
+
* the UI warning disappears.
|
|
2276
|
+
*/
|
|
2277
|
+
function clearPersistDegraded(session) {
|
|
2278
|
+
if (!session.persistDegraded) return
|
|
2279
|
+
session.persistDegraded = null
|
|
2280
|
+
emitSessionEvent(session, { type: 'persist_degraded', persistDegraded: false })
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2156
2283
|
/**
|
|
2157
2284
|
* Persist a full session record in one authoritative SQLite transaction with
|
|
2158
2285
|
* revision CAS. When the CAS fails because a concurrent metadata-only change
|
|
@@ -2201,6 +2328,7 @@ async function persistAuthoritativeSessionState(session, sessionData, metadata)
|
|
|
2201
2328
|
session.persistedMessageCount = Array.isArray(sessionData.messages) ? sessionData.messages.length : 0
|
|
2202
2329
|
}
|
|
2203
2330
|
session.persistedTailDigest = sessionMessagesTailDigest(sessionData.messages)
|
|
2331
|
+
clearPersistDegraded(session)
|
|
2204
2332
|
return { ...saved, metadata: mergedMetadata }
|
|
2205
2333
|
} catch (error) {
|
|
2206
2334
|
if (error?.errorCode !== 'SESSION_STATE_CONFLICT') throw error
|
|
@@ -2222,11 +2350,13 @@ async function persistAuthoritativeSessionState(session, sessionData, metadata)
|
|
|
2222
2350
|
}
|
|
2223
2351
|
session.persistConflictCount += 1
|
|
2224
2352
|
logger.warn(`Session ${sessionId} persist conflict: storage changed agent-owned fields; skipping overwrite`, { sessionId })
|
|
2353
|
+
markPersistDegraded(session, attempt)
|
|
2225
2354
|
return null
|
|
2226
2355
|
}
|
|
2227
2356
|
}
|
|
2228
2357
|
session.persistConflictCount += 1
|
|
2229
2358
|
logger.warn(`Session ${sessionId} persist still conflicting after ${maxAttempts} merge retries`, { sessionId, reason: lastConflict?.message })
|
|
2359
|
+
markPersistDegraded(session, maxAttempts)
|
|
2230
2360
|
return null
|
|
2231
2361
|
}
|
|
2232
2362
|
|
|
@@ -2239,14 +2369,7 @@ async function persistSessionUnlocked(session) {
|
|
|
2239
2369
|
|
|
2240
2370
|
if (messages.length === 0) {
|
|
2241
2371
|
try {
|
|
2242
|
-
|
|
2243
|
-
else {
|
|
2244
|
-
await deleteSessionValue(sessionId)
|
|
2245
|
-
await atomicSessionMetadataUpdate(scope, projectId, (data) => {
|
|
2246
|
-
delete data[sessionId]
|
|
2247
|
-
return data
|
|
2248
|
-
})
|
|
2249
|
-
}
|
|
2372
|
+
deleteSessionState(sessionId, { expectedRevision: session.persistedStorageRevision })
|
|
2250
2373
|
session.persistedStorageRevision = null
|
|
2251
2374
|
session.persistedStateVersion = null
|
|
2252
2375
|
session.persistedStateJson = null
|
|
@@ -2353,34 +2476,14 @@ async function persistSessionUnlocked(session) {
|
|
|
2353
2476
|
idleRetention: session.idleRetention || undefined,
|
|
2354
2477
|
}
|
|
2355
2478
|
|
|
2356
|
-
// Write body + metadata as one authoritative SQLite transaction
|
|
2479
|
+
// Write body + metadata as one authoritative SQLite transaction.
|
|
2357
2480
|
let persistedMetadata
|
|
2358
2481
|
try {
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
persistedMetadata = saved.metadata
|
|
2365
|
-
} else {
|
|
2366
|
-
await writeSessionValue(sessionId, sessionData)
|
|
2367
|
-
await atomicSessionMetadataUpdate(scope, projectId, (data) => {
|
|
2368
|
-
const existingMetadata = data[sessionId]
|
|
2369
|
-
const archivedAt = existingMetadata?.archivedAt
|
|
2370
|
-
// Merge onto the existing record so storage-only fields the in-memory
|
|
2371
|
-
// session does not own (pinnedAt, archivedAt, …) survive the rebuild.
|
|
2372
|
-
persistedMetadata = {
|
|
2373
|
-
...existingMetadata,
|
|
2374
|
-
...metadata,
|
|
2375
|
-
...(archivedAt ? { archivedAt } : {}),
|
|
2376
|
-
}
|
|
2377
|
-
data[sessionId] = persistedMetadata
|
|
2378
|
-
return data
|
|
2379
|
-
})
|
|
2380
|
-
if (persistedMetadata?.archivedAt) {
|
|
2381
|
-
await writeSessionValue(sessionId, { ...sessionData, archivedAt: persistedMetadata.archivedAt })
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2482
|
+
const saved = await persistAuthoritativeSessionState(session, sessionData, metadata)
|
|
2483
|
+
if (!saved) return null
|
|
2484
|
+
session.persistedStorageRevision = saved.revision
|
|
2485
|
+
session.persistedStateVersion = saved.stateVersion
|
|
2486
|
+
persistedMetadata = saved.metadata
|
|
2384
2487
|
} catch (err) {
|
|
2385
2488
|
logger.error(`Failed to persist session ${sessionId}:`, err, { sessionId })
|
|
2386
2489
|
return null
|
|
@@ -2410,11 +2513,10 @@ const SLOW_PERSIST_LOG_MS = 1_000
|
|
|
2410
2513
|
|
|
2411
2514
|
async function persistSession(session) {
|
|
2412
2515
|
const startedAt = performance.now()
|
|
2413
|
-
// Serialize per session
|
|
2414
|
-
//
|
|
2415
|
-
//
|
|
2416
|
-
|
|
2417
|
-
const lockKey = isSessionStateAuthoritative() ? `session:${session.sessionId}` : ''
|
|
2516
|
+
// Serialize per session (per-row revision CAS guarantees correctness);
|
|
2517
|
+
// cross-session persists run independently instead of piling onto one
|
|
2518
|
+
// global chain that also gates destroyAgent.
|
|
2519
|
+
const lockKey = `session:${session.sessionId}`
|
|
2418
2520
|
const result = await withSessionPersistenceLock(() => persistSessionUnlocked(session), lockKey)
|
|
2419
2521
|
const durationMs = performance.now() - startedAt
|
|
2420
2522
|
if (durationMs >= SLOW_PERSIST_LOG_MS) {
|
|
@@ -2973,9 +3075,11 @@ export function getSessionState(sessionId) {
|
|
|
2973
3075
|
contextUsage: getSessionContextUsage(session),
|
|
2974
3076
|
pendingToolApproval: getPendingApprovalForSession(session.sessionId),
|
|
2975
3077
|
pendingAutoCompactApproval: getPendingAutoCompactApprovalForSession(session.sessionId),
|
|
3078
|
+
pendingAsk: getPendingAskForSession(session.sessionId),
|
|
2976
3079
|
acpSession: session.harness === AGENT_HARNESS_OPENCODE ? session.agent.state.acpSession : undefined,
|
|
2977
3080
|
isStreaming: session.abortPending ? false : session.agent.state.isStreaming,
|
|
2978
3081
|
errorMessage: session.agent.state.errorMessage,
|
|
3082
|
+
persistDegraded: session.persistDegraded ? true : undefined,
|
|
2979
3083
|
}
|
|
2980
3084
|
}
|
|
2981
3085
|
|
|
@@ -3008,6 +3112,7 @@ export function getSessionStatus(sessionId) {
|
|
|
3008
3112
|
errorMessage: session.agent.state.errorMessage,
|
|
3009
3113
|
messageCount: messages.length,
|
|
3010
3114
|
lastMessageTimestamp: lastMessage?.timestamp ?? null,
|
|
3115
|
+
persistDegraded: session.persistDegraded ? true : undefined,
|
|
3011
3116
|
}
|
|
3012
3117
|
}
|
|
3013
3118
|
|
|
@@ -3039,6 +3144,21 @@ export function releaseSse(sessionId) {
|
|
|
3039
3144
|
if (session) session.sseConnected = false
|
|
3040
3145
|
}
|
|
3041
3146
|
|
|
3147
|
+
/**
|
|
3148
|
+
* Count sessions with a live SSE connection — the background-migration idle
|
|
3149
|
+
* signal's "no active stream" input
|
|
3150
|
+
* (docs/architecture/session-storage-background-migration-design.zh-CN.md
|
|
3151
|
+
* §3.3). Derived from the per-session sseConnected flags the SSE route
|
|
3152
|
+
* already maintains, so no extra bookkeeping is required.
|
|
3153
|
+
*/
|
|
3154
|
+
export function countActiveSseStreams() {
|
|
3155
|
+
let count = 0
|
|
3156
|
+
for (const session of agentSessions.values()) {
|
|
3157
|
+
if (session.sseConnected) count += 1
|
|
3158
|
+
}
|
|
3159
|
+
return count
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3042
3162
|
/**
|
|
3043
3163
|
* Get the event bus for a session (for SSE connections).
|
|
3044
3164
|
*/
|
|
@@ -3128,8 +3248,8 @@ async function restoreAgentUnlocked(sessionId) {
|
|
|
3128
3248
|
// Read the authoritative storage record once: the body (split marker),
|
|
3129
3249
|
// revision for CAS, and the stored message rows (count + tail digest) used
|
|
3130
3250
|
// for split-representation conflict detection after restore.
|
|
3131
|
-
const record =
|
|
3132
|
-
const storedMessages =
|
|
3251
|
+
const record = readSessionStateRecord(sessionId)
|
|
3252
|
+
const storedMessages = record?.state?.messageStorage === 'split'
|
|
3133
3253
|
? storedMessagesState(sessionId)
|
|
3134
3254
|
: null
|
|
3135
3255
|
|
|
@@ -3156,9 +3276,9 @@ async function restoreAgentUnlocked(sessionId) {
|
|
|
3156
3276
|
openCodeUsage: sessionData.openCodeUsage || null,
|
|
3157
3277
|
idleRetention: sessionData.idleRetention || null,
|
|
3158
3278
|
stateVersion: sessionData.stateVersion,
|
|
3159
|
-
persistedStateVersion:
|
|
3160
|
-
persistedStorageRevision:
|
|
3161
|
-
persistedStateJson:
|
|
3279
|
+
persistedStateVersion: sessionData.stateVersion,
|
|
3280
|
+
persistedStorageRevision: record?.revision ?? null,
|
|
3281
|
+
persistedStateJson: canonicalStateJson(stripStorageOwnedStateFields(record?.state || {})),
|
|
3162
3282
|
persistedMessageStorage: record?.state?.messageStorage === 'split' ? 'split' : null,
|
|
3163
3283
|
persistedMessageCount: storedMessages?.count ?? (Array.isArray(sessionData.messages) ? sessionData.messages.length : 0),
|
|
3164
3284
|
persistedTailDigest: storedMessages?.tailDigest || sessionMessagesTailDigest(sessionData.messages),
|
|
@@ -3212,6 +3332,30 @@ export function rejectAutoCompact(sessionId, approvalId) {
|
|
|
3212
3332
|
return { rejected: true, approvalId }
|
|
3213
3333
|
}
|
|
3214
3334
|
|
|
3335
|
+
/**
|
|
3336
|
+
* Resolve a pending ask_user call with the user's answers (or a skip).
|
|
3337
|
+
* `answers` is an array aligned with the ask's questions:
|
|
3338
|
+
* `[{ choices: string[], custom?: string }]`.
|
|
3339
|
+
*/
|
|
3340
|
+
export function answerAsk(sessionId, askId, { answers, skipped = false } = {}) {
|
|
3341
|
+
const ask = pendingAsks.get(askId)
|
|
3342
|
+
if (!ask || ask.sessionId !== sessionId) {
|
|
3343
|
+
throw Object.assign(new Error('No pending ask for this session'), { statusCode: 404 })
|
|
3344
|
+
}
|
|
3345
|
+
const normalizedAnswers = (Array.isArray(answers) ? answers : []).slice(0, ask.questions.length).map((answer) => ({
|
|
3346
|
+
choices: (Array.isArray(answer?.choices) ? answer.choices : [])
|
|
3347
|
+
.filter((choice) => typeof choice === 'string')
|
|
3348
|
+
.map((choice) => choice.slice(0, 500))
|
|
3349
|
+
.slice(0, 8),
|
|
3350
|
+
...(typeof answer?.custom === 'string' && answer.custom.trim()
|
|
3351
|
+
? { custom: answer.custom.slice(0, 4000) }
|
|
3352
|
+
: {}),
|
|
3353
|
+
}))
|
|
3354
|
+
if (skipped) ask.finish({ skipped: true })
|
|
3355
|
+
else ask.finish({ answers: normalizedAnswers })
|
|
3356
|
+
return { answered: true, askId, skipped: !!skipped }
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3215
3359
|
export function abortToolCall(sessionId, toolCallId) {
|
|
3216
3360
|
const session = agentSessions.get(sessionId)
|
|
3217
3361
|
if (!session) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask-user store — shared pending-question state.
|
|
3
|
+
*
|
|
4
|
+
* The Promise-based ask flow (createAskUserPromise) lives in agent-manager.mjs
|
|
5
|
+
* because it depends on the agent event buses; this module only holds the
|
|
6
|
+
* pending queue, mirroring approval-store.mjs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Waiting for a user decision is not a security gate — allow generous time
|
|
10
|
+
// before resolving as skipped so the agent can continue on its own.
|
|
11
|
+
export const ASK_TIMEOUT_MS = 30 * 60 * 1000 // 30 minutes
|
|
12
|
+
|
|
13
|
+
/** askId → { finish, sessionId, toolCallId, questions, requestedAt, expiresAt } */
|
|
14
|
+
export const pendingAsks = new Map()
|
|
15
|
+
|
|
16
|
+
export function getPendingAskForSession(sessionId) {
|
|
17
|
+
for (const [askId, ask] of pendingAsks) {
|
|
18
|
+
if (ask.sessionId === sessionId) {
|
|
19
|
+
return {
|
|
20
|
+
askId,
|
|
21
|
+
toolCallId: ask.toolCallId,
|
|
22
|
+
questions: ask.questions,
|
|
23
|
+
requestedAt: ask.requestedAt,
|
|
24
|
+
expiresAt: ask.expiresAt,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Pure helpers (no dependencies — unit-tested directly)
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/** Normalize ask_user params into a bounded questions list (robust against model shorthand). */
|
|
36
|
+
export function normalizeAskQuestions(params) {
|
|
37
|
+
const rawQuestions = Array.isArray(params?.questions)
|
|
38
|
+
? params.questions
|
|
39
|
+
: (typeof params?.question === 'string' ? [{ ...params }] : [])
|
|
40
|
+
return rawQuestions
|
|
41
|
+
.filter((q) => q && typeof q.question === 'string' && q.question.trim())
|
|
42
|
+
.slice(0, 4)
|
|
43
|
+
.map((q) => ({
|
|
44
|
+
question: String(q.question),
|
|
45
|
+
multiSelect: q.multiSelect === true,
|
|
46
|
+
allowCustom: q.allowCustom === false ? false : true,
|
|
47
|
+
options: (Array.isArray(q.options) ? q.options : [])
|
|
48
|
+
.filter((option) => option && typeof option.label === 'string' && option.label.trim())
|
|
49
|
+
.slice(0, 4)
|
|
50
|
+
.map((option) => ({
|
|
51
|
+
label: String(option.label),
|
|
52
|
+
...(typeof option.description === 'string' && option.description ? { description: option.description } : {}),
|
|
53
|
+
})),
|
|
54
|
+
}))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Render the tool-result text returned to the model. */
|
|
58
|
+
export function formatAskResult(questions, answers, skipped, reason) {
|
|
59
|
+
if (skipped) {
|
|
60
|
+
const note = reason === 'timeout'
|
|
61
|
+
? '(等待超时)'
|
|
62
|
+
: reason === 'aborted'
|
|
63
|
+
? '(运行被停止)'
|
|
64
|
+
: reason === 'no-questions'
|
|
65
|
+
? '(问题列表无效)'
|
|
66
|
+
: '(用户跳过)'
|
|
67
|
+
return `用户没有回答这些问题${note}。请按你的默认方案继续,不要再重复追问。`
|
|
68
|
+
}
|
|
69
|
+
const lines = questions.map((q, index) => {
|
|
70
|
+
const answer = answers?.[index]
|
|
71
|
+
if (!answer) return `${index + 1}. ${q.question} → 用户未回答`
|
|
72
|
+
const choiceText = Array.isArray(answer.choices) && answer.choices.length ? answer.choices.join('、') : ''
|
|
73
|
+
const customText = typeof answer.custom === 'string' && answer.custom.trim() ? answer.custom.trim() : ''
|
|
74
|
+
const text = [choiceText, customText].filter(Boolean).join(' 补充:')
|
|
75
|
+
return `${index + 1}. ${q.question} → ${text || '用户未回答'}`
|
|
76
|
+
})
|
|
77
|
+
return `用户的回答:\n${lines.join('\n')}`
|
|
78
|
+
}
|
package/server/auto-archive.mjs
CHANGED
|
@@ -7,10 +7,20 @@ const defaultStorage = { atomicSessionValueUpdate, atomicUpdate, atomicSessionRe
|
|
|
7
7
|
export const AUTO_ARCHIVE_SETTINGS_KEY = 'auto-archive-settings'
|
|
8
8
|
export const AUTO_ARCHIVE_AFTER_MS = 30 * 24 * 60 * 60 * 1000
|
|
9
9
|
const AUTO_ARCHIVE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
|
|
10
|
+
export const AUTO_ARCHIVE_INITIAL_DELAY_MS = 30_000
|
|
10
11
|
|
|
11
12
|
let autoArchiveTimer = null
|
|
13
|
+
let autoArchiveInitialTimer = null
|
|
12
14
|
let autoArchiveRunning = false
|
|
13
15
|
|
|
16
|
+
// Yield one macrotask turn between heavy archive commits: each archive is a
|
|
17
|
+
// synchronous SQLite transaction (or JSON read-modify-write) that blocks the
|
|
18
|
+
// thread while it runs, and without yields a large batch of back-to-back
|
|
19
|
+
// archives starves every pending HTTP request for the whole batch.
|
|
20
|
+
function yieldToEventLoop() {
|
|
21
|
+
return new Promise((resolve) => setImmediate(resolve))
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
export function normalizeAutoArchiveSettings(value) {
|
|
15
25
|
if (!value || typeof value !== 'object') return { enabled: false }
|
|
16
26
|
return { enabled: value.enabled === true }
|
|
@@ -65,8 +75,25 @@ async function archiveInactiveSessionsUnlocked(options = {}) {
|
|
|
65
75
|
const metadataStore = await storage.readStore('sessions-metadata')
|
|
66
76
|
const candidates = []
|
|
67
77
|
|
|
78
|
+
// Metadata-first scan: metadata alone decides activeness whenever it carries
|
|
79
|
+
// any timestamp, so the common case never loads the session body (loading and
|
|
80
|
+
// JSON.parsing every 2.9GB-library body is what stalled the event loop for
|
|
81
|
+
// ~minutes on startup). The body is only read when metadata has no usable
|
|
82
|
+
// timestamp at all. The scan is therefore deliberately conservative — a body
|
|
83
|
+
// newer than its (stale) metadata is treated as a candidate — which is safe:
|
|
84
|
+
// a wrongly suspected candidate is rejected by the full-state re-check inside
|
|
85
|
+
// the archive transactions below, and a missed candidate is simply picked up
|
|
86
|
+
// by the next scan. persistSession refreshes metadata timestamps on every
|
|
87
|
+
// save, so active sessions always stay fresh in metadata.
|
|
68
88
|
for (const [sessionId, metadata] of Object.entries(metadataStore)) {
|
|
69
89
|
if (!metadata || metadata.archivedAt || metadata.messageCount === 0 || metadata.taskStatus === 'running') continue
|
|
90
|
+
const metadataActivityTime = sessionActivityTime(metadata)
|
|
91
|
+
if (metadataActivityTime !== null) {
|
|
92
|
+
if (metadataActivityTime >= cutoff) continue
|
|
93
|
+
candidates.push(sessionId)
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
// Metadata carries no timestamp at all: fall back to the body.
|
|
70
97
|
const session = await storage.readSessionValue(sessionId)
|
|
71
98
|
if (!session || session.archivedAt) continue
|
|
72
99
|
const activityTime = sessionActivityTime(metadata, session)
|
|
@@ -75,10 +102,13 @@ async function archiveInactiveSessionsUnlocked(options = {}) {
|
|
|
75
102
|
}
|
|
76
103
|
|
|
77
104
|
const archivedSessionIds = []
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
for (const sessionId of candidates) {
|
|
105
|
+
// Authoritative: archive body + metadata in one SQLite transaction (CAS
|
|
106
|
+
// retried on concurrent metadata-only changes), so no half-archived state.
|
|
107
|
+
if (typeof storage.atomicSessionRecordUpdate === 'function') {
|
|
108
|
+
for (const [index, sessionId] of candidates.entries()) {
|
|
109
|
+
// Let pending requests run between archive transactions (not before the
|
|
110
|
+
// first candidate — nothing to yield from yet).
|
|
111
|
+
if (index > 0) await yieldToEventLoop()
|
|
82
112
|
const updated = await storage.atomicSessionRecordUpdate(sessionId, ({ state, metadata }) => {
|
|
83
113
|
const activityTime = sessionActivityTime(metadata, state)
|
|
84
114
|
if (state.archivedAt || metadata.archivedAt || metadata.messageCount === 0 || metadata.taskStatus === 'running' || activityTime === null || activityTime >= cutoff) return null
|
|
@@ -87,7 +117,10 @@ async function archiveInactiveSessionsUnlocked(options = {}) {
|
|
|
87
117
|
if (updated?.state?.archivedAt === archivedAt) archivedSessionIds.push(sessionId)
|
|
88
118
|
}
|
|
89
119
|
} else {
|
|
90
|
-
for (const sessionId of candidates) {
|
|
120
|
+
for (const [index, sessionId] of candidates.entries()) {
|
|
121
|
+
// Let pending requests run between archive transactions (not before the
|
|
122
|
+
// first candidate — nothing to yield from yet).
|
|
123
|
+
if (index > 0) await yieldToEventLoop()
|
|
91
124
|
const updatedSession = await storage.atomicSessionValueUpdate(sessionId, (session) => {
|
|
92
125
|
const activityTime = sessionActivityTime(session)
|
|
93
126
|
if (session.archivedAt || activityTime === null || activityTime >= cutoff) return session
|
|
@@ -134,16 +167,30 @@ export async function archiveInactiveSessions(options = {}) {
|
|
|
134
167
|
}
|
|
135
168
|
}
|
|
136
169
|
|
|
137
|
-
|
|
170
|
+
// `options` exists for tests (inject storage/now like archiveInactiveSessions);
|
|
171
|
+
// production callers pass nothing and get defaultStorage + Date.now().
|
|
172
|
+
export function startAutoArchiveRunner(options = {}) {
|
|
138
173
|
if (autoArchiveTimer) return
|
|
139
174
|
autoArchiveTimer = setInterval(() => {
|
|
140
|
-
archiveInactiveSessions().catch((error) => logger.error('Automatic conversation archive failed:', error))
|
|
175
|
+
archiveInactiveSessions(options).catch((error) => logger.error('Automatic conversation archive failed:', error))
|
|
141
176
|
}, AUTO_ARCHIVE_CHECK_INTERVAL_MS)
|
|
142
177
|
autoArchiveTimer.unref?.()
|
|
143
|
-
|
|
178
|
+
// Delay the first pass off the startup path: right after listen() the server
|
|
179
|
+
// is absorbing the browser's initial request burst, and the scan + archive
|
|
180
|
+
// transactions on a large library can block the event loop for a long
|
|
181
|
+
// stretch. The timer is unref'd so it never keeps the process alive.
|
|
182
|
+
autoArchiveInitialTimer = setTimeout(() => {
|
|
183
|
+
autoArchiveInitialTimer = null
|
|
184
|
+
archiveInactiveSessions(options).catch((error) => logger.error('Initial automatic conversation archive failed:', error))
|
|
185
|
+
}, AUTO_ARCHIVE_INITIAL_DELAY_MS)
|
|
186
|
+
autoArchiveInitialTimer.unref?.()
|
|
144
187
|
}
|
|
145
188
|
|
|
146
189
|
export function stopAutoArchiveRunner() {
|
|
190
|
+
if (autoArchiveInitialTimer) {
|
|
191
|
+
clearTimeout(autoArchiveInitialTimer)
|
|
192
|
+
autoArchiveInitialTimer = null
|
|
193
|
+
}
|
|
147
194
|
if (!autoArchiveTimer) return
|
|
148
195
|
clearInterval(autoArchiveTimer)
|
|
149
196
|
autoArchiveTimer = null
|