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.
Files changed (47) hide show
  1. package/README.md +13 -1
  2. package/dist/agent/core.js +14 -936
  3. package/dist/agent/index.js +37 -13
  4. package/dist/agent/model-turn.js +218 -0
  5. package/dist/agent/pipeline.js +18 -0
  6. package/dist/agent/run-contracts.js +1 -0
  7. package/dist/agent/run-coordinator.js +758 -0
  8. package/dist/agent/runtime-context.js +118 -24
  9. package/dist/agent/spawn.js +11 -7
  10. package/dist/agent/stages/context-trimmer.js +63 -0
  11. package/dist/agent/stages/contracts.js +12 -0
  12. package/dist/agent/stages/history-manager.js +178 -0
  13. package/dist/agent/stages/legacy-adapters.js +19 -0
  14. package/dist/agent/stages/model-runner.js +29 -0
  15. package/dist/agent/stages/run-policy.js +73 -0
  16. package/dist/agent/stages/tool-dispatcher.js +341 -0
  17. package/dist/agent/tool-helpers.js +12 -12
  18. package/dist/agent/tool-turn.js +87 -0
  19. package/dist/agent/trace-state.js +97 -101
  20. package/dist/agent/turn-lifecycle.js +110 -0
  21. package/dist/config/index.js +14 -0
  22. package/dist/host/stdio.js +101 -40
  23. package/dist/llm/index.js +51 -35
  24. package/dist/llm/providers/anthropic.js +16 -10
  25. package/dist/llm/runtime.js +1 -0
  26. package/dist/permissions/index.js +21 -5
  27. package/dist/repl/commands/compact.js +2 -2
  28. package/dist/repl/commands/session.js +3 -12
  29. package/dist/repl/message-format.js +5 -0
  30. package/dist/repl/runtime.js +95 -55
  31. package/dist/rollback/index.js +29 -624
  32. package/dist/rollback/store.js +593 -0
  33. package/dist/runtime/index.js +1 -0
  34. package/dist/runtime/runtime.js +307 -0
  35. package/dist/session/compact.js +22 -14
  36. package/dist/session/index.js +1 -0
  37. package/dist/session/persist.js +10 -146
  38. package/dist/session/scheduler.js +28 -16
  39. package/dist/session/state.js +16 -12
  40. package/dist/session/store.js +218 -0
  41. package/dist/session/trace.js +5 -15
  42. package/dist/tools/policy.js +19 -15
  43. package/dist/tools/registry.js +21 -229
  44. package/dist/tools/router.js +5 -3
  45. package/dist/tools/tool-runtime.js +267 -0
  46. package/dist/ui/layout-internal/content-write.js +4 -0
  47. package/package.json +7 -3
@@ -0,0 +1,307 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { runAgentCore } from '../agent/core.js';
3
+ import { createAgentRuntimeContext, defaultAgentRuntimeContext, } from '../agent/runtime-context.js';
4
+ import { contextState as defaultContextState, createContextState, manualCompact, } from '../session/index.js';
5
+ const activeRuntimes = new AsyncLocalStorage();
6
+ /** Runtime facade active in the current agent async tree, used by nested agents to borrow the parent lifecycle. */
7
+ export function getActiveRuntime() {
8
+ return activeRuntimes.getStore();
9
+ }
10
+ function firstLineOf(input) {
11
+ const text = typeof input === 'string' ? input : (input.find((part) => part.type === 'text')?.text ?? '');
12
+ return Array.from(text.split('\n')[0] ?? '')
13
+ .slice(0, 40)
14
+ .join('');
15
+ }
16
+ function toChatTools(context) {
17
+ return context.toolRuntime.tools.map((tool) => ({
18
+ type: 'function',
19
+ function: {
20
+ name: tool.name,
21
+ description: tool.description,
22
+ parameters: tool.parameters,
23
+ },
24
+ }));
25
+ }
26
+ /** High-level owner for one AgentRuntimeContext and all host-facing lifecycle operations. */
27
+ export class Runtime {
28
+ context;
29
+ contextState;
30
+ session;
31
+ rollback;
32
+ listeners = new Map();
33
+ activeControllers = new Set();
34
+ activeWork = new Set();
35
+ mainRunActive = false;
36
+ controlOperation = null;
37
+ onStart;
38
+ onClose;
39
+ startPromise = null;
40
+ closePromise = null;
41
+ closed = false;
42
+ constructor(init = {}) {
43
+ const { context, contextState, onStart, onClose, ...contextInit } = init;
44
+ this.context = context ?? createAgentRuntimeContext(contextInit);
45
+ this.contextState = contextState ?? createContextState();
46
+ this.onStart = onStart;
47
+ this.onClose = onClose;
48
+ this.session = {
49
+ create: () => this.createSession(false),
50
+ resume: (id) => this.resumeSession(id),
51
+ save: (history, id, queryHistory, lastToolGroups) => this.saveSession(history, id, queryHistory, lastToolGroups),
52
+ list: (limit) => {
53
+ this.assertOpen();
54
+ return this.context.sessionStore.list(limit);
55
+ },
56
+ clear: () => this.createSession(true),
57
+ };
58
+ this.rollback = {
59
+ list: () => {
60
+ this.assertOpen();
61
+ return this.context.rollbackStore.listTurns();
62
+ },
63
+ plan: (n, history) => {
64
+ this.assertIdle('plan rollback');
65
+ const plan = this.context.rollbackStore.planRollback(n, history);
66
+ this.emit('rollback.planned', { cutoffTurnId: plan.cutoffTurnId, retainedTurns: plan.n });
67
+ return plan;
68
+ },
69
+ apply: (plan, history, revertPaths = new Set()) => {
70
+ this.assertIdle('apply rollback');
71
+ const result = this.context.rollbackStore.applyRollback(plan, history, revertPaths);
72
+ this.emit('rollback.applied', {
73
+ cutoffTurnId: plan.cutoffTurnId,
74
+ retainedTurns: plan.n,
75
+ ...result,
76
+ });
77
+ return result;
78
+ },
79
+ };
80
+ }
81
+ start() {
82
+ if (this.closed)
83
+ return Promise.reject(new Error('Runtime is closed.'));
84
+ if (this.startPromise)
85
+ return this.startPromise;
86
+ this.startPromise = Promise.resolve(this.onStart?.(this)).then(() => {
87
+ this.emit('lifecycle.started', { sandboxRoot: this.context.sandboxRoot });
88
+ });
89
+ return this.startPromise;
90
+ }
91
+ async run(options) {
92
+ await this.start();
93
+ this.assertOpen();
94
+ if (this.controlOperation) {
95
+ throw new Error(`Runtime cannot start a run while ${this.controlOperation} is active.`);
96
+ }
97
+ const { turn = 'new', turnLabel, signal: callerSignal, traceContext, ...coreOptions } = options;
98
+ let turnId = traceContext?.turnId;
99
+ if (turn === 'new') {
100
+ if (this.mainRunActive) {
101
+ throw new Error("A main Runtime turn is already active. Use turn: 'inherit' for nested agents.");
102
+ }
103
+ this.mainRunActive = true;
104
+ try {
105
+ turnId = this.context.beginTurn(turnLabel ?? firstLineOf(options.userInput));
106
+ }
107
+ catch (cause) {
108
+ this.mainRunActive = false;
109
+ throw cause;
110
+ }
111
+ }
112
+ else if (turn === 'inherit') {
113
+ turnId = this.context.getCurrentTurnId();
114
+ }
115
+ const controller = new AbortController();
116
+ const relayAbort = () => controller.abort(callerSignal?.reason);
117
+ if (callerSignal?.aborted)
118
+ relayAbort();
119
+ else
120
+ callerSignal?.addEventListener('abort', relayAbort, { once: true });
121
+ this.activeControllers.add(controller);
122
+ const finishWork = this.beginTrackedWork();
123
+ this.emit('run.started', { turn, turnId });
124
+ try {
125
+ const result = await activeRuntimes.run(this, () => runAgentCore({
126
+ ...coreOptions,
127
+ contextState: coreOptions.contextState ?? this.contextState,
128
+ signal: controller.signal,
129
+ traceContext: turnId === undefined ? traceContext : { ...traceContext, turnId },
130
+ runtimeContext: this.context,
131
+ }));
132
+ this.emit('run.completed', {
133
+ turn,
134
+ turnId,
135
+ terminationReason: result.terminationReason,
136
+ completed: result.completed,
137
+ });
138
+ return result;
139
+ }
140
+ catch (cause) {
141
+ this.emit('run.failed', {
142
+ turn,
143
+ turnId,
144
+ aborted: controller.signal.aborted,
145
+ message: cause instanceof Error ? cause.message : String(cause),
146
+ });
147
+ throw cause;
148
+ }
149
+ finally {
150
+ callerSignal?.removeEventListener('abort', relayAbort);
151
+ this.activeControllers.delete(controller);
152
+ if (turn === 'new')
153
+ this.mainRunActive = false;
154
+ finishWork();
155
+ }
156
+ }
157
+ cancel(reason) {
158
+ let count = 0;
159
+ for (const controller of this.activeControllers) {
160
+ if (controller.signal.aborted)
161
+ continue;
162
+ controller.abort(reason);
163
+ count += 1;
164
+ }
165
+ this.emit('run.cancel_requested', { count });
166
+ return count;
167
+ }
168
+ async compact(history, options = {}) {
169
+ await this.start();
170
+ this.assertIdle('compact');
171
+ this.controlOperation = 'compact';
172
+ const controller = new AbortController();
173
+ const relayAbort = () => controller.abort(options.signal?.reason);
174
+ if (options.signal?.aborted)
175
+ relayAbort();
176
+ else
177
+ options.signal?.addEventListener('abort', relayAbort, { once: true });
178
+ this.activeControllers.add(controller);
179
+ const finishWork = this.beginTrackedWork();
180
+ this.emit('compact.started', { focus: options.focus, force: options.force === true });
181
+ try {
182
+ const result = await this.context.runInScope(() => manualCompact(history, options.focus, {
183
+ force: options.force,
184
+ signal: controller.signal,
185
+ contextState: options.contextState ?? this.contextState,
186
+ activeTools: options.activeTools ?? toChatTools(this.context),
187
+ runtime: this.context,
188
+ }));
189
+ this.emit('compact.completed', {
190
+ compactHistoryCalled: result.compactHistoryCalled,
191
+ historyMutation: result.historyMutation,
192
+ reason: result.compactDetail?.reason,
193
+ });
194
+ return result;
195
+ }
196
+ catch (cause) {
197
+ this.emit('compact.failed', { message: cause instanceof Error ? cause.message : String(cause) });
198
+ throw cause;
199
+ }
200
+ finally {
201
+ options.signal?.removeEventListener('abort', relayAbort);
202
+ this.activeControllers.delete(controller);
203
+ this.controlOperation = null;
204
+ finishWork();
205
+ }
206
+ }
207
+ on(type, listener) {
208
+ const listeners = this.listeners.get(type) ?? new Set();
209
+ listeners.add(listener);
210
+ this.listeners.set(type, listeners);
211
+ return () => listeners.delete(listener);
212
+ }
213
+ close() {
214
+ if (this.closePromise)
215
+ return this.closePromise;
216
+ this.closed = true;
217
+ this.emit('lifecycle.closing', {});
218
+ this.closePromise = (async () => {
219
+ if (this.startPromise)
220
+ await this.startPromise.catch(() => undefined);
221
+ this.cancel(new DOMException('Runtime closed.', 'AbortError'));
222
+ await Promise.allSettled([...this.activeWork]);
223
+ try {
224
+ await this.onClose?.(this);
225
+ }
226
+ finally {
227
+ this.emit('lifecycle.closed', {});
228
+ this.listeners.clear();
229
+ }
230
+ })();
231
+ return this.closePromise;
232
+ }
233
+ createSession(cleared) {
234
+ this.assertIdle(cleared ? 'clear session' : 'create session');
235
+ const id = this.context.sessionStore.createId();
236
+ this.context.sessionStore.setCurrentSessionId(id);
237
+ this.context.rollbackStore.resetState();
238
+ this.emit(cleared ? 'session.cleared' : 'session.created', { sessionId: id });
239
+ return id;
240
+ }
241
+ resumeSession(id) {
242
+ this.assertIdle('resume session');
243
+ const loaded = this.context.sessionStore.load(id);
244
+ if (!loaded?.history.length)
245
+ return null;
246
+ this.context.sessionStore.setCurrentSessionId(loaded.id);
247
+ this.context.rollbackStore.resetState();
248
+ const snapshotsLoaded = this.context.rollbackStore.loadSnapshots(loaded.id);
249
+ if (!snapshotsLoaded)
250
+ this.context.rollbackStore.rebuildFromHistory(loaded.history);
251
+ this.emit('session.resumed', { sessionId: loaded.id, snapshotsLoaded });
252
+ return loaded;
253
+ }
254
+ saveSession(history, id = this.context.sessionStore.getCurrentSessionId(), queryHistory = [], lastToolGroups = []) {
255
+ this.assertIdle('save session');
256
+ if (!id)
257
+ throw new Error('Cannot save without an active session.');
258
+ this.context.sessionStore.setCurrentSessionId(id);
259
+ const meta = this.context.sessionStore.save(history, id, queryHistory, lastToolGroups);
260
+ this.context.rollbackStore.persistSnapshots(id);
261
+ this.emit('session.saved', { sessionId: id });
262
+ return meta;
263
+ }
264
+ assertOpen() {
265
+ if (this.closed)
266
+ throw new Error('Runtime is closed.');
267
+ }
268
+ assertIdle(operation) {
269
+ this.assertOpen();
270
+ if (this.activeControllers.size > 0 || this.controlOperation) {
271
+ const active = this.controlOperation ?? 'an agent run';
272
+ throw new Error(`Runtime cannot ${operation} while ${active} is active.`);
273
+ }
274
+ }
275
+ beginTrackedWork() {
276
+ let resolve;
277
+ const settled = new Promise((done) => {
278
+ resolve = done;
279
+ });
280
+ this.activeWork.add(settled);
281
+ return () => {
282
+ this.activeWork.delete(settled);
283
+ resolve();
284
+ };
285
+ }
286
+ emit(type, data) {
287
+ const event = { type, timestamp: new Date().toISOString(), data };
288
+ for (const key of [type, '*']) {
289
+ for (const listener of this.listeners.get(key) ?? []) {
290
+ try {
291
+ const result = listener(event);
292
+ if (result && typeof result.then === 'function') {
293
+ void Promise.resolve(result).catch(() => undefined);
294
+ }
295
+ }
296
+ catch {
297
+ // Runtime events are observational and must never affect hooks or lifecycle behavior.
298
+ }
299
+ }
300
+ }
301
+ }
302
+ }
303
+ export function createRuntime(init = {}) {
304
+ return new Runtime(init);
305
+ }
306
+ /** Compatibility facade explicitly bound to all historical process-level singletons. */
307
+ export const defaultRuntime = new Runtime({ context: defaultAgentRuntimeContext, contextState: defaultContextState });
@@ -4,11 +4,17 @@ import { MAX_HISTORY_RESULT, MAX_MEMORY_RESULT, MAX_OLD_TOOL_STUB, MAX_SKILL_RES
4
4
  import { ui } from '../ui/theme.js';
5
5
  import { Spinner } from '../ui/spinner.js';
6
6
  import * as layout from '../ui/layout.js';
7
- import { pruneAfterCompaction } from '../rollback/index.js';
7
+ import { defaultRollbackStore } from '../rollback/index.js';
8
8
  import { toText } from '../context/utils.js';
9
9
  import { DEFAULT_BUDGET_POLICY } from '../context/budget.js';
10
10
  import { collectArtifactRefs } from '../context/artifacts.js';
11
11
  import { writeCompactionSnapshot } from './notes.js';
12
+ /** 默认兼容依赖:旧调用方继续读取进程级 config/chat。 */
13
+ export const defaultCompactionRuntime = {
14
+ config,
15
+ modelTransport: chat,
16
+ rollbackStore: defaultRollbackStore,
17
+ };
12
18
  export function createContextState() {
13
19
  return { lastEstimate: 0, correction: 1, calibrationSamples: 0 };
14
20
  }
@@ -424,7 +430,7 @@ function buildTranscript(stripped, scale) {
424
430
  })
425
431
  .join('\n');
426
432
  }
427
- async function defaultSummarize(older, focus, signal) {
433
+ async function defaultSummarize(older, focus, signal, runtime = defaultCompactionRuntime) {
428
434
  // 已中断就别白拼转录了(几千 token 的字符串拼接 + 一去不回的 LLM 请求)。
429
435
  if (signal?.aborted) {
430
436
  throw new DOMException('This operation was aborted', 'AbortError');
@@ -435,13 +441,13 @@ async function defaultSummarize(older, focus, signal) {
435
441
  // 不需要"先压一遍再摘要"。封顶策略:先按满额封顶;总量仍超窗口 55% 预算时等比缩小
436
442
  // 配额重拼(优先保条数/每轮都有代表,其次保单条长度);再超才整段中截兜底(罕见)。
437
443
  let transcript = buildTranscript(stripped, 1);
438
- const tokenBudget = Math.floor(config.contextWindowTokens * SUMMARY_TRANSCRIPT_WINDOW_RATIO);
444
+ const tokenBudget = Math.floor(runtime.config.contextWindowTokens * SUMMARY_TRANSCRIPT_WINDOW_RATIO);
439
445
  let tokens = estimateTokens(transcript);
440
446
  if (tokens > tokenBudget) {
441
447
  transcript = buildTranscript(stripped, tokenBudget / tokens);
442
448
  tokens = estimateTokens(transcript);
443
- if (tokens > Math.floor(config.contextWindowTokens * 0.6)) {
444
- transcript = truncateMid(transcript, Math.floor(config.contextWindowTokens * 0.5));
449
+ if (tokens > Math.floor(runtime.config.contextWindowTokens * 0.6)) {
450
+ transcript = truncateMid(transcript, Math.floor(runtime.config.contextWindowTokens * 0.5));
445
451
  }
446
452
  }
447
453
  const sysMsg = {
@@ -473,7 +479,7 @@ async function defaultSummarize(older, focus, signal) {
473
479
  // signal 必须透传:摘要是几十秒的 LLM 调用,不串进来 Ctrl+C 只能干等它跑完。
474
480
  // 空 handlers:不打印、不外显流式;tools=[] 不带工具表——摘要纯文本任务,
475
481
  // 全量工具 schema 白占几千 token 窗口,还诱导幻觉工具调用。
476
- const r = await chat([sysMsg, userMsg], {}, signal, []);
482
+ const r = await runtime.modelTransport([sysMsg, userMsg], {}, signal, []);
477
483
  // 推理模型可能只返 reasoning_content(content 为 null),或幻觉出 tool_calls → 视为失败
478
484
  if (r.toolCalls.length > 0 || !r.content)
479
485
  return null;
@@ -622,7 +628,8 @@ export async function compactHistory(history, opts) {
622
628
  ...older,
623
629
  ];
624
630
  }
625
- const summarizeFn = opts.summarize ?? defaultSummarize;
631
+ const summarizeFn = opts.summarize ??
632
+ ((older, focus, signal) => defaultSummarize(older, focus, signal, opts.runtime ?? defaultCompactionRuntime));
626
633
  let summary = null;
627
634
  try {
628
635
  summary = await summarizeFn(older, opts.focus, opts.signal);
@@ -661,7 +668,7 @@ export async function compactHistory(history, opts) {
661
668
  const rebuilt = [systemMsg, summaryMsg, ...flattenGroups(kept)];
662
669
  history.length = 0;
663
670
  history.push(...rebuilt);
664
- pruneAfterCompaction(history); // 摘要删了旧轮次 → 按存活轮次裁剪回滚日志
671
+ (opts.runtime?.rollbackStore ?? defaultRollbackStore).pruneAfterCompaction(history);
665
672
  const estimateAfter = estimatePromptTokens(history, activeTools, state.correction);
666
673
  state.lastEstimate = estimateAfter;
667
674
  state.lastUsage = undefined; // 压缩后旧 usage 失效,/context 改用校正估算
@@ -735,8 +742,8 @@ export async function compactHistory(history, opts) {
735
742
  * 默认 manual=false 自动路径完全不变。
736
743
  */
737
744
  export async function maybeCompact(history, report, manualOpts, state = contextState, activeTools = chatTools,
738
- /** 主 agent 的 abort signal;透传给 compactHistory → 摘要器 → chat()。 */
739
- signal) {
745
+ /** 主 agent 的 abort signal;透传给 compactHistory → 摘要器 → model transport。 */
746
+ signal, runtime = defaultCompactionRuntime) {
740
747
  const est = estimatePromptTokens(history, activeTools, state.correction);
741
748
  state.lastEstimate = est;
742
749
  const isManual = manualOpts?.manual === true;
@@ -747,7 +754,7 @@ signal) {
747
754
  report.rawTotal >= DEFAULT_BUDGET_POLICY.pressureTriggerRatio * report.window;
748
755
  // 手动路径:旁路 autoCompact / report / 总阈三重门
749
756
  if (!isManual) {
750
- if (!hardCap && !config.autoCompact)
757
+ if (!hardCap && !runtime.config.autoCompact)
751
758
  return;
752
759
  if (report) {
753
760
  // Scheduler mode: the shared pressure report is the sole automatic trigger.
@@ -758,14 +765,14 @@ signal) {
758
765
  else {
759
766
  // Fallback mode uses the same threshold; there is no second compact gate.
760
767
  const raw = estimatePromptTokens(history, activeTools, 1);
761
- hardCap = raw >= DEFAULT_BUDGET_POLICY.pressureTriggerRatio * config.contextWindowTokens;
762
- const pressureLine = DEFAULT_BUDGET_POLICY.pressureTriggerRatio * config.contextWindowTokens;
768
+ hardCap = raw >= DEFAULT_BUDGET_POLICY.pressureTriggerRatio * runtime.config.contextWindowTokens;
769
+ const pressureLine = DEFAULT_BUDGET_POLICY.pressureTriggerRatio * runtime.config.contextWindowTokens;
763
770
  if (!hardCap && est < pressureLine)
764
771
  return;
765
772
  }
766
773
  }
767
774
  const r = await compactHistory(history, {
768
- window: config.contextWindowTokens,
775
+ window: runtime.config.contextWindowTokens,
769
776
  threshold: DEFAULT_BUDGET_POLICY.pressureTriggerRatio,
770
777
  focus: manualOpts?.focus,
771
778
  manual: isManual,
@@ -773,6 +780,7 @@ signal) {
773
780
  force: manualOpts?.force === true || hardCap,
774
781
  tools: activeTools,
775
782
  contextState: state,
783
+ runtime,
776
784
  signal,
777
785
  });
778
786
  return r;
@@ -11,6 +11,7 @@ export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, con
11
11
  // repl /compact 命令调 manualCompact(history, focus?):与自动路径共享决策,focus 透传摘要 prompt。
12
12
  export { runScheduler, manualCompact, createBudgetScheduler } from './scheduler.js';
13
13
  export { newSessionId, saveSession, loadSession, listSessions, sessionDir } from './persist.js';
14
+ export { SessionStore, defaultSessionStore, getActiveSessionStore, withSessionStore } from './store.js';
14
15
  export { appendCurrentSessionTrace, appendCurrentSessionTraceEvent, appendCurrentSessionRuntimeEvent, createTraceEvent, } from './trace.js';
15
16
  export { reduceTraceMetrics, readTraceEvents, readTraceMetrics } from './trace-metrics.js';
16
17
  export { summarizeToolArguments, hashTraceValue, safeProviderId } from './trace-sanitize.js';
@@ -1,157 +1,21 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import path from 'node:path';
3
- import { config, getActiveModel } from '../config/index.js';
4
- import { isToolRouteGroupName } from '../config/profiles.js';
5
- import { truncateDisplay } from '../ui/render.js';
1
+ import { defaultSessionStore } from './store.js';
6
2
  /** 会话目录(确保存在)。 */
7
3
  export function sessionDir() {
8
- mkdirSync(config.sessionDir, { recursive: true });
9
- return config.sessionDir;
4
+ return defaultSessionStore.sessionDir();
10
5
  }
11
- /** 新会话 id:YYYYMMDD-HHmmss(运行时 Date 可用)。 */
6
+ /** 新会话 id:可排序时间前缀 + 碰撞防护后缀。 */
12
7
  export function newSessionId() {
13
- const d = new Date();
14
- const p = (n) => String(n).padStart(2, '0');
15
- return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
8
+ return defaultSessionStore.createId();
16
9
  }
17
- /** id(YYYYMMDD-HHmmss)→ ISO 字符串,稳定可排序。解析失败回退原 id。 */
18
- function idToIso(id) {
19
- const m = /^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})$/.exec(id);
20
- if (!m)
21
- return id;
22
- return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}`;
23
- }
24
- function toText(content) {
25
- if (content == null)
26
- return '';
27
- if (typeof content === 'string')
28
- return content;
29
- try {
30
- return JSON.stringify(content);
31
- }
32
- catch {
33
- return String(content);
34
- }
35
- }
36
- function firstUserOf(history) {
37
- for (const m of history) {
38
- if (m.role === 'user') {
39
- const text = toText(m.content)
40
- .replace(/\n/g, ' ')
41
- .trim();
42
- return truncateDisplay(text, 40);
43
- }
44
- }
45
- return '';
46
- }
47
- function sessionPath(id) {
48
- return path.join(config.sessionDir, id, 'session.json');
49
- }
50
- /** 保存会话到磁盘。全新且没有 query 的会话不创建文件;已有会话即使回滚为空也必须覆盖旧记录。 */
10
+ /** 保存会话;保留旧同步 API 与磁盘格式。 */
51
11
  export function saveSession(history, id, queryHistory = [], lastToolGroups = []) {
52
- const meta = {
53
- id,
54
- createdAt: idToIso(id),
55
- model: getActiveModel(),
56
- firstUser: history.length > 1
57
- ? firstUserOf(history)
58
- : truncateDisplay((queryHistory[0] ?? '').replace(/\n/g, ' ').trim(), 40),
59
- };
60
- const currentPath = sessionPath(id);
61
- const legacyPath = path.join(config.sessionDir, `${id}.json`);
62
- if (history.length <= 1 && queryHistory.length === 0 && !existsSync(currentPath) && !existsSync(legacyPath)) {
63
- return meta;
64
- }
65
- const dir = path.join(config.sessionDir, id);
66
- mkdirSync(dir, { recursive: true });
67
- const record = {
68
- ...meta,
69
- history,
70
- queryHistory: [...queryHistory],
71
- lastToolGroups: [...lastToolGroups],
72
- };
73
- writeFileSync(currentPath, JSON.stringify(record), 'utf8');
74
- // 一旦写入新式目录,删除旧式扁平副本,避免已回滚消息仍残留在磁盘。
75
- if (existsSync(legacyPath))
76
- unlinkSync(legacyPath);
77
- return meta;
12
+ return defaultSessionStore.save(history, id, queryHistory, lastToolGroups);
78
13
  }
79
- /** 加载会话;不存在 / 损坏返 null(不抛)。优先新式目录,回退旧式文件。 */
14
+ /** 加载会话;不存在或损坏时返回 null */
80
15
  export function loadSession(id) {
81
- // 新式: .mocode/sessions/<id>/session.json
82
- const newPath = path.join(config.sessionDir, id, 'session.json');
83
- // 旧式: .mocode/sessions/<id>.json
84
- const oldPath = path.join(config.sessionDir, `${id}.json`);
85
- const p = existsSync(newPath) ? newPath : oldPath;
86
- if (!existsSync(p))
87
- return null;
88
- try {
89
- const raw = readFileSync(p, 'utf8');
90
- const rec = JSON.parse(raw);
91
- if (!rec || !Array.isArray(rec.history))
92
- return null;
93
- return {
94
- id: rec.id,
95
- createdAt: rec.createdAt ?? idToIso(rec.id ?? id),
96
- model: rec.model ?? '',
97
- firstUser: rec.firstUser ?? '',
98
- history: rec.history,
99
- queryHistory: Array.isArray(rec.queryHistory)
100
- ? rec.queryHistory.filter((query) => typeof query === 'string')
101
- : undefined,
102
- lastToolGroups: Array.isArray(rec.lastToolGroups) ? rec.lastToolGroups.filter(isToolRouteGroupName) : undefined,
103
- };
104
- }
105
- catch {
106
- return null;
107
- }
16
+ return defaultSessionStore.load(id);
108
17
  }
109
- /** 列出最近会话,按 createdAt 降序。损坏文件跳过。
110
- * - limit?: 仅返回前 N 条。会话目录名是 YYYYMMDD-HHmmss,字典序=时间序;
111
- * 按目录名降序逐个解析,收集到 N 个有效会话就停止,避免 /resume 在
112
- * sessions 目录堆了几百个子目录时全量 JSON.parse;同时不让只有笔记或
113
- * 快照、没有 session.json 的目录占掉最近 N 条的名额。
114
- * - 不传 limit 时读全部(向后兼容,供裸 --resume 列全表用)。
115
- * - 向后兼容:同时扫描旧式 <id>.json 文件(扁平结构),优先读新式目录。
116
- */
18
+ /** 列出最近会话,按 createdAt 降序。 */
117
19
  export function listSessions(limit) {
118
- if (!existsSync(config.sessionDir))
119
- return [];
120
- const entries = readdirSync(config.sessionDir, { withFileTypes: true });
121
- const ids = [];
122
- for (const e of entries) {
123
- if (e.isDirectory() && /^\d{8}-\d{6}$/.test(e.name)) {
124
- ids.push(e.name);
125
- }
126
- else if (e.isFile() && e.name.endsWith('.json') && !e.name.endsWith('.snapshots.json')) {
127
- // 旧式扁平文件:兼容读取
128
- ids.push(e.name.replace(/\.json$/, ''));
129
- }
130
- }
131
- const all = ids.sort().reverse(); // 降序:最新在前
132
- const maxResults = typeof limit === 'number' ? Math.max(0, limit) : Infinity;
133
- const out = [];
134
- for (const id of all) {
135
- if (out.length >= maxResults)
136
- break;
137
- try {
138
- // 优先新式目录,回退旧式文件
139
- const newPath = path.join(config.sessionDir, id, 'session.json');
140
- const oldPath = path.join(config.sessionDir, `${id}.json`);
141
- const p = existsSync(newPath) ? newPath : oldPath;
142
- const rec = JSON.parse(readFileSync(p, 'utf8'));
143
- if (rec && typeof rec.id === 'string') {
144
- out.push({
145
- id: rec.id,
146
- createdAt: rec.createdAt ?? idToIso(rec.id),
147
- model: rec.model ?? '',
148
- firstUser: rec.firstUser ?? '',
149
- });
150
- }
151
- }
152
- catch {
153
- // 跳过损坏文件
154
- }
155
- }
156
- return out;
20
+ return defaultSessionStore.list(limit);
157
21
  }