@shawnstack/quickforge 1.6.12 → 1.7.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 +1 -1
- package/dist/assets/AgentProfilesPage-DQ8URegq.js +1 -0
- package/dist/assets/ChatPanelHost-vi1tDqlJ.js +291 -0
- package/dist/assets/{PluginsPage-qXTjDrpd.js → PluginsPage-B8bHMwfv.js} +1 -1
- package/dist/assets/ScheduledTasksPage-AaUz5el2.js +2 -0
- package/dist/assets/{SettingsWorkspacePage-CnFDmGvW.js → SettingsWorkspacePage-BOP9CtoI.js} +112 -105
- package/dist/assets/{SharedConversationPage-CkQ-XwYS.js → SharedConversationPage-9A4L9UcQ.js} +1 -1
- package/dist/assets/TerminalDock-DQOXpaDe.js +2 -0
- package/dist/assets/WorkspaceInspector-DaM7TJHb.js +13 -0
- package/dist/assets/icons-ko_i0WpN.js +1 -0
- package/dist/assets/index-B2eauKpo.css +3 -0
- package/dist/assets/index-B8Awq5FV.js +63 -0
- package/dist/assets/{mcp-servers-dialog-CsRQxx7A.js → mcp-servers-dialog-XE4K8PCO.js} +2 -2
- package/dist/assets/{monaco-CdOyGlWD.js → monaco-C13M09wD.js} +1 -1
- package/dist/assets/{react-vendor-VqdnQHHS.js → react-vendor-2RKYr-A4.js} +1 -1
- package/dist/assets/{skills-dialog-CmVLi_h6.js → skills-dialog-MpoxntiV.js} +1 -1
- package/dist/index.html +6 -6
- package/dist/licenses/material-icon-theme.txt +8 -0
- package/package.json +1 -1
- package/server/agent-manager.mjs +182 -39
- package/server/auto-archive.mjs +137 -0
- package/server/auto-compaction.mjs +15 -18
- package/server/conversation-compaction.mjs +69 -3
- package/server/custom-commands.mjs +17 -0
- package/server/image-generation.mjs +130 -0
- package/server/index.mjs +12 -1
- package/server/provider-config.mjs +78 -0
- package/server/routes/agent.mjs +9 -0
- package/server/routes/session-assets.mjs +49 -0
- package/server/routes/shared-conversation.mjs +10 -0
- package/server/routes/storage.mjs +4 -0
- package/server/routes/tools.mjs +1 -1
- package/server/routes/workspace.mjs +5 -2
- package/server/session-assets.mjs +134 -0
- package/server/session-persistence-lock.mjs +9 -0
- package/server/storage.mjs +24 -1
- package/server/tools/definitions.mjs +10 -0
- package/server/tools/index.mjs +2 -0
- package/dist/assets/AgentProfilesPage-CooGLweI.js +0 -1
- package/dist/assets/ChatPanelHost-TEbv0lnL.js +0 -260
- package/dist/assets/ScheduledTasksPage-BBor8Yb2.js +0 -2
- package/dist/assets/TerminalDock-CvqBBMPg.js +0 -2
- package/dist/assets/WorkspaceInspector-6JGc6VGb.js +0 -13
- package/dist/assets/icons-C7j5jdKo.js +0 -1
- package/dist/assets/index-DxNzlPmc.js +0 -63
- package/dist/assets/index-HVxfqXfB.css +0 -3
package/server/agent-manager.mjs
CHANGED
|
@@ -28,11 +28,14 @@ import {
|
|
|
28
28
|
import { projectContextFromId, defaultGlobalWorkspaceContext, readProjectConfig } from './project-config.mjs'
|
|
29
29
|
import { ensureStorage, readStore, atomicUpdate, atomicSessionMetadataUpdate, readSessionValue, writeSessionValue, deleteSessionValue, tempAgentsDir } from './storage.mjs'
|
|
30
30
|
import { logger } from './utils/logger.mjs'
|
|
31
|
+
import { withSessionPersistenceLock } from './session-persistence-lock.mjs'
|
|
31
32
|
import { getGlobalMemoryRevision, isGlobalMemoryEnabled } from './global-memory.mjs'
|
|
32
33
|
import { buildSystemPrompt, generateAiTitle, generateTitle } from './session-utils.mjs'
|
|
33
34
|
import { restoreReasoningContentInPayload } from './reasoning-cache.mjs'
|
|
34
35
|
import {
|
|
35
36
|
compactConversation,
|
|
37
|
+
compactionMessageDetails,
|
|
38
|
+
isCompactSummaryMessage,
|
|
36
39
|
parseCompactArgs,
|
|
37
40
|
saveCompactBackup,
|
|
38
41
|
} from './conversation-compaction.mjs'
|
|
@@ -93,6 +96,8 @@ async function createServerTools(projectId, projectContext, skillsContext, inclu
|
|
|
93
96
|
includeMcpTools = true,
|
|
94
97
|
includePluginTools = true,
|
|
95
98
|
parentSessionId = null,
|
|
99
|
+
sessionId = null,
|
|
100
|
+
scope = 'global',
|
|
96
101
|
} = options
|
|
97
102
|
const allowedTools = allowedToolNames ? new Set(allowedToolNames) : null
|
|
98
103
|
const isAllowed = (definition) => !allowedTools || allowedTools.has(definition.name)
|
|
@@ -107,7 +112,11 @@ async function createServerTools(projectId, projectContext, skillsContext, inclu
|
|
|
107
112
|
projectSkillNames: skillsContext.projectSkillNames,
|
|
108
113
|
workspaceRoot: projectContext?.workspaceRoot,
|
|
109
114
|
})
|
|
110
|
-
const toolContext = {
|
|
115
|
+
const toolContext = {
|
|
116
|
+
...projectContext,
|
|
117
|
+
...skillToolContext,
|
|
118
|
+
...(sessionId ? { sessionId, scope, projectId } : {}),
|
|
119
|
+
}
|
|
111
120
|
const tools = skillTools
|
|
112
121
|
.filter(isAllowed)
|
|
113
122
|
.map((definition) => wrapToolDefinition(definition, toolContext, toolPermissions))
|
|
@@ -150,8 +159,14 @@ async function rebuildSessionTools(session) {
|
|
|
150
159
|
includeSubagentTool: false,
|
|
151
160
|
includeMcpTools: false,
|
|
152
161
|
parentSessionId: session.sessionId,
|
|
162
|
+
sessionId: session.sessionId,
|
|
163
|
+
scope: session.scope,
|
|
153
164
|
}
|
|
154
|
-
: {
|
|
165
|
+
: {
|
|
166
|
+
parentSessionId: session.sessionId,
|
|
167
|
+
sessionId: session.sessionId,
|
|
168
|
+
scope: session.scope,
|
|
169
|
+
},
|
|
155
170
|
)
|
|
156
171
|
}
|
|
157
172
|
|
|
@@ -329,7 +344,7 @@ function createAutoCompactApprovalPromise(session, details = {}) {
|
|
|
329
344
|
})
|
|
330
345
|
}
|
|
331
346
|
|
|
332
|
-
function assistantTextMessage(text, model) {
|
|
347
|
+
function assistantTextMessage(text, model, details) {
|
|
333
348
|
return {
|
|
334
349
|
role: 'assistant',
|
|
335
350
|
content: [{ type: 'text', text }],
|
|
@@ -346,14 +361,16 @@ function assistantTextMessage(text, model) {
|
|
|
346
361
|
},
|
|
347
362
|
stopReason: 'stop',
|
|
348
363
|
timestamp: Date.now(),
|
|
364
|
+
...(details ? { details } : {}),
|
|
349
365
|
}
|
|
350
366
|
}
|
|
351
367
|
|
|
352
|
-
function userTextMessage(text) {
|
|
368
|
+
function userTextMessage(text, details) {
|
|
353
369
|
return {
|
|
354
370
|
role: 'user',
|
|
355
371
|
content: [{ type: 'text', text }],
|
|
356
372
|
timestamp: Date.now(),
|
|
373
|
+
...(details ? { details } : {}),
|
|
357
374
|
}
|
|
358
375
|
}
|
|
359
376
|
|
|
@@ -523,12 +540,12 @@ async function summarySession(session, initialUserMessage, summaryOptions) {
|
|
|
523
540
|
'<compact_summary>',
|
|
524
541
|
result.summary,
|
|
525
542
|
'</compact_summary>',
|
|
526
|
-
].join('\n'))
|
|
543
|
+
].join('\n'), compactionMessageDetails('summary'))
|
|
527
544
|
const notice = assistantTextMessage([
|
|
528
545
|
`已基于当前对话创建压缩后的新对话:原 ${result.originalCount} 条消息 → ${result.recentTail.length + 2} 条消息。`,
|
|
529
546
|
`当前原对话已完整保留,保留最近 ${result.keepTurns} 个用户回合原文,估算新对话上下文减少约 ${reduction}%。`,
|
|
530
547
|
'压缩前历史已保存到本地备份。',
|
|
531
|
-
].join('\n'), session.model)
|
|
548
|
+
].join('\n'), session.model, compactionMessageDetails('notice'))
|
|
532
549
|
|
|
533
550
|
const compactedMessages = [summaryMessage, notice, ...result.recentTail]
|
|
534
551
|
const titleSourceMessages = [summaryMessage, ...result.recentTail]
|
|
@@ -700,7 +717,8 @@ async function clearSession(session) {
|
|
|
700
717
|
session.startedAt = null
|
|
701
718
|
session.finishedAt = new Date().toISOString()
|
|
702
719
|
session.title = 'New chat'
|
|
703
|
-
session.
|
|
720
|
+
session.titleSource = 'default'
|
|
721
|
+
session.titleGenerationId += 1
|
|
704
722
|
session.agent.state.isStreaming = false
|
|
705
723
|
session.agent.state.streamingMessage = undefined
|
|
706
724
|
session.agent.state.errorMessage = undefined
|
|
@@ -776,6 +794,15 @@ async function resolveCommandState(session, userMessage, promptCommand = null) {
|
|
|
776
794
|
if (internalResponse?.plan) {
|
|
777
795
|
return planCommandState(userMessage, internalResponse.args)
|
|
778
796
|
}
|
|
797
|
+
if (internalResponse?.init) {
|
|
798
|
+
if (!session.projectId) return { textResponse: 'Initialization requires an active project chat.' }
|
|
799
|
+
return {
|
|
800
|
+
userMessage,
|
|
801
|
+
commandPrompt: formatInitCommandPrompt(),
|
|
802
|
+
permissions: { allowEdit: true, allowCommands: true, allowSubagents: true },
|
|
803
|
+
commandName: 'init',
|
|
804
|
+
}
|
|
805
|
+
}
|
|
779
806
|
if (internalResponse?.review) {
|
|
780
807
|
return {
|
|
781
808
|
userMessage,
|
|
@@ -817,6 +844,53 @@ async function resolveCommandState(session, userMessage, promptCommand = null) {
|
|
|
817
844
|
}
|
|
818
845
|
}
|
|
819
846
|
|
|
847
|
+
function formatInitCommandPrompt() {
|
|
848
|
+
return `<init_command_invocation name="init">
|
|
849
|
+
This /init command applies only to the current user request. Work in the current repository root. Inspect the repository as needed, then create or update the root-level \`AGENTS.md\`. If the file already exists, read it first and preserve useful repository-specific guidance while bringing it in line with the requirements below. Do not modify unrelated files.
|
|
850
|
+
|
|
851
|
+
Generate a file named AGENTS.md that serves as a contributor guide for this repository.
|
|
852
|
+
Your goal is to produce a clear, concise, and well-structured document with descriptive headings and actionable explanations for each section.
|
|
853
|
+
Follow the outline below, but adapt as needed — add sections if relevant, and omit those that do not apply to this project.
|
|
854
|
+
|
|
855
|
+
Document Requirements
|
|
856
|
+
|
|
857
|
+
- Title the document "Repository Guidelines".
|
|
858
|
+
- Use Markdown headings (#, ##, etc.) for structure.
|
|
859
|
+
- Keep the document concise. 200-400 words is optimal.
|
|
860
|
+
- Keep explanations short, direct, and specific to this repository.
|
|
861
|
+
- Provide examples where helpful (commands, directory paths, naming patterns).
|
|
862
|
+
- Maintain a professional, instructional tone.
|
|
863
|
+
|
|
864
|
+
Recommended Sections
|
|
865
|
+
|
|
866
|
+
Project Structure & Module Organization
|
|
867
|
+
|
|
868
|
+
- Outline the project structure, including where the source code, tests, and assets are located.
|
|
869
|
+
|
|
870
|
+
Build, Test, and Development Commands
|
|
871
|
+
|
|
872
|
+
- List key commands for building, testing, and running locally (e.g., npm test, make build).
|
|
873
|
+
- Briefly explain what each command does.
|
|
874
|
+
|
|
875
|
+
Coding Style & Naming Conventions
|
|
876
|
+
|
|
877
|
+
- Specify indentation rules, language-specific style preferences, and naming patterns.
|
|
878
|
+
- Include any formatting or linting tools used.
|
|
879
|
+
|
|
880
|
+
Testing Guidelines
|
|
881
|
+
|
|
882
|
+
- Identify testing frameworks and coverage requirements.
|
|
883
|
+
- State test naming conventions and how to run tests.
|
|
884
|
+
|
|
885
|
+
Commit & Pull Request Guidelines
|
|
886
|
+
|
|
887
|
+
- Summarize commit message conventions found in the project’s Git history.
|
|
888
|
+
- Outline pull request requirements (descriptions, linked issues, screenshots, etc.).
|
|
889
|
+
|
|
890
|
+
(Optional) Add other sections if relevant, such as Security & Configuration Tips, Architecture Overview, or Agent-Specific Instructions.
|
|
891
|
+
</init_command_invocation>`
|
|
892
|
+
}
|
|
893
|
+
|
|
820
894
|
function formatPlanCommandPrompt(task) {
|
|
821
895
|
const taskText = String(task || '').trim()
|
|
822
896
|
return `<plan_command_invocation name="plan">
|
|
@@ -1247,14 +1321,7 @@ function applyActiveCapabilityPrompt(messages, capabilityPrompt) {
|
|
|
1247
1321
|
|
|
1248
1322
|
function compactSummaryIndex(messages) {
|
|
1249
1323
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1250
|
-
|
|
1251
|
-
const content = message?.content
|
|
1252
|
-
const text = typeof content === 'string'
|
|
1253
|
-
? content
|
|
1254
|
-
: Array.isArray(content)
|
|
1255
|
-
? content.filter((block) => block?.type === 'text').map((block) => block.text ?? '').join('\n')
|
|
1256
|
-
: ''
|
|
1257
|
-
if (message?.role === 'user' && text.includes('<compact_summary>')) return index
|
|
1324
|
+
if (isCompactSummaryMessage(messages[index])) return index
|
|
1258
1325
|
}
|
|
1259
1326
|
return -1
|
|
1260
1327
|
}
|
|
@@ -1372,11 +1439,13 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1372
1439
|
messages = [],
|
|
1373
1440
|
systemPrompt = null,
|
|
1374
1441
|
title = 'New chat',
|
|
1442
|
+
titleSource = title === 'New chat' ? 'default' : 'manual',
|
|
1375
1443
|
createdAt = new Date().toISOString(),
|
|
1376
1444
|
lastModified = null,
|
|
1377
1445
|
contextCompaction = null,
|
|
1378
1446
|
agentProfile = null,
|
|
1379
1447
|
idleRetention = null,
|
|
1448
|
+
stateVersion = 0,
|
|
1380
1449
|
} = config
|
|
1381
1450
|
const accessMode = normalizeAccessMode(rawAccessMode, yoloMode)
|
|
1382
1451
|
const resolvedYoloMode = yoloModeFromAccessMode(accessMode)
|
|
@@ -1450,8 +1519,14 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1450
1519
|
includeSubagentTool: false,
|
|
1451
1520
|
includeMcpTools: false,
|
|
1452
1521
|
parentSessionId: sessionId,
|
|
1522
|
+
sessionId,
|
|
1523
|
+
scope,
|
|
1453
1524
|
}
|
|
1454
|
-
: {
|
|
1525
|
+
: {
|
|
1526
|
+
parentSessionId: sessionId,
|
|
1527
|
+
sessionId,
|
|
1528
|
+
scope,
|
|
1529
|
+
},
|
|
1455
1530
|
)
|
|
1456
1531
|
|
|
1457
1532
|
// Resolve API key
|
|
@@ -1531,6 +1606,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1531
1606
|
thinkingLevel: resolvedThinkingLevel,
|
|
1532
1607
|
scope,
|
|
1533
1608
|
title,
|
|
1609
|
+
titleSource,
|
|
1534
1610
|
createdAt,
|
|
1535
1611
|
lastModified,
|
|
1536
1612
|
globalSkillNames: skillsContext.globalSkillNames,
|
|
@@ -1544,7 +1620,10 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1544
1620
|
eventBus,
|
|
1545
1621
|
idleTimer: null,
|
|
1546
1622
|
persistTimer: null,
|
|
1547
|
-
|
|
1623
|
+
titleGenerationId: 0,
|
|
1624
|
+
titleGenerationPromise: null,
|
|
1625
|
+
titleGenerationPromiseId: null,
|
|
1626
|
+
sessionCreatedEmitted: messages.length > 0,
|
|
1548
1627
|
toolTimings: new Map(),
|
|
1549
1628
|
getApiKey,
|
|
1550
1629
|
contextCompaction,
|
|
@@ -1552,7 +1631,7 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1552
1631
|
idleRetention,
|
|
1553
1632
|
lastTransformedContextMessages: null,
|
|
1554
1633
|
autoCompacting: false,
|
|
1555
|
-
stateVersion: 0,
|
|
1634
|
+
stateVersion: Number.isFinite(stateVersion) ? Math.max(0, stateVersion) : 0,
|
|
1556
1635
|
lastAutoCompactAt: null,
|
|
1557
1636
|
lastAutoCompactRejected: null,
|
|
1558
1637
|
memoryEnabled: initialMemoryEnabled,
|
|
@@ -1623,11 +1702,17 @@ export async function createAgent(sessionId, config = {}) {
|
|
|
1623
1702
|
const isInitialUserMessage = (event.message?.role === 'user' || event.message?.role === 'user-with-attachments')
|
|
1624
1703
|
&& session.agent.state.messages.length === 1
|
|
1625
1704
|
if (isInitialUserMessage) {
|
|
1626
|
-
//
|
|
1627
|
-
//
|
|
1628
|
-
// disk before a long/failed agent run can exit or be restarted.
|
|
1705
|
+
// Persist the first user message before announcing the session or
|
|
1706
|
+
// starting the asynchronous AI title request.
|
|
1629
1707
|
try {
|
|
1630
|
-
await flushSessionPersist(session)
|
|
1708
|
+
const metadata = await flushSessionPersist(session)
|
|
1709
|
+
if (metadata) {
|
|
1710
|
+
if (!session.sessionCreatedEmitted) {
|
|
1711
|
+
session.sessionCreatedEmitted = true
|
|
1712
|
+
emitSessionEvent(session, { type: 'session_created', metadata })
|
|
1713
|
+
}
|
|
1714
|
+
scheduleSessionTitleGeneration(session, event.message)
|
|
1715
|
+
}
|
|
1631
1716
|
} catch (err) {
|
|
1632
1717
|
logger.error(`Failed to persist initial user message for session ${sessionId}:`, err, { sessionId })
|
|
1633
1718
|
}
|
|
@@ -1672,8 +1757,8 @@ function sessionLastModifiedFromMessages(messages, fallback) {
|
|
|
1672
1757
|
/**
|
|
1673
1758
|
* Persist session data to storage.
|
|
1674
1759
|
*/
|
|
1675
|
-
async function
|
|
1676
|
-
const { sessionId, agent, scope, projectId, title, createdAt, lastModified: storedLastModified, status, startedAt, finishedAt, model, thinkingLevel, accessMode, yoloMode, contextCompaction } = session
|
|
1760
|
+
async function persistSessionUnlocked(session) {
|
|
1761
|
+
const { sessionId, agent, scope, projectId, title, titleSource, createdAt, lastModified: storedLastModified, status, startedAt, finishedAt, model, thinkingLevel, accessMode, yoloMode, contextCompaction } = session
|
|
1677
1762
|
const messages = agent.state.messages
|
|
1678
1763
|
|
|
1679
1764
|
if (messages.length === 0) {
|
|
@@ -1694,6 +1779,7 @@ async function persistSession(session) {
|
|
|
1694
1779
|
const sessionData = {
|
|
1695
1780
|
id: sessionId,
|
|
1696
1781
|
title,
|
|
1782
|
+
titleSource,
|
|
1697
1783
|
model,
|
|
1698
1784
|
thinkingLevel,
|
|
1699
1785
|
accessMode,
|
|
@@ -1708,6 +1794,7 @@ async function persistSession(session) {
|
|
|
1708
1794
|
taskFinishedAt: finishedAt,
|
|
1709
1795
|
contextCompaction: contextCompaction || undefined,
|
|
1710
1796
|
idleRetention: session.idleRetention || undefined,
|
|
1797
|
+
stateVersion: Number.isFinite(session.stateVersion) ? session.stateVersion : 0,
|
|
1711
1798
|
}
|
|
1712
1799
|
session.lastModified = lastModified
|
|
1713
1800
|
|
|
@@ -1744,6 +1831,7 @@ async function persistSession(session) {
|
|
|
1744
1831
|
const metadata = {
|
|
1745
1832
|
id: sessionId,
|
|
1746
1833
|
title,
|
|
1834
|
+
titleSource,
|
|
1747
1835
|
createdAt: createdAt || now,
|
|
1748
1836
|
lastModified,
|
|
1749
1837
|
messageCount: messages.length,
|
|
@@ -1788,7 +1876,14 @@ async function persistSession(session) {
|
|
|
1788
1876
|
}
|
|
1789
1877
|
} catch (err) {
|
|
1790
1878
|
logger.error(`Failed to persist session ${sessionId}:`, err, { sessionId })
|
|
1879
|
+
return null
|
|
1791
1880
|
}
|
|
1881
|
+
|
|
1882
|
+
return metadata
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
async function persistSession(session) {
|
|
1886
|
+
return withSessionPersistenceLock(() => persistSessionUnlocked(session))
|
|
1792
1887
|
}
|
|
1793
1888
|
|
|
1794
1889
|
export async function persistSessionState(session) {
|
|
@@ -1830,7 +1925,7 @@ async function flushSessionPersist(session) {
|
|
|
1830
1925
|
clearTimeout(session.persistTimer)
|
|
1831
1926
|
session.persistTimer = null
|
|
1832
1927
|
}
|
|
1833
|
-
|
|
1928
|
+
return persistSession(session)
|
|
1834
1929
|
}
|
|
1835
1930
|
|
|
1836
1931
|
export function rollbackStartIndexFromMessage(messages, messageIndex) {
|
|
@@ -1888,6 +1983,39 @@ export async function rollbackSessionMessages(sessionId, rollbackMessageIndex) {
|
|
|
1888
1983
|
return { session: getSessionState(sessionId), rollbackIndex }
|
|
1889
1984
|
}
|
|
1890
1985
|
|
|
1986
|
+
export function canApplyGeneratedTitle(session, generationId, expectedTitle) {
|
|
1987
|
+
return session.titleGenerationId === generationId
|
|
1988
|
+
&& session.titleSource === 'fallback'
|
|
1989
|
+
&& session.title === expectedTitle
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function scheduleSessionTitleGeneration(session, userMessage) {
|
|
1993
|
+
if (session.titleSource !== 'fallback') return
|
|
1994
|
+
if (session.titleGenerationPromise && session.titleGenerationPromiseId === session.titleGenerationId) return
|
|
1995
|
+
const generationId = ++session.titleGenerationId
|
|
1996
|
+
const expectedTitle = session.title
|
|
1997
|
+
const promise = generateAiTitle([userMessage], session.model, session.thinkingLevel, session.getApiKey)
|
|
1998
|
+
.then(async (aiTitle) => {
|
|
1999
|
+
if (!aiTitle || aiTitle === 'New chat') return
|
|
2000
|
+
if (!canApplyGeneratedTitle(session, generationId, expectedTitle)) return
|
|
2001
|
+
session.title = aiTitle
|
|
2002
|
+
session.titleSource = 'ai'
|
|
2003
|
+
await persistSession(session)
|
|
2004
|
+
emitSessionEvent(session, { type: 'title_updated', title: aiTitle, titleSource: 'ai' })
|
|
2005
|
+
})
|
|
2006
|
+
.catch((err) => {
|
|
2007
|
+
logger.warn(`Title generation failed for session ${session.sessionId}:`, err.message || err, { sessionId: session.sessionId })
|
|
2008
|
+
})
|
|
2009
|
+
.finally(() => {
|
|
2010
|
+
if (session.titleGenerationPromise === promise) {
|
|
2011
|
+
session.titleGenerationPromise = null
|
|
2012
|
+
session.titleGenerationPromiseId = null
|
|
2013
|
+
}
|
|
2014
|
+
})
|
|
2015
|
+
session.titleGenerationPromise = promise
|
|
2016
|
+
session.titleGenerationPromiseId = generationId
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1891
2019
|
/**
|
|
1892
2020
|
* Send a user message to the agent and start the agent loop.
|
|
1893
2021
|
* Returns immediately; events are streamed via the event bus.
|
|
@@ -1940,24 +2068,14 @@ export async function runPrompt(sessionId, message, selectedCapabilities = [], p
|
|
|
1940
2068
|
return compactSession(session, initialUserMessage, commandState.compact)
|
|
1941
2069
|
}
|
|
1942
2070
|
|
|
1943
|
-
//
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
// meaningful even if AI title generation fails or is slow.
|
|
2071
|
+
// Set a meaningful fallback immediately. The AI title request starts only
|
|
2072
|
+
// after the first user message has been persisted by the message_end handler.
|
|
2073
|
+
if (session.titleSource === 'default' && session.title === 'New chat') {
|
|
1947
2074
|
const simpleTitle = generateTitle([userMessage])
|
|
2075
|
+
session.titleSource = 'fallback'
|
|
1948
2076
|
if (simpleTitle !== 'New chat') {
|
|
1949
2077
|
session.title = simpleTitle
|
|
1950
2078
|
}
|
|
1951
|
-
session.titleGenerated = true
|
|
1952
|
-
generateAiTitle([userMessage], session.model, session.thinkingLevel, session.getApiKey).then(async (aiTitle) => {
|
|
1953
|
-
if (aiTitle && aiTitle !== 'New chat') {
|
|
1954
|
-
session.title = aiTitle
|
|
1955
|
-
await persistSession(session)
|
|
1956
|
-
emitSessionEvent(session, { type: 'title_updated', title: aiTitle })
|
|
1957
|
-
}
|
|
1958
|
-
}).catch((err) => {
|
|
1959
|
-
logger.warn(`Title generation failed for session ${sessionId}:`, err.message || err, { sessionId })
|
|
1960
|
-
})
|
|
1961
2079
|
}
|
|
1962
2080
|
|
|
1963
2081
|
session.activeCommandName = commandState.commandName ?? null
|
|
@@ -2142,6 +2260,7 @@ export function getSessionState(sessionId) {
|
|
|
2142
2260
|
model: session.model,
|
|
2143
2261
|
thinkingLevel: session.thinkingLevel,
|
|
2144
2262
|
title: session.title,
|
|
2263
|
+
titleSource: session.titleSource,
|
|
2145
2264
|
createdAt: session.createdAt,
|
|
2146
2265
|
lastModified: session.lastModified,
|
|
2147
2266
|
stateVersion: session.stateVersion || 0,
|
|
@@ -2289,10 +2408,12 @@ export async function restoreAgent(sessionId) {
|
|
|
2289
2408
|
thinkingLevel: sessionData.thinkingLevel || 'off',
|
|
2290
2409
|
messages: sessionData.messages || [],
|
|
2291
2410
|
title: sessionData.title || 'New chat',
|
|
2411
|
+
titleSource: sessionData.titleSource || (sessionData.title && sessionData.title !== 'New chat' ? 'ai' : 'default'),
|
|
2292
2412
|
createdAt: sessionData.createdAt,
|
|
2293
2413
|
lastModified: sessionData.lastModified,
|
|
2294
2414
|
contextCompaction: sessionData.contextCompaction || null,
|
|
2295
2415
|
idleRetention: sessionData.idleRetention || null,
|
|
2416
|
+
stateVersion: sessionData.stateVersion,
|
|
2296
2417
|
})
|
|
2297
2418
|
} catch (err) {
|
|
2298
2419
|
logger.error(`Failed to restore agent ${sessionId}:`, err, { sessionId })
|
|
@@ -2387,6 +2508,28 @@ export async function refreshAllSessionTools() {
|
|
|
2387
2508
|
return result
|
|
2388
2509
|
}
|
|
2389
2510
|
|
|
2511
|
+
export async function updateSessionTitle(sessionId, title) {
|
|
2512
|
+
let session = agentSessions.get(sessionId)
|
|
2513
|
+
if (!session) session = await restoreAgent(sessionId)
|
|
2514
|
+
if (!session) {
|
|
2515
|
+
throw Object.assign(new Error('Session not found'), { statusCode: 404 })
|
|
2516
|
+
}
|
|
2517
|
+
const normalizedTitle = typeof title === 'string' ? title.trim() : ''
|
|
2518
|
+
if (!normalizedTitle) {
|
|
2519
|
+
throw Object.assign(new Error('Title must not be empty'), { statusCode: 400 })
|
|
2520
|
+
}
|
|
2521
|
+
if (normalizedTitle.length > 200) {
|
|
2522
|
+
throw Object.assign(new Error('Title is too long'), { statusCode: 400 })
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
session.titleGenerationId += 1
|
|
2526
|
+
session.title = normalizedTitle
|
|
2527
|
+
session.titleSource = 'manual'
|
|
2528
|
+
await persistSession(session)
|
|
2529
|
+
emitSessionEvent(session, { type: 'title_updated', title: normalizedTitle, titleSource: 'manual' })
|
|
2530
|
+
return { sessionId, title: normalizedTitle, titleSource: 'manual' }
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2390
2533
|
export async function updateSessionAccessMode(sessionId, accessMode) {
|
|
2391
2534
|
const session = agentSessions.get(sessionId)
|
|
2392
2535
|
if (!session) {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { atomicSessionValueUpdate, atomicUpdate, readSessionValue, readStore } from './storage.mjs'
|
|
2
|
+
import { logger } from './utils/logger.mjs'
|
|
3
|
+
import { withSessionPersistenceLock } from './session-persistence-lock.mjs'
|
|
4
|
+
|
|
5
|
+
const defaultStorage = { atomicSessionValueUpdate, atomicUpdate, readSessionValue, readStore }
|
|
6
|
+
|
|
7
|
+
export const AUTO_ARCHIVE_SETTINGS_KEY = 'auto-archive-settings'
|
|
8
|
+
export const AUTO_ARCHIVE_AFTER_MS = 30 * 24 * 60 * 60 * 1000
|
|
9
|
+
const AUTO_ARCHIVE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
|
|
10
|
+
|
|
11
|
+
let autoArchiveTimer = null
|
|
12
|
+
let autoArchiveRunning = false
|
|
13
|
+
|
|
14
|
+
export function normalizeAutoArchiveSettings(value) {
|
|
15
|
+
if (!value || typeof value !== 'object') return { enabled: false }
|
|
16
|
+
return { enabled: value.enabled === true }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseTimestamp(value) {
|
|
20
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
21
|
+
if (typeof value !== 'string') return null
|
|
22
|
+
const normalized = value.trim()
|
|
23
|
+
if (!normalized) return null
|
|
24
|
+
const numeric = Number(normalized)
|
|
25
|
+
if (Number.isFinite(numeric)) return numeric
|
|
26
|
+
const timestamp = Date.parse(normalized)
|
|
27
|
+
return Number.isNaN(timestamp) ? null : timestamp
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function latestMessageTimestamp(messages) {
|
|
31
|
+
if (!Array.isArray(messages)) return null
|
|
32
|
+
let latest = null
|
|
33
|
+
for (const message of messages) {
|
|
34
|
+
const timestamp = parseTimestamp(message?.timestamp)
|
|
35
|
+
if (timestamp !== null && (latest === null || timestamp > latest)) latest = timestamp
|
|
36
|
+
}
|
|
37
|
+
return latest
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function sessionActivityTime(...values) {
|
|
41
|
+
let latest = null
|
|
42
|
+
for (const value of values) {
|
|
43
|
+
if (!value || typeof value !== 'object') continue
|
|
44
|
+
for (const candidate of [
|
|
45
|
+
parseTimestamp(value.lastModified),
|
|
46
|
+
latestMessageTimestamp(value.messages),
|
|
47
|
+
parseTimestamp(value.createdAt),
|
|
48
|
+
]) {
|
|
49
|
+
if (candidate !== null && (latest === null || candidate > latest)) latest = candidate
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return latest
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function archiveInactiveSessionsUnlocked(options = {}) {
|
|
56
|
+
const storage = options.storage || defaultStorage
|
|
57
|
+
const settings = await storage.readStore('settings')
|
|
58
|
+
if (!normalizeAutoArchiveSettings(settings[AUTO_ARCHIVE_SETTINGS_KEY]).enabled) {
|
|
59
|
+
return { archivedCount: 0, disabled: true }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now()
|
|
63
|
+
const cutoff = now - AUTO_ARCHIVE_AFTER_MS
|
|
64
|
+
const archivedAt = new Date(now).toISOString()
|
|
65
|
+
const metadataStore = await storage.readStore('sessions-metadata')
|
|
66
|
+
const candidates = []
|
|
67
|
+
|
|
68
|
+
for (const [sessionId, metadata] of Object.entries(metadataStore)) {
|
|
69
|
+
if (!metadata || metadata.archivedAt || metadata.messageCount === 0 || metadata.taskStatus === 'running') continue
|
|
70
|
+
const session = await storage.readSessionValue(sessionId)
|
|
71
|
+
if (!session || session.archivedAt) continue
|
|
72
|
+
const activityTime = sessionActivityTime(metadata, session)
|
|
73
|
+
if (activityTime === null || activityTime >= cutoff) continue
|
|
74
|
+
candidates.push(sessionId)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const archivedSessionIds = []
|
|
78
|
+
for (const sessionId of candidates) {
|
|
79
|
+
const updatedSession = await storage.atomicSessionValueUpdate(sessionId, (session) => {
|
|
80
|
+
const activityTime = sessionActivityTime(session)
|
|
81
|
+
if (session.archivedAt || activityTime === null || activityTime >= cutoff) return session
|
|
82
|
+
return { ...session, archivedAt }
|
|
83
|
+
})
|
|
84
|
+
if (!updatedSession || updatedSession.archivedAt !== archivedAt) continue
|
|
85
|
+
|
|
86
|
+
let metadataArchived = false
|
|
87
|
+
await storage.atomicUpdate('sessions-metadata', (current) => {
|
|
88
|
+
const metadata = current[sessionId]
|
|
89
|
+
const activityTime = sessionActivityTime(metadata, updatedSession)
|
|
90
|
+
if (!metadata || metadata.archivedAt || metadata.messageCount === 0 || metadata.taskStatus === 'running' || activityTime === null || activityTime >= cutoff) {
|
|
91
|
+
return current
|
|
92
|
+
}
|
|
93
|
+
current[sessionId] = { ...metadata, archivedAt }
|
|
94
|
+
metadataArchived = true
|
|
95
|
+
return current
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
if (metadataArchived) {
|
|
99
|
+
archivedSessionIds.push(sessionId)
|
|
100
|
+
} else {
|
|
101
|
+
await storage.atomicSessionValueUpdate(sessionId, (session) => {
|
|
102
|
+
if (session.archivedAt !== archivedAt) return session
|
|
103
|
+
const next = { ...session }
|
|
104
|
+
delete next.archivedAt
|
|
105
|
+
return next
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (archivedSessionIds.length > 0) logger.info(`Automatically archived ${archivedSessionIds.length} inactive conversation(s).`)
|
|
111
|
+
return { archivedCount: archivedSessionIds.length, archivedSessionIds, archivedAt }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function archiveInactiveSessions(options = {}) {
|
|
115
|
+
if (autoArchiveRunning) return { archivedCount: 0, skipped: true }
|
|
116
|
+
autoArchiveRunning = true
|
|
117
|
+
try {
|
|
118
|
+
return await withSessionPersistenceLock(() => archiveInactiveSessionsUnlocked(options))
|
|
119
|
+
} finally {
|
|
120
|
+
autoArchiveRunning = false
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function startAutoArchiveRunner() {
|
|
125
|
+
if (autoArchiveTimer) return
|
|
126
|
+
autoArchiveTimer = setInterval(() => {
|
|
127
|
+
archiveInactiveSessions().catch((error) => logger.error('Automatic conversation archive failed:', error))
|
|
128
|
+
}, AUTO_ARCHIVE_CHECK_INTERVAL_MS)
|
|
129
|
+
autoArchiveTimer.unref?.()
|
|
130
|
+
archiveInactiveSessions().catch((error) => logger.error('Initial automatic conversation archive failed:', error))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function stopAutoArchiveRunner() {
|
|
134
|
+
if (!autoArchiveTimer) return
|
|
135
|
+
clearInterval(autoArchiveTimer)
|
|
136
|
+
autoArchiveTimer = null
|
|
137
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { readStore } from './storage.mjs'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
compactConversation,
|
|
4
|
+
compactionMessageDetails,
|
|
5
|
+
extractCompactSummaryText,
|
|
6
|
+
isCompactSummaryMessage,
|
|
7
|
+
saveCompactBackup,
|
|
8
|
+
} from './conversation-compaction.mjs'
|
|
3
9
|
import { estimateContextUsage, shouldCompactContextByPercent } from './context-usage.mjs'
|
|
4
10
|
|
|
5
11
|
export const AUTO_COMPACT_SETTINGS_KEY = 'auto-compact-settings'
|
|
@@ -7,7 +13,7 @@ export const AUTO_COMPACT_SETTINGS_KEY = 'auto-compact-settings'
|
|
|
7
13
|
export const DEFAULT_AUTO_COMPACT_SETTINGS = {
|
|
8
14
|
enabled: true,
|
|
9
15
|
thresholdPercent: 80,
|
|
10
|
-
keepRecentTurns:
|
|
16
|
+
keepRecentTurns: 3,
|
|
11
17
|
minSourceChars: 1600,
|
|
12
18
|
requireConfirmation: true,
|
|
13
19
|
}
|
|
@@ -66,7 +72,8 @@ function estimateMessagesChars(messages) {
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
function isUserMessage(message) {
|
|
69
|
-
return message?.role === 'user' || message?.role === 'user-with-attachments'
|
|
75
|
+
return (message?.role === 'user' || message?.role === 'user-with-attachments')
|
|
76
|
+
&& !isCompactSummaryMessage(message)
|
|
70
77
|
}
|
|
71
78
|
|
|
72
79
|
function tailStartForRecentTurns(messages, keepRecentTurns) {
|
|
@@ -108,22 +115,12 @@ function clearAutoCompactRejected(session) {
|
|
|
108
115
|
session.lastAutoCompactRejected = null
|
|
109
116
|
}
|
|
110
117
|
|
|
111
|
-
function
|
|
112
|
-
const content = message?.content
|
|
113
|
-
const text = typeof content === 'string'
|
|
114
|
-
? content
|
|
115
|
-
: Array.isArray(content)
|
|
116
|
-
? content.filter((block) => block?.type === 'text').map((block) => block.text || '').join('\n')
|
|
117
|
-
: ''
|
|
118
|
-
const match = text.match(/<compact_summary>\s*([\s\S]*?)\s*<\/compact_summary>/)
|
|
119
|
-
return match?.[1]?.trim() || text.trim()
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function userTextMessage(text) {
|
|
118
|
+
function userTextMessage(text, details) {
|
|
123
119
|
return {
|
|
124
120
|
role: 'user',
|
|
125
121
|
content: [{ type: 'text', text }],
|
|
126
122
|
timestamp: Date.now(),
|
|
123
|
+
...(details ? { details } : {}),
|
|
127
124
|
}
|
|
128
125
|
}
|
|
129
126
|
|
|
@@ -135,9 +132,9 @@ function buildCompactionSourceMessages(session, messages, tailStart) {
|
|
|
135
132
|
'Existing rolling compact summary from earlier conversation history:',
|
|
136
133
|
'',
|
|
137
134
|
'<compact_summary>',
|
|
138
|
-
|
|
135
|
+
extractCompactSummaryText(previousSummary),
|
|
139
136
|
'</compact_summary>',
|
|
140
|
-
].join('\n')))
|
|
137
|
+
].join('\n'), compactionMessageDetails('summary')))
|
|
141
138
|
}
|
|
142
139
|
source.push(...messages.slice(session.contextCompaction?.compactedUpToIndex || 0, tailStart))
|
|
143
140
|
return source
|
|
@@ -190,7 +187,7 @@ export async function compactSessionInPlace({
|
|
|
190
187
|
'<compact_summary>',
|
|
191
188
|
result.summary,
|
|
192
189
|
'</compact_summary>',
|
|
193
|
-
].join('\n'))
|
|
190
|
+
].join('\n'), compactionMessageDetails('summary'))
|
|
194
191
|
session.contextCompaction = {
|
|
195
192
|
summaryMessage,
|
|
196
193
|
compactedUpToIndex: tailStart,
|