@shawnstack/quickforge 1.3.30 → 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 +10 -10
- 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-D0c0FMPa.css → index-CPAWYhzz.css} +1 -1
- 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 +104 -18
- 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-DYkQmon0.js +0 -39
- package/dist/assets/azure-openai-responses-B1_ZuuCX.js +0 -1
- package/dist/assets/github-copilot-headers-CMb2BbzT.js +0 -1
- package/dist/assets/google-Bx1PGUtS.js +0 -1
- package/dist/assets/google-shared-Cqjw1plk.js +0 -11
- package/dist/assets/google-vertex-1iRQw75f.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-CQq-kPng.js +0 -3837
- package/dist/assets/mistral-B1j5S2k5.js +0 -44
- package/dist/assets/openai-Bf1npfRy.js +0 -16
- package/dist/assets/openai-codex-responses-BJKEqst-.js +0 -7
- package/dist/assets/openai-completions-B_cU49Pc.js +0 -5
- package/dist/assets/openai-prompt-cache-CErE62Yt.js +0 -1
- package/dist/assets/openai-responses-DgGY16ph.js +0 -1
- package/dist/assets/openai-responses-shared-J1-i-goZ.js +0 -12
- package/dist/assets/openrouter-BVaMghZV.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
|
}
|
|
@@ -1138,6 +1140,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1138
1140
|
activeCommandPrompt: null,
|
|
1139
1141
|
eventBus,
|
|
1140
1142
|
idleTimer: null,
|
|
1143
|
+
persistTimer: null,
|
|
1141
1144
|
titleGenerated: false,
|
|
1142
1145
|
toolTimings: new Map(),
|
|
1143
1146
|
getApiKey,
|
|
@@ -1145,6 +1148,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1145
1148
|
agentProfile: agentProfile ? agentProfileSnapshot(agentProfile) : null,
|
|
1146
1149
|
lastTransformedContextMessages: null,
|
|
1147
1150
|
autoCompacting: false,
|
|
1151
|
+
stateVersion: 0,
|
|
1148
1152
|
lastAutoCompactAt: null,
|
|
1149
1153
|
lastAutoCompactRejected: null,
|
|
1150
1154
|
/** Track active SSE connections. Only one SSE stream allowed per session to prevent
|
|
@@ -1159,8 +1163,20 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1159
1163
|
// complete session history. Replace with the authoritative full state
|
|
1160
1164
|
// before forwarding to clients.
|
|
1161
1165
|
const timedEvent = addToolTimingToEvent(session, event)
|
|
1162
|
-
const
|
|
1163
|
-
?
|
|
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
|
+
}
|
|
1164
1180
|
: timedEvent
|
|
1165
1181
|
|
|
1166
1182
|
// Forward all events to the session event bus and the global bus.
|
|
@@ -1178,22 +1194,21 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1178
1194
|
}
|
|
1179
1195
|
|
|
1180
1196
|
if (event.type === 'agent_end') {
|
|
1181
|
-
session.status = session.agent.state.errorMessage ? 'error' : 'idle'
|
|
1197
|
+
session.status = eventEndStatus || (session.agent.state.errorMessage ? 'error' : 'idle')
|
|
1182
1198
|
session.finishedAt = new Date().toISOString()
|
|
1183
1199
|
session.toolTimings?.clear()
|
|
1184
1200
|
resetIdleTimer(session)
|
|
1185
1201
|
|
|
1186
|
-
// Persist after run ends
|
|
1187
|
-
|
|
1202
|
+
// Persist after run ends. Flush any debounced write so the final state is durable.
|
|
1203
|
+
flushSessionPersist(session).catch((err) =>
|
|
1188
1204
|
logger.error(`Failed to persist session ${sessionId}:`, err, { sessionId }),
|
|
1189
1205
|
)
|
|
1190
1206
|
}
|
|
1191
1207
|
|
|
1192
1208
|
if (event.type === 'message_end') {
|
|
1193
|
-
//
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
)
|
|
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)
|
|
1197
1212
|
}
|
|
1198
1213
|
})
|
|
1199
1214
|
|
|
@@ -1337,6 +1352,44 @@ async function persistSession(session) {
|
|
|
1337
1352
|
}
|
|
1338
1353
|
|
|
1339
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
|
+
}
|
|
1340
1393
|
await persistSession(session)
|
|
1341
1394
|
}
|
|
1342
1395
|
|
|
@@ -1562,6 +1615,7 @@ export async function abortRun(sessionId) {
|
|
|
1562
1615
|
)
|
|
1563
1616
|
const event = {
|
|
1564
1617
|
type: 'agent_end',
|
|
1618
|
+
status: 'aborted',
|
|
1565
1619
|
messages: session.agent.state.messages,
|
|
1566
1620
|
}
|
|
1567
1621
|
emitSessionEvent(session, event)
|
|
@@ -1631,6 +1685,7 @@ export function getSessionState(sessionId) {
|
|
|
1631
1685
|
title: session.title,
|
|
1632
1686
|
createdAt: session.createdAt,
|
|
1633
1687
|
lastModified: session.lastModified,
|
|
1688
|
+
stateVersion: session.stateVersion || 0,
|
|
1634
1689
|
status: session.status,
|
|
1635
1690
|
startedAt: session.startedAt,
|
|
1636
1691
|
finishedAt: session.finishedAt,
|
|
@@ -1643,6 +1698,33 @@ export function getSessionState(sessionId) {
|
|
|
1643
1698
|
}
|
|
1644
1699
|
}
|
|
1645
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
|
+
|
|
1646
1728
|
/**
|
|
1647
1729
|
* Try to claim the SSE slot for a session. Returns true if acquired, false if
|
|
1648
1730
|
* another tab already holds the SSE connection for this session.
|
|
@@ -1689,6 +1771,10 @@ export async function destroyAgent(sessionId) {
|
|
|
1689
1771
|
logger.info(`Destroying session ${sessionId} (status: ${session.status})`, { sessionId, status: session.status })
|
|
1690
1772
|
|
|
1691
1773
|
if (session.idleTimer) clearTimeout(session.idleTimer)
|
|
1774
|
+
if (session.persistTimer) {
|
|
1775
|
+
clearTimeout(session.persistTimer)
|
|
1776
|
+
session.persistTimer = null
|
|
1777
|
+
}
|
|
1692
1778
|
session.toolTimings?.clear()
|
|
1693
1779
|
|
|
1694
1780
|
try {
|
|
@@ -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
|