c0de-agent 1.10.0 → 1.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.
- package/README.md +28 -6
- package/dist/cli/commands/acp.js +3 -3
- package/dist/cli/commands/auth.d.ts +17 -0
- package/dist/cli/commands/auth.js +73 -0
- package/dist/cli/commands/chat.js +95 -1
- package/dist/cli/commands/config.d.ts +2 -9
- package/dist/cli/commands/config.js +25 -54
- package/dist/cli/commands/init.js +4 -2
- package/dist/cli/commands/plugin.js +8 -4
- package/dist/cli/commands/sessions.d.ts +10 -0
- package/dist/cli/commands/sessions.js +94 -0
- package/dist/cli/commands/update.js +18 -10
- package/dist/cli/index.js +51 -4
- package/dist/cli/modes/print.js +19 -2
- package/dist/core/compact.d.ts +2 -0
- package/dist/core/compact.js +1 -0
- package/dist/core/config-path.d.ts +9 -0
- package/dist/core/config-path.js +48 -0
- package/dist/core/config.d.ts +20 -3
- package/dist/core/config.js +103 -14
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/loop/compaction.d.ts +2 -1
- package/dist/core/loop/compaction.js +19 -6
- package/dist/core/loop/stream-collect.js +9 -1
- package/dist/core/loop/subagent.js +23 -5
- package/dist/core/loop.js +13 -3
- package/dist/core/redact.d.ts +11 -0
- package/dist/core/redact.js +49 -0
- package/dist/core/slash.js +167 -63
- package/dist/core/title.js +22 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/db/client.d.ts +4 -0
- package/dist/db/client.js +5 -0
- package/dist/db/schema.d.ts +51 -0
- package/dist/db/schema.js +6 -0
- package/dist/kanban/index.d.ts +1 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +5 -1
- package/dist/kanban/store.js +76 -2
- package/dist/llm/index.d.ts +1 -1
- package/dist/llm/index.js +1 -1
- package/dist/llm/provider.js +1 -1
- package/dist/llm/retry.d.ts +5 -2
- package/dist/llm/retry.js +6 -5
- package/dist/llm/routing.d.ts +24 -1
- package/dist/llm/routing.js +29 -1
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.js +1 -1
- package/dist/project/project.d.ts +14 -0
- package/dist/project/project.js +77 -1
- package/dist/server/agent-manager.d.ts +14 -0
- package/dist/server/agent-manager.js +41 -0
- package/dist/server/app.js +12 -2
- package/dist/server/auth-manager.d.ts +85 -0
- package/dist/server/auth-manager.js +289 -0
- package/dist/server/context.js +4 -0
- package/dist/server/middleware/auth.d.ts +4 -3
- package/dist/server/middleware/auth.js +5 -5
- package/dist/server/permission/interactive.d.ts +7 -2
- package/dist/server/permission/interactive.js +25 -5
- package/dist/server/permission/store.d.ts +15 -1
- package/dist/server/permission/store.js +31 -2
- package/dist/server/registry-config.d.ts +14 -0
- package/dist/server/registry-config.js +44 -0
- package/dist/server/routes/auth.d.ts +4 -0
- package/dist/server/routes/auth.js +124 -0
- package/dist/server/routes/chat.d.ts +18 -1
- package/dist/server/routes/chat.js +295 -37
- package/dist/server/routes/commands.d.ts +4 -2
- package/dist/server/routes/commands.js +10 -3
- package/dist/server/routes/config.js +143 -15
- package/dist/server/routes/files.js +22 -4
- package/dist/server/routes/kanban.js +68 -6
- package/dist/server/routes/permissions.d.ts +0 -1
- package/dist/server/routes/permissions.js +153 -4
- package/dist/server/routes/project.js +106 -3
- package/dist/server/routes/provider.js +48 -3
- package/dist/server/routes/session.js +319 -19
- package/dist/server/routes/terminal.js +17 -2
- package/dist/server/routes/update.d.ts +11 -5
- package/dist/server/routes/update.js +98 -16
- package/dist/server/server.d.ts +4 -12
- package/dist/server/server.js +235 -100
- package/dist/server/types.d.ts +15 -1
- package/dist/session/archive.d.ts +4 -2
- package/dist/session/archive.js +13 -1
- package/dist/session/branch.d.ts +1 -0
- package/dist/session/branch.js +32 -7
- package/dist/session/import.d.ts +44 -0
- package/dist/session/import.js +83 -0
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/jobs.d.ts +12 -0
- package/dist/session/jobs.js +53 -0
- package/dist/session/message.d.ts +7 -1
- package/dist/session/message.js +21 -1
- package/dist/session/session.d.ts +143 -6
- package/dist/session/session.js +561 -11
- package/dist/session/snapshot.d.ts +5 -2
- package/dist/session/snapshot.js +10 -2
- package/dist/session/types.d.ts +12 -2
- package/dist/shared/types/agent.d.ts +22 -0
- package/dist/shared/types/config.d.ts +10 -1
- package/dist/shared/types/kanban.d.ts +13 -0
- package/dist/shared/types/message.d.ts +15 -2
- package/dist/tools/index.js +2 -3
- package/dist/tools/websearch/index.js +6 -3
- package/dist/update/index.d.ts +3 -3
- package/dist/update/index.js +2 -2
- package/dist/update/ipc.d.ts +19 -2
- package/dist/update/ipc.js +43 -6
- package/dist/update/snapshot.d.ts +5 -0
- package/dist/update/snapshot.js +10 -0
- package/dist/update/updater.d.ts +69 -8
- package/dist/update/updater.js +263 -32
- package/drizzle/0006_sour_susan_delgado.sql +2 -0
- package/drizzle/0007_same_wilson_fisk.sql +2 -0
- package/drizzle/0008_tranquil_lily_hollister.sql +1 -0
- package/drizzle/meta/0006_snapshot.json +867 -0
- package/drizzle/meta/0007_snapshot.json +869 -0
- package/drizzle/meta/0008_snapshot.json +875 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +41 -42
package/dist/cli/index.js
CHANGED
|
@@ -6,11 +6,13 @@ import { loadConfig } from '../core/config.js';
|
|
|
6
6
|
import { createDB, migrateDB } from '../db/index.js';
|
|
7
7
|
import { acquireDevDbLock, releaseDevDbLock, resolveDbDir } from '../server/server.js';
|
|
8
8
|
import { runAcpCommand } from './commands/acp.js';
|
|
9
|
+
import { runAuthCommand } from './commands/auth.js';
|
|
9
10
|
import { runChatCommand } from './commands/chat.js';
|
|
10
11
|
import { runConfigCommand } from './commands/config.js';
|
|
11
12
|
import { runInitCommand } from './commands/init.js';
|
|
12
13
|
import { runPluginCommand } from './commands/plugin.js';
|
|
13
14
|
import { runServeCommand } from './commands/serve.js';
|
|
15
|
+
import { runSessionsCommand } from './commands/sessions.js';
|
|
14
16
|
import { runUpdateCommand } from './commands/update.js';
|
|
15
17
|
import { buildAgentDeps } from './deps.js';
|
|
16
18
|
import { parseCommand } from './parser.js';
|
|
@@ -23,6 +25,9 @@ const COMMANDS = [
|
|
|
23
25
|
{ name: 'format', type: 'string' },
|
|
24
26
|
{ name: 'yes', type: 'boolean', short: 'y' },
|
|
25
27
|
{ name: 'continue', type: 'string' },
|
|
28
|
+
// P2 修复:serve 运行时持久库被占用,此前静默退化为内存库(会话不保存,
|
|
29
|
+
// 仅 stderr 一行提示极易错过)。现须显式 --temp 才允许临时模式。
|
|
30
|
+
{ name: 'temp', type: 'boolean' },
|
|
26
31
|
],
|
|
27
32
|
},
|
|
28
33
|
{
|
|
@@ -50,10 +55,23 @@ const COMMANDS = [
|
|
|
50
55
|
description: 'List or enable plugins.',
|
|
51
56
|
options: [],
|
|
52
57
|
},
|
|
58
|
+
{
|
|
59
|
+
name: 'auth',
|
|
60
|
+
description: 'Manage authorized devices (list / revoke / reset).',
|
|
61
|
+
options: [],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'sessions',
|
|
65
|
+
description: 'Manage CLI/web sessions (list / delete / restore / deleted).',
|
|
66
|
+
options: [{ name: 'project', type: 'string' }],
|
|
67
|
+
},
|
|
53
68
|
{
|
|
54
69
|
name: 'acp',
|
|
55
70
|
description: 'Run in Agent Client Protocol mode (editor integration).',
|
|
56
|
-
options: [
|
|
71
|
+
options: [
|
|
72
|
+
// serve 占用持久库时的显式临时模式开关(与 chat --temp 同语义)。
|
|
73
|
+
{ name: 'temp', type: 'boolean' },
|
|
74
|
+
],
|
|
57
75
|
},
|
|
58
76
|
{
|
|
59
77
|
name: 'update',
|
|
@@ -96,11 +114,23 @@ async function withAgentDeps(cwd, opts, fn) {
|
|
|
96
114
|
holdLock = false;
|
|
97
115
|
if (!isDbLockConflict(err))
|
|
98
116
|
throw err;
|
|
117
|
+
if (opts.requirePersistent) {
|
|
118
|
+
throw new Error('c0de serve 正在运行并占用会话库,CLI 会话管理暂不可用(单写者限制)。\n' +
|
|
119
|
+
' 1) 在 Web 界面管理会话;\n' +
|
|
120
|
+
' 2) 或停止 serve(Ctrl+C)后重试本命令。');
|
|
121
|
+
}
|
|
99
122
|
if (opts.continueSessionId) {
|
|
100
123
|
throw new Error(`无法续聊会话 ${opts.continueSessionId}:持久库被 c0de serve 占用,内存模式无法续聊。\n` +
|
|
101
124
|
`请停止 serve 后重试,或去掉 --continue 开启新会话。`);
|
|
102
125
|
}
|
|
103
|
-
|
|
126
|
+
if (!opts.allowTemp) {
|
|
127
|
+
// P2 修复:锁冲突不再静默降级为不保存会话的内存模式(用户极易错过 stderr
|
|
128
|
+
// 提示导致对话丢失)。必须显式 --temp 确认放弃持久化。
|
|
129
|
+
throw new Error('c0de serve 正在运行并占用会话库,本次对话将无法保存。\n' +
|
|
130
|
+
' 1) 停止 serve 后重试(对话可持久化,推荐);\n' +
|
|
131
|
+
' 2) 或加 --temp 显式接受临时模式(消息与工具调用不会保存)。');
|
|
132
|
+
}
|
|
133
|
+
process.stderr.write('[c0de] ⚠ 临时模式:消息与工具调用不会保存。停止 serve 后重试可持久化,或直接使用浏览器界面。\n');
|
|
104
134
|
db = await createDB({ driver: 'pglite' });
|
|
105
135
|
await migrateDB(db);
|
|
106
136
|
}
|
|
@@ -139,7 +169,12 @@ async function dispatch(argv, overrides = {}) {
|
|
|
139
169
|
// --yes / -y 显式放行写操作;否则按 config.permission.defaultMode 决定(默认 safe)。
|
|
140
170
|
const strategy = args.options.yes ? 'full-auto' : undefined;
|
|
141
171
|
const continueId = args.options.continue;
|
|
142
|
-
|
|
172
|
+
const allowTemp = args.options.temp === true;
|
|
173
|
+
await withAgentDeps(cwd, {
|
|
174
|
+
...(strategy ? { strategy } : {}),
|
|
175
|
+
...(continueId ? { continueSessionId: continueId } : {}),
|
|
176
|
+
...(allowTemp ? { allowTemp } : {}),
|
|
177
|
+
}, (config, deps) => runChatCommand({ args, config, deps }));
|
|
143
178
|
return;
|
|
144
179
|
}
|
|
145
180
|
case 'init': {
|
|
@@ -154,9 +189,21 @@ async function dispatch(argv, overrides = {}) {
|
|
|
154
189
|
await runPluginCommand({ args, cwd });
|
|
155
190
|
return;
|
|
156
191
|
}
|
|
192
|
+
case 'auth': {
|
|
193
|
+
await runAuthCommand({ args });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
case 'sessions': {
|
|
197
|
+
// 需要持久库列出/清理会话;serve 运行时内存库不含数据,直接失败而不是列空表。
|
|
198
|
+
await withAgentDeps(cwd, { requirePersistent: true }, (_config, deps) => runSessionsCommand({ args, db: deps.db }));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
157
201
|
case 'acp': {
|
|
158
202
|
// ACP 非交互:所有工具放行(编辑器侧自行控制执行授权)。
|
|
159
|
-
await withAgentDeps(cwd, {
|
|
203
|
+
await withAgentDeps(cwd, {
|
|
204
|
+
strategy: 'full-auto',
|
|
205
|
+
...(args.options.temp === true ? { allowTemp: true } : {}),
|
|
206
|
+
}, (config, deps) => runAcpCommand({ config, deps }));
|
|
160
207
|
return;
|
|
161
208
|
}
|
|
162
209
|
case 'update': {
|
package/dist/cli/modes/print.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createAgent, runAgent } from '../../core/agent.js';
|
|
2
|
-
import {
|
|
2
|
+
import { resolveRoute } from '../../llm/registry.js';
|
|
3
|
+
import { createSession, getSession, upgradeTemporarySession } from '../../session/session.js';
|
|
3
4
|
import { resolveEnabledToolNames } from '../../tools/index.js';
|
|
4
5
|
/** 从事件流累积 assistant 文本。纯函数。 */
|
|
5
6
|
function collectAssistantText(events) {
|
|
@@ -23,10 +24,17 @@ async function runPrintMode(config, message, deps, opts = {}) {
|
|
|
23
24
|
const existing = await getSession(deps.db, opts.sessionId);
|
|
24
25
|
if (!existing)
|
|
25
26
|
throw new Error(`session not found: ${opts.sessionId}`);
|
|
27
|
+
// 续接即升级为持久会话:30 天临时清理不再触及(P1:此前 --continue 的
|
|
28
|
+
// 会话同样会在 30 天不活动后被物理删除,用户显式续接的历史静默丢失)。
|
|
29
|
+
if (existing.agentType === 'print') {
|
|
30
|
+
await upgradeTemporarySession(deps.db, opts.sessionId);
|
|
31
|
+
}
|
|
26
32
|
session = existing;
|
|
27
33
|
}
|
|
28
34
|
else {
|
|
29
|
-
|
|
35
|
+
// agentType='print':一次性 print 会话标记。purgeTemporarySessions 仅清理
|
|
36
|
+
// print/workflow 会话;普通 CLI 会话(ACP 等)与 --continue 续接的历史永不误删。
|
|
37
|
+
session = await createSession(deps.db, 'cli-print', undefined, 'print', 'cli');
|
|
30
38
|
}
|
|
31
39
|
const agentConfig = {
|
|
32
40
|
provider: config.defaultProvider,
|
|
@@ -36,6 +44,15 @@ async function runPrintMode(config, message, deps, opts = {}) {
|
|
|
36
44
|
plugins: config.plugins.enabled,
|
|
37
45
|
...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}),
|
|
38
46
|
};
|
|
47
|
+
// P3:首跑友好报错——未配置 provider 时不再让底层 NoRoute 异常裸抛,
|
|
48
|
+
// 给出与 Web 端一致的引导(c0de serve → 设置 → Provider)。
|
|
49
|
+
try {
|
|
50
|
+
resolveRoute(deps.llmRegistry, agentConfig.provider, agentConfig.model);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error('未配置可用的 AI 服务。请运行 `c0de serve` 并在「设置 → Provider」中添加 API 服务并测试连接,' +
|
|
54
|
+
'或使用 `c0de config set` 配置 providers。');
|
|
55
|
+
}
|
|
39
56
|
const state = await createAgent(session, agentConfig, deps);
|
|
40
57
|
const events = [];
|
|
41
58
|
for await (const event of runAgent(state, [{ _tag: 'text', text: message }], deps)) {
|
package/dist/core/compact.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { DB } from '../db/client.js';
|
|
2
2
|
import type { Registry } from '../llm/registry.js';
|
|
3
|
+
import type { FallbackChain } from '../llm/routing.js';
|
|
3
4
|
import type { CompactionConfig, CompactionResult, Summarizer } from '../session/types.js';
|
|
4
5
|
/** Create a Summarizer backed by an LLM streaming call. */
|
|
5
6
|
declare function createSummarizer(registry: Registry, provider: string, model: string, opts?: {
|
|
6
7
|
maxTokens?: number;
|
|
7
8
|
signal?: AbortSignal;
|
|
9
|
+
fallback?: FallbackChain;
|
|
8
10
|
}): Summarizer;
|
|
9
11
|
/** Run compaction on a session using the provided summarizer. */
|
|
10
12
|
declare function runCompaction(db: DB, sessionId: string, summarizer: Summarizer, config?: Partial<CompactionConfig>): Promise<CompactionResult>;
|
package/dist/core/compact.js
CHANGED
|
@@ -13,6 +13,7 @@ function createSummarizer(registry, provider, model, opts) {
|
|
|
13
13
|
for await (const chunk of chatStream({ registry, signal: opts?.signal }, request, {
|
|
14
14
|
provider,
|
|
15
15
|
model,
|
|
16
|
+
...(opts?.fallback ? { fallback: opts.fallback } : {}),
|
|
16
17
|
})) {
|
|
17
18
|
if (chunk._tag === 'text') {
|
|
18
19
|
chunks.push(chunk);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** 按 a.b.c 点路径从对象取值;找不到抛错。 */
|
|
2
|
+
declare function getByPath(obj: unknown, path: string): unknown;
|
|
3
|
+
/** 按 a.b.c 点路径写值(创建中间对象)。 */
|
|
4
|
+
declare function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void;
|
|
5
|
+
/** 把字符串解析为合适的标量类型。 */
|
|
6
|
+
declare function coerce(value: string): unknown;
|
|
7
|
+
/** 构造 a.b.c → { a: { b: { c: value } } } 的 patch 对象。 */
|
|
8
|
+
declare function setPathPatch(path: string, value: unknown): Record<string, unknown>;
|
|
9
|
+
export { coerce, getByPath, setByPath, setPathPatch };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// 配置点路径(a.b.c)读写工具:CLI `config set/get` 与斜杠 `/config` 共用。
|
|
2
|
+
/** 按 a.b.c 点路径从对象取值;找不到抛错。 */
|
|
3
|
+
function getByPath(obj, path) {
|
|
4
|
+
const parts = path.split('.');
|
|
5
|
+
let cur = obj;
|
|
6
|
+
for (const p of parts) {
|
|
7
|
+
if (cur === null || typeof cur !== 'object')
|
|
8
|
+
throw new Error(`config: key "${path}" not found`);
|
|
9
|
+
cur = cur[p];
|
|
10
|
+
if (cur === undefined)
|
|
11
|
+
throw new Error(`config: key "${path}" not found`);
|
|
12
|
+
}
|
|
13
|
+
return cur;
|
|
14
|
+
}
|
|
15
|
+
/** 按 a.b.c 点路径写值(创建中间对象)。 */
|
|
16
|
+
function setByPath(obj, path, value) {
|
|
17
|
+
const parts = path.split('.');
|
|
18
|
+
let cur = obj;
|
|
19
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
20
|
+
const k = parts[i];
|
|
21
|
+
const next = cur[k];
|
|
22
|
+
cur[k] = (next !== null && typeof next === 'object' ? next : {});
|
|
23
|
+
cur = cur[k];
|
|
24
|
+
}
|
|
25
|
+
cur[parts[parts.length - 1]] = value;
|
|
26
|
+
}
|
|
27
|
+
/** 把字符串解析为合适的标量类型。 */
|
|
28
|
+
function coerce(value) {
|
|
29
|
+
if (value === 'true')
|
|
30
|
+
return true;
|
|
31
|
+
if (value === 'false')
|
|
32
|
+
return false;
|
|
33
|
+
if (/^-?\d+(\.\d+)?$/.test(value))
|
|
34
|
+
return Number(value);
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(value);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** 构造 a.b.c → { a: { b: { c: value } } } 的 patch 对象。 */
|
|
43
|
+
function setPathPatch(path, value) {
|
|
44
|
+
const root = {};
|
|
45
|
+
setByPath(root, path, value);
|
|
46
|
+
return root;
|
|
47
|
+
}
|
|
48
|
+
export { coerce, getByPath, setByPath, setPathPatch };
|
package/dist/core/config.d.ts
CHANGED
|
@@ -10,15 +10,32 @@ declare function loadConfigScopes(projectDir?: string): {
|
|
|
10
10
|
global: Partial<Config> | undefined;
|
|
11
11
|
project: Partial<Config> | undefined;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* 把 patch 应用到某个作用域的原始配置(scoped patch,null=删除):
|
|
15
|
+
* - 深合并:嵌套普通对象递归合并,数组整体替换(providers 等列表语义);
|
|
16
|
+
* - 值为 undefined 的键跳过;
|
|
17
|
+
* - 值为 null 的键从结果中**删除**——作用域内取消覆盖,回落到另一作用域/默认值。
|
|
18
|
+
* Config 各字段均无合法的 null 值(可选字段用 undefined),null 作为「unset」标记是安全的。
|
|
19
|
+
* 不注入默认值。CLI config set / 服务端 PATCH /api/config 共用。
|
|
20
|
+
*/
|
|
21
|
+
declare function applyScopedPatch(base: Record<string, unknown> | undefined, patch: Record<string, unknown>): Record<string, unknown>;
|
|
13
22
|
/**
|
|
14
23
|
* 不注入默认值的深合并:仅合并传入对象的自有键。
|
|
15
24
|
* 数组整体替换(providers 等列表语义);嵌套普通对象递归合并。
|
|
16
25
|
* 用于「patch 合并进某个作用域的原始文件」后再写盘。
|
|
17
26
|
*/
|
|
18
27
|
declare function mergeRaw(...cfgs: (Record<string, unknown> | undefined)[]): Record<string, unknown>;
|
|
19
|
-
/** 把 raw JSON 写回指定作用域配置文件(不含默认值)。
|
|
28
|
+
/** 把 raw JSON 写回指定作用域配置文件(不含默认值)。
|
|
29
|
+
* - 落盘前对 providers[].apiKey 加密(已带 enc: 前缀/空值透传)——
|
|
30
|
+
* CLI config set 与 /config 斜杠命令共用此路径,保证「apiKey 不明文落盘」全链路成立。
|
|
31
|
+
* - 写入后 chmod 600:配置文件可能含 token/加密密钥,默认 0644 同机可读。 */
|
|
20
32
|
declare function saveConfigScoped(scope: 'global' | 'project', projectDir: string | undefined, data: Record<string, unknown>): Promise<void>;
|
|
21
33
|
declare function loadConfig(projectDir?: string): Promise<Config>;
|
|
22
|
-
|
|
34
|
+
/** Config 全部顶层键(DEFAULT_CONFIG + 可选键)。未知顶层键校验与告警共用。 */
|
|
35
|
+
declare const KNOWN_CONFIG_KEYS: Set<string>;
|
|
36
|
+
/** 收集对象中不属于 Config 顶层键的未知键。 */
|
|
37
|
+
declare function collectUnknownConfigKeys(data: Record<string, unknown> | undefined): string[];
|
|
38
|
+
/** 加载配置时对未知顶层键告警(拼写错误/旧版键静默失效的唯一提示)。 */
|
|
39
|
+
declare function warnUnknownConfigKeys(scope: 'global' | 'project', data: Record<string, unknown> | undefined): void;
|
|
23
40
|
export type { AgentsConfig, CompactionConfig, Config, MCPServerConfig, PermissionConfig, SecurityConfig, ToolMetricsConfig, UpdateConfig, WebSearchConfig, };
|
|
24
|
-
export { DEFAULT_CONFIG, loadConfig, loadConfigScopes, mergeConfig, mergeRaw,
|
|
41
|
+
export { applyScopedPatch, collectUnknownConfigKeys, DEFAULT_CONFIG, KNOWN_CONFIG_KEYS, loadConfig, loadConfigScopes, mergeConfig, mergeRaw, saveConfigScoped, warnUnknownConfigKeys, };
|
package/dist/core/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { encryptSecret, isEncryptedSecret } from './secret.js';
|
|
4
5
|
const GLOBAL_CONFIG_DIR = '.c0de';
|
|
5
6
|
const CONFIG_FILENAME = 'config.json';
|
|
6
7
|
const DEFAULT_CONFIG = {
|
|
@@ -21,7 +22,9 @@ const DEFAULT_CONFIG = {
|
|
|
21
22
|
tools: { enabled: [], disabled: [] },
|
|
22
23
|
plugins: { enabled: [] },
|
|
23
24
|
mcpServers: [],
|
|
24
|
-
slashCommands: {
|
|
25
|
+
slashCommands: {
|
|
26
|
+
enabled: ['/compact', '/model', '/clear', '/help', '/fork', '/config', '/workflow'],
|
|
27
|
+
},
|
|
25
28
|
toolMetrics: { enabled: true, threshold: 0.8, minSamples: 5 },
|
|
26
29
|
security: { authEnabled: true, allowedOrigins: [] },
|
|
27
30
|
websearch: { provider: 'auto' },
|
|
@@ -29,7 +32,6 @@ const DEFAULT_CONFIG = {
|
|
|
29
32
|
permission: { defaultMode: 'default' },
|
|
30
33
|
update: { enabled: true, intervalMs: 60 * 60 * 1000, initialDelayMs: 10_000 },
|
|
31
34
|
theme: 'system',
|
|
32
|
-
locale: 'en',
|
|
33
35
|
};
|
|
34
36
|
function mergeConfig(...configs) {
|
|
35
37
|
const result = structuredClone(DEFAULT_CONFIG);
|
|
@@ -78,6 +80,37 @@ function loadConfigScopes(projectDir) {
|
|
|
78
80
|
const projectPath = join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME);
|
|
79
81
|
return { global: readJsonIfExists(globalPath), project: readJsonIfExists(projectPath) };
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* 把 patch 应用到某个作用域的原始配置(scoped patch,null=删除):
|
|
85
|
+
* - 深合并:嵌套普通对象递归合并,数组整体替换(providers 等列表语义);
|
|
86
|
+
* - 值为 undefined 的键跳过;
|
|
87
|
+
* - 值为 null 的键从结果中**删除**——作用域内取消覆盖,回落到另一作用域/默认值。
|
|
88
|
+
* Config 各字段均无合法的 null 值(可选字段用 undefined),null 作为「unset」标记是安全的。
|
|
89
|
+
* 不注入默认值。CLI config set / 服务端 PATCH /api/config 共用。
|
|
90
|
+
*/
|
|
91
|
+
function applyScopedPatch(base, patch) {
|
|
92
|
+
const result = { ...(base ?? {}) };
|
|
93
|
+
for (const [key, val] of Object.entries(patch)) {
|
|
94
|
+
if (val === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
if (val === null) {
|
|
97
|
+
delete result[key];
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const current = result[key];
|
|
101
|
+
if (typeof val === 'object' &&
|
|
102
|
+
!Array.isArray(val) &&
|
|
103
|
+
current !== null &&
|
|
104
|
+
typeof current === 'object' &&
|
|
105
|
+
!Array.isArray(current)) {
|
|
106
|
+
result[key] = applyScopedPatch(current, val);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
result[key] = val;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
81
114
|
/**
|
|
82
115
|
* 不注入默认值的深合并:仅合并传入对象的自有键。
|
|
83
116
|
* 数组整体替换(providers 等列表语义);嵌套普通对象递归合并。
|
|
@@ -107,7 +140,10 @@ function mergeRaw(...cfgs) {
|
|
|
107
140
|
}
|
|
108
141
|
return result;
|
|
109
142
|
}
|
|
110
|
-
/** 把 raw JSON 写回指定作用域配置文件(不含默认值)。
|
|
143
|
+
/** 把 raw JSON 写回指定作用域配置文件(不含默认值)。
|
|
144
|
+
* - 落盘前对 providers[].apiKey 加密(已带 enc: 前缀/空值透传)——
|
|
145
|
+
* CLI config set 与 /config 斜杠命令共用此路径,保证「apiKey 不明文落盘」全链路成立。
|
|
146
|
+
* - 写入后 chmod 600:配置文件可能含 token/加密密钥,默认 0644 同机可读。 */
|
|
111
147
|
async function saveConfigScoped(scope, projectDir, data) {
|
|
112
148
|
const dir = scope === 'global'
|
|
113
149
|
? join(homedir(), GLOBAL_CONFIG_DIR)
|
|
@@ -115,22 +151,75 @@ async function saveConfigScoped(scope, projectDir, data) {
|
|
|
115
151
|
if (!existsSync(dir))
|
|
116
152
|
mkdirSync(dir, { recursive: true });
|
|
117
153
|
const path = join(dir, CONFIG_FILENAME);
|
|
118
|
-
|
|
154
|
+
const hardened = redactSensitiveOnSave(data);
|
|
155
|
+
writeFileSync(path, JSON.stringify(hardened, null, 2), 'utf-8');
|
|
156
|
+
try {
|
|
157
|
+
chmodSync(path, 0o600);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// 平台不支持(Windows 等)时忽略;内容仍已加密。
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** 落盘前的敏感值处理:providers[].apiKey 与 websearch 后端 key 明文 → enc: 加密
|
|
164
|
+
* (spec §24.2;此前 websearch.tavilyApiKey/braveApiKey 明文落盘,与 provider
|
|
165
|
+
* apiKey 安全叙事不一致——git 误提交警告会命中它们,加密却不会)。 */
|
|
166
|
+
function redactSensitiveOnSave(data) {
|
|
167
|
+
const out = {};
|
|
168
|
+
for (const [key, val] of Object.entries(data)) {
|
|
169
|
+
if (key === 'providers' && Array.isArray(val)) {
|
|
170
|
+
out[key] = val.map((p) => {
|
|
171
|
+
if (p === null || typeof p !== 'object')
|
|
172
|
+
return p;
|
|
173
|
+
const apiKey = p.apiKey;
|
|
174
|
+
if (typeof apiKey === 'string' && apiKey.length > 0 && !isEncryptedSecret(apiKey)) {
|
|
175
|
+
return { ...p, apiKey: encryptSecret(apiKey) };
|
|
176
|
+
}
|
|
177
|
+
return p;
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (key === 'websearch' && val !== null && typeof val === 'object') {
|
|
182
|
+
const ws = { ...val };
|
|
183
|
+
for (const k of ['tavilyApiKey', 'braveApiKey']) {
|
|
184
|
+
const v = ws[k];
|
|
185
|
+
if (typeof v === 'string' && v.length > 0 && !isEncryptedSecret(v)) {
|
|
186
|
+
ws[k] = encryptSecret(v);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
out[key] = ws;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
out[key] = val;
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
119
195
|
}
|
|
120
196
|
async function loadConfig(projectDir) {
|
|
121
197
|
const globalPath = join(homedir(), GLOBAL_CONFIG_DIR, CONFIG_FILENAME);
|
|
122
198
|
const projectPath = join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME);
|
|
123
199
|
const global = readJsonIfExists(globalPath);
|
|
124
200
|
const project = readJsonIfExists(projectPath);
|
|
201
|
+
warnUnknownConfigKeys('global', global);
|
|
202
|
+
warnUnknownConfigKeys('project', project);
|
|
125
203
|
return mergeConfig(global, project);
|
|
126
204
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
205
|
+
/** Config 全部顶层键(DEFAULT_CONFIG + 可选键)。未知顶层键校验与告警共用。 */
|
|
206
|
+
const KNOWN_CONFIG_KEYS = new Set([
|
|
207
|
+
...Object.keys(DEFAULT_CONFIG),
|
|
208
|
+
// 可选键不在 DEFAULT_CONFIG 中(缺失=未配置),但属于合法键。
|
|
209
|
+
'commitModel',
|
|
210
|
+
]);
|
|
211
|
+
/** 收集对象中不属于 Config 顶层键的未知键。 */
|
|
212
|
+
function collectUnknownConfigKeys(data) {
|
|
213
|
+
if (!data)
|
|
214
|
+
return [];
|
|
215
|
+
return Object.keys(data).filter((k) => !KNOWN_CONFIG_KEYS.has(k));
|
|
216
|
+
}
|
|
217
|
+
/** 加载配置时对未知顶层键告警(拼写错误/旧版键静默失效的唯一提示)。 */
|
|
218
|
+
function warnUnknownConfigKeys(scope, data) {
|
|
219
|
+
const unknown = collectUnknownConfigKeys(data);
|
|
220
|
+
if (unknown.length === 0)
|
|
221
|
+
return;
|
|
222
|
+
console.warn(`[config] ${scope} 配置包含未知键:${unknown.join(', ')}。` +
|
|
223
|
+
`这些键不会生效——请检查拼写,或移除过时配置。有效顶层键:${[...KNOWN_CONFIG_KEYS].join(', ')}`);
|
|
135
224
|
}
|
|
136
|
-
export { DEFAULT_CONFIG, loadConfig, loadConfigScopes, mergeConfig, mergeRaw,
|
|
225
|
+
export { applyScopedPatch, collectUnknownConfigKeys, DEFAULT_CONFIG, KNOWN_CONFIG_KEYS, loadConfig, loadConfigScopes, mergeConfig, mergeRaw, saveConfigScoped, warnUnknownConfigKeys, };
|
package/dist/core/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { abortAgent, createAgent, getAgentStatus, isAgentPaused, pauseAgent, resumeAgent, runAgent, } from './agent.js';
|
|
2
2
|
export { createSummarizer, runCompaction } from './compact.js';
|
|
3
3
|
export type { CompactionConfig, Config, MCPServerConfig } from './config.js';
|
|
4
|
-
export { DEFAULT_CONFIG, loadConfig, mergeConfig
|
|
4
|
+
export { DEFAULT_CONFIG, loadConfig, mergeConfig } from './config.js';
|
|
5
5
|
export { calibrateEstimate, createTokenBudget, estimateBudget, fitToBudget, shouldCompact, } from './context.js';
|
|
6
6
|
export { agentLoop } from './loop.js';
|
|
7
7
|
export { buildSystemPrompt } from './prompt.js';
|
package/dist/core/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// steering, slash commands, and compaction bridge.
|
|
3
3
|
export { abortAgent, createAgent, getAgentStatus, isAgentPaused, pauseAgent, resumeAgent, runAgent, } from './agent.js';
|
|
4
4
|
export { createSummarizer, runCompaction } from './compact.js';
|
|
5
|
-
export { DEFAULT_CONFIG, loadConfig, mergeConfig
|
|
5
|
+
export { DEFAULT_CONFIG, loadConfig, mergeConfig } from './config.js';
|
|
6
6
|
export { calibrateEstimate, createTokenBudget, estimateBudget, fitToBudget, shouldCompact, } from './context.js';
|
|
7
7
|
export { agentLoop } from './loop.js';
|
|
8
8
|
export { buildSystemPrompt } from './prompt.js';
|
|
@@ -27,5 +27,6 @@ export declare function recoverFromOverflow(state: AgentState, deps: LoopDeps):
|
|
|
27
27
|
* 死锁在下一轮用户输入(agentLoop 重入)时重置。
|
|
28
28
|
* - mid-turn:midTurnEnabled 单独 opt-in(默认关闭)时按 enabled:true 阈值静默压缩,
|
|
29
29
|
* 复用 shouldCompact 逻辑但不发 error/warning,压缩后刷新内存消息视图。
|
|
30
|
-
* 两者都静默消费 compactContext 的 text_delta
|
|
30
|
+
* 两者都静默消费 compactContext 的 text_delta 通知,但把 compaction_done 透传给前端,
|
|
31
|
+
* 让用户在上下文被改写时得到提示(可在归档面板查看原始内容)。 */
|
|
31
32
|
export declare function runCompactionIfNeeded(state: AgentState, deps: LoopDeps, latestMessages: Message[]): AsyncGenerator<AgentEvent>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildFallbackChain } from '../../llm/routing.js';
|
|
1
2
|
import { getMessages } from '../../session/message.js';
|
|
2
3
|
import { createSummarizer, runCompaction } from '../compact.js';
|
|
3
4
|
import { estimateBudget, shouldCompact } from '../context.js';
|
|
@@ -13,7 +14,13 @@ import { estimateBudget, shouldCompact } from '../context.js';
|
|
|
13
14
|
export async function* compactContext(state, deps) {
|
|
14
15
|
// summarizer 优先用 compactionModel 覆盖,否则回退当前会话 provider/model。
|
|
15
16
|
const cm = state.compactionModel;
|
|
16
|
-
const
|
|
17
|
+
const provider = cm ? cm.provider : state.config.provider;
|
|
18
|
+
const model = cm ? cm.model : state.config.model;
|
|
19
|
+
const fallback = buildFallbackChain(deps.config, provider, model);
|
|
20
|
+
const summarizer = createSummarizer(deps.llmRegistry, provider, model, {
|
|
21
|
+
signal: state.abortController.signal,
|
|
22
|
+
...(fallback ? { fallback } : {}),
|
|
23
|
+
});
|
|
17
24
|
const result = await runCompaction(deps.db, state.session.id, summarizer, {
|
|
18
25
|
keepRecentTokens: deps.config.compaction.keepRecentTokens,
|
|
19
26
|
});
|
|
@@ -77,15 +84,19 @@ export async function recoverFromOverflow(state, deps) {
|
|
|
77
84
|
* 死锁在下一轮用户输入(agentLoop 重入)时重置。
|
|
78
85
|
* - mid-turn:midTurnEnabled 单独 opt-in(默认关闭)时按 enabled:true 阈值静默压缩,
|
|
79
86
|
* 复用 shouldCompact 逻辑但不发 error/warning,压缩后刷新内存消息视图。
|
|
80
|
-
* 两者都静默消费 compactContext 的 text_delta
|
|
87
|
+
* 两者都静默消费 compactContext 的 text_delta 通知,但把 compaction_done 透传给前端,
|
|
88
|
+
* 让用户在上下文被改写时得到提示(可在归档面板查看原始内容)。 */
|
|
81
89
|
export async function* runCompactionIfNeeded(state, deps, latestMessages) {
|
|
82
90
|
// 死锁时跳过自动压缩:上一轮压缩已证明无法释放足够空间,重试只会无限循环。
|
|
83
91
|
if (!state.compactionDeadEnd &&
|
|
84
92
|
shouldCompact(latestMessages, state.tokenBudget, deps.config.compaction)) {
|
|
85
93
|
// 自动压缩:复用 compactContext;失败非致命,仅记录警告不中断主循环。
|
|
86
94
|
try {
|
|
87
|
-
for await (const
|
|
88
|
-
//
|
|
95
|
+
for await (const ev of compactContext(state, deps)) {
|
|
96
|
+
// 静默消费 text_delta 通知,但把 compaction_done 透传给前端——
|
|
97
|
+
// 上下文被改写,用户应被告知(可在归档面板查看原始内容),而非悄然失忆。
|
|
98
|
+
if (ev._tag === 'compaction_done')
|
|
99
|
+
yield ev;
|
|
89
100
|
}
|
|
90
101
|
// 进度保护:compactContext 已重算 state.tokenBudget.used。若压缩后仍超阈值
|
|
91
102
|
// (典型成因:keepRecentTokens 本身已超 historyBudget),压缩无法再释放足够
|
|
@@ -124,8 +135,10 @@ export async function* runCompactionIfNeeded(state, deps, latestMessages) {
|
|
|
124
135
|
// 中轮压缩静默执行:compactContext 内部已发 text_delta 通知,
|
|
125
136
|
// 不再 yield error/warning(与 turn-end 自动压缩的进度保护语义不同)。
|
|
126
137
|
try {
|
|
127
|
-
for await (const
|
|
128
|
-
//
|
|
138
|
+
for await (const ev of compactContext(state, deps)) {
|
|
139
|
+
// 静默消费 text_delta 通知,透传 compaction_done(与 turn-end 一致)。
|
|
140
|
+
if (ev._tag === 'compaction_done')
|
|
141
|
+
yield ev;
|
|
129
142
|
}
|
|
130
143
|
// 压缩改写了消息历史 → 刷新内存视图,供下一轮上下文重建使用
|
|
131
144
|
state.messages = await getMessages(deps.db, state.session.id);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isContextOverflowFailure } from '../../llm/provider-error.js';
|
|
2
|
+
import { buildFallbackChain } from '../../llm/routing.js';
|
|
2
3
|
import { isLLMError } from '../../llm/schema/errors.js';
|
|
3
4
|
import { recoverFromOverflow } from './compaction.js';
|
|
4
5
|
/** 入参是否为协议层/loop 标记的解析失败(携带 _parseError / _raw 容错标记)。
|
|
@@ -60,10 +61,17 @@ export async function* collectStreamChunks(state, deps, streamFn, request, overf
|
|
|
60
61
|
...over,
|
|
61
62
|
});
|
|
62
63
|
try {
|
|
64
|
+
// config.fallback 接线:enabled 时按配置重试参数 + 声明同模型的其他 provider 回退。
|
|
65
|
+
// 未启用时保持内置默认(3 次重试、无跨 provider 回退),行为与以往一致。
|
|
66
|
+
const fallback = buildFallbackChain(deps.config, state.config.provider, state.config.model);
|
|
63
67
|
for await (const chunk of streamFn({
|
|
64
68
|
registry: deps.llmRegistry,
|
|
65
69
|
signal: state.abortController.signal,
|
|
66
|
-
}, request, {
|
|
70
|
+
}, request, {
|
|
71
|
+
provider: state.config.provider,
|
|
72
|
+
model: state.config.model,
|
|
73
|
+
...(fallback ? { fallback } : {}),
|
|
74
|
+
})) {
|
|
67
75
|
if (firstTokenTime === null && chunk._tag !== 'done') {
|
|
68
76
|
firstTokenTime = Date.now();
|
|
69
77
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { appendMessage } from '../../session/message.js';
|
|
2
|
-
import { createSession } from '../../session/session.js';
|
|
2
|
+
import { createSession, updateSessionLastRun } from '../../session/session.js';
|
|
3
3
|
import { generateId } from '../../shared/index.js';
|
|
4
4
|
import { createAgent, runAgent } from '../agent.js';
|
|
5
5
|
import { applyPatchToParent, captureBaseline, captureDeltaPatch, createWorktree, removeWorktree, } from '../worktree.js';
|
|
@@ -35,10 +35,11 @@ export async function runSubAgent(deps, parent, request) {
|
|
|
35
35
|
description: request.description ?? '',
|
|
36
36
|
background: request.background ?? false,
|
|
37
37
|
});
|
|
38
|
-
// 2. 创建子 session(记录 agentType
|
|
38
|
+
// 2. 创建子 session(记录 agentType;parentId 挂到父会话——树内嵌套、
|
|
39
|
+
// 删除父会话时级联,不再是游离根节点)
|
|
39
40
|
let childSession;
|
|
40
41
|
try {
|
|
41
|
-
childSession = await createSession(deps.db, title, parent.session.projectId ?? undefined, request.agentType);
|
|
42
|
+
childSession = await createSession(deps.db, title, parent.session.projectId ?? undefined, request.agentType, parent.session.source ?? undefined, parent.session.id);
|
|
42
43
|
}
|
|
43
44
|
catch (e) {
|
|
44
45
|
return { _tag: 'error', error: e instanceof Error ? e.message : String(e) };
|
|
@@ -147,13 +148,30 @@ export async function runSubAgent(deps, parent, request) {
|
|
|
147
148
|
// 6. background 模式:fork 异步运行,立即返回 running;完成时向父 session 注入合成通知
|
|
148
149
|
if (request.background) {
|
|
149
150
|
const jobId = childSession.id;
|
|
151
|
+
// P2:落库 running 标记——进程崩溃后 markDeadBackgroundJobs 可识别悬空任务
|
|
152
|
+
// 并给父会话发失败通知(此前仅内存 jobId,重启即静默丢失)。
|
|
153
|
+
// 此写 await 完成再返回:保证「返回 running」时 DB 已记录;同时避免与
|
|
154
|
+
// 子 loop 的并发写入叠加。
|
|
155
|
+
await updateSessionLastRun(deps.db, childSession.id, {
|
|
156
|
+
status: 'running',
|
|
157
|
+
agentName: request.agentType,
|
|
158
|
+
startedAt: Date.now(),
|
|
159
|
+
}).catch(() => { });
|
|
150
160
|
void runBody()
|
|
151
|
-
.then((result) => {
|
|
161
|
+
.then(async (result) => {
|
|
162
|
+
// 无论成败都置 completed:任务已终结,状态体现在合成通知里。
|
|
163
|
+
// 顺序 await(不并发发起):PGLite WASM 对同实例并发查询会忙等,
|
|
164
|
+
// 与 appendMessage 并发叠加曾导致 100% CPU 自旋(loop.test.ts 复现)。
|
|
165
|
+
await updateSessionLastRun(deps.db, childSession.id, {
|
|
166
|
+
status: 'completed',
|
|
167
|
+
agentName: request.agentType,
|
|
168
|
+
startedAt: Date.now(),
|
|
169
|
+
}).catch(() => { });
|
|
152
170
|
const success = result._tag === 'success';
|
|
153
171
|
const output = success ? result.output : result.error;
|
|
154
172
|
const tag = success ? 'task_result' : 'task_error';
|
|
155
173
|
const synthetic = `<task id="${childSession.id}" state="${success ? 'completed' : 'failed'}">\n<${tag}>\n${output}\n</${tag}>\n</task>`;
|
|
156
|
-
|
|
174
|
+
await appendMessage(deps.db, parent.session.id, {
|
|
157
175
|
role: 'user',
|
|
158
176
|
content: [{ _tag: 'text', text: synthetic }],
|
|
159
177
|
}).catch((e) => {
|
package/dist/core/loop.js
CHANGED
|
@@ -3,6 +3,7 @@ import { resolveRoute } from '../llm/registry.js';
|
|
|
3
3
|
import { detectProjectInfo } from '../project/detect.js';
|
|
4
4
|
import { entriesToChatMessages, getSessionContext } from '../session/context.js';
|
|
5
5
|
import { getMessages } from '../session/message.js';
|
|
6
|
+
import { updateSessionLastRun } from '../session/session.js';
|
|
6
7
|
import { estimateTokens } from '../session/token.js';
|
|
7
8
|
import { calibrateEstimate, createTokenBudget, estimateBudget } from './context.js';
|
|
8
9
|
import { runCompactionIfNeeded } from './loop/compaction.js';
|
|
@@ -69,11 +70,20 @@ export async function* agentLoop(state, deps) {
|
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
71
72
|
if (state.status._tag === 'paused') {
|
|
72
|
-
|
|
73
|
+
// 暂停点:上一轮原子操作(工具执行批次)已完成。
|
|
74
|
+
// 持久化 paused 状态供热更新/重启后恢复,随后阻塞等待 resume。
|
|
75
|
+
state.pauseYielded = false;
|
|
73
76
|
yield { _tag: 'status_change', status: state.status };
|
|
77
|
+
await updateSessionLastRun(deps.db, state.session.id, {
|
|
78
|
+
status: 'paused',
|
|
79
|
+
agentName: state.config.agentName,
|
|
80
|
+
provider: state.config.provider,
|
|
81
|
+
model: state.config.model,
|
|
82
|
+
startedAt: state.lastRunStartedAt ?? Date.now(),
|
|
83
|
+
}).catch(() => { });
|
|
74
84
|
await waitForResume(state);
|
|
75
|
-
if (state.status._tag
|
|
76
|
-
return;
|
|
85
|
+
if (state.status._tag === 'paused')
|
|
86
|
+
return; // 等待期间被 abort 等终结
|
|
77
87
|
}
|
|
78
88
|
state.status = { _tag: 'running', turnCount: turn };
|
|
79
89
|
const steering = drainSteering(state);
|