mocode-ai 1.4.2 → 1.4.3
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 +13 -1
- package/dist/agent/core.js +14 -936
- package/dist/agent/index.js +37 -13
- package/dist/agent/model-turn.js +218 -0
- package/dist/agent/pipeline.js +18 -0
- package/dist/agent/run-contracts.js +1 -0
- package/dist/agent/run-coordinator.js +758 -0
- package/dist/agent/runtime-context.js +118 -24
- package/dist/agent/spawn.js +11 -7
- package/dist/agent/stages/context-trimmer.js +63 -0
- package/dist/agent/stages/contracts.js +12 -0
- package/dist/agent/stages/history-manager.js +178 -0
- package/dist/agent/stages/legacy-adapters.js +19 -0
- package/dist/agent/stages/model-runner.js +29 -0
- package/dist/agent/stages/run-policy.js +73 -0
- package/dist/agent/stages/tool-dispatcher.js +341 -0
- package/dist/agent/tool-helpers.js +12 -12
- package/dist/agent/tool-turn.js +87 -0
- package/dist/agent/trace-state.js +97 -101
- package/dist/agent/turn-lifecycle.js +110 -0
- package/dist/config/index.js +14 -0
- package/dist/host/stdio.js +101 -40
- package/dist/llm/index.js +51 -35
- package/dist/llm/providers/anthropic.js +16 -10
- package/dist/llm/runtime.js +1 -0
- package/dist/permissions/index.js +21 -5
- package/dist/repl/commands/compact.js +2 -2
- package/dist/repl/commands/session.js +3 -12
- package/dist/repl/message-format.js +5 -0
- package/dist/repl/runtime.js +95 -55
- package/dist/rollback/index.js +29 -624
- package/dist/rollback/store.js +593 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/runtime.js +307 -0
- package/dist/session/compact.js +22 -14
- package/dist/session/index.js +1 -0
- package/dist/session/persist.js +10 -146
- package/dist/session/scheduler.js +28 -16
- package/dist/session/state.js +16 -12
- package/dist/session/store.js +218 -0
- package/dist/session/trace.js +5 -15
- package/dist/tools/policy.js +19 -15
- package/dist/tools/registry.js +21 -229
- package/dist/tools/router.js +5 -3
- package/dist/tools/tool-runtime.js +267 -0
- package/dist/ui/layout-internal/content-write.js +4 -0
- package/package.json +7 -3
package/dist/llm/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { tools } from '../tools/registry.js';
|
|
|
4
4
|
import { getPlanDisabledTools, getProfileDisabledTools } from '../tools/constants.js';
|
|
5
5
|
import { ThinkTagFilter } from './think-filter.js';
|
|
6
6
|
import { sanitizeToolSchemas } from './tool-schema.js';
|
|
7
|
-
import { anthropicChatOnce } from './providers/anthropic.js';
|
|
7
|
+
import { defaultAnthropicFetch, anthropicChatOnce } from './providers/anthropic.js';
|
|
8
8
|
import { registerModelProvider, getModelProvider, listModelProviders } from './provider.js';
|
|
9
9
|
// 强制关闭第三方调试日志泄漏:openai SDK 在 process.env.DEBUG === 'true' 时用裸
|
|
10
10
|
// console.log 把请求/响应直写 stdout,会污染 TUI 输入框(并泄露 headers/URL)。
|
|
@@ -33,11 +33,31 @@ const RETRY_JITTER = 0.2;
|
|
|
33
33
|
* 与 OpenAI 兼容协议的独立 `reasoning_content` 字段不同,这些模型把 thinking 直接嵌进 content
|
|
34
34
|
* 字符串,期间不调 onText(spinner 持续转 ⠹ 思考中…),也不写入可见 content(history 不被思考段污染)。
|
|
35
35
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
function newOpenAIClient(runtimeConfig) {
|
|
37
|
+
return new OpenAI({
|
|
38
|
+
baseURL: runtimeConfig.baseURL,
|
|
39
|
+
apiKey: runtimeConfig.apiKey,
|
|
40
|
+
maxRetries: 0,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** Build mutable client state owned by one runtime; no module-global overrides are consulted. */
|
|
44
|
+
export function createChatClientState(runtimeConfig, overrides = {}) {
|
|
45
|
+
return {
|
|
46
|
+
openAI: newOpenAIClient(runtimeConfig),
|
|
47
|
+
openAICreateImpl: overrides.openAICreateImpl ?? null,
|
|
48
|
+
anthropicFetchImpl: overrides.anthropicFetchImpl ?? fetch,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Rebuild only the OpenAI SDK client while preserving runtime-local test/transport overrides. */
|
|
52
|
+
export function reconfigureChatClient(state, runtimeConfig) {
|
|
53
|
+
state.openAI = newOpenAIClient(runtimeConfig);
|
|
54
|
+
}
|
|
55
|
+
const defaultClientState = createChatClientState(config, { anthropicFetchImpl: defaultAnthropicFetch });
|
|
56
|
+
const defaultProviderRuntime = {
|
|
57
|
+
config,
|
|
58
|
+
getModel: getActiveModel,
|
|
59
|
+
clientState: defaultClientState,
|
|
60
|
+
};
|
|
41
61
|
/**
|
|
42
62
|
* 运行时重建 OpenAI 客户端(/model 切换 baseURL/apiKey 后调)。
|
|
43
63
|
* config.model 已在 chat() 每次读取(热切),但 client 的 baseURL/apiKey 是构造时固化的实例字段,
|
|
@@ -45,11 +65,7 @@ let client = new OpenAI({
|
|
|
45
65
|
* 子 agent 复用本模块 chat(),故只此一处重建即全链路生效。
|
|
46
66
|
*/
|
|
47
67
|
export function reconfigureClient() {
|
|
48
|
-
|
|
49
|
-
baseURL: config.baseURL,
|
|
50
|
-
apiKey: config.apiKey,
|
|
51
|
-
maxRetries: 0,
|
|
52
|
-
});
|
|
68
|
+
reconfigureChatClient(defaultClientState, config);
|
|
53
69
|
}
|
|
54
70
|
// ── 重试 helper(纯函数 + sleep,被 chat() 调用;亦可单测导入验证)──────
|
|
55
71
|
/** 可被 AbortSignal 取消的 sleep;signal 已 abort 时立即抛 AbortError。 */
|
|
@@ -209,10 +225,9 @@ function logRetry(attempt, err, waitMs) {
|
|
|
209
225
|
// 不写前导 \n —— contentWrite 由续写位管位置,前置换行会留空行;结尾 \n 由劫持逻辑补。
|
|
210
226
|
console.error(`[llm] 第 ${attempt}/${RETRY_MAX_ATTEMPTS} 次失败(${tag}: ${msg}),${(waitMs / 1000).toFixed(1)}s 后重试…`);
|
|
211
227
|
}
|
|
212
|
-
let createImplOverride = null;
|
|
213
228
|
/** 仅供单测用:覆盖 chat() 内部实际调用的 create 桩。生产代码不要碰。 */
|
|
214
229
|
export function __setChatCreateImpl(impl) {
|
|
215
|
-
|
|
230
|
+
defaultClientState.openAICreateImpl = impl;
|
|
216
231
|
}
|
|
217
232
|
/**
|
|
218
233
|
* 工具 schema 保持稳定数组引用,再在 MCP 发现或扩展变动后原地刷新;
|
|
@@ -249,11 +264,11 @@ export function refreshChatTools() {
|
|
|
249
264
|
// 新增 provider 只需 registerModelProvider,不必改 chat()。
|
|
250
265
|
registerModelProvider({
|
|
251
266
|
name: 'openai',
|
|
252
|
-
chatOnce: (messages, handlers, signal, tools) => chatOnce(messages, handlers, signal, tools),
|
|
267
|
+
chatOnce: (messages, handlers, signal, tools, runtime) => chatOnce(messages, handlers, signal, tools, runtime),
|
|
253
268
|
});
|
|
254
269
|
registerModelProvider({
|
|
255
270
|
name: 'anthropic',
|
|
256
|
-
chatOnce: (messages, handlers, signal, tools) => anthropicChatOnce(messages, handlers, signal, tools ?? chatTools),
|
|
271
|
+
chatOnce: (messages, handlers, signal, tools, runtime) => anthropicChatOnce(messages, handlers, signal, tools ?? chatTools, runtime),
|
|
257
272
|
});
|
|
258
273
|
/**
|
|
259
274
|
* 多 provider 兼容的 cache / reasoning 字段提取。
|
|
@@ -325,29 +340,28 @@ function retryErrorCode(error) {
|
|
|
325
340
|
return `HTTP_${value.status}`;
|
|
326
341
|
return value.code ?? value.name ?? 'RETRYABLE_ERROR';
|
|
327
342
|
}
|
|
328
|
-
/**
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
* 包了一层重试:429/5xx/timeout/网络错按指数退避重试(默认 4 次),400/401/用户中断立即抛。
|
|
334
|
-
* 重试由 chat() 统一管,chatOnce() 只负责单次请求,职责单一便于单测。
|
|
335
|
-
*/
|
|
336
|
-
export async function chat(messages, handlers = {}, signal,
|
|
343
|
+
/** Bind chat dispatch, retries and built-in providers to one explicit runtime. */
|
|
344
|
+
export function createChatTransport(runtime) {
|
|
345
|
+
return (messages, handlers = {}, signal, toolsOverride) => chatWithRuntime(runtime, messages, handlers, signal, toolsOverride);
|
|
346
|
+
}
|
|
347
|
+
export function chat(messages, handlers = {}, signal,
|
|
337
348
|
/** 覆盖默认工具 schema;plan 模式传 planChatTools(只读子集),缺省=全量 chatTools。 */
|
|
338
349
|
toolsOverride) {
|
|
350
|
+
return chatWithRuntime(defaultProviderRuntime, messages, handlers, signal, toolsOverride);
|
|
351
|
+
}
|
|
352
|
+
async function chatWithRuntime(runtime, messages, handlers, signal, toolsOverride) {
|
|
339
353
|
let lastErr;
|
|
340
354
|
for (let attempt = 1; attempt <= RETRY_MAX_ATTEMPTS; attempt++) {
|
|
341
355
|
if (signal?.aborted) {
|
|
342
356
|
throw new DOMException('This operation was aborted', 'AbortError');
|
|
343
357
|
}
|
|
344
358
|
try {
|
|
345
|
-
const provider = getModelProvider(config.provider);
|
|
359
|
+
const provider = getModelProvider(runtime.config.provider);
|
|
346
360
|
if (!provider) {
|
|
347
361
|
// 未注册的 provider:给出可用名单,避免悄悄落到错误实现。
|
|
348
|
-
throw new Error(`未知的 LLM provider "${config.provider}";已注册:${listModelProviders().join(', ') || '(空)'}`);
|
|
362
|
+
throw new Error(`未知的 LLM provider "${runtime.config.provider}";已注册:${listModelProviders().join(', ') || '(空)'}`);
|
|
349
363
|
}
|
|
350
|
-
return await provider.chatOnce(messages, handlers, signal, toolsOverride);
|
|
364
|
+
return await provider.chatOnce(messages, handlers, signal, toolsOverride, runtime);
|
|
351
365
|
}
|
|
352
366
|
catch (err) {
|
|
353
367
|
lastErr = err;
|
|
@@ -411,7 +425,7 @@ export function normalizeImageDetail(messages) {
|
|
|
411
425
|
return changed ? next : messages;
|
|
412
426
|
}
|
|
413
427
|
/** 单次流式 LLM 请求(无重试);chat() 的内部实现,可被 __setChatCreateImpl 注入桩以做单测。 */
|
|
414
|
-
async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
428
|
+
async function chatOnce(messages, handlers, signal, toolsOverride, runtime = defaultProviderRuntime) {
|
|
415
429
|
// 防御:messages 必须至少含一条非空 user 消息,否则 OpenAI/Anthropic 都会 400。
|
|
416
430
|
// compact force 分支曾把所有 user 丢进摘要 → 重建 history 无 user → 下一轮 400。
|
|
417
431
|
// 在 transport 边界拦住所有类似回归(compact / 外部注入 / resume 损坏)。
|
|
@@ -429,15 +443,17 @@ async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
|
429
443
|
throw new Error('messages must contain at least one non-empty user message');
|
|
430
444
|
}
|
|
431
445
|
// signal 透传给 SDK 第二参(RequestOptions);abort 后 for await 抛错,chat 不 catch,透传 runAgent 处理。
|
|
432
|
-
//
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
446
|
+
// openAICreateImpl 的 body 类型故意宽成 Record(测试桩用),生产走 client 分支时由 OpenAI 自己的类型守门。
|
|
447
|
+
const runtimeConfig = runtime.config;
|
|
448
|
+
const runtimeClientState = runtime.clientState;
|
|
449
|
+
const create = runtimeClientState.openAICreateImpl
|
|
450
|
+
? runtimeClientState.openAICreateImpl
|
|
451
|
+
: (body, opts) => runtimeClientState.openAI.chat.completions.create(body, opts);
|
|
436
452
|
// transport 边界消毒:剔除部分后端(kimi-k3@dashscope 实测)整请求 400 拒绝的
|
|
437
453
|
// uniqueItems 关键字。无需改写时返回原引用,前缀缓存逐字节稳定不受影响。
|
|
438
454
|
const activeTools = sanitizeToolSchemas(toolsOverride ?? chatTools);
|
|
439
455
|
const stream = await create({
|
|
440
|
-
model:
|
|
456
|
+
model: runtime.getModel(),
|
|
441
457
|
messages: normalizeImageDetail(messages),
|
|
442
458
|
stream: true,
|
|
443
459
|
// 空工具表(如摘要请求)不发 tools 字段——部分网关拒绝空数组。
|
|
@@ -449,8 +465,8 @@ async function chatOnce(messages, handlers, signal, toolsOverride) {
|
|
|
449
465
|
parallel_tool_calls: true,
|
|
450
466
|
}
|
|
451
467
|
: {}),
|
|
452
|
-
...(
|
|
453
|
-
...(
|
|
468
|
+
...(runtimeConfig.maxTokens ? { max_tokens: runtimeConfig.maxTokens } : {}),
|
|
469
|
+
...(runtimeConfig.includeUsage ? { stream_options: { include_usage: true } } : {}),
|
|
454
470
|
}, signal ? { signal } : undefined);
|
|
455
471
|
// content 内嵌 think 标签由独立增量状态机过滤。它只暂存“可能组成标签”的后缀,
|
|
456
472
|
// 因而既能覆盖标签任意位置跨 chunk,也不会让普通正文固定延迟数个字符。
|
|
@@ -4,6 +4,10 @@ let fetchImplOverride = null;
|
|
|
4
4
|
export function __setAnthropicFetchImpl(impl) {
|
|
5
5
|
fetchImplOverride = impl;
|
|
6
6
|
}
|
|
7
|
+
/** Default runtime keeps the historical process-level test override behavior. */
|
|
8
|
+
export function defaultAnthropicFetch(input, init) {
|
|
9
|
+
return (fetchImplOverride ?? fetch)(input, init);
|
|
10
|
+
}
|
|
7
11
|
function parseJsonObject(value) {
|
|
8
12
|
if (!value.trim())
|
|
9
13
|
return {};
|
|
@@ -142,12 +146,13 @@ function endpoint(baseURL) {
|
|
|
142
146
|
return base;
|
|
143
147
|
return `${base}/v1/messages`;
|
|
144
148
|
}
|
|
145
|
-
export function buildAnthropicRequest(messages, tools) {
|
|
146
|
-
const
|
|
147
|
-
const
|
|
149
|
+
export function buildAnthropicRequest(messages, tools, runtime) {
|
|
150
|
+
const runtimeConfig = runtime?.config ?? config;
|
|
151
|
+
const encoded = encodeAnthropicMessages(messages, runtimeConfig.anthropicPromptCache);
|
|
152
|
+
const anthropicTools = encodeAnthropicTools(tools, runtimeConfig.anthropicPromptCache);
|
|
148
153
|
return {
|
|
149
|
-
model: getActiveModel(),
|
|
150
|
-
max_tokens:
|
|
154
|
+
model: runtime?.getModel() ?? getActiveModel(),
|
|
155
|
+
max_tokens: runtimeConfig.maxTokens ?? 8192,
|
|
151
156
|
stream: true,
|
|
152
157
|
system: encoded.system,
|
|
153
158
|
messages: encoded.messages,
|
|
@@ -249,17 +254,18 @@ function addLiveCount(text, state) {
|
|
|
249
254
|
}
|
|
250
255
|
}
|
|
251
256
|
/** Anthropic Messages API 单次请求;外层 chat() 继续统一负责重试。 */
|
|
252
|
-
export async function anthropicChatOnce(messages, handlers, signal, tools) {
|
|
253
|
-
const
|
|
254
|
-
const
|
|
257
|
+
export async function anthropicChatOnce(messages, handlers, signal, tools, runtime) {
|
|
258
|
+
const runtimeConfig = runtime?.config ?? config;
|
|
259
|
+
const fetchImpl = runtime?.clientState.anthropicFetchImpl ?? defaultAnthropicFetch;
|
|
260
|
+
const response = await fetchImpl(endpoint(runtimeConfig.baseURL), {
|
|
255
261
|
method: 'POST',
|
|
256
262
|
headers: {
|
|
257
263
|
'content-type': 'application/json',
|
|
258
264
|
accept: 'text/event-stream',
|
|
259
|
-
'x-api-key':
|
|
265
|
+
'x-api-key': runtimeConfig.apiKey,
|
|
260
266
|
'anthropic-version': process.env.ANTHROPIC_VERSION || '2023-06-01',
|
|
261
267
|
},
|
|
262
|
-
body: JSON.stringify(buildAnthropicRequest(messages, tools)),
|
|
268
|
+
body: JSON.stringify(buildAnthropicRequest(messages, tools, runtime)),
|
|
263
269
|
signal,
|
|
264
270
|
});
|
|
265
271
|
if (!response.ok)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -147,8 +147,8 @@ function matches(grant, tool, fingerprint, projectRoot) {
|
|
|
147
147
|
grant.fingerprint === fingerprint &&
|
|
148
148
|
(grant.scope !== 'project' || grant.projectRoot === projectRoot));
|
|
149
149
|
}
|
|
150
|
-
|
|
151
|
-
if (!
|
|
150
|
+
async function checkPermissionWithSessionGrants(runtimeConfig, runtimeSessionGrants, tool, args, signal, options = {}) {
|
|
151
|
+
if (!runtimeConfig.permissionEnabled || getToolRisk(tool) === 'safe')
|
|
152
152
|
return 'allow';
|
|
153
153
|
if (signal?.aborted)
|
|
154
154
|
return 'deny';
|
|
@@ -165,13 +165,13 @@ export async function checkPermission(tool, args, signal, options = {}) {
|
|
|
165
165
|
const fingerprint = permissionFingerprint(tool, args);
|
|
166
166
|
const projectRoot = canonicalProjectRoot(options.projectRoot ?? getSandboxRoot() ?? process.cwd());
|
|
167
167
|
if (!forceOnce) {
|
|
168
|
-
if (
|
|
168
|
+
if (runtimeSessionGrants.some((grant) => matches(grant, tool.name, fingerprint, projectRoot)))
|
|
169
169
|
return 'allow';
|
|
170
170
|
if (permanentGrants.some((grant) => matches(grant, tool.name, fingerprint, projectRoot)))
|
|
171
171
|
return 'allow';
|
|
172
172
|
}
|
|
173
173
|
// CI/pipes must fail closed. Operators can deliberately restore unattended behavior.
|
|
174
|
-
if (!process.stdin.isTTY && !
|
|
174
|
+
if (!process.stdin.isTTY && !runtimeConfig.permissionNonInteractiveAllow && !options.prompt)
|
|
175
175
|
return 'deny';
|
|
176
176
|
if (signal?.aborted)
|
|
177
177
|
return 'deny';
|
|
@@ -214,7 +214,7 @@ export async function checkPermission(tool, args, signal, options = {}) {
|
|
|
214
214
|
if (signal?.aborted || result.action === 'cancelled' || result.value === denyOption)
|
|
215
215
|
return 'deny';
|
|
216
216
|
if (result.value === sessionOption) {
|
|
217
|
-
|
|
217
|
+
runtimeSessionGrants.push({ tool: tool.name, fingerprint, scope: 'session' });
|
|
218
218
|
}
|
|
219
219
|
else if (result.value === projectOption) {
|
|
220
220
|
const grant = { tool: tool.name, fingerprint, scope: 'project', projectRoot };
|
|
@@ -230,6 +230,22 @@ export async function checkPermission(tool, args, signal, options = {}) {
|
|
|
230
230
|
}
|
|
231
231
|
return 'allow';
|
|
232
232
|
}
|
|
233
|
+
/** 兼容入口:显式 config 仍共享进程级 session grants。 */
|
|
234
|
+
export function checkPermissionWithConfig(runtimeConfig, tool, args, signal, options = {}) {
|
|
235
|
+
return checkPermissionWithSessionGrants(runtimeConfig, sessionGrants, tool, args, signal, options);
|
|
236
|
+
}
|
|
237
|
+
/** 为独立 RuntimeContext 创建私有 session grants;项目/永久授权仍按原语义进程共享。 */
|
|
238
|
+
export function createPermissionChecker(runtimeConfig, defaultProjectRoot) {
|
|
239
|
+
const runtimeSessionGrants = [];
|
|
240
|
+
return (tool, args, signal, options = {}) => checkPermissionWithSessionGrants(runtimeConfig, runtimeSessionGrants, tool, args, signal, {
|
|
241
|
+
...options,
|
|
242
|
+
projectRoot: options.projectRoot ?? defaultProjectRoot,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/** 默认兼容入口:继续读取进程级 config。独立 RuntimeContext 使用 checkPermissionWithConfig。 */
|
|
246
|
+
export function checkPermission(tool, args, signal, options = {}) {
|
|
247
|
+
return checkPermissionWithConfig(config, tool, args, signal, options);
|
|
248
|
+
}
|
|
233
249
|
export function revokePermanentAllow(toolName, fingerprint) {
|
|
234
250
|
loadPermanent();
|
|
235
251
|
permanentGrants = permanentGrants.filter((grant) => grant.tool !== toolName || (fingerprint !== undefined && grant.fingerprint !== fingerprint));
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import * as layout from '../../ui/layout.js';
|
|
13
13
|
import { ui } from '../../ui/theme.js';
|
|
14
14
|
import { t } from '../../i18n/index.js';
|
|
15
|
-
import {
|
|
15
|
+
import { appendCurrentSessionRuntimeEvent, hashTraceValue } from '../../session/index.js';
|
|
16
16
|
import { startRunningListener, stopRunningListener } from '../running-input.js';
|
|
17
17
|
import { unhandled, next } from './types.js';
|
|
18
18
|
export const compactCommands = [
|
|
@@ -38,7 +38,7 @@ export const compactCommands = [
|
|
|
38
38
|
const log = await (async () => {
|
|
39
39
|
const signal = startRunningListener(t('running.compacting'));
|
|
40
40
|
try {
|
|
41
|
-
return await
|
|
41
|
+
return await ctx.runtime.compact(ctx.history, { focus, force, signal, contextState: ctx.contextState });
|
|
42
42
|
}
|
|
43
43
|
finally {
|
|
44
44
|
stopRunningListener();
|
|
@@ -8,9 +8,6 @@ import * as layout from '../../ui/layout.js';
|
|
|
8
8
|
import { ui } from '../../ui/theme.js';
|
|
9
9
|
import { bannerLines } from '../../ui/render.js';
|
|
10
10
|
import { t } from '../../i18n/index.js';
|
|
11
|
-
import { newSessionId, listSessions } from '../../session/index.js';
|
|
12
|
-
import { resetState } from '../../rollback/index.js';
|
|
13
|
-
import { setCurrentSessionId } from '../../session/state.js';
|
|
14
11
|
import { promptSessionPicker } from '../../ui/prompt.js';
|
|
15
12
|
import { unhandled, next } from './types.js';
|
|
16
13
|
export const sessionCommands = [
|
|
@@ -18,13 +15,7 @@ export const sessionCommands = [
|
|
|
18
15
|
if (ctx.line !== '/clear')
|
|
19
16
|
return unhandled();
|
|
20
17
|
ctx.history.length = 1; // 保留 system 提示
|
|
21
|
-
|
|
22
|
-
// /clear 立刻换新会话 id:之前延后到 turn 收尾分配,但 runAgent 期间模型
|
|
23
|
-
// 调 plan_update / note_append 会命中「no active session」——它们写 notes.md 靠
|
|
24
|
-
// getNotesFilePath(getCurrentSessionId()),而下一个 turn 还没走完。改为与启动对齐
|
|
25
|
-
// 立即分配:notes.md 由写入时按需建文件,这里预分配 id 不触盘。
|
|
26
|
-
ctx.state.currentSessionId = newSessionId();
|
|
27
|
-
setCurrentSessionId(ctx.state.currentSessionId, process.cwd()); // 同步到 session/state
|
|
18
|
+
ctx.state.currentSessionId = ctx.runtime.session.clear();
|
|
28
19
|
ctx.state.turnCount = 0; // 反思 cadence 重新计数
|
|
29
20
|
ctx.contextState.lastUsage = undefined;
|
|
30
21
|
ctx.contextState.lifecycleStats = undefined;
|
|
@@ -44,7 +35,7 @@ export const sessionCommands = [
|
|
|
44
35
|
async (ctx) => {
|
|
45
36
|
if (ctx.line !== '/sessions')
|
|
46
37
|
return unhandled();
|
|
47
|
-
const sessions =
|
|
38
|
+
const sessions = ctx.runtime.session.list(); // 不传 limit = 全量
|
|
48
39
|
if (sessions.length === 0) {
|
|
49
40
|
layout.contentWrite(`${ui.dim}(没有已保存的会话)${ui.reset}\n`);
|
|
50
41
|
return next();
|
|
@@ -71,7 +62,7 @@ export const sessionCommands = [
|
|
|
71
62
|
async (ctx) => {
|
|
72
63
|
if (ctx.line !== '/resume')
|
|
73
64
|
return unhandled();
|
|
74
|
-
const sessions =
|
|
65
|
+
const sessions = ctx.runtime.session.list(10);
|
|
75
66
|
if (sessions.length === 0) {
|
|
76
67
|
layout.contentWrite(`${ui.dim}(没有已保存的会话)${ui.reset}\n`);
|
|
77
68
|
return next();
|
|
@@ -57,10 +57,15 @@ export function renderHistory(history) {
|
|
|
57
57
|
let pendingBatches = [];
|
|
58
58
|
let normalBatch = null;
|
|
59
59
|
const flushBatch = () => {
|
|
60
|
+
if (pendingBatches.length === 0)
|
|
61
|
+
return false;
|
|
62
|
+
// 气泡/正文 → 摘要行边界:尾部视觉空行幂等收成 1 条(与实时 writeToolHeader 同语义)。
|
|
63
|
+
layout.normalizeMutationBoundary();
|
|
60
64
|
for (const entries of pendingBatches)
|
|
61
65
|
batch.writeSummaryOnly(entries, layout);
|
|
62
66
|
pendingBatches = [];
|
|
63
67
|
normalBatch = null;
|
|
68
|
+
return true;
|
|
64
69
|
};
|
|
65
70
|
for (let idx = 0; idx < history.length; idx++) {
|
|
66
71
|
const m = history[idx];
|