c0de-agent 1.0.0 → 1.2.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 (46) hide show
  1. package/dist/cli/deps.d.ts +16 -5
  2. package/dist/cli/deps.js +18 -5
  3. package/dist/cli/index.js +8 -2
  4. package/dist/core/agent.js +5 -0
  5. package/dist/core/config.js +1 -0
  6. package/dist/core/index.d.ts +1 -0
  7. package/dist/core/index.js +1 -0
  8. package/dist/core/loop.d.ts +10 -0
  9. package/dist/core/loop.js +559 -295
  10. package/dist/core/slash.js +3 -4
  11. package/dist/core/title.js +3 -2
  12. package/dist/core/types.d.ts +2 -0
  13. package/dist/core/workflow.d.ts +25 -0
  14. package/dist/core/workflow.js +98 -0
  15. package/dist/core/worktree.js +6 -4
  16. package/dist/dap/session.js +3 -3
  17. package/dist/llm/provider.js +5 -1
  18. package/dist/plugins/loader.js +3 -2
  19. package/dist/server/agent-manager.d.ts +2 -0
  20. package/dist/server/agent-manager.js +8 -0
  21. package/dist/server/app.js +3 -0
  22. package/dist/server/context.js +2 -0
  23. package/dist/server/dev.d.ts +4 -1
  24. package/dist/server/dev.js +92 -27
  25. package/dist/server/permission/store.d.ts +2 -0
  26. package/dist/server/permission/store.js +9 -0
  27. package/dist/server/routes/chat.js +44 -1
  28. package/dist/server/routes/session.js +75 -1
  29. package/dist/server/routes/terminal.d.ts +5 -0
  30. package/dist/server/routes/terminal.js +66 -0
  31. package/dist/server/server.d.ts +14 -1
  32. package/dist/server/server.js +100 -28
  33. package/dist/server/terminal/pty-manager.d.ts +53 -0
  34. package/dist/server/terminal/pty-manager.js +160 -0
  35. package/dist/server/types.d.ts +3 -0
  36. package/dist/session/archive.d.ts +1 -1
  37. package/dist/session/compaction.d.ts +8 -2
  38. package/dist/session/compaction.js +85 -16
  39. package/dist/session/shake.d.ts +66 -0
  40. package/dist/session/shake.js +304 -0
  41. package/dist/session/types.d.ts +1 -1
  42. package/dist/shared/types/agent.d.ts +27 -0
  43. package/dist/shared/types/config.d.ts +10 -0
  44. package/dist/shared/types/llm.d.ts +1 -0
  45. package/dist/shared/types/tool.d.ts +3 -0
  46. package/package.json +11 -3
@@ -40,10 +40,9 @@ const compactCommand = {
40
40
  name: 'compact',
41
41
  description: 'Manually trigger context compaction',
42
42
  execute: async () => {
43
- return {
44
- _tag: 'success',
45
- message: 'Compaction queued. Use the agent API to trigger with a summarizer.',
46
- };
43
+ // 仅声明意图:真正的压缩由消费方(loop.compactContext / chat 路由)执行,
44
+ // 复用 createSummarizer + runCompaction,且不把 /compact 当作 user 消息发给 LLM。
45
+ return { _tag: 'compact' };
47
46
  },
48
47
  };
49
48
  const modelCommand = {
@@ -108,8 +108,9 @@ async function generateSessionTitle(deps, sessionId, firstMessage, chatProvider,
108
108
  return;
109
109
  await updateSessionTitle(db, sessionId, title);
110
110
  }
111
- catch {
112
- // 标题生成是尽力而为的辅助功能:失败不阻塞主对话流。
111
+ catch (e) {
112
+ // 标题生成是尽力而为的辅助功能:失败不阻塞主对话流,但记录以便排查。
113
+ console.warn('[title] generateSessionTitle failed:', e instanceof Error ? e.message : String(e));
113
114
  }
114
115
  }
115
116
  export { DEFAULT_SESSION_TITLE, generateSessionTitle, isDefaultTitle };
@@ -80,6 +80,8 @@ type CommandResult = {
80
80
  } | {
81
81
  _tag: 'text';
82
82
  text: string;
83
+ } | {
84
+ _tag: 'compact';
83
85
  };
84
86
  type CommandContext = {
85
87
  cwd: string;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * "workflowz" keyword support — Dynamic Workflow(spec: dynamic-workflow-design)。
3
+ *
4
+ * 用户在消息中包含独立单词 `workflowz` 时:
5
+ * 1. 后端检测到关键词(prose 感知,忽略代码块/路径/大小写变体)
6
+ * 2. 向当轮注入一条隐藏 system 通知(steering),引导模型用 `task` 工具批量 fan-out
7
+ * 做确定性的多子 agent 工作流分解
8
+ *
9
+ * 复刻自 oh-my-pi 的 modes/workflow.ts,适配 c0de-agent 的 task 工具 schema:
10
+ * - 批量模式:`{ subagent_type, context, tasks: [{ prompt, description? }] }`
11
+ * - 单任务模式:`{ subagent_type?, prompt, description? }`
12
+ */
13
+ /**
14
+ * 判断 `text` 是否在 prose 中包含独立关键词 "workflowz"
15
+ * (小写、空白分隔)——不在代码块、行内代码或路径中。
16
+ *
17
+ * 简化版 prose 检测:移除 ``` 代码块和 `行内代码` 后再匹配。
18
+ */
19
+ export declare function containsWorkflow(text: string): boolean;
20
+ /**
21
+ * 工作流通知:注入为 steering system 消息,引导模型用 task 工具做批量 fan-out。
22
+ *
23
+ * 适配 c0de-agent 的 task 工具 schema(subagent_type + context + tasks[])。
24
+ */
25
+ export declare const WORKFLOW_NOTICE = "<workflow-notice>\nThe user's message contains the **workflowz** keyword: drive this task as a deterministic multi-subagent workflow. Use the `task` tool for batched fan-out \u2014 to be comprehensive (decompose and cover in parallel), to be confident (independent perspectives and adversarial checks before you commit), or to take on scale one context can't hold (audits, migrations, broad sweeps). This overrides any default tendency to do the whole task inline when fanning out would be more thorough.\n\n<when>\nWorth it when the task benefits from decomposition + parallel coverage, or from independent/adversarial cross-checking. For a quick lookup or single edit, just do it directly \u2014 don't spin up agents. Scout inline first (list the files, scope the diff, find the call sites) to discover the work list, then fan out over it. Common shapes:\n- **Understand** \u2014 parallel readers over subsystems \u2192 structured map.\n- **Design** \u2014 independent approaches \u2192 scored synthesis.\n- **Review** \u2014 split dimensions \u2192 find per dimension \u2192 adversarially verify each finding.\n- **Research** \u2014 multi-modal sweep \u2192 deep-read the hits \u2192 synthesize.\n- **Migrate** \u2014 discover sites \u2192 transform each \u2192 verify.\n</when>\n\n<task-contract>\nCall `task` once per independent fan-out batch using the batch form:\n\n task({\n subagent_type: \"coder\",\n context: \"shared background all subagents need\",\n tasks: [\n { prompt: \"specific assignment for agent 1\", description: \"short label\" },\n { prompt: \"specific assignment for agent 2\", description: \"short label\" },\n ]\n })\n\nAvailable subagent types: `general` (full tools, recursive), `coder` (implementation), `researcher` (read-only scout), `reviewer` (code review). Pick the type that matches each task's intent.\n\n`context` carries shared background prepended to every subagent's prompt \u2014 put the shared contract, conventions, and coordination rules here.\n\nEach task in `tasks[]` must be self-contained:\n- `prompt`: exact target (files, symbols, subsystem) + what to do + acceptance criteria\n- `description`: short label for the UI\n\nEach subagent runs in an isolated session and returns its result via the `yield` tool. Subagents skip formatters, linters, and project-wide tests \u2014 the parent runs shared proof once after all results return.\n</task-contract>\n\n<structure>\nDecompose first, then batch the independent leaves:\n\n task({\n subagent_type: \"coder\",\n context: \"# Goal\\nImplement feature X across the codebase\\n# Constraints\\nFollow existing patterns...\\n# Contract\\nReturn findings as structured data...\",\n tasks: [\n { prompt: \"# Target\\nsrc/auth/login.ts\\n# Change\\nAdd rate limiting to login endpoint\\n# Acceptance\\nRate limiter works, tests pass\", description: \"Login rate limiting\" },\n { prompt: \"# Target\\nsrc/auth/signup.ts\\n# Change\\nAdd input validation\\n# Acceptance\\nValidation blocks invalid emails\", description: \"Signup validation\" },\n ]\n })\n\nPrefer one wide batch over serial calls when work items do not share files. If tasks overlap, have agents coordinate before editing.\n</structure>\n\n<patterns>\n- **Adversarial verify** \u2014 dispatch skeptical reviewers with distinct targets, then keep only findings you can verify against source.\n- **Perspective-diverse review** \u2014 use separate correctness, security, performance roles instead of identical reviewers.\n- **Completeness critic** \u2014 after the first batch, dispatch one read-only critic that asks what was missed.\n- **No silent caps** \u2014 if you bound coverage (top-N, sampling), state what was dropped and why.\n- **Parent owns closure** \u2014 subagents return evidence; the parent reads it, resolves contradictions, runs proof, and makes the final decision.\n</patterns>\n\n<execution>\n- Capture multi-phase workflow state in the visible todo system when available.\n- Batch independent subagents in one `task` call.\n- Give every subagent a narrow target, explicit non-goals, and a concrete return packet.\n- After fan-out returns, read the results, patch or decide, and run the shared gate.\n- Keep going until the task is closed \u2014 returned fan-out is a step, not a stopping point.\n</execution>\n</workflow-notice>";
@@ -0,0 +1,98 @@
1
+ /**
2
+ * "workflowz" keyword support — Dynamic Workflow(spec: dynamic-workflow-design)。
3
+ *
4
+ * 用户在消息中包含独立单词 `workflowz` 时:
5
+ * 1. 后端检测到关键词(prose 感知,忽略代码块/路径/大小写变体)
6
+ * 2. 向当轮注入一条隐藏 system 通知(steering),引导模型用 `task` 工具批量 fan-out
7
+ * 做确定性的多子 agent 工作流分解
8
+ *
9
+ * 复刻自 oh-my-pi 的 modes/workflow.ts,适配 c0de-agent 的 task 工具 schema:
10
+ * - 批量模式:`{ subagent_type, context, tasks: [{ prompt, description? }] }`
11
+ * - 单任务模式:`{ subagent_type?, prompt, description? }`
12
+ */
13
+ // 检测:小写关键词,两侧为空白或字符串边界。非全局,`.test` 无状态。
14
+ const WORKFLOW_WORD = /(?<!\S)workflowz(?!\S)/;
15
+ /**
16
+ * 判断 `text` 是否在 prose 中包含独立关键词 "workflowz"
17
+ * (小写、空白分隔)——不在代码块、行内代码或路径中。
18
+ *
19
+ * 简化版 prose 检测:移除 ``` 代码块和 `行内代码` 后再匹配。
20
+ */
21
+ export function containsWorkflow(text) {
22
+ // 移除 ``` ... ``` 代码块
23
+ const withoutBlocks = text.replace(/```[\s\S]*?```/g, '');
24
+ // 移除 `行内代码`
25
+ const withoutInline = withoutBlocks.replace(/`[^`]*`/g, '');
26
+ return WORKFLOW_WORD.test(withoutInline);
27
+ }
28
+ /**
29
+ * 工作流通知:注入为 steering system 消息,引导模型用 task 工具做批量 fan-out。
30
+ *
31
+ * 适配 c0de-agent 的 task 工具 schema(subagent_type + context + tasks[])。
32
+ */
33
+ export const WORKFLOW_NOTICE = `<workflow-notice>
34
+ The user's message contains the **workflowz** keyword: drive this task as a deterministic multi-subagent workflow. Use the \`task\` tool for batched fan-out — to be comprehensive (decompose and cover in parallel), to be confident (independent perspectives and adversarial checks before you commit), or to take on scale one context can't hold (audits, migrations, broad sweeps). This overrides any default tendency to do the whole task inline when fanning out would be more thorough.
35
+
36
+ <when>
37
+ Worth it when the task benefits from decomposition + parallel coverage, or from independent/adversarial cross-checking. For a quick lookup or single edit, just do it directly — don't spin up agents. Scout inline first (list the files, scope the diff, find the call sites) to discover the work list, then fan out over it. Common shapes:
38
+ - **Understand** — parallel readers over subsystems → structured map.
39
+ - **Design** — independent approaches → scored synthesis.
40
+ - **Review** — split dimensions → find per dimension → adversarially verify each finding.
41
+ - **Research** — multi-modal sweep → deep-read the hits → synthesize.
42
+ - **Migrate** — discover sites → transform each → verify.
43
+ </when>
44
+
45
+ <task-contract>
46
+ Call \`task\` once per independent fan-out batch using the batch form:
47
+
48
+ task({
49
+ subagent_type: "coder",
50
+ context: "shared background all subagents need",
51
+ tasks: [
52
+ { prompt: "specific assignment for agent 1", description: "short label" },
53
+ { prompt: "specific assignment for agent 2", description: "short label" },
54
+ ]
55
+ })
56
+
57
+ Available subagent types: \`general\` (full tools, recursive), \`coder\` (implementation), \`researcher\` (read-only scout), \`reviewer\` (code review). Pick the type that matches each task's intent.
58
+
59
+ \`context\` carries shared background prepended to every subagent's prompt — put the shared contract, conventions, and coordination rules here.
60
+
61
+ Each task in \`tasks[]\` must be self-contained:
62
+ - \`prompt\`: exact target (files, symbols, subsystem) + what to do + acceptance criteria
63
+ - \`description\`: short label for the UI
64
+
65
+ Each subagent runs in an isolated session and returns its result via the \`yield\` tool. Subagents skip formatters, linters, and project-wide tests — the parent runs shared proof once after all results return.
66
+ </task-contract>
67
+
68
+ <structure>
69
+ Decompose first, then batch the independent leaves:
70
+
71
+ task({
72
+ subagent_type: "coder",
73
+ context: "# Goal\\nImplement feature X across the codebase\\n# Constraints\\nFollow existing patterns...\\n# Contract\\nReturn findings as structured data...",
74
+ tasks: [
75
+ { prompt: "# Target\\nsrc/auth/login.ts\\n# Change\\nAdd rate limiting to login endpoint\\n# Acceptance\\nRate limiter works, tests pass", description: "Login rate limiting" },
76
+ { prompt: "# Target\\nsrc/auth/signup.ts\\n# Change\\nAdd input validation\\n# Acceptance\\nValidation blocks invalid emails", description: "Signup validation" },
77
+ ]
78
+ })
79
+
80
+ Prefer one wide batch over serial calls when work items do not share files. If tasks overlap, have agents coordinate before editing.
81
+ </structure>
82
+
83
+ <patterns>
84
+ - **Adversarial verify** — dispatch skeptical reviewers with distinct targets, then keep only findings you can verify against source.
85
+ - **Perspective-diverse review** — use separate correctness, security, performance roles instead of identical reviewers.
86
+ - **Completeness critic** — after the first batch, dispatch one read-only critic that asks what was missed.
87
+ - **No silent caps** — if you bound coverage (top-N, sampling), state what was dropped and why.
88
+ - **Parent owns closure** — subagents return evidence; the parent reads it, resolves contradictions, runs proof, and makes the final decision.
89
+ </patterns>
90
+
91
+ <execution>
92
+ - Capture multi-phase workflow state in the visible todo system when available.
93
+ - Batch independent subagents in one \`task\` call.
94
+ - Give every subagent a narrow target, explicit non-goals, and a concrete return packet.
95
+ - After fan-out returns, read the results, patch or decide, and run the shared gate.
96
+ - Keep going until the task is closed — returned fan-out is a step, not a stopping point.
97
+ </execution>
98
+ </workflow-notice>`;
@@ -48,8 +48,9 @@ async function applyPatchToParent(repoRoot, patch, commitMessage) {
48
48
  try {
49
49
  gitWithInput(repoRoot, ['apply'], patch);
50
50
  }
51
- catch {
52
- // apply 失败:尝试 3-way 合并
51
+ catch (e) {
52
+ // apply 失败:记录后尝试 3-way 合并
53
+ console.warn('[worktree] git apply failed, retrying with --3way:', e instanceof Error ? e.message : String(e));
53
54
  gitWithInput(repoRoot, ['apply', '--3way'], patch);
54
55
  }
55
56
  git(repoRoot, ['add', '-A']);
@@ -61,8 +62,9 @@ function removeWorktree(repoRoot, worktreeDir) {
61
62
  try {
62
63
  git(repoRoot, ['worktree', 'remove', '--force', worktreeDir]);
63
64
  }
64
- catch {
65
- // 清理失败忽略(可能已删)
65
+ catch (e) {
66
+ // 清理失败忽略(可能已删),但记录以便排查残留 worktree
67
+ console.warn('[worktree] worktree cleanup failed:', e instanceof Error ? e.message : String(e));
66
68
  }
67
69
  }
68
70
  export { applyPatchToParent, captureBaseline, captureDeltaPatch, createWorktree, removeWorktree };
@@ -112,12 +112,12 @@ function createDebugSessionManager() {
112
112
  if (!s)
113
113
  return;
114
114
  s.session.state = 'stopped';
115
- // disconnect 适配器可能已退出;吞错。
115
+ // disconnect 适配器可能已退出;吞错但记录,便于排查非正常退出。
116
116
  try {
117
117
  await s.client.request('disconnect', {});
118
118
  }
119
- catch {
120
- /* adapter gone */
119
+ catch (e) {
120
+ console.warn('[dap] disconnect failed:', e instanceof Error ? e.message : String(e));
121
121
  }
122
122
  s.client.dispose();
123
123
  sessions.delete(sessionId);
@@ -93,7 +93,11 @@ const toStreamChunk = (event) => {
93
93
  case 'provider-error':
94
94
  return {
95
95
  _tag: 'error',
96
- error: { message: event.message, retryable: event.retryable ?? false },
96
+ error: {
97
+ message: event.message,
98
+ retryable: event.retryable ?? false,
99
+ ...(event.classification !== undefined ? { classification: event.classification } : {}),
100
+ },
97
101
  };
98
102
  default:
99
103
  return null;
@@ -59,8 +59,9 @@ async function discoverPlugins(projectDir) {
59
59
  const plugin = await loadPlugin(join(entry.path, 'index.js'));
60
60
  results.push({ name: entry.name, path: entry.path, plugin });
61
61
  }
62
- catch {
63
- // Skip plugins that fail to load
62
+ catch (e) {
63
+ // Plugin failed to load skip but warn (otherwise silently inactive)
64
+ console.warn(`[plugin] failed to load "${entry.name}" from ${entry.path}:`, e instanceof Error ? e.message : String(e));
64
65
  }
65
66
  }
66
67
  return results;
@@ -26,6 +26,8 @@ type AgentManager = {
26
26
  children(parentSessionId: string): ActiveRun[];
27
27
  /** 查询所有后台任务(jobId 非空的 run)。 */
28
28
  backgroundJobs(): ActiveRun[];
29
+ /** 中止所有活跃 run 并清空(dev 热重载重建前调用)。 */
30
+ dispose(): void;
29
31
  };
30
32
  declare function createAgentManager(): AgentManager;
31
33
  export type { ActiveRun, AgentManager };
@@ -49,6 +49,14 @@ function createAgentManager() {
49
49
  backgroundJobs() {
50
50
  return Array.from(runs.values()).filter((r) => r.jobId !== undefined);
51
51
  },
52
+ dispose() {
53
+ // dev 热重载重建前调用:中止所有活跃 run(loop 在 turn/流边界检测 signal
54
+ // → unwind → 调用方 finally 持久化 + unregister),然后清空 Map。
55
+ for (const run of runs.values()) {
56
+ abortAgent(run.state);
57
+ }
58
+ runs.clear();
59
+ },
52
60
  };
53
61
  }
54
62
  export { createAgentManager };
@@ -18,6 +18,7 @@ import { createPermissionsRoute } from './routes/permissions.js';
18
18
  import { createProjectRoute } from './routes/project.js';
19
19
  import { createProviderRoute } from './routes/provider.js';
20
20
  import { createSessionRoute } from './routes/session.js';
21
+ import { createTerminalRoute } from './routes/terminal.js';
21
22
  import { createToolRoute } from './routes/tool.js';
22
23
  import { createUpdateRoute } from './routes/update.js';
23
24
  /** 创建完整的 Hono 应用,挂载所有路由 + 中间件。 */
@@ -44,6 +45,7 @@ function createApp(ctx) {
44
45
  app.route('/api/config', createConfigRoute(ctx));
45
46
  app.route('/api/permissions', createPermissionsRoute(ctx));
46
47
  app.route('/api/files', createFilesRoute(ctx));
48
+ app.route('/api/terminal', createTerminalRoute(ctx));
47
49
  // 根路径
48
50
  app.get('/', (c) => c.json({
49
51
  name: 'c0de-agent',
@@ -63,6 +65,7 @@ function createApp(ctx) {
63
65
  '/api/config',
64
66
  '/api/permissions',
65
67
  '/api/files',
68
+ '/api/terminal',
66
69
  ],
67
70
  }));
68
71
  // 静态文件服务(生产环境 dist-web/ 存在时启用)
@@ -6,6 +6,7 @@ import { createDefaultRegistry, createDefaultURLRegistry } from '../tools/index.
6
6
  import { createUpdateScheduler } from '../update/index.js';
7
7
  import { createAgentManager } from './agent-manager.js';
8
8
  import { createPermissionStore } from './permission/store.js';
9
+ import { PTYManager } from './terminal/pty-manager.js';
9
10
  function createServerContext(opts) {
10
11
  // 测试/dev 工厂:补足 urlRegistry + hookRunner + pluginRegistry(空壳,不激活插件),
11
12
  // 生产启动走 bootstrapServerContext → initPlugins(激活 builtin + 发现外部插件)。
@@ -41,6 +42,7 @@ function createServerContext(opts) {
41
42
  }),
42
43
  }),
43
44
  cwd: opts.cwd ?? process.cwd(),
45
+ ptyManager: new PTYManager(),
44
46
  ...(opts.chatStream ? { chatStream: opts.chatStream } : {}),
45
47
  };
46
48
  }
@@ -1,6 +1,9 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  import type { Hono } from 'hono';
3
+ import type { ServerContext } from './types.js';
3
4
  declare function getDevApp(): Promise<Hono>;
5
+ /** 获取当前 dev ctx(WebSocket 升级等需要直接访问 ptyManager 的场景)。 */
6
+ declare function getDevCtx(): Promise<ServerContext>;
4
7
  /** vite dev server 关闭时调用,确保 PGLite WASM 正常 close 并刷写 WAL。 */
5
8
  declare function closeDevApp(): Promise<void>;
6
9
  /**
@@ -8,4 +11,4 @@ declare function closeDevApp(): Promise<void>;
8
11
  * 用 Readable.fromWeb 管道转发 Response body,原生处理背压与分块。
9
12
  */
10
13
  declare function handleApiRequest(app: Hono, req: IncomingMessage, res: ServerResponse): Promise<void>;
11
- export { closeDevApp, getDevApp, handleApiRequest };
14
+ export { closeDevApp, getDevApp, getDevCtx, handleApiRequest };
@@ -1,35 +1,92 @@
1
1
  // src/server/dev.ts
2
2
  import { Readable } from 'node:stream';
3
3
  import { createApp } from './app.js';
4
- import { bootstrapServerContext } from './server.js';
4
+ import { buildServerContext, createDevDb } from './server.js';
5
5
  /**
6
6
  * 开发环境入口:初始化并返回 Hono app。
7
7
  * 供 vite dev server 中间件复用,使前后端共享同一端口。
8
8
  *
9
- * globalThis 缓存而非模块级变量:Vite ssrLoadModule 在 server 端代码变更时会
10
- * 重新执行本模块,模块级 cachedApp 会重置;这会导致运行中的 AgentManager(含
11
- * 权限确认 pending)丢失,前端 POST /api/tools/confirm 拿不到 pending 而 404。
12
- * globalThis 跨模块重载保持单例,避免权限流程中途断裂。
9
+ * ## 全量重建(B 方案)
10
+ *
11
+ * 目标:编辑任意 server/core 代码后热重载,新代码对**新请求**全部生效——与进程重启同语义,
12
+ * 只是不开新进程。
13
+ *
14
+ * 缓存策略分两层:
15
+ *
16
+ * 1. **PGLite DB handle** → globalThis(唯一跨重载存活物)。
17
+ * PGLite 是单写者 WASM DB,同一 dataDir 不能开第二个连接(WAL 冲突 / Aborted()),
18
+ * 所以 db 必须跨重载复用。其余一切重建。
19
+ *
20
+ * 2. **ctx + app** → 模块级变量。
21
+ * Vite 重载本模块时归 null,下次请求触发 rebuild:
22
+ * dispose 旧 ctx(abort 活跃 run + settle pending permission + stop scheduler + close handoff)
23
+ * → 围绕复用的 db 调 buildServerContext(重建 agentManager/permissionStore/registries/plugins/app)
24
+ * → 全部用最新代码。
25
+ *
26
+ * ## 与热升级的关系
27
+ *
28
+ * 和 `performHotUpdate` 是同一原理,差只在 spawn 新进程 vs 进程内重建。两者都:
29
+ * - 保留 durable 状态(DB rows)
30
+ * - 丢弃非 durable 状态(in-flight 工具执行、pending Promise resolver、活跃 run 内存态)
31
+ * - 活跃 run 从最后持久化消息重启
32
+ *
33
+ * 活跃 run 被 abort 后,loop 在 turn/流边界检测 signal → unwind → 调用方 finally 持久化
34
+ * 状态(标记 interrupted/completed)。前端再发消息时从 DB 历史起新 run,全用新代码。
13
35
  */
14
- const DEV_APP_KEY = '__c0de_dev_app__';
15
- const DEV_CLOSE_KEY = '__c0de_dev_close__';
16
- async function getDevApp() {
36
+ const DEV_DB_KEY = '__c0de_dev_db__';
37
+ /** 模块级:随本模块重载而重置。 */
38
+ let ctx = null;
39
+ let app = null;
40
+ let disposeCtx = null;
41
+ async function rebuild() {
42
+ // 1. dispose 旧 ctx(不 close db)
43
+ if (disposeCtx) {
44
+ await disposeCtx();
45
+ disposeCtx = null;
46
+ ctx = null;
47
+ app = null;
48
+ }
49
+ // 2. 取/建 devDb(globalThis,PGLite 单写者,只建一次)
17
50
  const g = globalThis;
18
- if (!g[DEV_APP_KEY]) {
19
- const { ctx, close } = await bootstrapServerContext({ cwd: process.cwd() });
20
- g[DEV_APP_KEY] = createApp(ctx);
21
- g[DEV_CLOSE_KEY] = close;
51
+ if (!g[DEV_DB_KEY]) {
52
+ g[DEV_DB_KEY] = await createDevDb(process.cwd());
22
53
  }
23
- return g[DEV_APP_KEY];
54
+ const db = g[DEV_DB_KEY];
55
+ // 3. 围绕复用的 db 重建 ctx(skipHandoff:dev 不跑热升级 handoff)
56
+ const built = await buildServerContext(db, { cwd: process.cwd(), skipHandoff: true });
57
+ ctx = built.ctx;
58
+ disposeCtx = built.dispose;
59
+ app = createApp(ctx);
60
+ }
61
+ async function getDevApp() {
62
+ if (app)
63
+ return app;
64
+ await rebuild();
65
+ if (!app)
66
+ throw new Error('dev app failed to initialize');
67
+ return app;
68
+ }
69
+ /** 获取当前 dev ctx(WebSocket 升级等需要直接访问 ptyManager 的场景)。 */
70
+ async function getDevCtx() {
71
+ if (!ctx)
72
+ await getDevApp();
73
+ if (!ctx)
74
+ throw new Error('dev ctx not initialized');
75
+ return ctx;
24
76
  }
25
77
  /** vite dev server 关闭时调用,确保 PGLite WASM 正常 close 并刷写 WAL。 */
26
78
  async function closeDevApp() {
79
+ if (disposeCtx) {
80
+ await disposeCtx();
81
+ disposeCtx = null;
82
+ ctx = null;
83
+ app = null;
84
+ }
27
85
  const g = globalThis;
28
- const close = g[DEV_CLOSE_KEY];
29
- if (close) {
30
- await close();
31
- delete g[DEV_APP_KEY];
32
- delete g[DEV_CLOSE_KEY];
86
+ const db = g[DEV_DB_KEY];
87
+ if (db) {
88
+ await db.close();
89
+ delete g[DEV_DB_KEY];
33
90
  }
34
91
  }
35
92
  /**
@@ -49,16 +106,20 @@ async function handleApiRequest(app, req, res) {
49
106
  }
50
107
  const headers = new Headers();
51
108
  for (const [key, value] of Object.entries(req.headers)) {
52
- if (value != null) {
53
- headers.set(key, Array.isArray(value) ? value.join(', ') : value);
109
+ if (typeof value === 'string') {
110
+ headers.set(key, value);
111
+ }
112
+ else if (Array.isArray(value)) {
113
+ for (const v of value)
114
+ headers.append(key, v);
54
115
  }
55
116
  }
56
117
  const init = {
57
118
  method: req.method,
58
119
  headers,
59
- body: body ?? undefined,
60
120
  };
61
121
  if (body !== undefined) {
122
+ init.body = body;
62
123
  init.duplex = 'half';
63
124
  }
64
125
  const request = new Request(url.toString(), init);
@@ -69,15 +130,19 @@ async function handleApiRequest(app, req, res) {
69
130
  });
70
131
  const responseBody = response.body;
71
132
  if (responseBody) {
72
- res.flushHeaders();
73
- await new Promise((resolve, reject) => {
74
- const nodeStream = Readable.fromWeb(responseBody);
75
- nodeStream.on('error', reject);
76
- nodeStream.pipe(res).on('finish', resolve);
133
+ // Readable.fromWeb 需要 web stream;Hono Response.body 已是 ReadableStream。
134
+ // native pipeline handles backpressure + chunked encoding for SSE.
135
+ Readable.fromWeb(responseBody)
136
+ .pipe(res)
137
+ .on('error', (err) => {
138
+ if (!res.headersSent)
139
+ res.statusCode = 500;
140
+ res.end();
141
+ console.error('[c0de-hono-api] response stream error:', err);
77
142
  });
78
143
  }
79
144
  else {
80
145
  res.end();
81
146
  }
82
147
  }
83
- export { closeDevApp, getDevApp, handleApiRequest };
148
+ export { closeDevApp, getDevApp, getDevCtx, handleApiRequest };
@@ -24,6 +24,8 @@ type PermissionStore = {
24
24
  resolve(toolCallId: string, approved: boolean): boolean;
25
25
  has(toolCallId: string): boolean;
26
26
  size(): number;
27
+ /** settle 所有 pending 为 deny 并清空(dev 热重载重建前调用)。 */
28
+ dispose(): void;
27
29
  };
28
30
  declare function createPermissionStore(opts?: PermissionStoreOptions): PermissionStore;
29
31
  export type { PendingPermission, PermissionRequest, PermissionStore, PermissionStoreOptions };
@@ -42,6 +42,15 @@ function createPermissionStore(opts = {}) {
42
42
  size() {
43
43
  return pending.size;
44
44
  },
45
+ dispose() {
46
+ // dev 热重载重建前调用:所有 pending settle 为 deny(避免悬空 Promise +
47
+ // timer 泄漏),clearTimeout 后清空 Map。
48
+ for (const [id, p] of pending) {
49
+ clearTimeout(p.timer);
50
+ p.resolve({ _tag: 'deny', reason: 'Server context disposed (hot reload)' });
51
+ pending.delete(id);
52
+ }
53
+ },
45
54
  };
46
55
  }
47
56
  export { createPermissionStore, DEFAULT_PERMISSION_TIMEOUT_MS };
@@ -2,7 +2,10 @@ import { readFile } from 'node:fs/promises';
2
2
  import { Hono } from 'hono';
3
3
  import { streamSSE } from 'hono/streaming';
4
4
  import { createAgent, runAgent } from '../../core/agent.js';
5
+ import { compactContext } from '../../core/loop.js';
5
6
  import { createSlashRegistry, parseSlashInput } from '../../core/slash.js';
7
+ import { injectSteering } from '../../core/steering.js';
8
+ import { containsWorkflow, WORKFLOW_NOTICE } from '../../core/workflow.js';
6
9
  import { getProject } from '../../project/project.js';
7
10
  import { getLLMSegments, getSession, updateSessionLastRun } from '../../session/session.js';
8
11
  import { upsertFileSnapshot } from '../../session/snapshot.js';
@@ -63,7 +66,42 @@ function createChatRoute(ctx) {
63
66
  return streamSSE(c, async (stream) => {
64
67
  try {
65
68
  const result = await cmd.execute(parsed.args, commandCtx);
66
- if (result._tag === 'error') {
69
+ if (result._tag === 'compact') {
70
+ // /compact:手动触发上下文压缩。复用 loop.compactContext(createSummarizer +
71
+ // runCompaction),不创建主 agent、不进入 LLM turn 循环(不把 /compact 当作
72
+ // user 消息发给模型)。
73
+ const provider = body.provider ?? ctx.config.defaultProvider;
74
+ const model = body.model ?? ctx.config.defaultModel;
75
+ const agentConfig = {
76
+ provider,
77
+ model,
78
+ tools: [],
79
+ plugins: ctx.config.plugins.enabled,
80
+ agentName: 'default',
81
+ };
82
+ const compactState = await createAgent(session, agentConfig, commandCtx.deps);
83
+ try {
84
+ for await (const event of compactContext(compactState, commandCtx.deps)) {
85
+ await stream.writeSSE({
86
+ event: event._tag,
87
+ data: JSON.stringify(event),
88
+ });
89
+ }
90
+ }
91
+ catch (e) {
92
+ await stream.writeSSE({
93
+ event: 'error',
94
+ data: JSON.stringify({
95
+ _tag: 'error',
96
+ error: {
97
+ _tag: 'unexpected',
98
+ message: e instanceof Error ? e.message : String(e),
99
+ },
100
+ }),
101
+ });
102
+ }
103
+ }
104
+ else if (result._tag === 'error') {
67
105
  await stream.writeSSE({
68
106
  event: 'error',
69
107
  data: JSON.stringify({
@@ -217,6 +255,11 @@ function createChatRoute(ctx) {
217
255
  startedAt: runStartedAt,
218
256
  });
219
257
  ctx.agentManager.register({ sessionId, state, deps });
258
+ // workflowz 关键词检测:用户消息包含独立关键词时注入工作流通知(steering),
259
+ // 引导模型用 task 工具批量 fan-out 做确定性多子 agent 分解。
260
+ if (containsWorkflow(message)) {
261
+ injectSteering(state, WORKFLOW_NOTICE);
262
+ }
220
263
  // 客户端断开时中止 agent
221
264
  stream.onAbort(() => {
222
265
  ctx.agentManager.abort(sessionId);