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.
- package/README.md +25 -8
- package/dist/cli/commands/acp.js +5 -3
- package/dist/cli/commands/auth.d.ts +17 -0
- package/dist/cli/commands/auth.js +73 -0
- package/dist/cli/commands/chat.js +10 -0
- package/dist/cli/commands/config.d.ts +2 -9
- package/dist/cli/commands/config.js +15 -54
- package/dist/cli/commands/init.js +4 -2
- package/dist/cli/commands/plugin.js +8 -4
- package/dist/cli/commands/serve.d.ts +1 -0
- package/dist/cli/commands/serve.js +4 -1
- package/dist/cli/commands/sessions.d.ts +10 -0
- package/dist/cli/commands/sessions.js +35 -0
- package/dist/cli/commands/update.js +18 -10
- package/dist/cli/deps.d.ts +7 -5
- package/dist/cli/deps.js +23 -7
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +96 -37
- package/dist/cli/modes/print.d.ts +3 -1
- package/dist/cli/modes/print.js +31 -4
- package/dist/core/agent.js +3 -4
- package/dist/core/config-path.d.ts +9 -0
- package/dist/core/config-path.js +48 -0
- package/dist/core/config.d.ts +27 -2
- package/dist/core/config.js +83 -13
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/loop.js +14 -4
- package/dist/core/slash.js +92 -30
- package/dist/core/title.js +15 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/core/workflows/discovery.d.ts +1 -1
- package/dist/core/workflows/index.d.ts +1 -1
- 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 +7 -1
- package/dist/server/agent-manager.d.ts +20 -0
- package/dist/server/agent-manager.js +63 -11
- package/dist/server/app.js +12 -1
- package/dist/server/auth-manager.d.ts +79 -0
- package/dist/server/auth-manager.js +270 -0
- package/dist/server/context.js +3 -0
- package/dist/server/middleware/auth.d.ts +4 -3
- package/dist/server/middleware/auth.js +5 -5
- package/dist/server/middleware/cors.d.ts +11 -2
- package/dist/server/middleware/cors.js +15 -12
- package/dist/server/permission/interactive.d.ts +2 -0
- package/dist/server/permission/interactive.js +5 -1
- package/dist/server/permission/store.d.ts +4 -0
- package/dist/server/permission/store.js +1 -0
- package/dist/server/routes/auth.d.ts +4 -0
- package/dist/server/routes/auth.js +94 -0
- package/dist/server/routes/chat.d.ts +6 -1
- package/dist/server/routes/chat.js +294 -153
- package/dist/server/routes/config.js +60 -8
- package/dist/server/routes/permissions.d.ts +4 -1
- package/dist/server/routes/permissions.js +27 -4
- package/dist/server/routes/project.js +56 -2
- package/dist/server/routes/session.js +37 -9
- package/dist/server/routes/terminal.js +29 -2
- package/dist/server/routes/update.d.ts +1 -0
- package/dist/server/routes/update.js +33 -4
- package/dist/server/routes/workflows.js +2 -2
- package/dist/server/server.d.ts +24 -1
- package/dist/server/server.js +149 -29
- package/dist/server/types.d.ts +17 -2
- package/dist/session/archive.d.ts +1 -1
- package/dist/session/branch.d.ts +11 -2
- package/dist/session/branch.js +107 -31
- package/dist/session/compaction.js +1 -0
- package/dist/session/context.d.ts +3 -2
- package/dist/session/context.js +36 -3
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/session.d.ts +25 -5
- package/dist/session/session.js +129 -10
- package/dist/session/snapshot.d.ts +9 -3
- package/dist/session/snapshot.js +45 -4
- package/dist/session/squash.js +1 -0
- package/dist/session/types.d.ts +3 -1
- package/dist/shared/types/agent.d.ts +11 -0
- package/dist/shared/types/config.d.ts +12 -6
- package/dist/shared/types/message.d.ts +6 -2
- package/dist/shared/utils/path.d.ts +2 -3
- package/dist/shared/utils/path.js +20 -4
- package/dist/tools/builtin/todo.js +5 -4
- package/dist/tools/index.d.ts +14 -3
- package/dist/tools/index.js +35 -14
- package/dist/update/index.d.ts +2 -2
- package/dist/update/index.js +2 -2
- package/dist/update/ipc.d.ts +38 -5
- package/dist/update/ipc.js +56 -7
- package/dist/update/snapshot.d.ts +4 -0
- package/dist/update/snapshot.js +8 -0
- package/dist/update/updater.d.ts +61 -10
- package/dist/update/updater.js +236 -25
- package/dist/update/version.d.ts +6 -1
- package/dist/update/version.js +18 -3
- package/drizzle/0005_snapshot_mtime.sql +1 -0
- package/drizzle/0006_sour_susan_delgado.sql +2 -0
- package/drizzle/0007_same_wilson_fisk.sql +2 -0
- package/drizzle/meta/0005_snapshot.json +855 -0
- package/drizzle/meta/0006_snapshot.json +867 -0
- package/drizzle/meta/0007_snapshot.json +869 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +1 -7
- package/dist/mcp/index.d.ts +0 -1
- 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
|
-
/**
|
|
15
|
-
declare function
|
|
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,
|
|
59
|
+
export { createSession, getSession, listAllSessions, listDeletedSessions, listSessions, listSessionsByProject, purgeDeletedSessions, restoreSession, softDeleteSession, touchLastOpened, touchSession, updateSessionLastRun, updateSessionTitle, };
|
package/dist/session/session.js
CHANGED
|
@@ -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({
|
|
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
|
|
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
|
-
/**
|
|
42
|
-
async function
|
|
43
|
-
await handle.db.
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
15
|
+
/** 把源会话每个文件的最新快照复制到目标会话(fork 分支用,P1-4)。 */
|
|
16
|
+
declare function copyFileSnapshots(handle: DB, fromSessionId: string, toSessionId: string): Promise<void>;
|
|
17
|
+
export { checkFileSnapshot, copyFileSnapshots, getFileSnapshots, getLatestFileSnapshot, upsertFileSnapshot, };
|
package/dist/session/snapshot.js
CHANGED
|
@@ -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
|
-
|
|
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({
|
|
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
|
-
|
|
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, };
|
package/dist/session/squash.js
CHANGED
|
@@ -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
|
}
|
package/dist/session/types.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
68
|
-
|
|
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
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* 是否启用 Bearer token 认证(默认 true)。
|
|
77
|
+
* 关闭为显式选择(可信网络/无人值守场景),会取消一切 API 鉴权。
|
|
78
|
+
*/
|
|
73
79
|
authEnabled: boolean;
|
|
74
|
-
/** Bearer token
|
|
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 {
|
|
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 (
|
|
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 (
|
|
429
|
+
if (output?._tag !== 'success')
|
|
430
430
|
continue;
|
|
431
431
|
const metadata = output.metadata;
|
|
432
|
-
|
|
433
|
-
|
|
432
|
+
const phases = metadata?.phases;
|
|
433
|
+
if (Array.isArray(phases)) {
|
|
434
|
+
return clonePhases(phases);
|
|
434
435
|
}
|
|
435
436
|
}
|
|
436
437
|
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -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):
|
|
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[];
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/update/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/update/index.js
CHANGED
|
@@ -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';
|