@shawnstack/quickforge 1.4.1 → 1.5.1

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.
Files changed (60) hide show
  1. package/README.md +12 -12
  2. package/bin/quickforge.mjs +9 -0
  3. package/dist/assets/AgentProfilesPage-BIwd5Nzg.js +1 -0
  4. package/dist/assets/ChatPanelHost-De-DMjx5.js +242 -0
  5. package/dist/assets/PluginsPage-kRzB5k8J.js +1 -0
  6. package/dist/assets/ScheduledTasksPage-ZnjohaPS.js +2 -0
  7. package/dist/assets/SharedConversationPage-EQdZgWCM.js +1 -0
  8. package/dist/assets/TerminalDock-P2pJH_tx.js +2 -0
  9. package/dist/assets/WorkspaceInspector-CkLAqYQ6.js +3 -0
  10. package/dist/assets/WorkspaceReaderDialog-BwzZ8Tgv.js +1 -0
  11. package/dist/assets/diff-line-counts-CeZC7b0z.js +10 -0
  12. package/dist/assets/icons-DJqt-rnw.js +1 -0
  13. package/dist/assets/index-CcGy4TXo.js +1354 -0
  14. package/dist/assets/index-DuTUuAMk.css +3 -0
  15. package/dist/assets/{monaco-evITXh-m.js → monaco-CNEfYIy1.js} +1 -1
  16. package/dist/assets/{react-vendor-Mthyt1p4.js → react-vendor-CZCcjpSR.js} +1 -1
  17. package/dist/favicon.svg +16 -1
  18. package/dist/index.html +5 -5
  19. package/dist/manifest.webmanifest +30 -30
  20. package/package.json +3 -2
  21. package/server/acp/server.mjs +921 -0
  22. package/server/agent-manager.mjs +200 -34
  23. package/server/agent-profile-files.mjs +179 -0
  24. package/server/agent-profiles.mjs +59 -5
  25. package/server/auto-compaction.mjs +82 -39
  26. package/server/channels/process-channel.mjs +278 -0
  27. package/server/channels/providers/wechat.mjs +271 -0
  28. package/server/channels/registry.mjs +58 -0
  29. package/server/custom-commands.mjs +13 -1
  30. package/server/frontmatter.mjs +167 -0
  31. package/server/index.mjs +52 -3
  32. package/server/project-config.mjs +43 -6
  33. package/server/routes/agent-profiles.mjs +6 -2
  34. package/server/routes/agent.mjs +12 -1
  35. package/server/routes/channels.mjs +145 -0
  36. package/server/routes/models.mjs +68 -0
  37. package/server/routes/project.mjs +2 -2
  38. package/server/routes/scheduled-tasks.mjs +6 -5
  39. package/server/routes/storage.mjs +4 -2
  40. package/server/routes/system.mjs +27 -0
  41. package/server/routes/tools.mjs +17 -6
  42. package/server/routes/workspace.mjs +142 -20
  43. package/server/session-utils.mjs +10 -2
  44. package/server/storage.mjs +29 -2
  45. package/server/system-prompt.mjs +1 -0
  46. package/server/tools/definitions.mjs +18 -0
  47. package/server/tools/index.mjs +86 -0
  48. package/server/utils/package-update.mjs +156 -0
  49. package/server/utils/workspace.mjs +1 -1
  50. package/dist/assets/AgentProfilesPage-CNK5PxA3.js +0 -1
  51. package/dist/assets/ChatPanelHost-FqPQwwMO.js +0 -217
  52. package/dist/assets/PluginsPage-BCu1Ept0.js +0 -1
  53. package/dist/assets/ScheduledTasksPage-Bx04rjui.js +0 -2
  54. package/dist/assets/SharedConversationPage-55vX9sqe.js +0 -1
  55. package/dist/assets/TerminalDock-DLN_pLkJ.js +0 -2
  56. package/dist/assets/WorkspaceInspector-DoemHHnY.js +0 -3
  57. package/dist/assets/WorkspaceReaderDialog-C6xUHBCw.js +0 -6
  58. package/dist/assets/icons-BWtivFsx.js +0 -1
  59. package/dist/assets/index-CxOHP41X.css +0 -3
  60. package/dist/assets/index-Dcf73EL8.js +0 -895
@@ -0,0 +1,179 @@
1
+ import { existsSync, promises as fs } from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { dataDir } from './storage.mjs'
5
+ import { firstOptionalBoolean, firstString, parseFrontmatter, splitDelimitedList } from './frontmatter.mjs'
6
+
7
+ const DEFAULT_MAX_RUNTIME_MS = 30 * 60 * 1000
8
+ const DEFAULT_MAX_TOOL_CALLS = 300
9
+ const nameRegex = /^[a-z][a-z0-9_-]{1,39}$/
10
+ const allowedToolNames = new Set(['read_file', 'grep_files', 'write_file', 'edit_file', 'run_command'])
11
+
12
+ const toolAliases = new Map([
13
+ ['Read', 'read_file'],
14
+ ['Grep', 'grep_files'],
15
+ ['Bash', 'run_command'],
16
+ ['Write', 'write_file'],
17
+ ['Edit', 'edit_file'],
18
+ ])
19
+
20
+ const claudeUserAgentsDir = path.join(os.homedir(), '.claude', 'agents')
21
+ const userAgentsDir = path.join(dataDir, 'agents')
22
+
23
+ function normalizeString(value) {
24
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined
25
+ }
26
+
27
+ function normalizeName(value) {
28
+ const name = normalizeString(value)?.toLowerCase()
29
+ return name && nameRegex.test(name) ? name : null
30
+ }
31
+
32
+ function normalizeRuntime(value) {
33
+ if (value === undefined || value === null || value === '') return DEFAULT_MAX_RUNTIME_MS
34
+ const parsed = Number(value)
35
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_RUNTIME_MS
36
+ return Math.min(Math.max(Math.round(parsed), 1000), DEFAULT_MAX_RUNTIME_MS)
37
+ }
38
+
39
+ function normalizeToolCalls(value) {
40
+ if (value === undefined || value === null || value === '') return DEFAULT_MAX_TOOL_CALLS
41
+ const parsed = Number(value)
42
+ if (!Number.isInteger(parsed) || parsed <= 0) return DEFAULT_MAX_TOOL_CALLS
43
+ return Math.min(parsed, DEFAULT_MAX_TOOL_CALLS)
44
+ }
45
+
46
+ function normalizeTools(value) {
47
+ const tools = []
48
+ const seen = new Set()
49
+ for (const item of splitDelimitedList(value)) {
50
+ const mapped = toolAliases.get(item) || item
51
+ if (!allowedToolNames.has(mapped) || seen.has(mapped)) continue
52
+ seen.add(mapped)
53
+ tools.push(mapped)
54
+ }
55
+ return tools.length ? tools : ['read_file', 'grep_files']
56
+ }
57
+
58
+ function hasMutationTool(allowedTools) {
59
+ return allowedTools.some((toolName) => toolName === 'write_file' || toolName === 'edit_file')
60
+ }
61
+
62
+ export function agentProfileFromMarkdown(file, text, options = {}) {
63
+ const parsed = parseFrontmatter(text)
64
+ if (!parsed.body) return null
65
+
66
+ const metadata = parsed.metadata || {}
67
+ const name = normalizeName(metadata.name) || normalizeName(path.basename(file, '.md'))
68
+ if (!name) return null
69
+ if (options.reservedNames?.has(name)) return null
70
+
71
+ const allowedTools = normalizeTools(
72
+ metadata.tools ?? metadata['allowed-tools'] ?? metadata.allowedTools,
73
+ )
74
+ const label = firstString(metadata.label, metadata.displayName, metadata.title) || name
75
+ const enabledAsSubagent = firstOptionalBoolean(
76
+ metadata['enabled-as-subagent'],
77
+ metadata.enabled_as_subagent,
78
+ metadata.enabledAsSubagent,
79
+ )
80
+
81
+ return {
82
+ id: `${options.idPrefix || 'file'}:${name}`,
83
+ name,
84
+ label: label.slice(0, 80),
85
+ description: String(firstString(metadata.description) || '').slice(0, 500),
86
+ systemPrompt: parsed.body,
87
+ allowedTools,
88
+ maxRuntimeMs: normalizeRuntime(metadata['max-runtime-ms'] ?? metadata.max_runtime_ms ?? metadata.maxRuntimeMs),
89
+ maxToolCalls: normalizeToolCalls(metadata['max-tool-calls'] ?? metadata.max_tool_calls ?? metadata.maxToolCalls),
90
+ enabledAsSubagent: enabledAsSubagent === undefined ? true : enabledAsSubagent,
91
+ builtin: false,
92
+ source: options.source || 'file',
93
+ readonly: true,
94
+ filePath: file,
95
+ relativePath: options.relativePath || path.basename(file),
96
+ allowFileMutations: hasMutationTool(allowedTools),
97
+ createdAt: 'file',
98
+ updatedAt: 'file',
99
+ }
100
+ }
101
+
102
+ async function listAgentFilesFromDirectory(dir, options = {}) {
103
+ if (!dir || !existsSync(dir)) return []
104
+ let entries
105
+ try {
106
+ entries = await fs.readdir(dir, { withFileTypes: true })
107
+ } catch (error) {
108
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR' || error?.code === 'EACCES' || error?.code === 'EPERM') return []
109
+ throw error
110
+ }
111
+
112
+ const profiles = []
113
+ for (const entry of entries) {
114
+ if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.md')) continue
115
+ const file = path.join(dir, entry.name)
116
+ try {
117
+ const relativePath = options.relativeRoot
118
+ ? `${options.relativeRoot}/${entry.name}`.replace(/\\/g, '/')
119
+ : entry.name
120
+ const profile = agentProfileFromMarkdown(file, await fs.readFile(file, 'utf8'), {
121
+ ...options,
122
+ relativePath,
123
+ })
124
+ if (profile) profiles.push(profile)
125
+ } catch (error) {
126
+ console.warn(`Failed to load agent profile ${file}:`, error.message || error)
127
+ }
128
+ }
129
+ return profiles
130
+ }
131
+
132
+ function projectClaudeAgentsDir(workspaceRoot) {
133
+ return workspaceRoot ? path.join(path.resolve(workspaceRoot), '.claude', 'agents') : ''
134
+ }
135
+
136
+ function projectQuickForgeAgentsDir(workspaceRoot) {
137
+ return workspaceRoot ? path.join(path.resolve(workspaceRoot), '.quickforge', 'agents') : ''
138
+ }
139
+
140
+ export async function loadUserAgentProfiles(options = {}) {
141
+ const byName = new Map()
142
+ const sources = [
143
+ { dir: claudeUserAgentsDir, source: 'user-claude', relativeRoot: '~/.claude/agents', idPrefix: 'user-claude' },
144
+ { dir: userAgentsDir, source: 'user', relativeRoot: '~/.quickforge/agents', idPrefix: 'user' },
145
+ ]
146
+ for (const source of sources) {
147
+ for (const profile of await listAgentFilesFromDirectory(source.dir, { ...options, ...source })) {
148
+ byName.set(profile.name, profile)
149
+ }
150
+ }
151
+ return [...byName.values()]
152
+ }
153
+
154
+ export async function loadProjectAgentProfiles(workspaceRoot, options = {}) {
155
+ if (!workspaceRoot) return []
156
+ const byName = new Map()
157
+ const sources = [
158
+ { dir: projectClaudeAgentsDir(workspaceRoot), source: 'project-claude', relativeRoot: '.claude/agents', idPrefix: 'project-claude' },
159
+ { dir: projectQuickForgeAgentsDir(workspaceRoot), source: 'project', relativeRoot: '.quickforge/agents', idPrefix: 'project' },
160
+ ]
161
+ for (const source of sources) {
162
+ for (const profile of await listAgentFilesFromDirectory(source.dir, { ...options, ...source })) {
163
+ byName.set(profile.name, profile)
164
+ }
165
+ }
166
+ return [...byName.values()]
167
+ }
168
+
169
+ export async function loadFileAgentProfiles(workspaceRoot, options = {}) {
170
+ const byName = new Map()
171
+ for (const profile of await loadUserAgentProfiles(options)) byName.set(profile.name, profile)
172
+ for (const profile of await loadProjectAgentProfiles(workspaceRoot, options)) byName.set(profile.name, profile)
173
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name))
174
+ }
175
+
176
+ export const agentProfileSearchPaths = {
177
+ global: ['~/.claude/agents', '~/.quickforge/agents'],
178
+ project: ['<project>/.claude/agents', '<project>/.quickforge/agents'],
179
+ }
@@ -2,6 +2,8 @@ import { randomUUID } from 'node:crypto'
2
2
  import { readStore, atomicUpdate } from './storage.mjs'
3
3
  import { subagentDefinitions } from './subagents.mjs'
4
4
  import { workspaceTools } from './tools/definitions.mjs'
5
+ import { defaultGlobalWorkspaceContext, projectContextFromId } from './project-config.mjs'
6
+ import { loadFileAgentProfiles } from './agent-profile-files.mjs'
5
7
 
6
8
  const STORE = 'custom-agents'
7
9
  const RESERVED_NAMES = new Set(subagentDefinitions.map((definition) => definition.name))
@@ -56,6 +58,9 @@ function builtinProfileFromSubagent(definition) {
56
58
  maxToolCalls: definition.maxToolCalls || DEFAULT_MAX_TOOL_CALLS,
57
59
  enabledAsSubagent: true,
58
60
  builtin: true,
61
+ source: 'builtin',
62
+ readonly: true,
63
+ allowFileMutations: definition.allowFileMutations === true,
59
64
  createdAt: 'builtin',
60
65
  updatedAt: 'builtin',
61
66
  }
@@ -92,6 +97,9 @@ function normalizeProfileInput(input, existing = null, { creating = false } = {}
92
97
  maxToolCalls: normalizeOptionalPositiveInteger(input?.maxToolCalls ?? existing?.maxToolCalls, DEFAULT_MAX_TOOL_CALLS, 300),
93
98
  enabledAsSubagent: input?.enabledAsSubagent === undefined ? Boolean(existing?.enabledAsSubagent ?? true) : input.enabledAsSubagent === true,
94
99
  builtin: false,
100
+ source: 'store',
101
+ readonly: false,
102
+ allowFileMutations: allowedTools.some((toolName) => toolName === 'write_file' || toolName === 'edit_file'),
95
103
  createdAt: existing?.createdAt || now,
96
104
  updatedAt: now,
97
105
  }
@@ -102,20 +110,61 @@ async function readCustomAgentMap() {
102
110
  return data && typeof data === 'object' ? data : {}
103
111
  }
104
112
 
113
+ async function resolveWorkspaceRoot(options = {}) {
114
+ if (options.workspaceRoot) return options.workspaceRoot
115
+ if (options.projectId) {
116
+ try {
117
+ return (await projectContextFromId(options.projectId))?.workspaceRoot || null
118
+ } catch {
119
+ return null
120
+ }
121
+ }
122
+ return defaultGlobalWorkspaceContext()?.workspaceRoot || null
123
+ }
124
+
125
+ function mergeProfiles({ builtin = [], file = [], custom = [] }) {
126
+ const reservedNames = new Set(builtin.map((profile) => profile.name))
127
+ const byName = new Map()
128
+
129
+ for (const profile of builtin) {
130
+ byName.set(profile.name, profile)
131
+ }
132
+ for (const profile of file) {
133
+ if (!profile?.name || reservedNames.has(profile.name)) continue
134
+ byName.set(profile.name, profile)
135
+ }
136
+ for (const profile of custom) {
137
+ if (!profile?.id) continue
138
+ if (!reservedNames.has(profile.name) && !byName.has(profile.name)) byName.set(profile.name, profile)
139
+ }
140
+
141
+ return [...byName.values()].sort((a, b) => {
142
+ if (a.builtin && !b.builtin) return -1
143
+ if (!a.builtin && b.builtin) return 1
144
+ return a.name.localeCompare(b.name)
145
+ })
146
+ }
147
+
105
148
  export async function listAgentProfiles(options = {}) {
106
149
  const custom = Object.values(await readCustomAgentMap())
107
- const profiles = [...listBuiltinAgentProfiles(), ...custom]
150
+ const workspaceRoot = await resolveWorkspaceRoot(options)
151
+ const file = await loadFileAgentProfiles(workspaceRoot, { reservedNames: RESERVED_NAMES })
152
+ const profiles = mergeProfiles({ builtin: listBuiltinAgentProfiles(), file, custom })
108
153
  return options.includeDisabled ? profiles : profiles.filter((profile) => profile.enabledAsSubagent || profile.builtin || profile.enabledAsSubagent === false)
109
154
  }
110
155
 
111
- export async function listSubagentProfiles() {
112
- return (await listAgentProfiles({ includeDisabled: true })).filter((profile) => profile.enabledAsSubagent)
156
+ export async function listSubagentProfiles(options = {}) {
157
+ return (await listAgentProfiles({ ...options, includeDisabled: true })).filter((profile) => profile.enabledAsSubagent)
113
158
  }
114
159
 
115
- export async function getAgentProfile(idOrName) {
160
+ export async function getAgentProfile(idOrName, options = {}) {
116
161
  const key = String(idOrName || '').trim().toLowerCase()
117
162
  if (!key) return null
118
- return (await listAgentProfiles({ includeDisabled: true })).find((profile) => profile.id === key || profile.name === key) || null
163
+ const profiles = await listAgentProfiles({ ...options, includeDisabled: true })
164
+ const byName = profiles.find((profile) => profile.name === key)
165
+ if (byName) return byName
166
+ const custom = Object.values(await readCustomAgentMap())
167
+ return custom.find((profile) => profile?.id === key) || profiles.find((profile) => profile.id === key) || null
119
168
  }
120
169
 
121
170
  export async function createCustomAgentProfile(input) {
@@ -167,7 +216,12 @@ export function agentProfileSnapshot(profile) {
167
216
  allowedTools: [...profile.allowedTools],
168
217
  maxRuntimeMs: profile.maxRuntimeMs,
169
218
  maxToolCalls: profile.maxToolCalls,
219
+ enabledAsSubagent: profile.enabledAsSubagent === true,
170
220
  builtin: profile.builtin === true,
221
+ source: profile.source || (profile.builtin ? 'builtin' : 'store'),
222
+ readonly: profile.readonly === true || profile.builtin === true,
223
+ filePath: profile.filePath,
224
+ relativePath: profile.relativePath,
171
225
  }
172
226
  }
173
227
 
@@ -151,6 +151,76 @@ export function buildAutoCompactLoopMessages(session, messages) {
151
151
  return [summaryMessage, ...source.slice(compactedUpToIndex)]
152
152
  }
153
153
 
154
+ export async function compactSessionInPlace({
155
+ session,
156
+ messages,
157
+ keepRecentTurns = DEFAULT_AUTO_COMPACT_SETTINGS.keepRecentTurns,
158
+ minSourceChars = DEFAULT_AUTO_COMPACT_SETTINGS.minSourceChars,
159
+ usage,
160
+ thresholdPercent,
161
+ emitSessionEvent,
162
+ persistSession,
163
+ reason = 'manual_compact',
164
+ summaryIntro = 'The previous conversation has been compacted. Treat the following summary as the authoritative replacement for earlier history. If information is missing, ask for clarification instead of guessing.',
165
+ onBeforePersist,
166
+ }) {
167
+ const source = Array.isArray(messages) ? messages : []
168
+ const normalizedKeepRecentTurns = clampNumber(keepRecentTurns, DEFAULT_AUTO_COMPACT_SETTINGS.keepRecentTurns, 1, 20)
169
+ const normalizedMinSourceChars = clampNumber(minSourceChars, DEFAULT_AUTO_COMPACT_SETTINGS.minSourceChars, 0, 200000)
170
+ const tailStart = tailStartForRecentTurns(source, normalizedKeepRecentTurns)
171
+ const sourceMessages = buildCompactionSourceMessages(session, source, tailStart)
172
+ if (sourceMessages.length < 2 || estimateMessagesChars(sourceMessages) < normalizedMinSourceChars) {
173
+ return { compacted: false, usage, reason: 'not_enough_history' }
174
+ }
175
+
176
+ const result = await compactConversation({
177
+ messages: sourceMessages,
178
+ model: session.model,
179
+ thinkingLevel: session.thinkingLevel,
180
+ getApiKey: session.getApiKey,
181
+ keepTurns: 0,
182
+ })
183
+
184
+ if (result.skipped) return { compacted: false, usage, reason: result.reason || 'skipped' }
185
+
186
+ await saveCompactBackup(session.sessionId, sourceMessages)
187
+ const summaryMessage = userTextMessage([
188
+ summaryIntro,
189
+ '',
190
+ '<compact_summary>',
191
+ result.summary,
192
+ '</compact_summary>',
193
+ ].join('\n'))
194
+ session.contextCompaction = {
195
+ summaryMessage,
196
+ compactedUpToIndex: tailStart,
197
+ compactedAt: new Date().toISOString(),
198
+ keepRecentTurns: normalizedKeepRecentTurns,
199
+ sourceMessageCount: source.length,
200
+ usageBefore: usage,
201
+ thresholdPercent,
202
+ }
203
+ onBeforePersist?.({ result, sourceMessages, tailStart, summaryMessage })
204
+ await persistSession?.(session)
205
+ const contextUsage = estimateSessionContextUsage(session, source)
206
+ emitSessionEvent?.(session, {
207
+ type: 'auto_compact_completed',
208
+ reason,
209
+ usage,
210
+ thresholdPercent,
211
+ contextCompaction: session.contextCompaction,
212
+ contextUsage,
213
+ })
214
+ emitSessionEvent?.(session, {
215
+ type: 'messages_replaced',
216
+ reason,
217
+ messages: source,
218
+ contextCompaction: session.contextCompaction,
219
+ contextUsage,
220
+ })
221
+ return { compacted: true, usage, result, sourceMessages, tailStart }
222
+ }
223
+
154
224
  export function estimateSessionContextUsage(session, messages = session?.agent?.state?.messages ?? []) {
155
225
  if (!session?.agent?.state) return null
156
226
  const sourceMessages = Array.isArray(messages) ? messages : []
@@ -274,50 +344,23 @@ export async function maybeAutoCompactSession({ session, messages, signal, emitS
274
344
 
275
345
  session.autoCompacting = true
276
346
  try {
277
- const result = await compactConversation({
278
- messages: sourceMessages,
279
- model: session.model,
280
- thinkingLevel: session.thinkingLevel,
281
- getApiKey: session.getApiKey,
282
- keepTurns: 0,
283
- })
284
-
285
- if (result.skipped) return { compacted: false, usage, reason: result.reason || 'skipped' }
286
-
287
- await saveCompactBackup(session.sessionId, sourceMessages)
288
- const summaryMessage = userTextMessage([
289
- 'The previous conversation has been automatically compacted. Treat the following summary as the authoritative replacement for earlier history. If information is missing, ask for clarification instead of guessing.',
290
- '',
291
- '<compact_summary>',
292
- result.summary,
293
- '</compact_summary>',
294
- ].join('\n'))
295
- session.contextCompaction = {
296
- summaryMessage,
297
- compactedUpToIndex: tailStart,
298
- compactedAt: new Date().toISOString(),
347
+ const result = await compactSessionInPlace({
348
+ session,
349
+ messages,
299
350
  keepRecentTurns: settings.keepRecentTurns,
300
- sourceMessageCount: messages.length,
301
- usageBefore: usage,
302
- thresholdPercent: settings.thresholdPercent,
303
- }
304
- clearAutoCompactRejected(session)
305
- session.lastAutoCompactAt = now
306
- await persistSession(session)
307
- emitSessionEvent(session, {
308
- type: 'auto_compact_completed',
351
+ minSourceChars: settings.minSourceChars,
309
352
  usage,
310
353
  thresholdPercent: settings.thresholdPercent,
311
- contextCompaction: session.contextCompaction,
312
- contextUsage: estimateSessionContextUsage(session, messages),
313
- })
314
- emitSessionEvent(session, {
315
- type: 'messages_replaced',
354
+ emitSessionEvent,
355
+ persistSession,
316
356
  reason: 'auto_compact',
317
- messages,
318
- contextCompaction: session.contextCompaction,
319
- contextUsage: estimateSessionContextUsage(session, messages),
357
+ summaryIntro: 'The previous conversation has been automatically compacted. Treat the following summary as the authoritative replacement for earlier history. If information is missing, ask for clarification instead of guessing.',
358
+ onBeforePersist: () => {
359
+ clearAutoCompactRejected(session)
360
+ session.lastAutoCompactAt = now
361
+ },
320
362
  })
363
+ if (!result.compacted) return result
321
364
  return { compacted: true, usage }
322
365
  } catch (error) {
323
366
  logger?.warn?.(`Auto compact failed for session ${session.sessionId}:`, error?.message || error, { sessionId: session.sessionId })