c0de-agent 1.11.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.
Files changed (98) hide show
  1. package/README.md +24 -5
  2. package/dist/cli/commands/chat.js +87 -1
  3. package/dist/cli/commands/config.js +17 -3
  4. package/dist/cli/commands/sessions.js +62 -3
  5. package/dist/cli/index.js +26 -7
  6. package/dist/cli/modes/print.js +9 -2
  7. package/dist/core/compact.d.ts +2 -0
  8. package/dist/core/compact.js +1 -0
  9. package/dist/core/config.d.ts +11 -2
  10. package/dist/core/config.js +73 -6
  11. package/dist/core/loop/compaction.d.ts +2 -1
  12. package/dist/core/loop/compaction.js +19 -6
  13. package/dist/core/loop/stream-collect.js +9 -1
  14. package/dist/core/loop/subagent.js +23 -5
  15. package/dist/core/redact.d.ts +11 -0
  16. package/dist/core/redact.js +49 -0
  17. package/dist/core/slash.js +93 -51
  18. package/dist/core/title.js +7 -1
  19. package/dist/core/types.d.ts +2 -0
  20. package/dist/db/schema.d.ts +17 -0
  21. package/dist/db/schema.js +3 -1
  22. package/dist/kanban/index.d.ts +1 -1
  23. package/dist/kanban/index.js +1 -1
  24. package/dist/kanban/store.d.ts +5 -1
  25. package/dist/kanban/store.js +76 -2
  26. package/dist/llm/index.d.ts +1 -1
  27. package/dist/llm/index.js +1 -1
  28. package/dist/llm/provider.js +1 -1
  29. package/dist/llm/retry.d.ts +5 -2
  30. package/dist/llm/retry.js +6 -5
  31. package/dist/llm/routing.d.ts +24 -1
  32. package/dist/llm/routing.js +29 -1
  33. package/dist/project/index.d.ts +1 -1
  34. package/dist/project/index.js +1 -1
  35. package/dist/project/project.d.ts +14 -0
  36. package/dist/project/project.js +77 -1
  37. package/dist/server/agent-manager.d.ts +10 -1
  38. package/dist/server/agent-manager.js +14 -1
  39. package/dist/server/auth-manager.d.ts +11 -5
  40. package/dist/server/auth-manager.js +39 -20
  41. package/dist/server/context.js +3 -0
  42. package/dist/server/permission/interactive.d.ts +6 -3
  43. package/dist/server/permission/interactive.js +20 -4
  44. package/dist/server/permission/store.d.ts +12 -2
  45. package/dist/server/permission/store.js +31 -3
  46. package/dist/server/registry-config.d.ts +14 -0
  47. package/dist/server/registry-config.js +44 -0
  48. package/dist/server/routes/auth.js +32 -2
  49. package/dist/server/routes/chat.d.ts +12 -0
  50. package/dist/server/routes/chat.js +216 -38
  51. package/dist/server/routes/commands.d.ts +4 -2
  52. package/dist/server/routes/commands.js +10 -3
  53. package/dist/server/routes/config.js +103 -23
  54. package/dist/server/routes/files.js +22 -4
  55. package/dist/server/routes/kanban.js +68 -6
  56. package/dist/server/routes/permissions.d.ts +0 -4
  57. package/dist/server/routes/permissions.js +133 -7
  58. package/dist/server/routes/project.js +59 -10
  59. package/dist/server/routes/provider.js +48 -3
  60. package/dist/server/routes/session.js +298 -22
  61. package/dist/server/routes/update.d.ts +11 -5
  62. package/dist/server/routes/update.js +88 -28
  63. package/dist/server/server.d.ts +4 -12
  64. package/dist/server/server.js +198 -102
  65. package/dist/server/types.d.ts +7 -0
  66. package/dist/session/archive.d.ts +3 -1
  67. package/dist/session/archive.js +13 -1
  68. package/dist/session/branch.js +20 -2
  69. package/dist/session/import.d.ts +44 -0
  70. package/dist/session/import.js +83 -0
  71. package/dist/session/index.d.ts +1 -1
  72. package/dist/session/index.js +1 -1
  73. package/dist/session/jobs.d.ts +12 -0
  74. package/dist/session/jobs.js +53 -0
  75. package/dist/session/message.d.ts +7 -1
  76. package/dist/session/message.js +21 -1
  77. package/dist/session/session.d.ts +128 -11
  78. package/dist/session/session.js +461 -30
  79. package/dist/session/snapshot.d.ts +5 -2
  80. package/dist/session/snapshot.js +10 -2
  81. package/dist/session/types.d.ts +11 -1
  82. package/dist/shared/types/agent.d.ts +11 -0
  83. package/dist/shared/types/config.d.ts +5 -1
  84. package/dist/shared/types/kanban.d.ts +13 -0
  85. package/dist/shared/types/message.d.ts +9 -0
  86. package/dist/tools/websearch/index.js +6 -3
  87. package/dist/update/index.d.ts +3 -3
  88. package/dist/update/index.js +2 -2
  89. package/dist/update/ipc.d.ts +13 -2
  90. package/dist/update/ipc.js +36 -2
  91. package/dist/update/snapshot.d.ts +1 -0
  92. package/dist/update/snapshot.js +2 -0
  93. package/dist/update/updater.d.ts +15 -1
  94. package/dist/update/updater.js +45 -17
  95. package/drizzle/0008_tranquil_lily_hollister.sql +1 -0
  96. package/drizzle/meta/0008_snapshot.json +875 -0
  97. package/drizzle/meta/_journal.json +7 -0
  98. package/package.json +41 -42
@@ -99,8 +99,10 @@ async function forkSession(handle, sessionId, messageIndex) {
99
99
  sourceSessionId: sessionId,
100
100
  },
101
101
  });
102
- // 复制源会话最新文件快照(P1-4:@文件上下文随分支保留)。
103
- await copyFileSnapshots(txHandle, sessionId, forked.id);
102
+ // 复制源会话文件快照(P1-4:@文件上下文随分支保留)。
103
+ // C3:按分支点时间过滤——分支点之后更新的快照是「未来」状态,不复制。
104
+ const branchPointMs = typeof target.createdAt === 'number' ? target.createdAt : new Date(target.createdAt).getTime();
105
+ await copyFileSnapshots(txHandle, sessionId, forked.id, branchPointMs);
104
106
  const created = await getSession(txHandle, forked.id);
105
107
  if (!created)
106
108
  throw new Error('Forked session not found after creation');
@@ -116,6 +118,21 @@ async function getBranches(handle, sessionId) {
116
118
  .where(and(eq(sessions.parentId, sessionId), isNull(sessions.deletedAt)));
117
119
  return rows.map(rowToSession);
118
120
  }
121
+ /** 从会话 metadata.segments 聚合用量(零额外查询——段数据已在行内)。 */
122
+ function sessionUsage(session) {
123
+ const calls = (session.metadata.segments ?? []).flatMap((s) => s.calls ?? []);
124
+ let inputTokens = 0;
125
+ let outputTokens = 0;
126
+ let cacheRead = 0;
127
+ let cost = 0;
128
+ for (const c of calls) {
129
+ inputTokens += c.usage.input;
130
+ outputTokens += c.usage.output;
131
+ cacheRead += c.usage.cacheRead ?? 0;
132
+ cost += c.cost;
133
+ }
134
+ return { inputTokens, outputTokens, cacheRead, cost, calls: calls.length };
135
+ }
119
136
  /** Build a full session tree from root sessions down.
120
137
  * 每层按 metadata.lastOpenedAt 降序(fallback updatedAt、createdAt)。
121
138
  * 排除软删除会话与 CLI 来源会话(Web 树仅展示 web 会话)。
@@ -140,6 +157,7 @@ async function getTree(handle) {
140
157
  .map((session) => ({
141
158
  session,
142
159
  children: build(session.id),
160
+ usage: sessionUsage(session),
143
161
  }));
144
162
  return build(null);
145
163
  }
@@ -0,0 +1,44 @@
1
+ import type { DB } from '../db/client.js';
2
+ /** 导入消息的宽松形状(导出 JSON 反序列化后,字段可能缺失/类型漂移)。 */
3
+ type ImportMessage = {
4
+ id?: string;
5
+ role?: string;
6
+ content?: unknown;
7
+ tokenCount?: number;
8
+ createdAt?: number | string;
9
+ };
10
+ /** 导入归档的宽松形状。 */
11
+ type ImportArchive = {
12
+ id?: string;
13
+ compactionId?: string;
14
+ archiveType?: string;
15
+ originalEntries?: unknown;
16
+ fileSnapshots?: unknown;
17
+ summary?: string;
18
+ tokenCount?: number;
19
+ searchableText?: string;
20
+ createdAt?: number | string;
21
+ };
22
+ /**
23
+ * 导入会话数据(GET /sessions/:id/export 的逆操作):
24
+ * 新建根会话(source 'web'),消息与归档**始终生成新 id**(保留 role/content/时间戳)——
25
+ * 导入副本可重复执行(同库内复制/恢复场景),不与原会话或既往导入冲突。
26
+ * 说明:导出仅含 tag='message' 的消息——compaction/steering 等特殊条目不随迁,
27
+ * 导入后会话显示完整原始消息流(上下文重建略长,但不丢内容)。
28
+ * P0:权限态(permissionMode/alwaysAllow)仅在调用方显式确认后随迁——
29
+ * metadata 由路由层过滤,未确认时不传入本函数。
30
+ */
31
+ declare function importSessionData(handle: DB, opts: {
32
+ title: string;
33
+ projectId?: string;
34
+ messages: ImportMessage[];
35
+ archives: ImportArchive[];
36
+ /** 随迁的会话 metadata(仅权限态等安全字段,由调用方过滤)。 */
37
+ metadata?: Record<string, unknown>;
38
+ }): Promise<{
39
+ sessionId: string;
40
+ messageCount: number;
41
+ archiveCount: number;
42
+ }>;
43
+ export type { ImportArchive, ImportMessage };
44
+ export { importSessionData };
@@ -0,0 +1,83 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { compactionArchives, sessionEntries, sessions } from '../db/schema.js';
3
+ import { generateId } from '../shared/index.js';
4
+ import { createSession } from './session.js';
5
+ const VALID_ROLES = new Set([
6
+ 'user',
7
+ 'assistant',
8
+ 'system',
9
+ 'tool',
10
+ ]);
11
+ function toDate(value) {
12
+ if (typeof value === 'number')
13
+ return new Date(value);
14
+ if (typeof value === 'string') {
15
+ const d = new Date(value);
16
+ if (!Number.isNaN(d.getTime()))
17
+ return d;
18
+ }
19
+ return new Date();
20
+ }
21
+ function sanitizeContent(content) {
22
+ if (Array.isArray(content)) {
23
+ // 只保留结构完整的分片;其余丢弃(导入宁少勿坏)
24
+ return content.filter((p) => p !== null && typeof p === 'object' && typeof p._tag === 'string');
25
+ }
26
+ if (typeof content === 'string' && content.length > 0) {
27
+ return [{ _tag: 'text', text: content }];
28
+ }
29
+ return [];
30
+ }
31
+ /**
32
+ * 导入会话数据(GET /sessions/:id/export 的逆操作):
33
+ * 新建根会话(source 'web'),消息与归档**始终生成新 id**(保留 role/content/时间戳)——
34
+ * 导入副本可重复执行(同库内复制/恢复场景),不与原会话或既往导入冲突。
35
+ * 说明:导出仅含 tag='message' 的消息——compaction/steering 等特殊条目不随迁,
36
+ * 导入后会话显示完整原始消息流(上下文重建略长,但不丢内容)。
37
+ * P0:权限态(permissionMode/alwaysAllow)仅在调用方显式确认后随迁——
38
+ * metadata 由路由层过滤,未确认时不传入本函数。
39
+ */
40
+ async function importSessionData(handle, opts) {
41
+ const session = await createSession(handle, opts.title, opts.projectId, undefined, 'web');
42
+ return handle.db.transaction(async (tx) => {
43
+ if (opts.metadata && Object.keys(opts.metadata).length > 0) {
44
+ await tx.update(sessions).set({ metadata: opts.metadata }).where(eq(sessions.id, session.id));
45
+ }
46
+ let messageCount = 0;
47
+ for (const m of opts.messages) {
48
+ const role = m.role;
49
+ if (typeof role !== 'string' || !VALID_ROLES.has(role))
50
+ continue;
51
+ await tx.insert(sessionEntries).values({
52
+ id: generateId(),
53
+ sessionId: session.id,
54
+ tag: 'message',
55
+ role,
56
+ content: sanitizeContent(m.content),
57
+ tokenCount: typeof m.tokenCount === 'number' ? m.tokenCount : 0,
58
+ createdAt: toDate(m.createdAt),
59
+ });
60
+ messageCount += 1;
61
+ }
62
+ let archiveCount = 0;
63
+ for (const a of opts.archives) {
64
+ await tx.insert(compactionArchives).values({
65
+ id: generateId(),
66
+ sessionId: session.id,
67
+ compactionId: typeof a.compactionId === 'string' && a.compactionId.length > 0
68
+ ? a.compactionId
69
+ : generateId(),
70
+ archiveType: typeof a.archiveType === 'string' ? a.archiveType : 'compaction',
71
+ originalEntries: Array.isArray(a.originalEntries) ? a.originalEntries : [],
72
+ fileSnapshots: Array.isArray(a.fileSnapshots) ? a.fileSnapshots : [],
73
+ summary: typeof a.summary === 'string' ? a.summary : '',
74
+ tokenCount: typeof a.tokenCount === 'number' ? a.tokenCount : null,
75
+ searchableText: typeof a.searchableText === 'string' ? a.searchableText : null,
76
+ createdAt: toDate(a.createdAt),
77
+ });
78
+ archiveCount += 1;
79
+ }
80
+ return { sessionId: session.id, messageCount, archiveCount };
81
+ });
82
+ }
83
+ export { importSessionData };
@@ -3,7 +3,7 @@ export { forkSession, getBranches, getTree } from './branch.js';
3
3
  export { buildCompactionPrompt, compactSession, extractHotFiles, findSafeCutPoint, } from './compaction.js';
4
4
  export { entriesToChatMessages, getSessionContext, injectSnapshots, messageToChatMessage, } from './context.js';
5
5
  export { appendMessage, deleteEntriesByIds, deleteMessagesAfter, getEntries, getMessageCount, getMessages, insertEntry, } from './message.js';
6
- export { createSession, getLLMSegments, getSession, listDeletedSessions, listSessions, migrateLegacyDetails, purgeDeletedSessions, restoreSession, saveLLMSegments, segmentFingerprint, softDeleteSession, touchSession, updateSessionTitle, } from './session.js';
6
+ export { createSession, getLLMSegments, getSession, listDeletedSessions, listSessions, migrateLegacyDetails, purgeDeletedSessions, restoreSession, restoreSessionCore, saveLLMSegments, segmentFingerprint, softDeleteSession, TRASH_RETENTION_MS, touchSession, touchTrashSeen, updateSessionTitle, } from './session.js';
7
7
  export { checkFileSnapshot, getFileSnapshots, getLatestFileSnapshot, upsertFileSnapshot, } from './snapshot.js';
8
8
  export { squashRecent } from './squash.js';
9
9
  export { estimateMessageTokens, estimateTokens } from './token.js';
@@ -4,7 +4,7 @@ export { forkSession, getBranches, getTree } from './branch.js';
4
4
  export { buildCompactionPrompt, compactSession, extractHotFiles, findSafeCutPoint, } from './compaction.js';
5
5
  export { entriesToChatMessages, getSessionContext, injectSnapshots, messageToChatMessage, } from './context.js';
6
6
  export { appendMessage, deleteEntriesByIds, deleteMessagesAfter, getEntries, getMessageCount, getMessages, insertEntry, } from './message.js';
7
- export { createSession, getLLMSegments, getSession, listDeletedSessions, listSessions, migrateLegacyDetails, purgeDeletedSessions, restoreSession, saveLLMSegments, segmentFingerprint, softDeleteSession, touchSession, updateSessionTitle, } from './session.js';
7
+ export { createSession, getLLMSegments, getSession, listDeletedSessions, listSessions, migrateLegacyDetails, purgeDeletedSessions, restoreSession, restoreSessionCore, saveLLMSegments, segmentFingerprint, softDeleteSession, TRASH_RETENTION_MS, touchSession, touchTrashSeen, updateSessionTitle, } from './session.js';
8
8
  export { checkFileSnapshot, getFileSnapshots, getLatestFileSnapshot, upsertFileSnapshot, } from './snapshot.js';
9
9
  export { squashRecent } from './squash.js';
10
10
  export { estimateMessageTokens, estimateTokens } from './token.js';
@@ -0,0 +1,12 @@
1
+ import type { DB } from '../db/client.js';
2
+ /**
3
+ * 标记崩溃遗留的后台子 agent 任务并通知父会话(P2)。
4
+ *
5
+ * background task 在子 session 上写 lastRun.status='running'(subagent.ts),
6
+ * 完成后置 'completed'。进程崩溃时悬空任务永远停在 'running',父会话收不到
7
+ * 完成通知(原实现仅内存 jobId,重启即丢)。启动时调用:
8
+ * - 悬空任务 lastRun 置 'completed'(死任务不再显示 interrupted);
9
+ * - 合成一条 state="failed" 通知写入父会话(子 session 已挂 parentId)。
10
+ * 返回标记数量。
11
+ */
12
+ export declare function markDeadBackgroundJobs(handle: DB): Promise<number>;
@@ -0,0 +1,53 @@
1
+ import { and, inArray, isNotNull, sql } from 'drizzle-orm';
2
+ import { sessions } from '../db/schema.js';
3
+ import { appendMessage } from './message.js';
4
+ import { updateSessionLastRun } from './session.js';
5
+ /**
6
+ * 标记崩溃遗留的后台子 agent 任务并通知父会话(P2)。
7
+ *
8
+ * background task 在子 session 上写 lastRun.status='running'(subagent.ts),
9
+ * 完成后置 'completed'。进程崩溃时悬空任务永远停在 'running',父会话收不到
10
+ * 完成通知(原实现仅内存 jobId,重启即丢)。启动时调用:
11
+ * - 悬空任务 lastRun 置 'completed'(死任务不再显示 interrupted);
12
+ * - 合成一条 state="failed" 通知写入父会话(子 session 已挂 parentId)。
13
+ * 返回标记数量。
14
+ */
15
+ export async function markDeadBackgroundJobs(handle) {
16
+ const rows = await handle.db
17
+ .select({
18
+ id: sessions.id,
19
+ parentId: sessions.parentId,
20
+ agentType: sessions.agentType,
21
+ })
22
+ .from(sessions)
23
+ .where(and(isNotNull(sessions.agentType), isNotNull(sessions.parentId), sql `${sessions.metadata}->'lastRun'->>'status' = 'running'`));
24
+ // 父会话已入回收站的不再注入失败通知(否则向 60 天后将清除的会话写死消息)。
25
+ const parentIds = Array.from(new Set(rows.map((r) => r.parentId).filter((p) => !!p)));
26
+ const trashedParents = new Set();
27
+ if (parentIds.length > 0) {
28
+ const parents = await handle.db
29
+ .select({ id: sessions.id, deletedAt: sessions.deletedAt })
30
+ .from(sessions)
31
+ .where(inArray(sessions.id, parentIds));
32
+ for (const p of parents)
33
+ if (p.deletedAt)
34
+ trashedParents.add(p.id);
35
+ }
36
+ for (const row of rows) {
37
+ await updateSessionLastRun(handle, row.id, {
38
+ status: 'completed',
39
+ agentName: row.agentType ?? undefined,
40
+ startedAt: Date.now(),
41
+ });
42
+ if (row.parentId && !trashedParents.has(row.parentId)) {
43
+ const synthetic = `<task id="${row.id}" state="failed">\n<task_error>\n后台任务在服务重启前未完成,已标记为失败(可重新派发)\n</task_error>\n</task>`;
44
+ await appendMessage(handle, row.parentId, {
45
+ role: 'user',
46
+ content: [{ _tag: 'text', text: synthetic }],
47
+ }).catch(() => {
48
+ // 通知写入失败:任务已标记,父会话可能收不到失败提示——仅记录
49
+ });
50
+ }
51
+ }
52
+ return rows.length;
53
+ }
@@ -9,6 +9,12 @@ declare function getMessages(handle: DB, sessionId: string, opts?: {
9
9
  limit?: number;
10
10
  offset?: number;
11
11
  }): Promise<Message[]>;
12
+ /**
13
+ * 会话 steering 条目映射为 Message 形态(role='user',内容为 steering part)。
14
+ * 时间线渲染与消息合并使用(P0:追加指令持久化后的展示路径);
15
+ * 不并入 getMessages——compaction/shake/export 等仅处理真实消息的消费者不受影响。
16
+ */
17
+ declare function getSteeringAsMessages(handle: DB, sessionId: string): Promise<Message[]>;
12
18
  /** Count messages in a session. */
13
19
  declare function getMessageCount(handle: DB, sessionId: string): Promise<number>;
14
20
  /** Delete all messages after the given 0-based index (keeps 0..index inclusive). */
@@ -19,4 +25,4 @@ declare function getEntries(handle: DB, sessionId: string): Promise<SessionEntry
19
25
  declare function deleteEntriesByIds(handle: DB, ids: string[]): Promise<void>;
20
26
  /** Low-level: insert a raw entry row (for compaction/squash/branch_summary/steering). */
21
27
  declare function insertEntry(handle: DB, values: typeof sessionEntries.$inferInsert): Promise<typeof sessionEntries.$inferSelect>;
22
- export { appendMessage, deleteEntriesByIds, deleteMessagesAfter, getEntries, getMessageCount, getMessages, insertEntry, };
28
+ export { appendMessage, deleteEntriesByIds, deleteMessagesAfter, getEntries, getMessageCount, getMessages, getSteeringAsMessages, insertEntry, };
@@ -104,6 +104,26 @@ async function getMessages(handle, sessionId, opts) {
104
104
  .offset(opts?.offset ?? 0);
105
105
  return rows.map(rowToMessage);
106
106
  }
107
+ /**
108
+ * 会话 steering 条目映射为 Message 形态(role='user',内容为 steering part)。
109
+ * 时间线渲染与消息合并使用(P0:追加指令持久化后的展示路径);
110
+ * 不并入 getMessages——compaction/shake/export 等仅处理真实消息的消费者不受影响。
111
+ */
112
+ async function getSteeringAsMessages(handle, sessionId) {
113
+ const rows = await handle.db
114
+ .select()
115
+ .from(sessionEntries)
116
+ .where(and(eq(sessionEntries.sessionId, sessionId), eq(sessionEntries.tag, 'steering')))
117
+ .orderBy(asc(sessionEntries.createdAt));
118
+ return rows.map((row) => ({
119
+ id: row.id,
120
+ sessionId: row.sessionId,
121
+ role: 'user',
122
+ content: [{ _tag: 'steering', text: row.content.text ?? '' }],
123
+ tokenCount: row.tokenCount ?? 0,
124
+ createdAt: toEpochMs(row.createdAt),
125
+ }));
126
+ }
107
127
  /** Count messages in a session. */
108
128
  async function getMessageCount(handle, sessionId) {
109
129
  const [result] = await handle.db
@@ -142,4 +162,4 @@ async function insertEntry(handle, values) {
142
162
  throw new Error('Failed to insert entry');
143
163
  return row;
144
164
  }
145
- export { appendMessage, deleteEntriesByIds, deleteMessagesAfter, getEntries, getMessageCount, getMessages, insertEntry, };
165
+ export { appendMessage, deleteEntriesByIds, deleteMessagesAfter, getEntries, getMessageCount, getMessages, getSteeringAsMessages, insertEntry, };
@@ -2,39 +2,144 @@ import type { DB } from '../db/client.js';
2
2
  import { sessions } from '../db/schema.js';
3
3
  import type { LLMSegment } from '../shared/types/agent.js';
4
4
  import type { ChatTool } from '../shared/types/llm.js';
5
- import type { LastRun, Session } from '../shared/types/message.js';
5
+ import type { LastRun, Session, SessionMetadata } 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, source?: 'web' | 'cli'): Promise<Session>;
9
+ declare function createSession(handle: DB, title: string, projectId?: string, agentType?: string, source?: 'web' | 'cli',
10
+ /** P1 会话树治理:子 agent 会话挂到父会话(树内嵌套 + 删除级联)。 */
11
+ parentId?: string): Promise<Session>;
10
12
  /** Get a session by id, or null if not found. */
11
13
  declare function getSession(handle: DB, id: string): Promise<Session | null>;
12
14
  /** List all active sessions (未软删除、非 CLI 来源;source 为 NULL 的旧数据视为 web). */
13
15
  declare function listSessions(handle: DB): Promise<Session[]>;
14
16
  /** 列出所有未软删除会话(含 CLI 来源;供 ACP/CLI 等非 Web 消费者使用)。 */
15
17
  declare function listAllSessions(handle: DB): Promise<Session[]>;
16
- /** List soft-deleted sessions (回收站). */
17
- declare function listDeletedSessions(handle: DB): Promise<Session[]>;
18
+ /** List soft-deleted sessions (回收站);projectId 提供时仅列出归属该项目的会话
19
+ * (P1-7:回收站此前全库共享,项目 A 的界面能清空项目 B 的删除会话)。 */
20
+ declare function listDeletedSessions(handle: DB, projectId?: string): Promise<Session[]>;
21
+ /** 列出未归属任何项目的已软删除会话(孤儿)。删除项目时 FK set null 使会话失去 projectId,
22
+ * 在任何项目的回收站视图都不可见(F1),需专门的全局视图暴露以便恢复/归巢。 */
23
+ declare function listOrphanDeletedSessions(handle: DB): Promise<Session[]>;
24
+ /**
25
+ * 标记回收站条目「已被用户看到」,记录 metadata.trashSeenAt = now(仅首次,不重置)。
26
+ * 回收站保留期自首次看到起算(见 purgeDeletedSessions)。软删除时会清除旧标记,
27
+ * 恢复后再次删除须重新看到回收站才重新起算——避免「未重新看到就被过早清空」。
28
+ * scope.orphan=true 时仅标记未归属项目的孤儿会话(与 listOrphanDeletedSessions 对齐);
29
+ * 孤儿标记仅在用户展开「未归属项目」分组时由前端显式调用(POST /deleted/orphans/seen),
30
+ * 避免打开任意项目回收站连带启动无关孤儿条目的倒计时。
31
+ */
32
+ declare function touchTrashSeen(handle: DB, scope?: {
33
+ projectId?: string;
34
+ orphan?: boolean;
35
+ }): Promise<number>;
18
36
  /**
19
37
  * 软删除会话(级联其所有 fork 后代)。设置 deletedAt = now;
20
- * 30 天后由 purgeDeletedSessions 物理清除。
38
+ * 60 天后由 purgeDeletedSessions 物理清除(到期先标记、宽限 7 天)。
39
+ * A3:同时清除 trashSeenAt/purgePendingAt——恢复后重删的会话必须重新被
40
+ * 用户看到才重新起算,宽限期标记不跨删除周期生效。
21
41
  * 会话不存在或已在回收站 → 返回 false(调用方按 404 处理)。
22
42
  */
23
43
  declare function softDeleteSession(handle: DB, id: string): Promise<boolean>;
44
+ /**
45
+ * A3:清除回收站倒计时/宽限期标记(软删除与项目删除共用)。
46
+ * 恢复后重删的会话须重新被看到才重新起算;宽限期标记不跨删除周期生效。
47
+ * 无标记时原样返回,避免无谓的 jsonb 覆写。
48
+ */
49
+ declare function clearTrashMarks(meta: SessionMetadata): SessionMetadata;
50
+ /**
51
+ * 把会话归属到指定项目(P1-2 孤儿会话补救):设置 projectId 与 worktreePath。
52
+ * 会话不存在返回 false。
53
+ */
54
+ declare function rebindSession(handle: DB, id: string, project: {
55
+ id: string;
56
+ worktree: string;
57
+ }): Promise<boolean>;
24
58
  /**
25
59
  * 从回收站恢复会话:默认连带恢复其已软删除的祖先链——
26
60
  * 否则恢复的会话因父仍在回收站而游离于会话树之外(P2-1:UI 不可达)。
27
61
  * 祖先中未删除的(活跃)节点不需要也不应该被改动。
62
+ *
63
+ * P2 修复:删除级联后代入回收站,恢复同样级联还原目标会话的整棵后代子树——
64
+ * 否则「删除根会话 → 恢复根会话」后分支仍滞留回收站且无任何提示,保留期后被清。
65
+ * 兄弟分支(祖先的其他后代)不受影响。
28
66
  */
29
- declare function restoreSession(handle: DB, id: string): Promise<boolean>;
67
+ export type RestoreResult = {
68
+ restored: boolean;
69
+ /** 为可达性连带还原的祖先会话数量(不含目标会话与其同批次后代)。 */
70
+ restoredAncestorCount: number;
71
+ /** 是否连带还原了与目标非同一次删除批次的祖先(用户早先单独删除的会话)。 */
72
+ crossedBatchAncestor: boolean;
73
+ /**
74
+ * A2:已删除但未随本次恢复的后代数量(删除批次不同——典型场景是项目删除时
75
+ * 各会话批次断裂)。这些 fork 分支滞留在回收站,必须显式告知用户单独恢复,
76
+ * 否则会被静默物理清除。
77
+ */
78
+ leftBehindDescendantCount: number;
79
+ };
80
+ declare function restoreSessionCore(handle: DB, id: string, opts?: {
81
+ includeDescendants?: boolean;
82
+ }): Promise<RestoreResult>;
83
+ declare function restoreSession(handle: DB, id: string, opts?: {
84
+ includeDescendants?: boolean;
85
+ }): Promise<boolean>;
30
86
  /**
31
- * 物理清除回收站中超过保留期(默认 30 天)的会话。
32
- * 子会话先于父会话删除(自引用 FK 要求)。
33
- * 返回清除数量。启动时与每日定时调用。
87
+ * P2-4:回收站保留期 60 天(原 30 天),自「用户首次在回收站看到该条目」
88
+ * (metadata.trashSeenAt)起算,而非删除时间(墙钟)——否则用户删除后长期不开服务、
89
+ * 重启即被静默物理清除,从未见过任何倒计时。临时会话(CLI print / workflow)保留期
90
+ * 仍为 30 天,见 purgeTemporarySessions。
91
+ */
92
+ export declare const TRASH_RETENTION_MS: number;
93
+ /**
94
+ * A3:到期标记 → 物理清除的宽限期(默认 7 天)。
95
+ * 条目到期后先写 metadata.purgePendingAt,UI 显示「即将清除」并可恢复;
96
+ * 宽限期满才物理清除——杜绝「到期即静默物理清空」的数据丢失死角。
97
+ */
98
+ export declare const TRASH_PURGE_GRACE_MS: number;
99
+ /** 清除结果:marked = 本次新标记进入宽限期的条目数;deleted = 本次物理清除数。 */
100
+ export type TrashPurgeResult = {
101
+ marked: number;
102
+ deleted: number;
103
+ };
104
+ /**
105
+ * 回收站两阶段清理(启动时与每日定时调用):
106
+ * 阶段一:trashSeenAt 超过保留期且未标记 → 标记 purgePendingAt(进入宽限期,可恢复)。
107
+ * 阶段二:purgePendingAt 早于宽限截止 → 物理清除(子会话先于父,自引用 FK 要求)。
108
+ * 未被看到过(trashSeenAt 缺失)或恢复后重删、尚未重新看到的会话永不进入任一阶段。
34
109
  */
35
- declare function purgeDeletedSessions(handle: DB, retentionMs?: number): Promise<number>;
110
+ declare function purgeDeletedSessions(handle: DB, retentionMs?: number, graceMs?: number): Promise<TrashPurgeResult>;
111
+ /**
112
+ * C4:清理长期空置的 web 会话(无任何条目、无子会话、未删除、创建早于保留期)。
113
+ * 覆盖「新建会话后未发言」的残留:首条消息失败清理依赖前端 JS 执行,
114
+ * 崩溃/断电/离线时必然残留空「New Session」行。CLI 临时会话由 purgeTemporarySessions 负责。
115
+ */
116
+ declare function purgeEmptySessions(handle: DB, retentionMs?: number): Promise<number>;
36
117
  /** Update a session's title. */
37
118
  declare function updateSessionTitle(handle: DB, id: string, title: string): Promise<void>;
119
+ /**
120
+ * 彻底删除某个回收站会话及其全部后代(含未软删除的 fork 后代,防御数据异常)。
121
+ * 子会话先于父会话删除(自引用 FK RESTRICT 要求);entries/archives 经 FK cascade 清理。
122
+ * 返回删除数量。会话不存在或不在回收站 → 返回 0。
123
+ */
124
+ declare function permanentlyDeleteSession(handle: DB, id: string): Promise<number>;
125
+ /** 清空回收站:物理删除所有已软删除会话(子先于父)。返回删除数量。
126
+ * projectId 提供时仅清空该项目(P1-7)。 */
127
+ declare function emptyTrash(handle: DB, projectId?: string): Promise<number>;
128
+ /**
129
+ * 清理过期临时会话(P2 → P1 收紧:仅清理显式标记的临时会话)。
130
+ * - agentType='print':CLI 一次性问答(c0de chat,非 --continue)创建的会话。
131
+ * - agentType='workflow':工作流运行产生的会话。
132
+ * 普通 CLI 会话(ACP)与 --continue 续接的会话(续接时已升级)永不清理——
133
+ * 此前按 source='cli' 全删会把用户显式续接的历史静默物理删除。
134
+ * 保留期默认 30 天;子条目经 FK cascade 一并删除。
135
+ * 返回清除数量。启动时与每日定时调用。
136
+ */
137
+ declare function purgeTemporarySessions(handle: DB, retentionMs?: number): Promise<number>;
138
+ /**
139
+ * 把临时会话升级为持久会话(--continue 续接时调用):
140
+ * 清除 print 标记,30 天临时清理不再触及。
141
+ */
142
+ declare function upgradeTemporarySession(handle: DB, id: string): Promise<void>;
38
143
  /** Bump updatedAt to now (used after appending messages). */
39
144
  declare function touchSession(handle: DB, id: string): Promise<void>;
40
145
  /** 记录会话上次打开时间(用于会话列表按最近打开排序)。 */
@@ -56,4 +161,16 @@ export declare function saveLLMSegments(handle: DB, id: string, segments: LLMSeg
56
161
  /** 更新会话 metadata.lastRun(agent run 开始/结束时写入;重启后检测中断用)。 */
57
162
  declare function updateSessionLastRun(handle: DB, id: string, lastRun: LastRun): Promise<void>;
58
163
  declare function listSessionsByProject(handle: DB, projectId: string): Promise<Session[]>;
59
- export { createSession, getSession, listAllSessions, listDeletedSessions, listSessions, listSessionsByProject, purgeDeletedSessions, restoreSession, softDeleteSession, touchLastOpened, touchSession, updateSessionLastRun, updateSessionTitle, };
164
+ /**
165
+ * 跨会话搜索(P2-6):标题 + 消息内容子串匹配。
166
+ * 默认仅搜索未软删除的非 CLI 会话(与 Web 会话树一致);projectId 提供时限定项目。
167
+ * opts.includeDeleted=true 时搜索回收站(P3:回收站无搜索,删后找内容只能逐行翻)。
168
+ * 返回 { session, matchedBy: 'title' | 'content' }。
169
+ */
170
+ declare function searchSessions(handle: DB, query: string, projectId?: string, opts?: {
171
+ includeDeleted?: boolean;
172
+ }): Promise<Array<{
173
+ session: Session;
174
+ matchedBy: 'title' | 'content';
175
+ }>>;
176
+ export { clearTrashMarks, createSession, emptyTrash, getSession, listAllSessions, listDeletedSessions, listOrphanDeletedSessions, listSessions, listSessionsByProject, permanentlyDeleteSession, purgeDeletedSessions, purgeEmptySessions, purgeTemporarySessions, rebindSession, restoreSession, restoreSessionCore, searchSessions, softDeleteSession, touchLastOpened, touchSession, touchTrashSeen, updateSessionLastRun, updateSessionTitle, upgradeTemporarySession, };