minimal-agent 0.11.0 → 0.12.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.
Files changed (128) hide show
  1. package/package.json +4 -1
  2. package/plugins/workflow-runner/plugin.js +27 -15
  3. package/plugins/workflow-runner/plugin.ts +34 -20
  4. package/plugins/workflow-runner/src/agentCommon.js +13 -12
  5. package/plugins/workflow-runner/src/agentCommon.ts +22 -13
  6. package/plugins/workflow-runner/src/index.js +5 -3
  7. package/plugins/workflow-runner/src/index.ts +31 -13
  8. package/plugins/workflow-runner/src/multimodal.js +2 -1
  9. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  10. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  11. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  12. package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
  13. package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
  14. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  15. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  16. package/plugins/workflow-runner/src/types.ts +26 -7
  17. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  18. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  19. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  20. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  21. package/src/cli/print.js +19 -4
  22. package/src/cli/print.ts +17 -4
  23. package/src/cli/streamJson.js +19 -4
  24. package/src/cli/streamJson.ts +17 -4
  25. package/src/config/configDefaults.js +25 -0
  26. package/src/config/configDefaults.ts +33 -0
  27. package/src/config/configFile.js +2 -21
  28. package/src/config/configFile.ts +11 -24
  29. package/src/config/configManagement.js +1 -1
  30. package/src/config/configManagement.ts +2 -2
  31. package/src/context/compact.js +4 -0
  32. package/src/context/compact.ts +4 -0
  33. package/src/context/contextBundle.js +38 -13
  34. package/src/context/contextBundle.ts +47 -13
  35. package/src/harness/stallGuard.js +19 -4
  36. package/src/harness/stallGuard.ts +30 -7
  37. package/src/loop.js +95 -11
  38. package/src/loop.ts +125 -16
  39. package/src/main.js +8 -1
  40. package/src/main.tsx +7 -1
  41. package/src/memory/constants.js +10 -0
  42. package/src/memory/constants.ts +13 -0
  43. package/src/memory/episodicMemory.js +274 -2
  44. package/src/memory/episodicMemory.ts +306 -1
  45. package/src/memory/fileStore.js +24 -4
  46. package/src/memory/fileStore.ts +22 -2
  47. package/src/memory/memoryService.js +79 -12
  48. package/src/memory/memoryService.ts +108 -9
  49. package/src/memory/repository.js +415 -85
  50. package/src/memory/repository.ts +437 -86
  51. package/src/memory/turnJournal.js +95 -0
  52. package/src/memory/turnJournal.ts +120 -0
  53. package/src/memory/types.ts +16 -0
  54. package/src/plugins/pluginApi.ts +30 -6
  55. package/src/plugins/pluginRunner.js +6 -2
  56. package/src/plugins/pluginRunner.ts +12 -7
  57. package/src/prompts/policyRegistry.js +27 -0
  58. package/src/prompts/policyRegistry.ts +27 -0
  59. package/src/prompts/promptAssembly.js +39 -7
  60. package/src/prompts/promptAssembly.ts +145 -59
  61. package/src/session/runAgentTurn.js +43 -21
  62. package/src/session/runAgentTurn.ts +68 -26
  63. package/src/session/runTurn.js +11 -46
  64. package/src/session/runTurn.ts +12 -46
  65. package/src/session/runtimeStore.js +149 -0
  66. package/src/session/runtimeStore.ts +197 -0
  67. package/src/session/sessionState.js +5 -0
  68. package/src/session/sessionState.ts +14 -0
  69. package/src/tools/agent/agent.js +143 -14
  70. package/src/tools/agent/agent.ts +149 -14
  71. package/src/tools/agent/agentTaskService.js +60 -0
  72. package/src/tools/agent/agentTaskService.ts +113 -0
  73. package/src/tools/agent/agentTypes.js +5 -0
  74. package/src/tools/agent/agentTypes.ts +5 -0
  75. package/src/tools/bash/bash.js +31 -11
  76. package/src/tools/bash/bash.ts +29 -10
  77. package/src/tools/capabilities.js +5 -0
  78. package/src/tools/capabilities.ts +11 -1
  79. package/src/tools/edit/edit.js +7 -4
  80. package/src/tools/edit/edit.ts +7 -4
  81. package/src/tools/edit/multi-edit.js +4 -2
  82. package/src/tools/edit/multi-edit.ts +4 -2
  83. package/src/tools/glob/glob.js +156 -40
  84. package/src/tools/glob/glob.ts +185 -46
  85. package/src/tools/index.js +35 -17
  86. package/src/tools/index.ts +81 -20
  87. package/src/tools/lsp/lsp.js +165 -22
  88. package/src/tools/lsp/lsp.ts +206 -36
  89. package/src/tools/powershell/powershell.js +116 -26
  90. package/src/tools/powershell/powershell.ts +155 -30
  91. package/src/tools/presentresult/presentresult.js +10 -0
  92. package/src/tools/presentresult/presentresult.ts +10 -0
  93. package/src/tools/read/read.js +244 -29
  94. package/src/tools/read/read.ts +391 -59
  95. package/src/tools/shared/diffSummary.js +44 -0
  96. package/src/tools/shared/diffSummary.ts +63 -0
  97. package/src/tools/shared/fileActivity.js +2 -1
  98. package/src/tools/shared/fileActivity.ts +3 -0
  99. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  100. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  101. package/src/tools/shared/toolOutputStore.js +97 -0
  102. package/src/tools/shared/toolOutputStore.ts +110 -0
  103. package/src/tools/skill/skill.js +86 -0
  104. package/src/tools/skill/skill.ts +94 -0
  105. package/src/tools/snip/snip.js +67 -0
  106. package/src/tools/snip/snip.ts +80 -0
  107. package/src/tools/tasks/backgroundTaskService.js +79 -28
  108. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  109. package/src/tools/tasks/taskManagement.js +163 -0
  110. package/src/tools/tasks/taskManagement.ts +180 -0
  111. package/src/tools/tasks/taskOutput.js +64 -14
  112. package/src/tools/tasks/taskOutput.ts +102 -23
  113. package/src/tools/tasks/taskStop.js +48 -8
  114. package/src/tools/tasks/taskStop.ts +69 -9
  115. package/src/tools/tasks/taskStore.js +46 -0
  116. package/src/tools/tasks/taskStore.ts +77 -0
  117. package/src/tools/terminal/terminalCapture.js +167 -0
  118. package/src/tools/terminal/terminalCapture.ts +179 -0
  119. package/src/tools/trash/trash.js +233 -0
  120. package/src/tools/trash/trash.ts +283 -0
  121. package/src/tools/worktree/sessionWorktree.js +128 -0
  122. package/src/tools/worktree/sessionWorktree.ts +158 -0
  123. package/src/tools/write/write.js +6 -2
  124. package/src/tools/write/write.ts +6 -2
  125. package/src/types.ts +12 -0
  126. package/src/ui/hooks/useChat.js +31 -11
  127. package/src/ui/hooks/useChat.ts +31 -13
  128. package/src/web/protocol.ts +78 -3
@@ -0,0 +1,158 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ import type { SessionState } from '../../session/sessionState.js';
4
+ import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
5
+ import { toToolParameters } from '../../utils/zodToJson.js';
6
+ import { join } from 'node:path';
7
+ import {
8
+ readRuntimeJson,
9
+ sessionRuntimeDir,
10
+ writeRuntimeJson,
11
+ } from '../../session/runtimeStore.js';
12
+ import {
13
+ applyAgentWorktree,
14
+ commitWorktreeChanges,
15
+ createAgentWorktree,
16
+ hasWorktreeChanges,
17
+ removeAgentWorktree,
18
+ } from '../agent/worktree.js';
19
+
20
+ const enterSchema = z.object({
21
+ name: z
22
+ .string()
23
+ .regex(/^[a-zA-Z0-9._-]{1,48}$/)
24
+ .optional(),
25
+ });
26
+ const exitSchema = z.object({ action: z.enum(['apply', 'keep', 'discard']) });
27
+ async function persist(
28
+ ctx: ToolCallContext,
29
+ state: SessionState['activeWorktree'],
30
+ ) {
31
+ await writeRuntimeJson(join(sessionRuntimeDir(ctx), 'worktree.json'), {
32
+ active: state ?? null,
33
+ });
34
+ }
35
+ export async function hydrateSessionWorktree(
36
+ ctx: ToolCallContext,
37
+ ): Promise<void> {
38
+ if (!ctx.sessionState || ctx.sessionState.activeWorktree) return;
39
+ const saved = await readRuntimeJson<{
40
+ active?: NonNullable<SessionState['activeWorktree']> | null;
41
+ }>(join(sessionRuntimeDir(ctx), 'worktree.json'), {});
42
+ if (saved.active) ctx.sessionState.activeWorktree = saved.active;
43
+ }
44
+ async function enter(
45
+ input: z.infer<typeof enterSchema>,
46
+ ctx: ToolCallContext,
47
+ ): Promise<ToolResult> {
48
+ const state = ctx.sessionState;
49
+ if (!state) return { ok: false, error: 'EnterWorktree 需要会话状态' };
50
+ if (state.activeWorktree)
51
+ return {
52
+ ok: true,
53
+ content: `当前已在 worktree ${state.activeWorktree.path}`,
54
+ };
55
+ const slug = `session-${input.name ?? randomUUID().slice(0, 8)}`;
56
+ const wt = await createAgentWorktree(slug, ctx.cwd);
57
+ if (!wt.ok) return wt;
58
+ state.activeWorktree = {
59
+ id: slug,
60
+ path: wt.path,
61
+ branch: wt.branch,
62
+ baseSha: wt.baseSha,
63
+ gitRoot: wt.gitRoot,
64
+ };
65
+ await persist(ctx, state.activeWorktree);
66
+ return {
67
+ ok: true,
68
+ content: `已进入主 Agent 隔离 worktree:${wt.path}\n分支:${wt.branch}\n后续工具调用立即使用该目录。`,
69
+ };
70
+ }
71
+ async function exit(
72
+ input: z.infer<typeof exitSchema>,
73
+ ctx: ToolCallContext,
74
+ ): Promise<ToolResult> {
75
+ const state = ctx.sessionState,
76
+ wt = state?.activeWorktree;
77
+ if (!state || !wt) return { ok: false, error: '当前会话没有活动 worktree' };
78
+ if (input.action === 'keep') {
79
+ state.activeWorktree = undefined;
80
+ await persist(ctx, undefined);
81
+ return {
82
+ ok: true,
83
+ content: `已退出有效 cwd,但保留 worktree:${wt.path}\n分支:${wt.branch}`,
84
+ };
85
+ }
86
+ const changed = await hasWorktreeChanges(wt.path, wt.baseSha);
87
+ if (input.action === 'discard' && changed) {
88
+ const committed = await commitWorktreeChanges(
89
+ wt.path,
90
+ `archive: preserve discarded session ${wt.id}`,
91
+ );
92
+ if (committed.ok) {
93
+ state.activeWorktree = undefined;
94
+ await persist(ctx, undefined);
95
+ }
96
+ return committed.ok
97
+ ? {
98
+ ok: true,
99
+ content: `检测到成果,未永久删除。已归档到分支 ${wt.branch},worktree 保留在 ${wt.path}。`,
100
+ }
101
+ : {
102
+ ok: false,
103
+ error: `无法安全归档,worktree 保持活动:${committed.error}`,
104
+ };
105
+ }
106
+ if (input.action === 'discard') {
107
+ await removeAgentWorktree(wt.path, wt.branch, wt.gitRoot);
108
+ state.activeWorktree = undefined;
109
+ await persist(ctx, undefined);
110
+ return {
111
+ ok: true,
112
+ content: 'worktree 无改动,已通过 Git worktree 管理安全清理。',
113
+ };
114
+ }
115
+ const committed = await commitWorktreeChanges(
116
+ wt.path,
117
+ `minimal-agent: apply session ${wt.id}`,
118
+ );
119
+ if (!committed.ok)
120
+ return {
121
+ ok: false,
122
+ error: `提交隔离改动失败,worktree 保持活动:${committed.error}`,
123
+ };
124
+ const applied = await applyAgentWorktree(wt.gitRoot, wt.id);
125
+ if (!applied.ok)
126
+ return {
127
+ ok: false,
128
+ error: `应用检查失败,worktree 保持活动:${applied.error}`,
129
+ };
130
+ state.activeWorktree = undefined;
131
+ await persist(ctx, undefined);
132
+ return {
133
+ ok: true,
134
+ content: `已通过 git apply --check 并将变更应用到原工作区;未自动提交。`,
135
+ };
136
+ }
137
+ export const enterWorktreeTool: Tool<z.infer<typeof enterSchema>> = {
138
+ name: 'EnterWorktree',
139
+ description:
140
+ 'Creates and enters a session-scoped Git worktree for the main Agent. Later tool calls use it immediately while project memory keeps the original workspace identity.',
141
+ inputSchema: enterSchema,
142
+ parameters: toToolParameters(enterSchema),
143
+ isReadOnly: false,
144
+ isConcurrencySafe: false,
145
+ call: async () => ({ ok: false, error: 'requires session context' }),
146
+ callWithContext: (i, _s, c) => enter(i, c),
147
+ };
148
+ export const exitWorktreeTool: Tool<z.infer<typeof exitSchema>> = {
149
+ name: 'ExitWorktree',
150
+ description:
151
+ 'Leaves the current session worktree. apply checks and applies its patch to the original workspace; keep preserves it; discard never destroys uncommitted results and archives changed work.',
152
+ inputSchema: exitSchema,
153
+ parameters: toToolParameters(exitSchema),
154
+ isReadOnly: false,
155
+ isConcurrencySafe: false,
156
+ call: async () => ({ ok: false, error: 'requires session context' }),
157
+ callWithContext: (i, _s, c) => exit(i, c),
158
+ };
@@ -22,6 +22,7 @@ import { filePathField } from '../shared/schemas.js';
22
22
  import { assertFresh, hashFileBytes, recordRead } from '../shared/fileState.js';
23
23
  import { fileActivityMetadata } from '../shared/fileActivity.js';
24
24
  import { emitFileMutation } from '../shared/fileMutationBus.js';
25
+ import { buildPatchSummary } from '../shared/diffSummary.js';
25
26
  /** 写入内容上限:1GB。防止模型试图写入超大内容撑爆内存/磁盘。 */
26
27
  const MAX_WRITE_SIZE_BYTES = 1024 * 1024 * 1024;
27
28
  // ---------------- 1. Zod 输入 schema ----------------
@@ -69,9 +70,11 @@ async function call(input, state, cwd) {
69
70
  catch { }
70
71
  }
71
72
  let contentToWrite = input.content;
73
+ let originalContent = '';
72
74
  let bomEncoding = 'utf8';
73
75
  if (fileExisted) {
74
76
  const snapshot = await readTextFile(filePath);
77
+ originalContent = snapshot.content;
75
78
  bomEncoding = snapshot.encoding;
76
79
  contentToWrite = applyLineEnding(input.content, snapshot.lineEnding);
77
80
  }
@@ -88,10 +91,11 @@ async function call(input, state, cwd) {
88
91
  const bomInfo = bomEncoding === 'utf8-bom'
89
92
  ? ',已保留 UTF-8 BOM'
90
93
  : bomEncoding === 'utf16le' ? ',已保留 UTF-16 LE' : '';
94
+ const patch = buildPatchSummary(originalContent, contentToWrite, input.file_path);
91
95
  return {
92
96
  ok: true,
93
- content: `${action} ${filePath}${sizeInfo}${bomInfo}`,
94
- metadata: await fileActivityMetadata(filePath, fileExisted ? 'modified' : 'created', 'not_run', 'support', cwd),
97
+ content: `${action} ${filePath}${sizeInfo}${bomInfo}\n${patch.unifiedDiff ?? '[diff omitted: too large]'}`,
98
+ metadata: await fileActivityMetadata(filePath, fileExisted ? 'modified' : 'created', 'not_run', 'support', cwd, patch),
95
99
  };
96
100
  }
97
101
  catch (e) {
@@ -31,6 +31,7 @@ import { filePathField } from '../shared/schemas.js';
31
31
  import { assertFresh, hashFileBytes, recordRead, type FileReadState } from '../shared/fileState.js';
32
32
  import { fileActivityMetadata } from '../shared/fileActivity.js';
33
33
  import { emitFileMutation } from '../shared/fileMutationBus.js';
34
+ import { buildPatchSummary } from '../shared/diffSummary.js';
34
35
 
35
36
  /** 写入内容上限:1GB。防止模型试图写入超大内容撑爆内存/磁盘。 */
36
37
  const MAX_WRITE_SIZE_BYTES = 1024 * 1024 * 1024;
@@ -86,9 +87,11 @@ async function call(input: WriteInput, state?: FileReadState, cwd?: string): Pro
86
87
  }
87
88
 
88
89
  let contentToWrite = input.content;
90
+ let originalContent = '';
89
91
  let bomEncoding: Awaited<ReturnType<typeof readTextFile>>['encoding'] = 'utf8';
90
92
  if (fileExisted) {
91
93
  const snapshot = await readTextFile(filePath);
94
+ originalContent = snapshot.content;
92
95
  bomEncoding = snapshot.encoding;
93
96
  contentToWrite = applyLineEnding(input.content, snapshot.lineEnding);
94
97
  }
@@ -106,11 +109,12 @@ async function call(input: WriteInput, state?: FileReadState, cwd?: string): Pro
106
109
  const bomInfo = bomEncoding === 'utf8-bom'
107
110
  ? ',已保留 UTF-8 BOM'
108
111
  : bomEncoding === 'utf16le' ? ',已保留 UTF-16 LE' : '';
112
+ const patch = buildPatchSummary(originalContent, contentToWrite, input.file_path);
109
113
 
110
114
  return {
111
115
  ok: true,
112
- content: `${action} ${filePath}${sizeInfo}${bomInfo}`,
113
- metadata: await fileActivityMetadata(filePath, fileExisted ? 'modified' : 'created', 'not_run', 'support', cwd),
116
+ content: `${action} ${filePath}${sizeInfo}${bomInfo}\n${patch.unifiedDiff ?? '[diff omitted: too large]'}`,
117
+ metadata: await fileActivityMetadata(filePath, fileExisted ? 'modified' : 'created', 'not_run', 'support', cwd, patch),
114
118
  };
115
119
  } catch (e) {
116
120
  return { ok: false, error: `写入失败:${(e as Error).message}` };
package/src/types.ts CHANGED
@@ -191,6 +191,8 @@ export interface AgentResultMetadata {
191
191
  kind: 'agent';
192
192
  agentType: 'explore' | 'general';
193
193
  description: string;
194
+ agentId?: string;
195
+ status?: 'running' | 'completed' | 'failed' | 'stopped' | 'interrupted';
194
196
  isolation: 'none' | 'worktree';
195
197
  turns: number;
196
198
  toolCalls: number;
@@ -214,6 +216,15 @@ export interface AgentResultMetadata {
214
216
  export type FileActivityKind = 'read' | 'created' | 'modified' | 'generated' | 'deleted';
215
217
  export type FileVerification = 'passed' | 'failed' | 'not_run';
216
218
  export type ConversationFilePurpose = 'source' | 'deliverable' | 'support';
219
+ export interface FilePatchSummary {
220
+ unifiedDiff?: string;
221
+ beforeHash?: string;
222
+ afterHash: string;
223
+ additions: number;
224
+ deletions: number;
225
+ truncated: boolean;
226
+ omittedReason?: 'too_large';
227
+ }
217
228
 
218
229
  /** Structured local file output. Paths are always workspace-relative. */
219
230
  export interface FileActivityMetadata {
@@ -225,6 +236,7 @@ export interface FileActivityMetadata {
225
236
  mime?: string;
226
237
  verification: FileVerification;
227
238
  purpose: ConversationFilePurpose;
239
+ patch?: FilePatchSummary;
228
240
  }>;
229
241
  }
230
242
 
@@ -27,6 +27,7 @@ import { resolveSessionFile, sessionFileFor, } from '../../context/sessionPath.j
27
27
  import { buildBaseSystemPrompt, buildTransientPromptBundle } from '../../prompts/system.js';
28
28
  import { createPromptCacheContext } from '../../llm/promptCache.js';
29
29
  import { createAgentRuntimeContext, filterToolsForRuntime, memoryContextFromRuntime, } from '../../runtime/agentRuntimeContext.js';
30
+ import { startTurnJournal } from '../../memory/turnJournal.js';
30
31
  import { ALL_TOOLS, getToolByName } from '../../tools/index.js';
31
32
  import { createInputQueue } from '../../messageQueue.js';
32
33
  import { createAskCoordinator, setAskHandler, } from '../../askBridge.js';
@@ -34,6 +35,7 @@ import { createAskSlot } from './askSlot.js';
34
35
  import { figures } from '../figures.js';
35
36
  import { clearFileState } from '../../tools/shared/fileState.js';
36
37
  import { getOrCreateSessionState, sessionStateKey } from '../../session/sessionState.js';
38
+ import { hydrateSessionWorktree } from '../../tools/worktree/sessionWorktree.js';
37
39
  import { runAdvisor } from '../../tools/advisor/advisor.js';
38
40
  import { runWithPlugins } from '../../plugins/pluginRunner.js';
39
41
  import { dlog } from '../../utils/debugLog.js';
@@ -394,6 +396,8 @@ export function useChat(args) {
394
396
  const ac = new AbortController();
395
397
  abortRef.current = ac;
396
398
  let releaseContextLock = null;
399
+ let turnJournal = null;
400
+ let turnJournalStart = 0;
397
401
  dlog('ui', 'turn START', { input: trimmed.slice(0, 80) });
398
402
  // 长时间运行时(如工作流 agent 步骤)的定时 token 刷新:
399
403
  // runQuery 的 text/reasoning 事件不触发 bump,导致 StatusLine token 数
@@ -441,10 +445,20 @@ export function useChat(args) {
441
445
  })
442
446
  : undefined;
443
447
  const toolGate = computerRun ? createComputerUseHostGate(computerRun) : undefined;
448
+ const turnSessionState = getOrCreateSessionState(sessionStateKey({
449
+ surface: 'tui', workspaceId: workspace?.id,
450
+ cwd: workspace?.id ? undefined : getWorkingDir(),
451
+ sessionId: activeSessionIdRef.current,
452
+ }));
453
+ await hydrateSessionWorktree({
454
+ sessionState: turnSessionState, workspaceId: workspace?.id,
455
+ sessionId: activeSessionIdRef.current, cwd: getWorkingDir(),
456
+ });
457
+ const effectiveCwd = turnSessionState.activeWorktree?.path ?? getWorkingDir();
444
458
  const runtimeContext = createAgentRuntimeContext({
445
459
  surface: 'tui',
446
460
  profile: 'main',
447
- cwd: getWorkingDir(),
461
+ cwd: effectiveCwd,
448
462
  workspaceId: workspace?.id,
449
463
  sessionId: activeSessionIdRef.current,
450
464
  owner: 'local-owner',
@@ -455,9 +469,9 @@ export function useChat(args) {
455
469
  toolGate,
456
470
  });
457
471
  const turnTools = filterToolsForRuntime(ALL_TOOLS, runtimeContext);
458
- const toolSignature = turnTools.map((tool) => tool.name).join(',');
472
+ const toolSignature = `${effectiveCwd}\0${turnTools.map((tool) => tool.name).join(',')}`;
459
473
  if (promptToolSignatureRef.current !== toolSignature) {
460
- const stablePrompt = await buildBaseSystemPrompt(getWorkingDir(), turnTools);
474
+ const stablePrompt = await buildBaseSystemPrompt(effectiveCwd, turnTools);
461
475
  const rest = historyRef.current[0]?.role === 'system'
462
476
  ? historyRef.current.slice(1)
463
477
  : [...historyRef.current];
@@ -467,30 +481,33 @@ export function useChat(args) {
467
481
  }
468
482
  // 每回合只刷新易变层;稳定层仅在实际能力目录变化时重建。
469
483
  const turnTransient = await buildTransientPromptBundle({
470
- cwd: getWorkingDir(),
484
+ cwd: effectiveCwd,
471
485
  tools: turnTools,
472
486
  runtime: {
473
487
  surface: 'tui',
474
- cwd: getWorkingDir(),
488
+ cwd: effectiveCwd,
475
489
  currentUserInput: trimmed,
476
490
  workspaceId: workspace?.id,
491
+ sessionId: activeSessionIdRef.current,
477
492
  interactiveAsk: true,
478
493
  supervisedBrowser: false,
479
494
  memoryAccess: 'read-write',
480
495
  },
481
496
  }).catch(() => undefined);
497
+ // 会话原文记账(与 session/runTurn.ts 共用同一份实现)。
498
+ // TUI 不经过 runTurn,此前整条路径都不入库,SearchConversations 对 TUI 用户静默失效。
499
+ turnJournalStart = historyRef.current.length;
500
+ turnJournal = await startTurnJournal({
501
+ memoryContext: memoryContextFromRuntime(runtimeContext),
502
+ userInput: trimmed,
503
+ }).catch(() => null);
482
504
  for await (const ev of runWithPlugins(trimmed, {
483
505
  provider: providerRef.current,
484
506
  history: historyRef.current,
485
507
  signal: ac.signal,
486
508
  runtimeContext,
487
509
  allowedTools: turnTools.map((tool) => tool.name),
488
- sessionState: getOrCreateSessionState(sessionStateKey({
489
- surface: 'tui',
490
- workspaceId: workspace?.id,
491
- cwd: workspace?.id ? undefined : getWorkingDir(),
492
- sessionId: activeSessionIdRef.current,
493
- })),
510
+ sessionState: turnSessionState,
494
511
  parseInlineImages: true,
495
512
  ...(turnTransient ? { transientBundle: turnTransient } : {}),
496
513
  promptCache: createPromptCacheContext(`workspace:${workspace?.id ?? getWorkingDir()}:session:${activeSessionIdRef.current}`),
@@ -558,6 +575,9 @@ export function useChat(args) {
558
575
  }
559
576
  activeComputerRunRef.current = null;
560
577
  runningToolsRef.current.clear();
578
+ // 记账失败不影响落盘与解锁(它是附加能力,不是功能)。
579
+ await turnJournal?.finish(historyRef.current, turnJournalStart).catch(() => undefined);
580
+ turnJournal = null;
561
581
  // v2 multi-session:持久化到当前 active session 文件。
562
582
  try {
563
583
  await saveContext(historyRef.current, sessionFileFor(getWorkingDir(), activeSessionIdRef.current));
@@ -45,6 +45,7 @@ import {
45
45
  filterToolsForRuntime,
46
46
  memoryContextFromRuntime,
47
47
  } from '../../runtime/agentRuntimeContext.js';
48
+ import { startTurnJournal, type TurnJournal } from '../../memory/turnJournal.js';
48
49
  import { ALL_TOOLS, getToolByName } from '../../tools/index.js';
49
50
  import { createInputQueue } from '../../messageQueue.js';
50
51
  import {
@@ -58,6 +59,7 @@ import { createAskSlot } from './askSlot.js';
58
59
  import { figures } from '../figures.js';
59
60
  import { clearFileState } from '../../tools/shared/fileState.js';
60
61
  import { getOrCreateSessionState, sessionStateKey } from '../../session/sessionState.js';
62
+ import { hydrateSessionWorktree } from '../../tools/worktree/sessionWorktree.js';
61
63
  import type {
62
64
  LoopEvent,
63
65
  Message,
@@ -627,6 +629,8 @@ export function useChat(args: {
627
629
  const ac = new AbortController();
628
630
  abortRef.current = ac;
629
631
  let releaseContextLock: (() => Promise<void>) | null = null;
632
+ let turnJournal: TurnJournal | null = null;
633
+ let turnJournalStart = 0;
630
634
  dlog('ui', 'turn START', { input: trimmed.slice(0, 80) });
631
635
 
632
636
  // 长时间运行时(如工作流 agent 步骤)的定时 token 刷新:
@@ -681,10 +685,20 @@ export function useChat(args: {
681
685
  })
682
686
  : undefined;
683
687
  const toolGate = computerRun ? createComputerUseHostGate(computerRun) : undefined;
688
+ const turnSessionState = getOrCreateSessionState(sessionStateKey({
689
+ surface: 'tui', workspaceId: workspace?.id,
690
+ cwd: workspace?.id ? undefined : getWorkingDir(),
691
+ sessionId: activeSessionIdRef.current,
692
+ }));
693
+ await hydrateSessionWorktree({
694
+ sessionState: turnSessionState, workspaceId: workspace?.id,
695
+ sessionId: activeSessionIdRef.current, cwd: getWorkingDir(),
696
+ });
697
+ const effectiveCwd = turnSessionState.activeWorktree?.path ?? getWorkingDir();
684
698
  const runtimeContext = createAgentRuntimeContext({
685
699
  surface: 'tui',
686
700
  profile: 'main',
687
- cwd: getWorkingDir(),
701
+ cwd: effectiveCwd,
688
702
  workspaceId: workspace?.id,
689
703
  sessionId: activeSessionIdRef.current,
690
704
  owner: 'local-owner',
@@ -695,9 +709,9 @@ export function useChat(args: {
695
709
  toolGate,
696
710
  });
697
711
  const turnTools = filterToolsForRuntime(ALL_TOOLS, runtimeContext);
698
- const toolSignature = turnTools.map((tool) => tool.name).join(',');
712
+ const toolSignature = `${effectiveCwd}\0${turnTools.map((tool) => tool.name).join(',')}`;
699
713
  if (promptToolSignatureRef.current !== toolSignature) {
700
- const stablePrompt = await buildBaseSystemPrompt(getWorkingDir(), turnTools);
714
+ const stablePrompt = await buildBaseSystemPrompt(effectiveCwd, turnTools);
701
715
  const rest = historyRef.current[0]?.role === 'system'
702
716
  ? historyRef.current.slice(1)
703
717
  : [...historyRef.current];
@@ -707,32 +721,33 @@ export function useChat(args: {
707
721
  }
708
722
  // 每回合只刷新易变层;稳定层仅在实际能力目录变化时重建。
709
723
  const turnTransient = await buildTransientPromptBundle({
710
- cwd: getWorkingDir(),
724
+ cwd: effectiveCwd,
711
725
  tools: turnTools,
712
726
  runtime: {
713
727
  surface: 'tui',
714
- cwd: getWorkingDir(),
728
+ cwd: effectiveCwd,
715
729
  currentUserInput: trimmed,
716
730
  workspaceId: workspace?.id,
731
+ sessionId: activeSessionIdRef.current,
717
732
  interactiveAsk: true,
718
733
  supervisedBrowser: false,
719
734
  memoryAccess: 'read-write',
720
735
  },
721
736
  }).catch(() => undefined);
737
+ // 会话原文记账(与 session/runTurn.ts 共用同一份实现)。
738
+ // TUI 不经过 runTurn,此前整条路径都不入库,SearchConversations 对 TUI 用户静默失效。
739
+ turnJournalStart = historyRef.current.length;
740
+ turnJournal = await startTurnJournal({
741
+ memoryContext: memoryContextFromRuntime(runtimeContext),
742
+ userInput: trimmed,
743
+ }).catch(() => null);
722
744
  for await (const ev of runWithPlugins(trimmed, {
723
745
  provider: providerRef.current,
724
746
  history: historyRef.current,
725
747
  signal: ac.signal,
726
748
  runtimeContext,
727
749
  allowedTools: turnTools.map((tool) => tool.name),
728
- sessionState: getOrCreateSessionState(
729
- sessionStateKey({
730
- surface: 'tui',
731
- workspaceId: workspace?.id,
732
- cwd: workspace?.id ? undefined : getWorkingDir(),
733
- sessionId: activeSessionIdRef.current,
734
- }),
735
- ),
750
+ sessionState: turnSessionState,
736
751
  parseInlineImages: true,
737
752
  ...(turnTransient ? { transientBundle: turnTransient } : {}),
738
753
  promptCache: createPromptCacheContext(
@@ -800,6 +815,9 @@ export function useChat(args: {
800
815
  }
801
816
  activeComputerRunRef.current = null;
802
817
  runningToolsRef.current.clear();
818
+ // 记账失败不影响落盘与解锁(它是附加能力,不是功能)。
819
+ await turnJournal?.finish(historyRef.current, turnJournalStart).catch(() => undefined);
820
+ turnJournal = null;
803
821
  // v2 multi-session:持久化到当前 active session 文件。
804
822
  try {
805
823
  await saveContext(historyRef.current, sessionFileFor(getWorkingDir(), activeSessionIdRef.current));
@@ -19,7 +19,7 @@
19
19
 
20
20
  import type { AskAnswer, AskOption, AskQuestionSpec, AskSource } from '../askBridge.js';
21
21
  import type { PluginEvent } from '../plugins/pluginApi.js';
22
- import type { AgentResultMetadata, ConversationFilePurpose, FileActivityMetadata, LoopEvent, Message, MessageAttachmentRef, MessageBrowserReference, ProgressStage, SubagentProgress, ToolCall, ToolProgressDetail, ToolResultMetadata } from '../types.js';
22
+ import type { AgentResultMetadata, ConversationFilePurpose, DeliverableManifest, FileActivityMetadata, LoopEvent, Message, MessageAttachmentRef, MessageBrowserReference, ProgressStage, SubagentProgress, ToolCall, ToolProgressDetail, ToolResultMetadata } from '../types.js';
23
23
  import type { ConversationSearchResult, MemoryCategory, MemoryDiagnostic, MemoryEditablePatch, MemoryRecord, MemorySettings, MemoryUpdate, PersonaDocument } from '../memory/memoryService.js';
24
24
  import type { HumanInterventionReason } from '../humanIntervention.js';
25
25
 
@@ -1067,7 +1067,7 @@ export interface GroupMessageAttachment {
1067
1067
  size: number;
1068
1068
  }
1069
1069
 
1070
- export type ConversationFileKind = 'workspace-file' | 'group-attachment' | 'agent-file';
1070
+ export type ConversationFileKind = 'workspace-file' | 'user-attachment' | 'group-attachment' | 'agent-file';
1071
1071
  export type ConversationFileActivity = 'referenced' | 'read' | 'created' | 'modified' | 'generated' | 'deleted';
1072
1072
 
1073
1073
  /** Safe metadata an Agent Node may publish to a group. Absolute paths and file bytes are forbidden. */
@@ -1105,6 +1105,65 @@ export interface ConversationFileRef {
1105
1105
  sha256?: string;
1106
1106
  modifiedAt?: number;
1107
1107
  contentChanged?: boolean;
1108
+ originalAvailable?: boolean;
1109
+ archivedAvailable?: boolean;
1110
+ previewSource?: 'original' | 'archive' | 'upload';
1111
+ }
1112
+
1113
+ /** A durable, turn-scoped formal delivery. File refs are resolved at read time. */
1114
+ export interface ConversationDelivery {
1115
+ id: string;
1116
+ turnId: string;
1117
+ toolCallId?: string;
1118
+ createdAt: number;
1119
+ manifest: DeliverableManifest;
1120
+ resolvedFiles: ConversationFileRef[];
1121
+ }
1122
+
1123
+ /** Durable user-supplied and formally delivered artifacts for one user turn. */
1124
+ export interface ConversationArtifactGroup {
1125
+ turnId: string;
1126
+ createdAt: number;
1127
+ compacted: boolean;
1128
+ userAttachments: MessageAttachmentRef[];
1129
+ deliveries: ConversationDelivery[];
1130
+ }
1131
+
1132
+ export type ConversationAssetCategory = 'document' | 'code' | 'image' | 'link' | 'other';
1133
+ export type ConversationAssetOrigin = 'user-upload' | 'user-reference' | 'ai-delivery';
1134
+ export type ConversationAssetKind = 'workspace-file' | 'user-attachment' | 'web-link';
1135
+
1136
+ /** A durable, session-scoped item explicitly supplied by the user or formally delivered by the Agent. */
1137
+ export interface ConversationAsset {
1138
+ id: string;
1139
+ conversationKey: string;
1140
+ kind: ConversationAssetKind;
1141
+ category: ConversationAssetCategory;
1142
+ name: string;
1143
+ origins: ConversationAssetOrigin[];
1144
+ firstTurnId: string;
1145
+ lastTurnId: string;
1146
+ firstSeenAt: number;
1147
+ lastSeenAt: number;
1148
+ pinned: boolean;
1149
+ available: boolean;
1150
+ previewable: boolean;
1151
+ relativePath?: string;
1152
+ url?: string;
1153
+ domain?: string;
1154
+ mime?: string;
1155
+ size?: number;
1156
+ previewUrl?: string;
1157
+ verification?: import('../types.js').FileVerification;
1158
+ sha256?: string;
1159
+ modifiedAt?: number;
1160
+ contentChanged?: boolean;
1161
+ /** The original workspace file still exists at its submitted path. */
1162
+ originalAvailable?: boolean;
1163
+ /** An immutable, content-addressed copy of the submitted file is available. */
1164
+ archivedAvailable?: boolean;
1165
+ /** Which bytes are opened by the default preview action. */
1166
+ previewSource?: 'original' | 'archive' | 'upload';
1108
1167
  }
1109
1168
 
1110
1169
  export interface GroupChatMessage {
@@ -1244,8 +1303,13 @@ export type ClientFrame =
1244
1303
  | { t: 'plain/rename'; sessionId: string; title: string }
1245
1304
  | { t: 'plain/delete'; sessionId: string }
1246
1305
  | { t: 'resource/files/list'; dir: string; sessionId: string }
1306
+ | { t: 'resource/assets/list'; requestId: string; target: ConversationTarget & { sessionId: string } }
1307
+ | { t: 'resource/assets/pin'; requestId: string; target: ConversationTarget & { sessionId: string }; assetId: string; pinned: boolean }
1308
+ | { t: 'resource/assets/open'; requestId: string; target: ConversationTarget & { sessionId: string }; assetId: string; source?: 'default' | 'original' | 'archive' }
1309
+ | { t: 'resource/assets/reveal'; requestId: string; target: ConversationTarget & { sessionId: string }; assetId: string; source?: 'original' | 'archive' }
1310
+ | { t: 'resource/files/open'; requestId: string; target: ConversationTarget & { sessionId: string }; relativePath: string }
1247
1311
  | { t: 'resource/files/pin'; dir: string; sessionId: string; relativePath: string; pinned: boolean }
1248
- | { t: 'resource/files/reveal'; dir: string; relativePath: string }
1312
+ | { t: 'resource/files/reveal'; requestId?: string; dir?: string; target?: ConversationTarget & { sessionId: string }; relativePath: string }
1249
1313
  | { t: 'resource/memories/list'; workspaceId?: string; query?: string }
1250
1314
  | { t: 'resource/memories/settings/update'; autoRemember: boolean; workspaceId?: string }
1251
1315
  | { t: 'resource/memories/update'; id: string; scope: 'personal' | 'workspace'; expectedVersion: string; patch: MemoryUpdate; workspaceId?: string }
@@ -1519,10 +1583,21 @@ export type ServerFrame =
1519
1583
  conversationKey: string;
1520
1584
  historyRevision: string;
1521
1585
  messages: Message[];
1586
+ deliveries?: ConversationDelivery[];
1587
+ assets?: ConversationAsset[];
1588
+ artifactGroups?: ConversationArtifactGroup[];
1522
1589
  runSnapshot?: ConversationRunSnapshot;
1523
1590
  contextUsage?: Extract<LoopEvent, { type: 'context_usage' }>;
1524
1591
  }
1592
+ | { t: 'conversation/delivery/changed'; conversationKey: string; delivery: ConversationDelivery }
1593
+ | { t: 'conversation/assets/changed'; conversationKey: string; assets: ConversationAsset[] }
1525
1594
  | { t: 'resource/files/list'; dir: string; sessionId: string; files: ConversationFileRef[] }
1595
+ | { t: 'resource/assets/list'; requestId: string; conversationKey: string; assets?: ConversationAsset[]; error?: string }
1596
+ | { t: 'resource/assets/pinned'; requestId: string; ok: boolean; error?: string }
1597
+ | { t: 'resource/assets/opened'; requestId: string; ref?: ConversationFileRef; error?: string }
1598
+ | { t: 'resource/assets/revealed'; requestId: string; ok: boolean; error?: string }
1599
+ | { t: 'resource/files/opened'; requestId: string; ref?: ConversationFileRef; error?: string }
1600
+ | { t: 'resource/files/revealed'; requestId: string; ok: boolean; error?: string }
1526
1601
  | { t: 'resource/memories'; records: MemoryRecord[]; settings: MemorySettings; workspaceId?: string; reviewRecords?: MemoryRecord[]; persona?: PersonaDocument | null }
1527
1602
  | { t: 'resource/memories/conversations'; requestId: string; results: ConversationSearchResult[]; error?: string }
1528
1603
  | { t: 'resource/memories/manage/state'; requestId: string; state: MemoryManagementState }