@shawnstack/quickforge 1.3.29 → 1.4.0
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/README.md +12 -12
- package/dist/assets/AgentProfilesPage-C79teCgh.js +1 -0
- package/dist/assets/ChatPanelHost-BjdIshtX.js +195 -0
- package/dist/assets/PluginsPage-Dt7Iiddo.js +1 -0
- package/dist/assets/ScheduledTasksPage-C047y3p3.js +2 -0
- package/dist/assets/SharedConversationPage-8X8kfztQ.js +1 -0
- package/dist/assets/TerminalDock-CEuJNf0m.js +2 -0
- package/dist/assets/WorkspaceInspector-BIa5gLVs.js +3 -0
- package/dist/assets/WorkspaceReaderDialog-bTeERaGd.js +6 -0
- package/dist/assets/{icons-BVM5--R9.js → icons-Dsc5yL3l.js} +1 -1
- package/dist/assets/index-CPAWYhzz.css +3 -0
- package/dist/assets/index-YTL26wyJ.js +814 -0
- package/dist/assets/logger-B65Akg8A.js +1 -0
- package/dist/assets/monaco-DG4TcBMc.js +11 -0
- package/dist/assets/pi-ai-Cx633yhb.js +134 -0
- package/dist/assets/pi-web-ui-CBet4bMl.js +2770 -0
- package/dist/assets/plugin-api-YfYj_Bd7.js +1 -0
- package/dist/assets/{react-vendor-DAoL5p8_.js → react-vendor-CiCXOLb5.js} +1 -1
- package/dist/assets/rolldown-runtime-DWdDZTNf.js +1 -0
- package/dist/assets/xterm-5XDrJ343.js +36 -0
- package/dist/assets/xterm-BrP-ENHg.css +1 -0
- package/dist/index.html +8 -5
- package/package.json +1 -1
- package/server/agent-manager.mjs +109 -20
- package/server/approval-store.mjs +1 -1
- package/server/auto-compaction.mjs +36 -1
- package/server/index.mjs +13 -0
- package/server/routes/agent.mjs +19 -0
- package/server/storage.mjs +32 -20
- package/server/utils/logger.mjs +0 -2
- package/dist/assets/anthropic-Bi2whCo9.js +0 -39
- package/dist/assets/azure-openai-responses-BIluwauz.js +0 -1
- package/dist/assets/github-copilot-headers-CMb2BbzT.js +0 -1
- package/dist/assets/google-DzMAdtX7.js +0 -1
- package/dist/assets/google-shared-Cqjw1plk.js +0 -11
- package/dist/assets/google-vertex-BPMvmXyu.js +0 -1
- package/dist/assets/hash-kZ2KD_no.js +0 -1
- package/dist/assets/headers-5EYI0_pl.js +0 -1
- package/dist/assets/index-CcvNhwdQ.css +0 -3
- package/dist/assets/index-CnT_4xVs.js +0 -3837
- package/dist/assets/mistral-C3NYr8yr.js +0 -44
- package/dist/assets/openai-Bf1npfRy.js +0 -16
- package/dist/assets/openai-codex-responses-jn0IUTnv.js +0 -7
- package/dist/assets/openai-completions-d4aAZ4cH.js +0 -5
- package/dist/assets/openai-prompt-cache-CErE62Yt.js +0 -1
- package/dist/assets/openai-responses-D5wQx0VD.js +0 -1
- package/dist/assets/openai-responses-shared-DkIGPnog.js +0 -12
- package/dist/assets/openrouter-DslZMI-g.js +0 -1
- package/dist/assets/rolldown-runtime-CkqCuyE9.js +0 -1
- package/dist/assets/sanitize-unicode-BhyPmlyt.js +0 -1
- package/dist/assets/transform-messages-Dhj_4OTw.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000) 0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset}
|
package/dist/index.html
CHANGED
|
@@ -11,13 +11,16 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="QuickForge" />
|
|
12
12
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
13
13
|
<title>速构 QuickForge</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-YTL26wyJ.js"></script>
|
|
15
|
+
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-DWdDZTNf.js">
|
|
16
|
+
<link rel="modulepreload" crossorigin href="/assets/pi-ai-Cx633yhb.js">
|
|
16
17
|
<link rel="modulepreload" crossorigin href="/assets/lit-vendor-Dr3cpBGF.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/pi-web-ui-CBet4bMl.js">
|
|
17
19
|
<link rel="modulepreload" crossorigin href="/assets/css-utils-rkE68RDy.js">
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/icons-
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/react-vendor-
|
|
20
|
-
<link rel="
|
|
20
|
+
<link rel="modulepreload" crossorigin href="/assets/icons-Dsc5yL3l.js">
|
|
21
|
+
<link rel="modulepreload" crossorigin href="/assets/react-vendor-CiCXOLb5.js">
|
|
22
|
+
<link rel="modulepreload" crossorigin href="/assets/logger-B65Akg8A.js">
|
|
23
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CPAWYhzz.css">
|
|
21
24
|
</head>
|
|
22
25
|
<body>
|
|
23
26
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/agent-manager.mjs
CHANGED
|
@@ -196,19 +196,14 @@ function createApprovalPromise(session, toolCallId, toolName, args, source) {
|
|
|
196
196
|
source,
|
|
197
197
|
})
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
// The global SSE handler (/api/agents/events) only listens to `agentEvents`,
|
|
201
|
-
// so events emitted only on session.eventBus never reach the client.
|
|
202
|
-
const approvalEvent = {
|
|
199
|
+
emitSessionEvent(session, {
|
|
203
200
|
type: 'tool_approval_required',
|
|
204
201
|
sessionId: session.sessionId,
|
|
205
202
|
toolCallId,
|
|
206
203
|
toolName,
|
|
207
204
|
args,
|
|
208
205
|
source,
|
|
209
|
-
}
|
|
210
|
-
session.eventBus.emit('agent_event', approvalEvent)
|
|
211
|
-
agentEvents.emit('agent_event', approvalEvent)
|
|
206
|
+
})
|
|
212
207
|
})
|
|
213
208
|
}
|
|
214
209
|
|
|
@@ -312,11 +307,18 @@ function estimateTokenReduction(originalChars, finalChars) {
|
|
|
312
307
|
return Math.max(0, Math.min(99, Math.round(((originalChars - finalChars) / originalChars) * 100)))
|
|
313
308
|
}
|
|
314
309
|
|
|
310
|
+
function nextSessionStateVersion(session) {
|
|
311
|
+
const current = Number.isFinite(session?.stateVersion) ? session.stateVersion : 0
|
|
312
|
+
session.stateVersion = current + 1
|
|
313
|
+
return session.stateVersion
|
|
314
|
+
}
|
|
315
|
+
|
|
315
316
|
function emitSessionEvent(session, event) {
|
|
317
|
+
const stateVersion = nextSessionStateVersion(session)
|
|
316
318
|
const enrichedEvent = (event?.type === 'message_end' || event?.type === 'agent_end' || event?.type === 'messages_replaced' || event?.type === 'auto_compact_completed')
|
|
317
319
|
&& event.contextUsage === undefined
|
|
318
|
-
? { ...event, contextUsage: getSessionContextUsage(session) }
|
|
319
|
-
: event
|
|
320
|
+
? { ...event, contextUsage: getSessionContextUsage(session), stateVersion }
|
|
321
|
+
: { ...event, stateVersion }
|
|
320
322
|
session.eventBus.emit('agent_event', enrichedEvent)
|
|
321
323
|
agentEvents.emit('agent_event', { sessionId: session.sessionId, ...enrichedEvent })
|
|
322
324
|
}
|
|
@@ -556,7 +558,7 @@ async function resolveCommandState(session, userMessage) {
|
|
|
556
558
|
return {
|
|
557
559
|
userMessage,
|
|
558
560
|
commandPrompt: formatPlanCommandPrompt(internalResponse.args),
|
|
559
|
-
permissions: { allowEdit: false, allowCommands: false },
|
|
561
|
+
permissions: { allowEdit: false, allowCommands: false, allowSubagents: true },
|
|
560
562
|
commandName: 'plan',
|
|
561
563
|
}
|
|
562
564
|
}
|
|
@@ -597,6 +599,7 @@ Rules for this turn:
|
|
|
597
599
|
- Do not run shell commands.
|
|
598
600
|
- Do not use write_file, edit_file, run_command, or any other state-changing tool.
|
|
599
601
|
- You may use read-only tools such as read_file and grep_files if needed to inspect the project.
|
|
602
|
+
- You may delegate bounded read-only research to subagents, but subagents must also obey this /plan turn: no file modifications and no shell commands.
|
|
600
603
|
- Output the plan and then stop. Do not start implementation.
|
|
601
604
|
|
|
602
605
|
Plan should include:
|
|
@@ -681,7 +684,7 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
681
684
|
true,
|
|
682
685
|
(toolName) => {
|
|
683
686
|
if (!definition.allowedTools.includes(toolName)) return `Subagent ${definition.name} is not allowed to use ${toolName}.`
|
|
684
|
-
return
|
|
687
|
+
return commandToolPermissionError(parentSession, toolName)
|
|
685
688
|
},
|
|
686
689
|
{
|
|
687
690
|
allowedToolNames: definition.allowedTools,
|
|
@@ -764,6 +767,8 @@ async function runSubagent(parentSession, params, parentSignal, onUpdate) {
|
|
|
764
767
|
if (!definition.allowedTools.includes(toolName)) {
|
|
765
768
|
return { block: true, reason: `Subagent ${definition.name} is not allowed to use ${toolName}.` }
|
|
766
769
|
}
|
|
770
|
+
const commandPermissionError = commandToolPermissionError(parentSession, toolName)
|
|
771
|
+
if (commandPermissionError) return { block: true, reason: commandPermissionError }
|
|
767
772
|
if (!parentSession.yoloMode) {
|
|
768
773
|
if (safeReadTools.has(toolName)) return undefined
|
|
769
774
|
return createApprovalPromise(parentSession, context.toolCall?.id, toolName, context.args, {
|
|
@@ -1135,6 +1140,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1135
1140
|
activeCommandPrompt: null,
|
|
1136
1141
|
eventBus,
|
|
1137
1142
|
idleTimer: null,
|
|
1143
|
+
persistTimer: null,
|
|
1138
1144
|
titleGenerated: false,
|
|
1139
1145
|
toolTimings: new Map(),
|
|
1140
1146
|
getApiKey,
|
|
@@ -1142,6 +1148,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1142
1148
|
agentProfile: agentProfile ? agentProfileSnapshot(agentProfile) : null,
|
|
1143
1149
|
lastTransformedContextMessages: null,
|
|
1144
1150
|
autoCompacting: false,
|
|
1151
|
+
stateVersion: 0,
|
|
1145
1152
|
lastAutoCompactAt: null,
|
|
1146
1153
|
lastAutoCompactRejected: null,
|
|
1147
1154
|
/** Track active SSE connections. Only one SSE stream allowed per session to prevent
|
|
@@ -1156,8 +1163,20 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1156
1163
|
// complete session history. Replace with the authoritative full state
|
|
1157
1164
|
// before forwarding to clients.
|
|
1158
1165
|
const timedEvent = addToolTimingToEvent(session, event)
|
|
1159
|
-
const
|
|
1160
|
-
?
|
|
1166
|
+
const eventEndStatus = event.type === 'agent_end'
|
|
1167
|
+
? session.agent.signal?.aborted
|
|
1168
|
+
? 'aborted'
|
|
1169
|
+
: session.agent.state.errorMessage
|
|
1170
|
+
? 'error'
|
|
1171
|
+
: 'idle'
|
|
1172
|
+
: undefined
|
|
1173
|
+
const forwardEvent = timedEvent.type === 'agent_end'
|
|
1174
|
+
? {
|
|
1175
|
+
...timedEvent,
|
|
1176
|
+
...(timedEvent.messages ? { messages: agent.state.messages } : {}),
|
|
1177
|
+
status: eventEndStatus,
|
|
1178
|
+
...(session.agent.state.errorMessage && timedEvent.errorMessage === undefined ? { errorMessage: session.agent.state.errorMessage } : {}),
|
|
1179
|
+
}
|
|
1161
1180
|
: timedEvent
|
|
1162
1181
|
|
|
1163
1182
|
// Forward all events to the session event bus and the global bus.
|
|
@@ -1175,22 +1194,21 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1175
1194
|
}
|
|
1176
1195
|
|
|
1177
1196
|
if (event.type === 'agent_end') {
|
|
1178
|
-
session.status = session.agent.state.errorMessage ? 'error' : 'idle'
|
|
1197
|
+
session.status = eventEndStatus || (session.agent.state.errorMessage ? 'error' : 'idle')
|
|
1179
1198
|
session.finishedAt = new Date().toISOString()
|
|
1180
1199
|
session.toolTimings?.clear()
|
|
1181
1200
|
resetIdleTimer(session)
|
|
1182
1201
|
|
|
1183
|
-
// Persist after run ends
|
|
1184
|
-
|
|
1202
|
+
// Persist after run ends. Flush any debounced write so the final state is durable.
|
|
1203
|
+
flushSessionPersist(session).catch((err) =>
|
|
1185
1204
|
logger.error(`Failed to persist session ${sessionId}:`, err, { sessionId }),
|
|
1186
1205
|
)
|
|
1187
1206
|
}
|
|
1188
1207
|
|
|
1189
1208
|
if (event.type === 'message_end') {
|
|
1190
|
-
//
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
)
|
|
1209
|
+
// Debounced persist for crash recovery; coalesces the many message_end
|
|
1210
|
+
// events within a single run into infrequent full-session writes.
|
|
1211
|
+
scheduleSessionPersist(session)
|
|
1194
1212
|
}
|
|
1195
1213
|
})
|
|
1196
1214
|
|
|
@@ -1334,6 +1352,44 @@ async function persistSession(session) {
|
|
|
1334
1352
|
}
|
|
1335
1353
|
|
|
1336
1354
|
export async function persistSessionState(session) {
|
|
1355
|
+
await flushSessionPersist(session)
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* Coalesce fire-and-forget session persists during a run.
|
|
1360
|
+
*
|
|
1361
|
+
* persistSession() serializes the ENTIRE session (all messages) on every call,
|
|
1362
|
+
* and the agent event loop calls it on agent_start / each message_end / agent_end.
|
|
1363
|
+
* Within a single run these events fire many times (one per assistant turn +
|
|
1364
|
+
* tool result), so writing on each one makes cumulative disk I/O O(n^2) as a
|
|
1365
|
+
* conversation grows. These message_end call sites are fire-and-forget
|
|
1366
|
+
* (crash-recovery only), so we debounce them into at most one write per
|
|
1367
|
+
* PERSIST_DEBOUNCE_MS. Run boundaries (agent_end) and explicit persists cancel
|
|
1368
|
+
* the pending timer and write the current state immediately, so the final
|
|
1369
|
+
* state is always durable.
|
|
1370
|
+
*/
|
|
1371
|
+
const PERSIST_DEBOUNCE_MS = 400
|
|
1372
|
+
|
|
1373
|
+
function scheduleSessionPersist(session) {
|
|
1374
|
+
if (session.persistTimer) return
|
|
1375
|
+
session.persistTimer = setTimeout(() => {
|
|
1376
|
+
session.persistTimer = null
|
|
1377
|
+
persistSession(session).catch((err) =>
|
|
1378
|
+
logger.error(`Failed to persist session ${session.sessionId}:`, err, { sessionId: session.sessionId }),
|
|
1379
|
+
)
|
|
1380
|
+
}, PERSIST_DEBOUNCE_MS).unref?.()
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Cancel any pending debounced write and persist the current state immediately.
|
|
1385
|
+
* Used at run boundaries (agent_end) and by explicit persistSessionState() so
|
|
1386
|
+
* the final state is always durable regardless of a pending timer.
|
|
1387
|
+
*/
|
|
1388
|
+
async function flushSessionPersist(session) {
|
|
1389
|
+
if (session.persistTimer) {
|
|
1390
|
+
clearTimeout(session.persistTimer)
|
|
1391
|
+
session.persistTimer = null
|
|
1392
|
+
}
|
|
1337
1393
|
await persistSession(session)
|
|
1338
1394
|
}
|
|
1339
1395
|
|
|
@@ -1559,6 +1615,7 @@ export async function abortRun(sessionId) {
|
|
|
1559
1615
|
)
|
|
1560
1616
|
const event = {
|
|
1561
1617
|
type: 'agent_end',
|
|
1618
|
+
status: 'aborted',
|
|
1562
1619
|
messages: session.agent.state.messages,
|
|
1563
1620
|
}
|
|
1564
1621
|
emitSessionEvent(session, event)
|
|
@@ -1628,6 +1685,7 @@ export function getSessionState(sessionId) {
|
|
|
1628
1685
|
title: session.title,
|
|
1629
1686
|
createdAt: session.createdAt,
|
|
1630
1687
|
lastModified: session.lastModified,
|
|
1688
|
+
stateVersion: session.stateVersion || 0,
|
|
1631
1689
|
status: session.status,
|
|
1632
1690
|
startedAt: session.startedAt,
|
|
1633
1691
|
finishedAt: session.finishedAt,
|
|
@@ -1640,6 +1698,33 @@ export function getSessionState(sessionId) {
|
|
|
1640
1698
|
}
|
|
1641
1699
|
}
|
|
1642
1700
|
|
|
1701
|
+
/**
|
|
1702
|
+
* Get a lightweight status snapshot for SSE-first state recovery.
|
|
1703
|
+
*/
|
|
1704
|
+
export function getSessionStatus(sessionId) {
|
|
1705
|
+
const session = agentSessions.get(sessionId)
|
|
1706
|
+
if (!session) return null
|
|
1707
|
+
|
|
1708
|
+
const messages = session.agent.state.messages || []
|
|
1709
|
+
const lastMessage = messages[messages.length - 1]
|
|
1710
|
+
return {
|
|
1711
|
+
sessionId: session.sessionId,
|
|
1712
|
+
scope: session.scope,
|
|
1713
|
+
projectId: session.projectId,
|
|
1714
|
+
title: session.title,
|
|
1715
|
+
createdAt: session.createdAt,
|
|
1716
|
+
lastModified: session.lastModified,
|
|
1717
|
+
stateVersion: session.stateVersion || 0,
|
|
1718
|
+
status: session.status,
|
|
1719
|
+
startedAt: session.startedAt,
|
|
1720
|
+
finishedAt: session.finishedAt,
|
|
1721
|
+
isStreaming: session.agent.state.isStreaming,
|
|
1722
|
+
errorMessage: session.agent.state.errorMessage,
|
|
1723
|
+
messageCount: messages.length,
|
|
1724
|
+
lastMessageTimestamp: lastMessage?.timestamp ?? null,
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1643
1728
|
/**
|
|
1644
1729
|
* Try to claim the SSE slot for a session. Returns true if acquired, false if
|
|
1645
1730
|
* another tab already holds the SSE connection for this session.
|
|
@@ -1686,6 +1771,10 @@ export async function destroyAgent(sessionId) {
|
|
|
1686
1771
|
logger.info(`Destroying session ${sessionId} (status: ${session.status})`, { sessionId, status: session.status })
|
|
1687
1772
|
|
|
1688
1773
|
if (session.idleTimer) clearTimeout(session.idleTimer)
|
|
1774
|
+
if (session.persistTimer) {
|
|
1775
|
+
clearTimeout(session.persistTimer)
|
|
1776
|
+
session.persistTimer = null
|
|
1777
|
+
}
|
|
1689
1778
|
session.toolTimings?.clear()
|
|
1690
1779
|
|
|
1691
1780
|
try {
|
|
@@ -49,7 +49,7 @@ export function commandToolPermissionError(session, toolName) {
|
|
|
49
49
|
if (toolName === 'run_command' && permissions.allowCommands === false) {
|
|
50
50
|
return `Command /${session.activeCommandName} does not allow running shell commands.`
|
|
51
51
|
}
|
|
52
|
-
if (toolName === 'run_subagent' &&
|
|
52
|
+
if (toolName === 'run_subagent' && permissions.allowSubagents === false) {
|
|
53
53
|
return `Command /${session.activeCommandName} does not allow running subagents.`
|
|
54
54
|
}
|
|
55
55
|
if ((toolName === 'write_file' || toolName === 'edit_file') && permissions.allowEdit === false) {
|
|
@@ -222,15 +222,50 @@ export function estimateSessionContextUsage(session, messages = session?.agent?.
|
|
|
222
222
|
if (sourceMessages.length === 0) {
|
|
223
223
|
return { inputTokens: 0, estimatedInputTokens: 0, knownInputTokens: 0, reservedOutputTokens: 0, totalTokens: 0, contextWindow, percent: 0 }
|
|
224
224
|
}
|
|
225
|
+
|
|
226
|
+
// Cache by input identity. estimateContextUsage() scans every message with a
|
|
227
|
+
// tokenizer regex (O(n)) and JSON-stringifies the full tools array, but its
|
|
228
|
+
// inputs (messages, model, systemPrompt, tools, contextCompaction) are stable
|
|
229
|
+
// within a run and only change on discrete events (message_end, tool result,
|
|
230
|
+
// compaction). Reference equality makes the cache check essentially free, so
|
|
231
|
+
// the repeated calls from emitSessionEvent() on message_end/agent_end/etc.
|
|
232
|
+
// only recompute when something actually changed.
|
|
233
|
+
const lastMessage = sourceMessages[sourceMessages.length - 1]
|
|
234
|
+
const cacheKey = {
|
|
235
|
+
messages,
|
|
236
|
+
messagesLength: sourceMessages.length,
|
|
237
|
+
lastMessage,
|
|
238
|
+
model: session.model,
|
|
239
|
+
systemPrompt: session.agent.state.systemPrompt,
|
|
240
|
+
tools: session.agent.state.tools,
|
|
241
|
+
contextCompaction: session.contextCompaction,
|
|
242
|
+
}
|
|
243
|
+
const cached = session._contextUsageCache
|
|
244
|
+
if (
|
|
245
|
+
cached &&
|
|
246
|
+
cached.key.messages === cacheKey.messages &&
|
|
247
|
+
cached.key.messagesLength === cacheKey.messagesLength &&
|
|
248
|
+
cached.key.lastMessage === cacheKey.lastMessage &&
|
|
249
|
+
cached.key.model === cacheKey.model &&
|
|
250
|
+
cached.key.systemPrompt === cacheKey.systemPrompt &&
|
|
251
|
+
cached.key.tools === cacheKey.tools &&
|
|
252
|
+
cached.key.contextCompaction === cacheKey.contextCompaction
|
|
253
|
+
) {
|
|
254
|
+
return cached.value
|
|
255
|
+
}
|
|
256
|
+
|
|
225
257
|
const loopMessages = buildAutoCompactLoopMessages(session, sourceMessages)
|
|
226
258
|
const knownInputTokens = latestKnownInputTokens(sourceMessages, latestCompactTimestampMs(session))
|
|
227
|
-
|
|
259
|
+
const value = estimateContextUsage({
|
|
228
260
|
systemPrompt: session.agent.state.systemPrompt,
|
|
229
261
|
messages: loopMessages,
|
|
230
262
|
tools: session.agent.state.tools,
|
|
231
263
|
model: session.model,
|
|
232
264
|
knownInputTokens,
|
|
233
265
|
})
|
|
266
|
+
|
|
267
|
+
session._contextUsageCache = { key: cacheKey, value }
|
|
268
|
+
return value
|
|
234
269
|
}
|
|
235
270
|
|
|
236
271
|
export async function maybeAutoCompactSession({ session, messages, signal, emitSessionEvent, persistSession, logger, confirmAutoCompact }) {
|
package/server/index.mjs
CHANGED
|
@@ -537,6 +537,19 @@ await initializeActiveProject()
|
|
|
537
537
|
setActiveWorkspaceRootForFilesystem(getWorkspaceRoot())
|
|
538
538
|
startScheduledTaskRunner()
|
|
539
539
|
|
|
540
|
+
server.on('error', (error) => {
|
|
541
|
+
// Handle listen errors (most commonly EADDRINUSE). Without this, Node would
|
|
542
|
+
// throw an uncaught exception and crash with only a raw stack trace in the log.
|
|
543
|
+
if (error.code === 'EADDRINUSE') {
|
|
544
|
+
logger.error(`Port ${port} is already in use. QuickForge could not start.`)
|
|
545
|
+
logger.error('Hint: stop the running instance with "quickforge stop" or "quickforge restart", or use a different port with QUICKFORGE_PORT=<port>.')
|
|
546
|
+
} else {
|
|
547
|
+
logger.error(`QuickForge failed to listen on ${host}:${port}: ${error.message}`)
|
|
548
|
+
}
|
|
549
|
+
flushLogger()
|
|
550
|
+
process.exit(1)
|
|
551
|
+
})
|
|
552
|
+
|
|
540
553
|
server.listen(port, host, () => {
|
|
541
554
|
logger.info(`QuickForge local API: http://${host}:${port}`)
|
|
542
555
|
if (shareLanEnabled) {
|
package/server/routes/agent.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
steerAgent,
|
|
7
7
|
followUpAgent,
|
|
8
8
|
getSessionState,
|
|
9
|
+
getSessionStatus,
|
|
9
10
|
getSessionEventBus,
|
|
10
11
|
tryAcquireSse,
|
|
11
12
|
releaseSse,
|
|
@@ -112,6 +113,24 @@ export async function handleAgentApi(req, res, url) {
|
|
|
112
113
|
return
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
// GET /api/agents/:sessionId/status - get lightweight session status
|
|
117
|
+
if (req.method === 'GET' && subPath === 'status') {
|
|
118
|
+
let status = getSessionStatus(sessionId)
|
|
119
|
+
if (!status) {
|
|
120
|
+
// Try to restore from persistent storage before giving up.
|
|
121
|
+
// This recovers sessions that were evicted by idle timeout.
|
|
122
|
+
await restoreAgent(sessionId)
|
|
123
|
+
status = getSessionStatus(sessionId)
|
|
124
|
+
}
|
|
125
|
+
if (!status) {
|
|
126
|
+
const error = new Error('Session not found')
|
|
127
|
+
error.statusCode = 404
|
|
128
|
+
throw error
|
|
129
|
+
}
|
|
130
|
+
sendJson(res, 200, status)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
115
134
|
// POST /api/agents/:sessionId — create/ensure agent
|
|
116
135
|
if (req.method === 'POST' && parts.length === 3) {
|
|
117
136
|
const body = await readJsonBody(req)
|
package/server/storage.mjs
CHANGED
|
@@ -705,26 +705,38 @@ export async function atomicProjectConfigUpdate(updateFn) {
|
|
|
705
705
|
})
|
|
706
706
|
}
|
|
707
707
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
fs.mkdir(
|
|
718
|
-
fs.mkdir(
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
708
|
+
// Cached storage-initialization promise. ensureStorage() is idempotent (mkdir
|
|
709
|
+
// recursive, one-shot migration gated by a marker file, ensureJsonFile), so once
|
|
710
|
+
// it succeeds we can skip the redundant syscalls (~20 per call) every later call
|
|
711
|
+
// would perform. Reset on failure so the next call can retry.
|
|
712
|
+
let storageInitPromise = null
|
|
713
|
+
|
|
714
|
+
export function ensureStorage() {
|
|
715
|
+
if (storageInitPromise) return storageInitPromise
|
|
716
|
+
storageInitPromise = (async () => {
|
|
717
|
+
await fs.mkdir(configDir, { recursive: true })
|
|
718
|
+
await fs.mkdir(storageDir, { recursive: true })
|
|
719
|
+
await fs.mkdir(cacheDir, { recursive: true })
|
|
720
|
+
await fs.mkdir(logsDir, { recursive: true })
|
|
721
|
+
await Promise.all([
|
|
722
|
+
fs.mkdir(path.join(cacheDir, 'global', 'llm'), { recursive: true }),
|
|
723
|
+
fs.mkdir(path.join(cacheDir, 'global', 'tmp'), { recursive: true }),
|
|
724
|
+
fs.mkdir(path.join(cacheDir, 'projects'), { recursive: true }),
|
|
725
|
+
fs.mkdir(path.join(storageDir, 'conversations', 'global', 'sessions'), { recursive: true }),
|
|
726
|
+
fs.mkdir(path.join(storageDir, 'conversations', 'projects'), { recursive: true }),
|
|
727
|
+
cleanOldLogs(),
|
|
728
|
+
])
|
|
729
|
+
|
|
730
|
+
await migrateUnifiedConfig()
|
|
731
|
+
|
|
732
|
+
await Promise.all([
|
|
733
|
+
ensureJsonFile(quickForgeConfigFile, defaultConfig()),
|
|
734
|
+
ensureJsonFile(sessionStoreFile('sessions-metadata', { scope: 'global' })),
|
|
735
|
+
])
|
|
736
|
+
})()
|
|
737
|
+
// Reset on failure so the next call can retry instead of caching a rejection.
|
|
738
|
+
storageInitPromise.catch(() => { storageInitPromise = null })
|
|
739
|
+
return storageInitPromise
|
|
728
740
|
}
|
|
729
741
|
|
|
730
742
|
export async function readStore(storeName) {
|
package/server/utils/logger.mjs
CHANGED
|
@@ -136,9 +136,7 @@ function writeLog(level, context, ...args) {
|
|
|
136
136
|
const jsonLine = JSON.stringify(jsonObj) + '\n'
|
|
137
137
|
|
|
138
138
|
try {
|
|
139
|
-
const s = getStream()
|
|
140
139
|
pendingLines.push(jsonLine)
|
|
141
|
-
s.write(jsonLine) // also try immediate write
|
|
142
140
|
scheduleFlush()
|
|
143
141
|
} catch {
|
|
144
142
|
// ignore write errors
|