c0de-agent 1.9.0 → 1.11.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 (109) hide show
  1. package/README.md +25 -8
  2. package/dist/cli/commands/acp.js +5 -3
  3. package/dist/cli/commands/auth.d.ts +17 -0
  4. package/dist/cli/commands/auth.js +73 -0
  5. package/dist/cli/commands/chat.js +10 -0
  6. package/dist/cli/commands/config.d.ts +2 -9
  7. package/dist/cli/commands/config.js +15 -54
  8. package/dist/cli/commands/init.js +4 -2
  9. package/dist/cli/commands/plugin.js +8 -4
  10. package/dist/cli/commands/serve.d.ts +1 -0
  11. package/dist/cli/commands/serve.js +4 -1
  12. package/dist/cli/commands/sessions.d.ts +10 -0
  13. package/dist/cli/commands/sessions.js +35 -0
  14. package/dist/cli/commands/update.js +18 -10
  15. package/dist/cli/deps.d.ts +7 -5
  16. package/dist/cli/deps.js +23 -7
  17. package/dist/cli/index.d.ts +1 -1
  18. package/dist/cli/index.js +96 -37
  19. package/dist/cli/modes/print.d.ts +3 -1
  20. package/dist/cli/modes/print.js +31 -4
  21. package/dist/core/agent.js +3 -4
  22. package/dist/core/config-path.d.ts +9 -0
  23. package/dist/core/config-path.js +48 -0
  24. package/dist/core/config.d.ts +27 -2
  25. package/dist/core/config.js +83 -13
  26. package/dist/core/index.d.ts +1 -1
  27. package/dist/core/index.js +1 -1
  28. package/dist/core/loop.js +14 -4
  29. package/dist/core/slash.js +92 -30
  30. package/dist/core/title.js +15 -0
  31. package/dist/core/types.d.ts +2 -0
  32. package/dist/core/workflows/discovery.d.ts +1 -1
  33. package/dist/core/workflows/index.d.ts +1 -1
  34. package/dist/db/client.d.ts +4 -0
  35. package/dist/db/client.js +5 -0
  36. package/dist/db/schema.d.ts +51 -0
  37. package/dist/db/schema.js +7 -1
  38. package/dist/server/agent-manager.d.ts +20 -0
  39. package/dist/server/agent-manager.js +63 -11
  40. package/dist/server/app.js +12 -1
  41. package/dist/server/auth-manager.d.ts +79 -0
  42. package/dist/server/auth-manager.js +270 -0
  43. package/dist/server/context.js +3 -0
  44. package/dist/server/middleware/auth.d.ts +4 -3
  45. package/dist/server/middleware/auth.js +5 -5
  46. package/dist/server/middleware/cors.d.ts +11 -2
  47. package/dist/server/middleware/cors.js +15 -12
  48. package/dist/server/permission/interactive.d.ts +2 -0
  49. package/dist/server/permission/interactive.js +5 -1
  50. package/dist/server/permission/store.d.ts +4 -0
  51. package/dist/server/permission/store.js +1 -0
  52. package/dist/server/routes/auth.d.ts +4 -0
  53. package/dist/server/routes/auth.js +94 -0
  54. package/dist/server/routes/chat.d.ts +6 -1
  55. package/dist/server/routes/chat.js +294 -153
  56. package/dist/server/routes/config.js +60 -8
  57. package/dist/server/routes/permissions.d.ts +4 -1
  58. package/dist/server/routes/permissions.js +27 -4
  59. package/dist/server/routes/project.js +56 -2
  60. package/dist/server/routes/session.js +37 -9
  61. package/dist/server/routes/terminal.js +29 -2
  62. package/dist/server/routes/update.d.ts +1 -0
  63. package/dist/server/routes/update.js +33 -4
  64. package/dist/server/routes/workflows.js +2 -2
  65. package/dist/server/server.d.ts +24 -1
  66. package/dist/server/server.js +149 -29
  67. package/dist/server/types.d.ts +17 -2
  68. package/dist/session/archive.d.ts +1 -1
  69. package/dist/session/branch.d.ts +11 -2
  70. package/dist/session/branch.js +107 -31
  71. package/dist/session/compaction.js +1 -0
  72. package/dist/session/context.d.ts +3 -2
  73. package/dist/session/context.js +36 -3
  74. package/dist/session/index.d.ts +1 -1
  75. package/dist/session/index.js +1 -1
  76. package/dist/session/session.d.ts +25 -5
  77. package/dist/session/session.js +129 -10
  78. package/dist/session/snapshot.d.ts +9 -3
  79. package/dist/session/snapshot.js +45 -4
  80. package/dist/session/squash.js +1 -0
  81. package/dist/session/types.d.ts +3 -1
  82. package/dist/shared/types/agent.d.ts +11 -0
  83. package/dist/shared/types/config.d.ts +12 -6
  84. package/dist/shared/types/message.d.ts +6 -2
  85. package/dist/shared/utils/path.d.ts +2 -3
  86. package/dist/shared/utils/path.js +20 -4
  87. package/dist/tools/builtin/todo.js +5 -4
  88. package/dist/tools/index.d.ts +14 -3
  89. package/dist/tools/index.js +35 -14
  90. package/dist/update/index.d.ts +2 -2
  91. package/dist/update/index.js +2 -2
  92. package/dist/update/ipc.d.ts +38 -5
  93. package/dist/update/ipc.js +56 -7
  94. package/dist/update/snapshot.d.ts +4 -0
  95. package/dist/update/snapshot.js +8 -0
  96. package/dist/update/updater.d.ts +61 -10
  97. package/dist/update/updater.js +236 -25
  98. package/dist/update/version.d.ts +6 -1
  99. package/dist/update/version.js +18 -3
  100. package/drizzle/0005_snapshot_mtime.sql +1 -0
  101. package/drizzle/0006_sour_susan_delgado.sql +2 -0
  102. package/drizzle/0007_same_wilson_fisk.sql +2 -0
  103. package/drizzle/meta/0005_snapshot.json +855 -0
  104. package/drizzle/meta/0006_snapshot.json +867 -0
  105. package/drizzle/meta/0007_snapshot.json +869 -0
  106. package/drizzle/meta/_journal.json +21 -0
  107. package/package.json +1 -7
  108. package/dist/mcp/index.d.ts +0 -1
  109. package/dist/mcp/index.js +0 -3
@@ -6,13 +6,33 @@ import type { LastRun, Session } from '../shared/types/message.js';
6
6
  /** Convert a DB row (with Date timestamps) to the shared Session type (with number timestamps). */
7
7
  export declare function rowToSession(row: typeof sessions.$inferSelect): Session;
8
8
  /** Create a new root session. */
9
- declare function createSession(handle: DB, title: string, projectId?: string, agentType?: string): Promise<Session>;
9
+ declare function createSession(handle: DB, title: string, projectId?: string, agentType?: string, source?: 'web' | 'cli'): Promise<Session>;
10
10
  /** Get a session by id, or null if not found. */
11
11
  declare function getSession(handle: DB, id: string): Promise<Session | null>;
12
- /** List all sessions. */
12
+ /** List all active sessions (未软删除、非 CLI 来源;source 为 NULL 的旧数据视为 web). */
13
13
  declare function listSessions(handle: DB): Promise<Session[]>;
14
- /** Delete a session (cascades to entries, archives, snapshots via FK). */
15
- declare function deleteSession(handle: DB, id: string): Promise<void>;
14
+ /** 列出所有未软删除会话(含 CLI 来源;供 ACP/CLI 等非 Web 消费者使用)。 */
15
+ declare function listAllSessions(handle: DB): Promise<Session[]>;
16
+ /** List soft-deleted sessions (回收站). */
17
+ declare function listDeletedSessions(handle: DB): Promise<Session[]>;
18
+ /**
19
+ * 软删除会话(级联其所有 fork 后代)。设置 deletedAt = now;
20
+ * 30 天后由 purgeDeletedSessions 物理清除。
21
+ * 会话不存在或已在回收站 → 返回 false(调用方按 404 处理)。
22
+ */
23
+ declare function softDeleteSession(handle: DB, id: string): Promise<boolean>;
24
+ /**
25
+ * 从回收站恢复会话:默认连带恢复其已软删除的祖先链——
26
+ * 否则恢复的会话因父仍在回收站而游离于会话树之外(P2-1:UI 不可达)。
27
+ * 祖先中未删除的(活跃)节点不需要也不应该被改动。
28
+ */
29
+ declare function restoreSession(handle: DB, id: string): Promise<boolean>;
30
+ /**
31
+ * 物理清除回收站中超过保留期(默认 30 天)的会话。
32
+ * 子会话先于父会话删除(自引用 FK 要求)。
33
+ * 返回清除数量。启动时与每日定时调用。
34
+ */
35
+ declare function purgeDeletedSessions(handle: DB, retentionMs?: number): Promise<number>;
16
36
  /** Update a session's title. */
17
37
  declare function updateSessionTitle(handle: DB, id: string, title: string): Promise<void>;
18
38
  /** Bump updatedAt to now (used after appending messages). */
@@ -36,4 +56,4 @@ export declare function saveLLMSegments(handle: DB, id: string, segments: LLMSeg
36
56
  /** 更新会话 metadata.lastRun(agent run 开始/结束时写入;重启后检测中断用)。 */
37
57
  declare function updateSessionLastRun(handle: DB, id: string, lastRun: LastRun): Promise<void>;
38
58
  declare function listSessionsByProject(handle: DB, projectId: string): Promise<Session[]>;
39
- export { createSession, deleteSession, getSession, listSessions, listSessionsByProject, touchLastOpened, touchSession, updateSessionLastRun, updateSessionTitle, };
59
+ export { createSession, getSession, listAllSessions, listDeletedSessions, listSessions, listSessionsByProject, purgeDeletedSessions, restoreSession, softDeleteSession, touchLastOpened, touchSession, updateSessionLastRun, updateSessionTitle, };
@@ -1,10 +1,15 @@
1
- import { eq } from 'drizzle-orm';
1
+ import { and, eq, gt, inArray, isNull, lt, ne, or } from 'drizzle-orm';
2
2
  import { sessions } from '../db/schema.js';
3
3
  import { generateId } from '../shared/index.js';
4
4
  /** Convert a DB row (with Date timestamps) to the shared Session type (with number timestamps). */
5
5
  export function rowToSession(row) {
6
6
  const created = row.createdAt instanceof Date ? row.createdAt.getTime() : new Date(row.createdAt).getTime();
7
7
  const updated = row.updatedAt instanceof Date ? row.updatedAt.getTime() : new Date(row.updatedAt).getTime();
8
+ const deleted = row.deletedAt
9
+ ? row.deletedAt instanceof Date
10
+ ? row.deletedAt.getTime()
11
+ : new Date(row.deletedAt).getTime()
12
+ : null;
8
13
  return {
9
14
  id: row.id,
10
15
  title: row.title,
@@ -14,15 +19,22 @@ export function rowToSession(row) {
14
19
  metadata: (row.metadata ?? {}),
15
20
  agentType: row.agentType ?? null,
16
21
  worktreePath: row.worktreePath ?? null,
22
+ source: row.source === 'web' || row.source === 'cli' ? row.source : null,
23
+ deletedAt: deleted,
17
24
  createdAt: created,
18
25
  updatedAt: updated,
19
26
  };
20
27
  }
21
28
  /** Create a new root session. */
22
- async function createSession(handle, title, projectId, agentType) {
29
+ async function createSession(handle, title, projectId, agentType, source) {
23
30
  const [row] = await handle.db
24
31
  .insert(sessions)
25
- .values({ title, projectId: projectId ?? null, agentType: agentType ?? null })
32
+ .values({
33
+ title,
34
+ projectId: projectId ?? null,
35
+ agentType: agentType ?? null,
36
+ source: source ?? null,
37
+ })
26
38
  .returning();
27
39
  if (!row)
28
40
  throw new Error('Failed to insert session');
@@ -33,14 +45,118 @@ async function getSession(handle, id) {
33
45
  const [row] = await handle.db.select().from(sessions).where(eq(sessions.id, id));
34
46
  return row ? rowToSession(row) : null;
35
47
  }
36
- /** List all sessions. */
48
+ /** List all active sessions (未软删除、非 CLI 来源;source 为 NULL 的旧数据视为 web). */
37
49
  async function listSessions(handle) {
38
- const rows = await handle.db.select().from(sessions);
50
+ const rows = await handle.db
51
+ .select()
52
+ .from(sessions)
53
+ .where(and(isNull(sessions.deletedAt), or(isNull(sessions.source), ne(sessions.source, 'cli'))));
39
54
  return rows.map(rowToSession);
40
55
  }
41
- /** Delete a session (cascades to entries, archives, snapshots via FK). */
42
- async function deleteSession(handle, id) {
43
- await handle.db.delete(sessions).where(eq(sessions.id, id));
56
+ /** 列出所有未软删除会话(含 CLI 来源;供 ACP/CLI 等非 Web 消费者使用)。 */
57
+ async function listAllSessions(handle) {
58
+ const rows = await handle.db.select().from(sessions).where(isNull(sessions.deletedAt));
59
+ return rows.map(rowToSession);
60
+ }
61
+ /** List soft-deleted sessions (回收站). */
62
+ async function listDeletedSessions(handle) {
63
+ const rows = await handle.db
64
+ .select()
65
+ .from(sessions)
66
+ .where(gt(sessions.deletedAt, new Date(0)));
67
+ return rows.map(rowToSession);
68
+ }
69
+ /**
70
+ * 软删除会话(级联其所有 fork 后代)。设置 deletedAt = now;
71
+ * 30 天后由 purgeDeletedSessions 物理清除。
72
+ * 会话不存在或已在回收站 → 返回 false(调用方按 404 处理)。
73
+ */
74
+ async function softDeleteSession(handle, id) {
75
+ const [row] = await handle.db
76
+ .select({ id: sessions.id, deletedAt: sessions.deletedAt })
77
+ .from(sessions)
78
+ .where(eq(sessions.id, id));
79
+ if (!row || row.deletedAt)
80
+ return false;
81
+ const ids = new Set([id]);
82
+ let frontier = [id];
83
+ while (frontier.length > 0) {
84
+ // 用 parentId 过滤:收集下一层子会话
85
+ const children = await handle.db
86
+ .select({ id: sessions.id })
87
+ .from(sessions)
88
+ .where(and(isNull(sessions.deletedAt), inArray(sessions.parentId, frontier)));
89
+ frontier = children.map((r) => r.id).filter((cid) => !ids.has(cid));
90
+ for (const r of children)
91
+ ids.add(r.id);
92
+ }
93
+ const now = new Date();
94
+ for (const sid of ids) {
95
+ await handle.db.update(sessions).set({ deletedAt: now }).where(eq(sessions.id, sid));
96
+ }
97
+ return true;
98
+ }
99
+ /**
100
+ * 从回收站恢复会话:默认连带恢复其已软删除的祖先链——
101
+ * 否则恢复的会话因父仍在回收站而游离于会话树之外(P2-1:UI 不可达)。
102
+ * 祖先中未删除的(活跃)节点不需要也不应该被改动。
103
+ */
104
+ async function restoreSession(handle, id) {
105
+ const [row] = await handle.db.select().from(sessions).where(eq(sessions.id, id));
106
+ if (!row || !row.deletedAt)
107
+ return false;
108
+ const ids = new Set([id]);
109
+ let parentId = row.parentId;
110
+ while (parentId) {
111
+ const [parent] = await handle.db.select().from(sessions).where(eq(sessions.id, parentId));
112
+ if (!parent)
113
+ break;
114
+ if (parent.deletedAt)
115
+ ids.add(parent.id);
116
+ parentId = parent.parentId;
117
+ }
118
+ for (const sid of ids) {
119
+ await handle.db.update(sessions).set({ deletedAt: null }).where(eq(sessions.id, sid));
120
+ }
121
+ return true;
122
+ }
123
+ /**
124
+ * 物理清除回收站中超过保留期(默认 30 天)的会话。
125
+ * 子会话先于父会话删除(自引用 FK 要求)。
126
+ * 返回清除数量。启动时与每日定时调用。
127
+ */
128
+ async function purgeDeletedSessions(handle, retentionMs = 30 * 24 * 60 * 60 * 1000) {
129
+ const cutoff = new Date(Date.now() - retentionMs);
130
+ const rows = await handle.db
131
+ .select({ id: sessions.id, parentId: sessions.parentId })
132
+ .from(sessions)
133
+ .where(lt(sessions.deletedAt, cutoff));
134
+ if (rows.length === 0)
135
+ return 0;
136
+ // 拓扑序:无子会话的先删
137
+ const remaining = new Set(rows.map((r) => r.id));
138
+ let deleted = 0;
139
+ while (remaining.size > 0) {
140
+ const hasChildParent = new Set(rows.filter((r) => r.parentId && remaining.has(r.parentId)).map((r) => r.parentId));
141
+ const leaves = rows
142
+ .filter((r) => remaining.has(r.id) && !hasChildParent.has(r.id))
143
+ .map((r) => r.id);
144
+ if (leaves.length === 0) {
145
+ // 循环引用兜底:强制按 id 逐个删除(自引用环数据异常场景)
146
+ for (const id of Array.from(remaining)) {
147
+ await handle.db.delete(sessions).where(eq(sessions.id, id));
148
+ remaining.delete(id);
149
+ deleted += 1;
150
+ }
151
+ break;
152
+ }
153
+ for (const id of leaves) {
154
+ await handle.db.delete(sessions).where(eq(sessions.id, id));
155
+ remaining.delete(id);
156
+ deleted += 1;
157
+ }
158
+ }
159
+ return deleted;
44
160
  }
45
161
  /** Update a session's title. */
46
162
  async function updateSessionTitle(handle, id, title) {
@@ -149,7 +265,10 @@ async function updateSessionLastRun(handle, id, lastRun) {
149
265
  .where(eq(sessions.id, id));
150
266
  }
151
267
  async function listSessionsByProject(handle, projectId) {
152
- const rows = await handle.db.select().from(sessions).where(eq(sessions.projectId, projectId));
268
+ const rows = await handle.db
269
+ .select()
270
+ .from(sessions)
271
+ .where(and(eq(sessions.projectId, projectId), isNull(sessions.deletedAt), or(isNull(sessions.source), ne(sessions.source, 'cli'))));
153
272
  return rows.map(rowToSession);
154
273
  }
155
- export { createSession, deleteSession, getSession, listSessions, listSessionsByProject, touchLastOpened, touchSession, updateSessionLastRun, updateSessionTitle, };
274
+ export { createSession, getSession, listAllSessions, listDeletedSessions, listSessions, listSessionsByProject, purgeDeletedSessions, restoreSession, softDeleteSession, touchLastOpened, touchSession, updateSessionLastRun, updateSessionTitle, };
@@ -1,11 +1,17 @@
1
1
  import type { DB } from '../db/client.js';
2
2
  import type { FileSnapshot } from './types.js';
3
- /** Create or update a file snapshot. Always creates a new version. Returns the snapshot id. */
4
- declare function upsertFileSnapshot(handle: DB, sessionId: string, filePath: string, content: string): Promise<string>;
3
+ /** Create or update a file snapshot. Always creates a new version. Returns the snapshot id.
4
+ * 未传 mtimeMs 时从同文件已有最新行透传(而非置空):compaction/squash 等从
5
+ * 工具结果重建快照的调用点拿不到磁盘 mtime,透传旧值可避免每次压缩后热文件
6
+ * 被误判过期而多一轮冗余重读(自愈无害但浪费;旧值 ≤ 内容实际新鲜度,
7
+ * 绝不会掩盖磁盘真实变更)。 */
8
+ declare function upsertFileSnapshot(handle: DB, sessionId: string, filePath: string, content: string, mtimeMs?: number): Promise<string>;
5
9
  /** Get all snapshots for a session. */
6
10
  declare function getFileSnapshots(handle: DB, sessionId: string): Promise<FileSnapshot[]>;
7
11
  /** Get the latest snapshot for a specific file, or null. */
8
12
  declare function getLatestFileSnapshot(handle: DB, sessionId: string, filePath: string): Promise<FileSnapshot | null>;
9
13
  /** Quick check: return cached file content if a snapshot exists, else null. */
10
14
  declare function checkFileSnapshot(handle: DB, sessionId: string, filePath: string): Promise<string | null>;
11
- export { checkFileSnapshot, getFileSnapshots, getLatestFileSnapshot, upsertFileSnapshot };
15
+ /** 把源会话每个文件的最新快照复制到目标会话(fork 分支用,P1-4)。 */
16
+ declare function copyFileSnapshots(handle: DB, fromSessionId: string, toSessionId: string): Promise<void>;
17
+ export { checkFileSnapshot, copyFileSnapshots, getFileSnapshots, getLatestFileSnapshot, upsertFileSnapshot, };
@@ -13,10 +13,15 @@ function rowToSnapshot(row) {
13
13
  tokenCount: row.tokenCount ?? 0,
14
14
  version: row.version ?? 1,
15
15
  createdAt,
16
+ ...(row.mtimeMs != null ? { mtimeMs: Number(row.mtimeMs) } : {}),
16
17
  };
17
18
  }
18
- /** Create or update a file snapshot. Always creates a new version. Returns the snapshot id. */
19
- async function upsertFileSnapshot(handle, sessionId, filePath, content) {
19
+ /** Create or update a file snapshot. Always creates a new version. Returns the snapshot id.
20
+ * 未传 mtimeMs 时从同文件已有最新行透传(而非置空):compaction/squash 等从
21
+ * 工具结果重建快照的调用点拿不到磁盘 mtime,透传旧值可避免每次压缩后热文件
22
+ * 被误判过期而多一轮冗余重读(自愈无害但浪费;旧值 ≤ 内容实际新鲜度,
23
+ * 绝不会掩盖磁盘真实变更)。 */
24
+ async function upsertFileSnapshot(handle, sessionId, filePath, content, mtimeMs) {
20
25
  const contentHash = createHash('sha256').update(content).digest('hex');
21
26
  const tokenCount = estimateTokens(content);
22
27
  const [existing] = await handle.db
@@ -26,9 +31,20 @@ async function upsertFileSnapshot(handle, sessionId, filePath, content) {
26
31
  .orderBy(desc(fileSnapshots.version))
27
32
  .limit(1);
28
33
  const version = (existing?.version ?? 0) + 1;
34
+ // mtimeMs 是浮点(stat 返回微秒精度),bigint 列不接受小数;取整存储。
35
+ // 调用方未显式提供时继承已有行的 mtimeMs(见函数头注释)。
36
+ const effectiveMtimeMs = mtimeMs ?? (existing?.mtimeMs != null ? Number(existing.mtimeMs) : null);
29
37
  const [row] = await handle.db
30
38
  .insert(fileSnapshots)
31
- .values({ sessionId, filePath, content, contentHash, tokenCount, version })
39
+ .values({
40
+ sessionId,
41
+ filePath,
42
+ content,
43
+ contentHash,
44
+ tokenCount,
45
+ version,
46
+ ...(effectiveMtimeMs != null ? { mtimeMs: Math.round(effectiveMtimeMs) } : {}),
47
+ })
32
48
  .returning();
33
49
  if (!row)
34
50
  throw new Error('Failed to insert file snapshot');
@@ -57,4 +73,29 @@ async function checkFileSnapshot(handle, sessionId, filePath) {
57
73
  const snapshot = await getLatestFileSnapshot(handle, sessionId, filePath);
58
74
  return snapshot?.content ?? null;
59
75
  }
60
- export { checkFileSnapshot, getFileSnapshots, getLatestFileSnapshot, upsertFileSnapshot };
76
+ /** 把源会话每个文件的最新快照复制到目标会话(fork 分支用,P1-4)。 */
77
+ async function copyFileSnapshots(handle, fromSessionId, toSessionId) {
78
+ const rows = await handle.db
79
+ .select()
80
+ .from(fileSnapshots)
81
+ .where(eq(fileSnapshots.sessionId, fromSessionId));
82
+ const latest = new Map();
83
+ for (const r of rows) {
84
+ const prev = latest.get(r.filePath);
85
+ if (!prev || (r.version ?? 0) > (prev.version ?? 0))
86
+ latest.set(r.filePath, r);
87
+ }
88
+ for (const r of latest.values()) {
89
+ await handle.db.insert(fileSnapshots).values({
90
+ sessionId: toSessionId,
91
+ entryId: r.entryId,
92
+ filePath: r.filePath,
93
+ content: r.content,
94
+ contentHash: r.contentHash,
95
+ tokenCount: r.tokenCount,
96
+ version: r.version,
97
+ mtimeMs: r.mtimeMs,
98
+ });
99
+ }
100
+ }
101
+ export { checkFileSnapshot, copyFileSnapshots, getFileSnapshots, getLatestFileSnapshot, upsertFileSnapshot, };
@@ -62,6 +62,7 @@ ${history}`;
62
62
  if (cfg.preserveFileSnapshots) {
63
63
  const hotFiles = extractHotFiles(toSquash);
64
64
  for (const file of hotFiles) {
65
+ // 不传 mtimeMs:由 upsertFileSnapshot 从已有行透传,压缩后热文件不误判过期。
65
66
  const id = await upsertFileSnapshot(txHandle, sessionId, file.path, file.content);
66
67
  fileSnapshotIds.push(id);
67
68
  }
@@ -101,7 +101,7 @@ type CompactionArchive = {
101
101
  id: string;
102
102
  sessionId: string;
103
103
  compactionId: string;
104
- archiveType: 'compaction' | 'squash' | 'shake';
104
+ archiveType: 'compaction' | 'squash' | 'shake' | 'clear';
105
105
  originalEntries: SessionEntry[];
106
106
  summary: string;
107
107
  tokenCount: number;
@@ -117,6 +117,8 @@ type FileSnapshot = {
117
117
  contentHash: string;
118
118
  tokenCount: number;
119
119
  version: number;
120
+ /** 快照创建时文件磁盘 mtime(毫秒);旧行无此字段(迁移前写入) */
121
+ mtimeMs?: number;
120
122
  createdAt: number;
121
123
  };
122
124
  export type { ArchiveRef, BranchSummaryEntry, CompactionArchive, CompactionConfig, CompactionEntry, CompactionResult, FileSnapshot, HotFile, MessageInput, SessionEntry, SessionTreeNode, SquashConfig, SquashEntry, SteeringEntry, Summarizer, };
@@ -148,6 +148,13 @@ type AgentEvent = {
148
148
  toolCallId: string;
149
149
  tool: string;
150
150
  input: unknown;
151
+ }
152
+ /** P1-6:权限确认超时(5 分钟)被自动拒绝时通知前端,提供「重新询问」入口。 */
153
+ | {
154
+ _tag: 'permission_timeout';
155
+ toolCallId: string;
156
+ tool: string;
157
+ input: unknown;
151
158
  } | {
152
159
  _tag: 'error';
153
160
  error: AgentError;
@@ -245,5 +252,9 @@ type AgentState = {
245
252
  remaining: number;
246
253
  noTextStreak: number;
247
254
  };
255
+ /** 本次 run 开始时间戳(ms)。暂停持久化 lastRun 时回填 startedAt。 */
256
+ lastRunStartedAt?: number;
257
+ /** 暂停状态是否已 yield 给前端(loop 内用,防重复 status_change)。 */
258
+ pauseYielded?: boolean;
248
259
  };
249
260
  export type { AgentConfig, AgentError, AgentEvent, AgentState, AgentStatus, LLMCall, LLMSegment, PendingToolCall, SegmentTrigger, TokenBudget, };
@@ -64,16 +64,22 @@ type UpdateConfig = {
64
64
  intervalMs: number;
65
65
  /** 启动后首次检查的延迟(毫秒),默认 10 秒。 */
66
66
  initialDelayMs: number;
67
- /** 发现新版本后自动应用(无感知),false 则仅前端提示手动确认(默认 false)。 */
68
- autoApply: boolean;
67
+ /** 热更新前暂停活跃 run 的等待超时(毫秒),默认 30 秒。
68
+ * 暂停会等当前原子操作(工具执行)完成;超时仍未暂停的 run 将被强制中止。 */
69
+ pauseTimeoutMs?: number;
70
+ /** 手动安装等待程序文件变更的超时(毫秒),默认 10 分钟。 */
71
+ manualWaitTimeoutMs?: number;
69
72
  };
70
- /** Server security configuration (spec §24.2). */
73
+ /** Server security configuration (spec §24.2)。 */
71
74
  type SecurityConfig = {
72
- /** 启用 Bearer token 认证(远程访问场景)。本地开发默认关闭。 */
75
+ /**
76
+ * 是否启用 Bearer token 认证(默认 true)。
77
+ * 关闭为显式选择(可信网络/无人值守场景),会取消一切 API 鉴权。
78
+ */
73
79
  authEnabled: boolean;
74
- /** Bearer token;authEnabled 为 true 时必填。 */
80
+ /** Bearer token;未提供时服务端自动生成并持久化到全局数据目录(跨重启/热更新稳定)。 */
75
81
  token?: string;
76
- /** 额外允许的 CORS origin(本地回环始终允许)。 */
82
+ /** 额外允许的 CORS origin(本地回环始终允许;局域网/远程访问需显式添加)。 */
77
83
  allowedOrigins: string[];
78
84
  };
79
85
  /** Global application configuration. */
@@ -35,9 +35,9 @@ type Message = {
35
35
  tokenCount: number;
36
36
  createdAt: number;
37
37
  };
38
- /** 上次 agent run 的持久化状态(服务重启后检测中断用)。 */
38
+ /** 上次 agent run 的持久化状态(服务重启后检测中断/恢复用)。 */
39
39
  type LastRun = {
40
- status: 'running' | 'completed';
40
+ status: 'running' | 'completed' | 'paused';
41
41
  agentName?: string;
42
42
  provider?: string;
43
43
  model?: string;
@@ -67,6 +67,10 @@ type Session = {
67
67
  agentType: string | null;
68
68
  /** 隔离 worktree 路径(null=共享父 cwd)。 */
69
69
  worktreePath: string | null;
70
+ /** 会话来源:'web'(Web UI)/ 'cli'(CLI print 模式);null=旧数据视为 web。 */
71
+ source: 'web' | 'cli' | null;
72
+ /** 软删除时间戳(ms);null=未删除。 */
73
+ deletedAt: number | null;
70
74
  createdAt: number;
71
75
  updatedAt: number;
72
76
  };
@@ -5,11 +5,10 @@
5
5
  * - `resolve(root, requestPath)` 解析后,若结果不在 root 子树内则返回 null。
6
6
  * - 判定逻辑:`relative(root, resolved)` 以 `..` 开头即越界;
7
7
  * 再用 `resolve(root, rel) === resolved` 兜底,防止相对段恰好名为 root 的边角情况。
8
+ * - symlink 防逃逸(P2-5):root 与目标(或其最近已存在祖先)经 realpath 解析后,
9
+ * 目标必须仍落在 root 实路径内;否则返回 null。
8
10
  *
9
11
  * 返回解析后的绝对路径;越界返回 null。
10
- *
11
- * 注意:本函数基于 `relative`,不解析符号链接(symlink)。若 root 内存在指向
12
- * root 外的符号链接,本函数无法阻止经由该链接的逃逸——symlink 逃逸不在本次范围内。
13
12
  */
14
13
  declare function safeResolve(root: string, requestPath: string): string | null;
15
14
  export { safeResolve };
@@ -1,4 +1,5 @@
1
- import { relative, resolve } from 'node:path';
1
+ import { existsSync, realpathSync } from 'node:fs';
2
+ import { dirname, relative, resolve, sep } from 'node:path';
2
3
  /**
3
4
  * 安全路径解析:确保 requestPath 解析后落在 root 内,防止路径穿越
4
5
  * (如 `../etc/passwd`、绝对路径 `/etc/passwd`)。
@@ -6,11 +7,10 @@ import { relative, resolve } from 'node:path';
6
7
  * - `resolve(root, requestPath)` 解析后,若结果不在 root 子树内则返回 null。
7
8
  * - 判定逻辑:`relative(root, resolved)` 以 `..` 开头即越界;
8
9
  * 再用 `resolve(root, rel) === resolved` 兜底,防止相对段恰好名为 root 的边角情况。
10
+ * - symlink 防逃逸(P2-5):root 与目标(或其最近已存在祖先)经 realpath 解析后,
11
+ * 目标必须仍落在 root 实路径内;否则返回 null。
9
12
  *
10
13
  * 返回解析后的绝对路径;越界返回 null。
11
- *
12
- * 注意:本函数基于 `relative`,不解析符号链接(symlink)。若 root 内存在指向
13
- * root 外的符号链接,本函数无法阻止经由该链接的逃逸——symlink 逃逸不在本次范围内。
14
14
  */
15
15
  function safeResolve(root, requestPath) {
16
16
  const resolved = resolve(root, requestPath);
@@ -18,6 +18,22 @@ function safeResolve(root, requestPath) {
18
18
  if (rel.startsWith('..') || resolve(root, rel) !== resolved) {
19
19
  return null;
20
20
  }
21
+ try {
22
+ const rootReal = realpathSync(root);
23
+ let probe = resolved;
24
+ while (!existsSync(probe)) {
25
+ const parent = dirname(probe);
26
+ if (parent === probe)
27
+ break;
28
+ probe = parent;
29
+ }
30
+ const probeReal = realpathSync(probe);
31
+ if (probeReal !== rootReal && !probeReal.startsWith(rootReal + sep))
32
+ return null;
33
+ }
34
+ catch {
35
+ // realpath 失败(root 不存在等):保持原有 relative 判定结果
36
+ }
21
37
  return resolved;
22
38
  }
23
39
  export { safeResolve };
@@ -421,16 +421,17 @@ export function getLatestTodoPhasesFromMessages(messages) {
421
421
  continue;
422
422
  for (let j = msg.content.length - 1; j >= 0; j--) {
423
423
  const part = msg.content[j];
424
- if (!part || part._tag !== 'tool_result')
424
+ if (part?._tag !== 'tool_result')
425
425
  continue;
426
426
  if (part.tool !== 'todo')
427
427
  continue;
428
428
  const output = part.output;
429
- if (!output || output._tag !== 'success')
429
+ if (output?._tag !== 'success')
430
430
  continue;
431
431
  const metadata = output.metadata;
432
- if (metadata && Array.isArray(metadata.phases)) {
433
- return clonePhases(metadata.phases);
432
+ const phases = metadata?.phases;
433
+ if (Array.isArray(phases)) {
434
+ return clonePhases(phases);
434
435
  }
435
436
  }
436
437
  }
@@ -6,11 +6,11 @@ export { dapTools } from './builtin/dap.js';
6
6
  export { editTool } from './builtin/edit.js';
7
7
  export { globTool, globToRegex } from './builtin/glob.js';
8
8
  export { grepTool } from './builtin/grep.js';
9
+ export { kanbanTool } from './builtin/kanban.js';
9
10
  export { readTool } from './builtin/read.js';
10
11
  export { createDefaultURLRegistry, createFileResolver, createSkillResolver, } from './builtin/resolvers.js';
11
- export { kanbanTool } from './builtin/kanban.js';
12
- export { todoTool } from './builtin/todo.js';
13
12
  export type { TodoInput, TodoItem, TodoPhase, TodoStatus } from './builtin/todo.js';
13
+ export { todoTool } from './builtin/todo.js';
14
14
  export { writeTool } from './builtin/write.js';
15
15
  export { yieldTool } from './builtin/yield.js';
16
16
  export { executeTool } from './executor.js';
@@ -23,10 +23,21 @@ export { formatForLLM, resolveProvider, runWebSearch } from './websearch/index.j
23
23
  export type { WebSearchProvider, WebSearchProviderId, WebSearchResponse, WebSearchSource, } from './websearch/types.js';
24
24
  export { createWebSearchTool } from './websearch/websearch.js';
25
25
  import type { Config } from '../shared/types/config.js';
26
+ import type { ToolRegistry } from './types.js';
26
27
  /**
27
28
  * Create a registry pre-loaded with all builtin tools:
28
29
  * read, write, edit, glob, grep, bash, task, websearch, and the debug_* set.
29
30
  *
31
+ * config.tools.disabled 中的工具**不注册**——registry 层过滤保证两个入口
32
+ * (CLI print / Web chat / /api/tools 列表)统一生效,禁用的工具对 agent 完全不可见。
30
33
  * @param config 可选配置;websearch 工具按 config.websearch 构造。省略时用 DEFAULT_CONFIG。
31
34
  */
32
- export declare function createDefaultRegistry(config?: Config): import("./types.js").ToolRegistry;
35
+ export declare function createDefaultRegistry(config?: Config): ToolRegistry;
36
+ /**
37
+ * 解析本轮启用的工具名列表(P1-1 修复:config.tools.enabled/disabled 双入口统一生效)。
38
+ *
39
+ * - explicit(前端显式选择)非 undefined → 以其为准,但过滤掉 disabled 与未注册名。
40
+ * - 否则 enabled 非空 → enabled ∩ registered;enabled 空 → 全部 registered(空=全部)。
41
+ * - disabled 恒过滤(registry 已排除,此处兜底)。
42
+ */
43
+ export declare function resolveEnabledToolNames(registry: ToolRegistry, config: Pick<Config, 'tools'>, explicit?: string[]): string[];
@@ -4,10 +4,10 @@ export { dapTools } from './builtin/dap.js';
4
4
  export { editTool } from './builtin/edit.js';
5
5
  export { globTool, globToRegex } from './builtin/glob.js';
6
6
  export { grepTool } from './builtin/grep.js';
7
+ export { kanbanTool } from './builtin/kanban.js';
7
8
  export { readTool } from './builtin/read.js';
8
9
  // ── Builtin tools ───────────────────────────────────────────
9
10
  export { createDefaultURLRegistry, createFileResolver, createSkillResolver, } from './builtin/resolvers.js';
10
- export { kanbanTool } from './builtin/kanban.js';
11
11
  export { todoTool } from './builtin/todo.js';
12
12
  export { writeTool } from './builtin/write.js';
13
13
  export { yieldTool } from './builtin/yield.js';
@@ -33,28 +33,49 @@ import { taskTool } from './builtin/task.js';
33
33
  import { todoTool } from './builtin/todo.js';
34
34
  import { writeTool } from './builtin/write.js';
35
35
  import { yieldTool } from './builtin/yield.js';
36
- import { createToolRegistry, registerTool } from './registry.js';
36
+ import { createToolRegistry, listTools, registerTool } from './registry.js';
37
37
  import { createWebSearchTool } from './websearch/websearch.js';
38
38
  /**
39
39
  * Create a registry pre-loaded with all builtin tools:
40
40
  * read, write, edit, glob, grep, bash, task, websearch, and the debug_* set.
41
41
  *
42
+ * config.tools.disabled 中的工具**不注册**——registry 层过滤保证两个入口
43
+ * (CLI print / Web chat / /api/tools 列表)统一生效,禁用的工具对 agent 完全不可见。
42
44
  * @param config 可选配置;websearch 工具按 config.websearch 构造。省略时用 DEFAULT_CONFIG。
43
45
  */
44
46
  export function createDefaultRegistry(config = DEFAULT_CONFIG) {
45
47
  const reg = createToolRegistry();
46
- registerTool(reg, readTool);
47
- registerTool(reg, writeTool);
48
- registerTool(reg, editTool);
49
- registerTool(reg, globTool);
50
- registerTool(reg, grepTool);
51
- registerTool(reg, bashTool);
52
- registerTool(reg, kanbanTool);
53
- registerTool(reg, taskTool);
54
- registerTool(reg, todoTool);
55
- registerTool(reg, yieldTool);
56
- registerTool(reg, createWebSearchTool(config.websearch));
48
+ const disabled = new Set(config.tools?.disabled ?? []);
49
+ const register = (tool) => {
50
+ if (!disabled.has(tool.name))
51
+ registerTool(reg, tool);
52
+ };
53
+ register(readTool);
54
+ register(writeTool);
55
+ register(editTool);
56
+ register(globTool);
57
+ register(grepTool);
58
+ register(bashTool);
59
+ register(kanbanTool);
60
+ register(taskTool);
61
+ register(todoTool);
62
+ register(yieldTool);
63
+ register(createWebSearchTool(config.websearch));
57
64
  for (const tool of dapTools)
58
- registerTool(reg, tool);
65
+ register(tool);
59
66
  return reg;
60
67
  }
68
+ /**
69
+ * 解析本轮启用的工具名列表(P1-1 修复:config.tools.enabled/disabled 双入口统一生效)。
70
+ *
71
+ * - explicit(前端显式选择)非 undefined → 以其为准,但过滤掉 disabled 与未注册名。
72
+ * - 否则 enabled 非空 → enabled ∩ registered;enabled 空 → 全部 registered(空=全部)。
73
+ * - disabled 恒过滤(registry 已排除,此处兜底)。
74
+ */
75
+ export function resolveEnabledToolNames(registry, config, explicit) {
76
+ const all = listTools(registry).map((t) => t.name);
77
+ const disabled = new Set(config.tools?.disabled ?? []);
78
+ const base = explicit ??
79
+ (config.tools?.enabled && config.tools.enabled.length > 0 ? config.tools.enabled : all);
80
+ return base.filter((n) => all.includes(n) && !disabled.has(n));
81
+ }
@@ -5,6 +5,6 @@ export { createUpdateScheduler } from './scheduler.js';
5
5
  export type { SerializedEntry, SerializedSession, SessionSnapshot } from './snapshot.js';
6
6
  export { orderSessionsByParent, restoreSessions, serializeSessions } from './snapshot.js';
7
7
  export type { HotUpdateOptions, HotUpdateResult, SpawnFn } from './updater.js';
8
- export { cleanupSnapshot, performHotUpdate } from './updater.js';
8
+ export { cleanupSnapshot, performHotUpdate, performInstall } from './updater.js';
9
9
  export type { UpdateCheckResult } from './version.js';
10
- export { checkForUpdate, compareSemver } from './version.js';
10
+ export { checkForUpdate, compareSemver, getCurrentVersion } from './version.js';
@@ -2,5 +2,5 @@
2
2
  export { createHandoffServer, requestHandoff } from './ipc.js';
3
3
  export { createUpdateScheduler } from './scheduler.js';
4
4
  export { orderSessionsByParent, restoreSessions, serializeSessions } from './snapshot.js';
5
- export { cleanupSnapshot, performHotUpdate } from './updater.js';
6
- export { checkForUpdate, compareSemver } from './version.js';
5
+ export { cleanupSnapshot, performHotUpdate, performInstall } from './updater.js';
6
+ export { checkForUpdate, compareSemver, getCurrentVersion } from './version.js';