c0de-agent 1.11.0 → 1.13.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 (177) hide show
  1. package/README.md +63 -16
  2. package/dist/cli/commands/acp.js +26 -8
  3. package/dist/cli/commands/chat.js +151 -1
  4. package/dist/cli/commands/config.d.ts +2 -0
  5. package/dist/cli/commands/config.js +63 -3
  6. package/dist/cli/commands/serve.js +11 -2
  7. package/dist/cli/commands/sessions.js +109 -3
  8. package/dist/cli/commands/trust.d.ts +11 -0
  9. package/dist/cli/commands/trust.js +31 -0
  10. package/dist/cli/commands/update.js +4 -0
  11. package/dist/cli/deps.d.ts +9 -6
  12. package/dist/cli/deps.js +66 -19
  13. package/dist/cli/index.d.ts +4 -1
  14. package/dist/cli/index.js +110 -11
  15. package/dist/cli/modes/acp.js +39 -8
  16. package/dist/cli/modes/print.d.ts +3 -0
  17. package/dist/cli/modes/print.js +78 -8
  18. package/dist/core/agent.d.ts +1 -1
  19. package/dist/core/agent.js +9 -4
  20. package/dist/core/agents/builtin.js +2 -2
  21. package/dist/core/agents/index.d.ts +0 -1
  22. package/dist/core/agents/index.js +0 -1
  23. package/dist/core/compact.d.ts +2 -0
  24. package/dist/core/compact.js +1 -0
  25. package/dist/core/config.d.ts +53 -2
  26. package/dist/core/config.js +235 -19
  27. package/dist/core/loop/compaction.d.ts +2 -1
  28. package/dist/core/loop/compaction.js +19 -6
  29. package/dist/core/loop/segment.d.ts +3 -1
  30. package/dist/core/loop/segment.js +24 -1
  31. package/dist/core/loop/stream-collect.js +9 -1
  32. package/dist/core/loop/subagent.js +88 -37
  33. package/dist/core/loop.d.ts +7 -0
  34. package/dist/core/loop.js +90 -6
  35. package/dist/core/redact.d.ts +11 -0
  36. package/dist/core/redact.js +49 -0
  37. package/dist/core/slash.d.ts +10 -0
  38. package/dist/core/slash.js +237 -65
  39. package/dist/core/title.js +7 -1
  40. package/dist/core/types.d.ts +22 -1
  41. package/dist/core/workflows/context.js +44 -4
  42. package/dist/core/workflows/discovery.d.ts +2 -2
  43. package/dist/core/workflows/discovery.js +5 -7
  44. package/dist/core/workflows/index.d.ts +2 -1
  45. package/dist/core/workflows/index.js +2 -1
  46. package/dist/core/workflows/registry.d.ts +13 -3
  47. package/dist/core/workflows/registry.js +14 -9
  48. package/dist/core/workflows/resolve.d.ts +22 -0
  49. package/dist/core/workflows/resolve.js +23 -0
  50. package/dist/core/workflows/runtime.d.ts +6 -1
  51. package/dist/core/workflows/runtime.js +19 -2
  52. package/dist/db/schema.d.ts +373 -1
  53. package/dist/db/schema.js +60 -4
  54. package/dist/kanban/index.d.ts +2 -1
  55. package/dist/kanban/index.js +1 -1
  56. package/dist/kanban/store.d.ts +87 -1
  57. package/dist/kanban/store.js +429 -22
  58. package/dist/llm/index.d.ts +1 -1
  59. package/dist/llm/index.js +1 -1
  60. package/dist/llm/provider.js +1 -1
  61. package/dist/llm/registry.d.ts +7 -1
  62. package/dist/llm/registry.js +7 -1
  63. package/dist/llm/retry.d.ts +5 -2
  64. package/dist/llm/retry.js +6 -5
  65. package/dist/llm/routing.d.ts +24 -1
  66. package/dist/llm/routing.js +29 -1
  67. package/dist/plugins/init.d.ts +7 -0
  68. package/dist/plugins/init.js +22 -1
  69. package/dist/plugins/loader.d.ts +7 -1
  70. package/dist/plugins/loader.js +12 -4
  71. package/dist/project/index.d.ts +1 -1
  72. package/dist/project/index.js +1 -1
  73. package/dist/project/project.d.ts +37 -0
  74. package/dist/project/project.js +142 -1
  75. package/dist/project/trust.d.ts +65 -0
  76. package/dist/project/trust.js +328 -0
  77. package/dist/server/agent-manager.d.ts +15 -2
  78. package/dist/server/agent-manager.js +18 -3
  79. package/dist/server/app.js +45 -30
  80. package/dist/server/auth-manager.d.ts +26 -9
  81. package/dist/server/auth-manager.js +78 -42
  82. package/dist/server/context.js +14 -1
  83. package/dist/server/index.d.ts +1 -1
  84. package/dist/server/index.js +1 -1
  85. package/dist/server/middleware/auth.d.ts +11 -1
  86. package/dist/server/middleware/auth.js +25 -3
  87. package/dist/server/permission/interactive.d.ts +11 -3
  88. package/dist/server/permission/interactive.js +23 -4
  89. package/dist/server/permission/store.d.ts +14 -2
  90. package/dist/server/permission/store.js +39 -3
  91. package/dist/server/registry-config.d.ts +14 -0
  92. package/dist/server/registry-config.js +44 -0
  93. package/dist/server/routes/auth.js +66 -10
  94. package/dist/server/routes/chat.d.ts +21 -0
  95. package/dist/server/routes/chat.js +685 -52
  96. package/dist/server/routes/commands.d.ts +4 -2
  97. package/dist/server/routes/commands.js +9 -4
  98. package/dist/server/routes/config.js +180 -23
  99. package/dist/server/routes/files.js +142 -28
  100. package/dist/server/routes/kanban.js +218 -26
  101. package/dist/server/routes/permissions.d.ts +0 -4
  102. package/dist/server/routes/permissions.js +140 -7
  103. package/dist/server/routes/project.js +132 -29
  104. package/dist/server/routes/provider.js +54 -3
  105. package/dist/server/routes/session.js +463 -23
  106. package/dist/server/routes/tool.js +24 -5
  107. package/dist/server/routes/update.d.ts +11 -5
  108. package/dist/server/routes/update.js +145 -29
  109. package/dist/server/routes/usage.d.ts +4 -0
  110. package/dist/server/routes/usage.js +133 -0
  111. package/dist/server/routes/workflows.js +476 -87
  112. package/dist/server/server.d.ts +16 -13
  113. package/dist/server/server.js +364 -107
  114. package/dist/server/terminal/pty-manager.d.ts +12 -0
  115. package/dist/server/terminal/pty-manager.js +63 -7
  116. package/dist/server/types.d.ts +16 -0
  117. package/dist/session/archive.d.ts +3 -1
  118. package/dist/session/archive.js +13 -1
  119. package/dist/session/branch.d.ts +2 -1
  120. package/dist/session/branch.js +92 -9
  121. package/dist/session/context.js +28 -2
  122. package/dist/session/import.d.ts +44 -0
  123. package/dist/session/import.js +83 -0
  124. package/dist/session/index.d.ts +1 -1
  125. package/dist/session/index.js +1 -1
  126. package/dist/session/jobs.d.ts +12 -0
  127. package/dist/session/jobs.js +53 -0
  128. package/dist/session/message.d.ts +7 -1
  129. package/dist/session/message.js +21 -1
  130. package/dist/session/session.d.ts +186 -12
  131. package/dist/session/session.js +577 -33
  132. package/dist/session/snapshot.d.ts +5 -2
  133. package/dist/session/snapshot.js +10 -2
  134. package/dist/session/types.d.ts +13 -1
  135. package/dist/session/usage.d.ts +62 -0
  136. package/dist/session/usage.js +194 -0
  137. package/dist/shared/types/agent.d.ts +29 -2
  138. package/dist/shared/types/config.d.ts +64 -5
  139. package/dist/shared/types/kanban.d.ts +17 -2
  140. package/dist/shared/types/message.d.ts +23 -0
  141. package/dist/tools/builtin/kanban.js +13 -2
  142. package/dist/tools/index.d.ts +14 -3
  143. package/dist/tools/index.js +23 -5
  144. package/dist/tools/websearch/index.d.ts +5 -1
  145. package/dist/tools/websearch/index.js +46 -7
  146. package/dist/tools/websearch/providers/brave.js +2 -2
  147. package/dist/tools/websearch/providers/duckduckgo.js +2 -1
  148. package/dist/tools/websearch/providers/tavily.js +2 -2
  149. package/dist/tools/websearch/types.d.ts +5 -1
  150. package/dist/tools/websearch/types.js +7 -1
  151. package/dist/update/index.d.ts +3 -3
  152. package/dist/update/index.js +2 -2
  153. package/dist/update/ipc.d.ts +13 -2
  154. package/dist/update/ipc.js +36 -2
  155. package/dist/update/scheduler.js +5 -2
  156. package/dist/update/snapshot.d.ts +18 -3
  157. package/dist/update/snapshot.js +5 -2
  158. package/dist/update/updater.d.ts +17 -1
  159. package/dist/update/updater.js +47 -18
  160. package/dist/update/version.d.ts +3 -0
  161. package/dist/update/version.js +9 -3
  162. package/drizzle/0008_tranquil_lily_hollister.sql +1 -0
  163. package/drizzle/0010_usage_events_ledger.sql +17 -0
  164. package/drizzle/0011_slow_slapstick.sql +12 -0
  165. package/drizzle/0012_chief_timeslip.sql +1 -0
  166. package/drizzle/0013_swift_zeigeist.sql +1 -0
  167. package/drizzle/0014_flimsy_legion.sql +1 -0
  168. package/drizzle/meta/0008_snapshot.json +875 -0
  169. package/drizzle/meta/0010_snapshot.json +1010 -0
  170. package/drizzle/meta/0011_snapshot.json +1055 -0
  171. package/drizzle/meta/0012_snapshot.json +1062 -0
  172. package/drizzle/meta/0013_snapshot.json +1068 -0
  173. package/drizzle/meta/0014_snapshot.json +1075 -0
  174. package/drizzle/meta/_journal.json +42 -0
  175. package/package.json +42 -42
  176. package/dist/core/agents/discovery.d.ts +0 -6
  177. package/dist/core/agents/discovery.js +0 -96
@@ -1,6 +1,8 @@
1
- import { and, eq, gt, inArray, isNull, lt, ne, or } from 'drizzle-orm';
2
- import { sessions } from '../db/schema.js';
1
+ import { and, eq, gt, ilike, inArray, isNull, lt, ne, notExists, notInArray, or, sql, } from 'drizzle-orm';
2
+ import { alias } from 'drizzle-orm/pg-core';
3
+ import { sessionEntries, sessions } from '../db/schema.js';
3
4
  import { generateId } from '../shared/index.js';
5
+ import { getEntries } from './message.js';
4
6
  /** Convert a DB row (with Date timestamps) to the shared Session type (with number timestamps). */
5
7
  export function rowToSession(row) {
6
8
  const created = row.createdAt instanceof Date ? row.createdAt.getTime() : new Date(row.createdAt).getTime();
@@ -26,7 +28,13 @@ export function rowToSession(row) {
26
28
  };
27
29
  }
28
30
  /** Create a new root session. */
29
- async function createSession(handle, title, projectId, agentType, source) {
31
+ async function createSession(handle, title, projectId, agentType, source,
32
+ /** P1 会话树治理:子 agent 会话挂到父会话(树内嵌套 + 删除级联)。 */
33
+ parentId,
34
+ /** 会话工作目录(CLI 会话必填:Web 打开时 agent 工具在该目录执行,而非 serve cwd)。 */
35
+ worktreePath,
36
+ /** 初始 metadata(如工作流运行会话记录 workflowName,供中断恢复指引展示)。 */
37
+ metadata) {
30
38
  const [row] = await handle.db
31
39
  .insert(sessions)
32
40
  .values({
@@ -34,23 +42,37 @@ async function createSession(handle, title, projectId, agentType, source) {
34
42
  projectId: projectId ?? null,
35
43
  agentType: agentType ?? null,
36
44
  source: source ?? null,
45
+ parentId: parentId ?? null,
46
+ worktreePath: worktreePath ?? null,
47
+ ...(metadata ? { metadata } : {}),
37
48
  })
38
49
  .returning();
39
50
  if (!row)
40
51
  throw new Error('Failed to insert session');
41
52
  return rowToSession(row);
42
53
  }
54
+ /**
55
+ * Web 会话树可见性条件:web 会话(source 为 null/web)或「持久的 CLI 会话」。
56
+ * 一次性 CLI print 会话与 CLI 来源的 workflow 会话是临时数据,隐藏;
57
+ * Web 来源的 workflow 会话(source=null)在树中可见(标题带时间戳)。
58
+ * 两类临时会话到期后统一**移入回收站**(purgeTemporarySessions),不再物理清除——
59
+ * 可见节点(含其子 agent 会话)享有 60 天可恢复承诺。
60
+ * --continue 续接后 agentType 已清除(upgradeTemporarySession),即对 Web 可见。
61
+ */
62
+ export function webVisibleSessionCondition() {
63
+ return or(isNull(sessions.source), ne(sessions.source, 'cli'), and(eq(sessions.source, 'cli'), or(isNull(sessions.agentType), notInArray(sessions.agentType, ['print', 'workflow']))));
64
+ }
43
65
  /** Get a session by id, or null if not found. */
44
66
  async function getSession(handle, id) {
45
67
  const [row] = await handle.db.select().from(sessions).where(eq(sessions.id, id));
46
68
  return row ? rowToSession(row) : null;
47
69
  }
48
- /** List all active sessions (未软删除、非 CLI 来源;source 为 NULL 的旧数据视为 web). */
70
+ /** List all active sessions(未软删除、Web 树可见:web 会话 + 持久化 CLI 会话)。 */
49
71
  async function listSessions(handle) {
50
72
  const rows = await handle.db
51
73
  .select()
52
74
  .from(sessions)
53
- .where(and(isNull(sessions.deletedAt), or(isNull(sessions.source), ne(sessions.source, 'cli'))));
75
+ .where(and(isNull(sessions.deletedAt), webVisibleSessionCondition()));
54
76
  return rows.map(rowToSession);
55
77
  }
56
78
  /** 列出所有未软删除会话(含 CLI 来源;供 ACP/CLI 等非 Web 消费者使用)。 */
@@ -58,82 +80,430 @@ async function listAllSessions(handle) {
58
80
  const rows = await handle.db.select().from(sessions).where(isNull(sessions.deletedAt));
59
81
  return rows.map(rowToSession);
60
82
  }
61
- /** List soft-deleted sessions (回收站). */
62
- async function listDeletedSessions(handle) {
83
+ /** List soft-deleted sessions (回收站);projectId 提供时仅列出归属该项目的会话
84
+ * (P1-7:回收站此前全库共享,项目 A 的界面能清空项目 B 的删除会话)。 */
85
+ async function listDeletedSessions(handle, projectId) {
63
86
  const rows = await handle.db
64
87
  .select()
65
88
  .from(sessions)
66
- .where(gt(sessions.deletedAt, new Date(0)));
89
+ .where(projectId
90
+ ? and(gt(sessions.deletedAt, new Date(0)), eq(sessions.projectId, projectId))
91
+ : gt(sessions.deletedAt, new Date(0)));
92
+ return rows.map(rowToSession);
93
+ }
94
+ /** 列出未归属任何项目的已软删除会话(孤儿)。删除项目时 FK set null 使会话失去 projectId,
95
+ * 在任何项目的回收站视图都不可见(F1),需专门的全局视图暴露以便恢复/归巢。 */
96
+ async function listOrphanDeletedSessions(handle) {
97
+ const rows = await handle.db
98
+ .select()
99
+ .from(sessions)
100
+ .where(and(gt(sessions.deletedAt, new Date(0)), isNull(sessions.projectId)));
67
101
  return rows.map(rowToSession);
68
102
  }
103
+ /**
104
+ * 标记回收站条目「已被用户看到」,记录 metadata.trashSeenAt = now(仅首次,不重置)。
105
+ * 回收站保留期自首次看到起算(见 purgeDeletedSessions)。语义为分组粒度(M1):
106
+ * 一次调用标记 scope 内全部条目——「首次看到」指用户打开该回收站分组,而非逐条
107
+ * 浏览到某个条目;分组内条目同时起算,列表按到期先后置顶展示以补偿该粒度。
108
+ * 软删除时会清除旧标记,恢复后再次删除须重新看到回收站才重新起算——
109
+ * 避免「未重新看到就被过早清空」。
110
+ * scope.orphan=true 时仅标记未归属项目的孤儿会话(与 listOrphanDeletedSessions 对齐);
111
+ * 孤儿标记仅在用户展开「未归属项目」分组时由前端显式调用(POST /deleted/orphans/seen),
112
+ * 避免打开任意项目回收站连带启动无关孤儿条目的倒计时。
113
+ */
114
+ async function touchTrashSeen(handle, scope = {}) {
115
+ const where = scope.orphan
116
+ ? and(gt(sessions.deletedAt, new Date(0)), isNull(sessions.projectId))
117
+ : scope.projectId
118
+ ? and(gt(sessions.deletedAt, new Date(0)), eq(sessions.projectId, scope.projectId))
119
+ : gt(sessions.deletedAt, new Date(0));
120
+ const rows = await handle.db
121
+ .select({ id: sessions.id, metadata: sessions.metadata })
122
+ .from(sessions)
123
+ .where(where);
124
+ const now = Date.now();
125
+ let touched = 0;
126
+ for (const row of rows) {
127
+ const meta = (row.metadata ?? {});
128
+ // A3:只标记首次看到,不随每次打开重置——否则「剩余天数」虚标,
129
+ // 经常打开回收站的用户条目永不清理,保留期形同虚设。
130
+ if (typeof meta.trashSeenAt === 'number')
131
+ continue;
132
+ await handle.db
133
+ .update(sessions)
134
+ .set({ metadata: { ...meta, trashSeenAt: now } })
135
+ .where(eq(sessions.id, row.id));
136
+ touched += 1;
137
+ }
138
+ return touched;
139
+ }
69
140
  /**
70
141
  * 软删除会话(级联其所有 fork 后代)。设置 deletedAt = now;
71
- * 30 天后由 purgeDeletedSessions 物理清除。
142
+ * 60 天后由 purgeDeletedSessions 物理清除(到期先标记、宽限 7 天)。
143
+ * A3:同时清除 trashSeenAt/purgePendingAt——恢复后重删的会话必须重新被
144
+ * 用户看到才重新起算,宽限期标记不跨删除周期生效。
72
145
  * 会话不存在或已在回收站 → 返回 false(调用方按 404 处理)。
73
146
  */
74
147
  async function softDeleteSession(handle, id) {
75
148
  const [row] = await handle.db
76
- .select({ id: sessions.id, deletedAt: sessions.deletedAt })
149
+ .select({ id: sessions.id, deletedAt: sessions.deletedAt, metadata: sessions.metadata })
77
150
  .from(sessions)
78
151
  .where(eq(sessions.id, id));
79
152
  if (!row || row.deletedAt)
80
153
  return false;
81
154
  const ids = new Set([id]);
155
+ const metas = new Map([[id, row.metadata]]);
82
156
  let frontier = [id];
83
157
  while (frontier.length > 0) {
84
158
  // 用 parentId 过滤:收集下一层子会话
85
159
  const children = await handle.db
86
- .select({ id: sessions.id })
160
+ .select({ id: sessions.id, metadata: sessions.metadata })
87
161
  .from(sessions)
88
162
  .where(and(isNull(sessions.deletedAt), inArray(sessions.parentId, frontier)));
89
163
  frontier = children.map((r) => r.id).filter((cid) => !ids.has(cid));
90
- for (const r of children)
164
+ for (const r of children) {
91
165
  ids.add(r.id);
166
+ metas.set(r.id, r.metadata);
167
+ }
92
168
  }
93
169
  const now = new Date();
170
+ // 同一次删除级联共享同一批次号:恢复时仅还原同批次后代,避免把早先
171
+ // 被用户单独删除的分支一并复活(删除/恢复级联的语义不对称缺陷)。
172
+ const batchId = generateId();
94
173
  for (const sid of ids) {
95
- await handle.db.update(sessions).set({ deletedAt: now }).where(eq(sessions.id, sid));
174
+ await handle.db
175
+ .update(sessions)
176
+ .set({
177
+ deletedAt: now,
178
+ deletedBatchId: batchId,
179
+ metadata: clearTrashMarks((metas.get(sid) ?? {})),
180
+ })
181
+ .where(eq(sessions.id, sid));
96
182
  }
97
183
  return true;
98
184
  }
99
185
  /**
100
- * 从回收站恢复会话:默认连带恢复其已软删除的祖先链——
101
- * 否则恢复的会话因父仍在回收站而游离于会话树之外(P2-1:UI 不可达)。
102
- * 祖先中未删除的(活跃)节点不需要也不应该被改动。
186
+ * A3:清除回收站倒计时/宽限期标记(软删除与项目删除共用)。
187
+ * 恢复后重删的会话须重新被看到才重新起算;宽限期标记不跨删除周期生效。
188
+ * 无标记时原样返回,避免无谓的 jsonb 覆写。
189
+ */
190
+ function clearTrashMarks(meta) {
191
+ if (typeof meta.trashSeenAt !== 'number' && typeof meta.purgePendingAt !== 'number')
192
+ return meta;
193
+ const { trashSeenAt: _seen, purgePendingAt: _purge, ...rest } = meta;
194
+ return rest;
195
+ }
196
+ /**
197
+ * 把会话归属到指定项目(P1-2 孤儿会话补救):设置 projectId 与 worktreePath。
198
+ * 会话不存在返回 false。
103
199
  */
104
- async function restoreSession(handle, id) {
200
+ async function rebindSession(handle, id, project) {
105
201
  const [row] = await handle.db.select().from(sessions).where(eq(sessions.id, id));
106
- if (!row || !row.deletedAt)
202
+ if (!row)
107
203
  return false;
204
+ await handle.db
205
+ .update(sessions)
206
+ .set({ projectId: project.id, worktreePath: project.worktree })
207
+ .where(eq(sessions.id, id));
208
+ return true;
209
+ }
210
+ async function restoreSessionCore(handle, id, opts = {}) {
211
+ const includeDescendants = opts.includeDescendants !== false;
212
+ const [row] = await handle.db.select().from(sessions).where(eq(sessions.id, id));
213
+ if (!row?.deletedAt)
214
+ return {
215
+ restored: false,
216
+ restoredAncestorCount: 0,
217
+ crossedBatchAncestor: false,
218
+ leftBehindDescendantCount: 0,
219
+ };
220
+ const targetBatch = row.deletedBatchId;
108
221
  const ids = new Set([id]);
222
+ // 后代 BFS:仅还原属于同一删除批次的后代(删除时父+后代共享 deletedBatchId)。
223
+ // 若只按「已删除」收集,会把早先被用户单独删除的分支一并复活(F2 不对称缺陷)。
224
+ if (includeDescendants) {
225
+ let frontier = [id];
226
+ while (frontier.length > 0) {
227
+ const children = await handle.db
228
+ .select({ id: sessions.id })
229
+ .from(sessions)
230
+ .where(and(gt(sessions.deletedAt, new Date(0)), inArray(sessions.parentId, frontier), row.deletedBatchId
231
+ ? eq(sessions.deletedBatchId, row.deletedBatchId)
232
+ : isNull(sessions.deletedBatchId)));
233
+ frontier = children.map((r) => r.id).filter((cid) => !ids.has(cid));
234
+ for (const r of children)
235
+ ids.add(r.id);
236
+ }
237
+ }
238
+ // A2:统计所有已删后代中未随本次恢复的数量(批次不同 → 滞留回收站)。
239
+ let leftBehindDescendantCount = 0;
240
+ if (includeDescendants) {
241
+ const seen = new Set();
242
+ let frontier = [id];
243
+ while (frontier.length > 0) {
244
+ const children = await handle.db
245
+ .select({ id: sessions.id })
246
+ .from(sessions)
247
+ .where(and(gt(sessions.deletedAt, new Date(0)), inArray(sessions.parentId, frontier)));
248
+ frontier = [];
249
+ for (const ch of children) {
250
+ if (seen.has(ch.id))
251
+ continue;
252
+ seen.add(ch.id);
253
+ frontier.push(ch.id);
254
+ if (!ids.has(ch.id))
255
+ leftBehindDescendantCount += 1;
256
+ }
257
+ }
258
+ }
259
+ // 祖先链(仅还原其中已软删除的节点):为保证恢复节点在会话树可达,
260
+ // 已删除的祖先无论批次均需一并还原。记录还原的祖先数量与是否跨越删除批次,
261
+ // 供前端提示「为保持会话树完整,同时还原了 N 个父会话」。
262
+ let restoredAncestorCount = 0;
263
+ let crossedBatchAncestor = false;
109
264
  let parentId = row.parentId;
110
265
  while (parentId) {
111
266
  const [parent] = await handle.db.select().from(sessions).where(eq(sessions.id, parentId));
112
267
  if (!parent)
113
268
  break;
114
- if (parent.deletedAt)
269
+ if (parent.deletedAt && !ids.has(parent.id)) {
115
270
  ids.add(parent.id);
271
+ restoredAncestorCount += 1;
272
+ if (parent.deletedBatchId !== targetBatch)
273
+ crossedBatchAncestor = true;
274
+ }
116
275
  parentId = parent.parentId;
117
276
  }
118
277
  for (const sid of ids) {
119
- await handle.db.update(sessions).set({ deletedAt: null }).where(eq(sessions.id, sid));
278
+ await handle.db
279
+ .update(sessions)
280
+ .set({ deletedAt: null, deletedBatchId: null })
281
+ .where(eq(sessions.id, sid));
120
282
  }
121
- return true;
283
+ return {
284
+ restored: true,
285
+ restoredAncestorCount,
286
+ crossedBatchAncestor,
287
+ leftBehindDescendantCount,
288
+ };
289
+ }
290
+ async function restoreSession(handle, id, opts = {}) {
291
+ return (await restoreSessionCore(handle, id, opts)).restored;
122
292
  }
123
293
  /**
124
- * 物理清除回收站中超过保留期(默认 30 天)的会话。
125
- * 子会话先于父会话删除(自引用 FK 要求)。
126
- * 返回清除数量。启动时与每日定时调用。
294
+ * P2-4:回收站保留期 60 天(原 30 天),自「用户首次在回收站看到该条目」
295
+ * (metadata.trashSeenAt)起算,而非删除时间(墙钟)——否则用户删除后长期不开服务、
296
+ * 重启即被静默物理清除,从未见过任何倒计时。临时会话(CLI print / workflow)保留期
297
+ * 仍为 30 天,见 purgeTemporarySessions。
127
298
  */
128
- async function purgeDeletedSessions(handle, retentionMs = 30 * 24 * 60 * 60 * 1000) {
299
+ export const TRASH_RETENTION_MS = 60 * 24 * 60 * 60 * 1000;
300
+ /**
301
+ * 绝对上限(365 天):自删除起超过此时长的条目,无论是否被看到都进入宽限期清理。
302
+ * 兜底「用户从不开回收站 → trashSeenAt 永不写入 → 条目永不清理」的无界增长死角。
303
+ * 到期仍走先标记(宽限 7 天、UI 显示「即将清除」可恢复)再物理清除的两阶段流程。
304
+ */
305
+ export const TRASH_ABSOLUTE_MAX_MS = 365 * 24 * 60 * 60 * 1000;
306
+ /**
307
+ * A3:到期标记 → 物理清除的宽限期(默认 7 天)。
308
+ * 条目到期后先写 metadata.purgePendingAt,UI 显示「即将清除」并可恢复;
309
+ * 宽限期满才物理清除——杜绝「到期即静默物理清空」的数据丢失死角。
310
+ */
311
+ export const TRASH_PURGE_GRACE_MS = 7 * 24 * 60 * 60 * 1000;
312
+ /**
313
+ * 回收站两阶段清理(启动时与每日定时调用):
314
+ * 阶段一:进入保留期截止(首次看到 + 60 天)或绝对上限(删除 + 365 天,无论是否
315
+ * 被看到)且未标记 → 标记 purgePendingAt(进入宽限期,可恢复)。
316
+ * 阶段二:purgePendingAt 早于宽限截止 → 物理清除(子会话先于父,自引用 FK 要求)。
317
+ * 恢复后重删、尚未重新看到的会话按「首次看到 + 60 天」计(绝对上限兜底仍生效)。
318
+ */
319
+ async function purgeDeletedSessions(handle, retentionMs = TRASH_RETENTION_MS, graceMs = TRASH_PURGE_GRACE_MS) {
320
+ const retentionCutoff = Date.now() - retentionMs;
321
+ const graceCutoff = Date.now() - graceMs;
322
+ const absoluteCutoff = Date.now() - TRASH_ABSOLUTE_MAX_MS;
323
+ const rows = await handle.db
324
+ .select({
325
+ id: sessions.id,
326
+ parentId: sessions.parentId,
327
+ deletedAt: sessions.deletedAt,
328
+ metadata: sessions.metadata,
329
+ })
330
+ .from(sessions)
331
+ .where(gt(sessions.deletedAt, new Date(0)));
332
+ // —— 阶段一:到期标记(先宽限,不直接清除)——
333
+ let marked = 0;
334
+ for (const r of rows) {
335
+ if (!r.deletedAt)
336
+ continue;
337
+ const meta = (r.metadata ?? {});
338
+ const seen = meta.trashSeenAt;
339
+ const deletedMs = r.deletedAt.getTime();
340
+ // 保留期自「首次看到」起算:seen 缺失或早于最近一次删除 → 未重新看到,不计。
341
+ const seenExpired = typeof seen === 'number' && seen > deletedMs && seen < retentionCutoff;
342
+ // 绝对上限兜底:删除后长期未被看到也进入宽限期,防条目永不清理的无界增长。
343
+ const absoluteExpired = deletedMs < absoluteCutoff;
344
+ if (!seenExpired && !absoluteExpired)
345
+ continue;
346
+ if (typeof meta.purgePendingAt === 'number')
347
+ continue;
348
+ await handle.db
349
+ .update(sessions)
350
+ .set({ metadata: { ...meta, purgePendingAt: Date.now() } })
351
+ .where(eq(sessions.id, r.id));
352
+ marked += 1;
353
+ }
354
+ // —— 阶段二:宽限期满物理清除 ——
355
+ const pending = rows.filter((r) => {
356
+ if (!r.deletedAt)
357
+ return false;
358
+ const meta = (r.metadata ?? {});
359
+ const p = meta.purgePendingAt;
360
+ return typeof p === 'number' && p < graceCutoff;
361
+ });
362
+ let deleted = 0;
363
+ if (pending.length > 0) {
364
+ // 拓扑序:无子会话的先删
365
+ const remaining = new Set(pending.map((r) => r.id));
366
+ while (remaining.size > 0) {
367
+ const hasChildParent = new Set(pending.filter((r) => r.parentId && remaining.has(r.parentId)).map((r) => r.parentId));
368
+ const leaves = pending
369
+ .filter((r) => remaining.has(r.id) && !hasChildParent.has(r.id))
370
+ .map((r) => r.id);
371
+ if (leaves.length === 0) {
372
+ // 循环引用兜底:强制按 id 逐个删除(自引用环数据异常场景)
373
+ for (const id of Array.from(remaining)) {
374
+ await handle.db.delete(sessions).where(eq(sessions.id, id));
375
+ remaining.delete(id);
376
+ deleted += 1;
377
+ }
378
+ break;
379
+ }
380
+ for (const id of leaves) {
381
+ await handle.db.delete(sessions).where(eq(sessions.id, id));
382
+ remaining.delete(id);
383
+ deleted += 1;
384
+ }
385
+ }
386
+ }
387
+ return { marked, deleted };
388
+ }
389
+ /** C4:空会话 GC 保留期(7 天)。仅覆盖「从未写入任何消息/工具条目」的会话。 */
390
+ const EMPTY_SESSION_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
391
+ /**
392
+ * C4:清理长期空置的 web 会话(无任何条目、无子会话、未删除、创建早于保留期)。
393
+ * 覆盖「新建会话后未发言」的残留:首条消息失败清理依赖前端 JS 执行,
394
+ * 崩溃/断电/离线时必然残留空「New Session」行。CLI 临时会话由 purgeTemporarySessions 负责。
395
+ */
396
+ async function purgeEmptySessions(handle, retentionMs = EMPTY_SESSION_RETENTION_MS) {
129
397
  const cutoff = new Date(Date.now() - retentionMs);
398
+ const children = alias(sessions, 'empty_children');
399
+ const result = await handle.db
400
+ .delete(sessions)
401
+ .where(and(lt(sessions.createdAt, cutoff), isNull(sessions.deletedAt), or(isNull(sessions.source), eq(sessions.source, 'web')), notExists(handle.db
402
+ .select({ one: sql `1` })
403
+ .from(sessionEntries)
404
+ .where(eq(sessionEntries.sessionId, sessions.id))), notExists(handle.db
405
+ .select({ one: sql `1` })
406
+ .from(children)
407
+ .where(eq(children.parentId, sessions.id)))))
408
+ .returning({ id: sessions.id });
409
+ return result.length;
410
+ }
411
+ /** Update a session's title. */
412
+ async function updateSessionTitle(handle, id, title) {
413
+ await handle.db.update(sessions).set({ title, updatedAt: new Date() }).where(eq(sessions.id, id));
414
+ }
415
+ /**
416
+ * P3-9:物理删除一个「真空会话」——无任何条目且无子会话(首条消息发送失败后
417
+ * 的前端清理路径使用)。非空会话绝不触碰(返回 false),防误删走错回收站语义。
418
+ * 软删除会话(回收站条目)不在本路径处理。
419
+ */
420
+ async function purgeEmptySession(handle, id) {
421
+ const [row] = await handle.db
422
+ .select({ id: sessions.id, deletedAt: sessions.deletedAt })
423
+ .from(sessions)
424
+ .where(eq(sessions.id, id));
425
+ if (!row || row.deletedAt)
426
+ return false;
427
+ const children = alias(sessions, 'purge_children');
428
+ const [entry] = await handle.db
429
+ .select({ one: sql `1` })
430
+ .from(sessionEntries)
431
+ .where(eq(sessionEntries.sessionId, id))
432
+ .limit(1);
433
+ if (entry)
434
+ return false;
435
+ const [child] = await handle.db
436
+ .select({ one: sql `1` })
437
+ .from(children)
438
+ .where(eq(children.parentId, id))
439
+ .limit(1);
440
+ if (child)
441
+ return false;
442
+ await handle.db.delete(sessions).where(eq(sessions.id, id));
443
+ return true;
444
+ }
445
+ /**
446
+ * 彻底删除某个回收站会话及其全部后代(含未软删除的 fork 后代,防御数据异常)。
447
+ * 子会话先于父会话删除(自引用 FK RESTRICT 要求);entries/archives 经 FK cascade 清理。
448
+ * 返回删除数量。会话不存在或不在回收站 → 返回 0。
449
+ */
450
+ async function permanentlyDeleteSession(handle, id) {
451
+ const [row] = await handle.db
452
+ .select({ id: sessions.id, deletedAt: sessions.deletedAt })
453
+ .from(sessions)
454
+ .where(eq(sessions.id, id));
455
+ if (!row?.deletedAt)
456
+ return 0;
457
+ const all = await handle.db
458
+ .select({ id: sessions.id, parentId: sessions.parentId })
459
+ .from(sessions);
460
+ const ids = new Set([id]);
461
+ let frontier = [id];
462
+ while (frontier.length > 0) {
463
+ const children = all
464
+ .filter((r) => r.parentId && frontier.includes(r.parentId))
465
+ .map((r) => r.id)
466
+ .filter((cid) => !ids.has(cid));
467
+ for (const cid of children)
468
+ ids.add(cid);
469
+ frontier = children;
470
+ }
471
+ // 拓扑序:无子会话的先删(与 purgeDeletedSessions 同策略)
472
+ const remaining = new Set(ids);
473
+ let deleted = 0;
474
+ while (remaining.size > 0) {
475
+ const hasChildParent = new Set(Array.from(remaining).filter((rid) => {
476
+ const r = all.find((x) => x.id === rid);
477
+ return r?.parentId && remaining.has(r.parentId);
478
+ }));
479
+ const leaves = Array.from(remaining).filter((rid) => !hasChildParent.has(rid));
480
+ if (leaves.length === 0) {
481
+ for (const rid of Array.from(remaining)) {
482
+ await handle.db.delete(sessions).where(eq(sessions.id, rid));
483
+ remaining.delete(rid);
484
+ deleted += 1;
485
+ }
486
+ break;
487
+ }
488
+ for (const rid of leaves) {
489
+ await handle.db.delete(sessions).where(eq(sessions.id, rid));
490
+ remaining.delete(rid);
491
+ deleted += 1;
492
+ }
493
+ }
494
+ return deleted;
495
+ }
496
+ /** 清空回收站:物理删除所有已软删除会话(子先于父)。返回删除数量。
497
+ * projectId 提供时仅清空该项目(P1-7)。 */
498
+ async function emptyTrash(handle, projectId) {
130
499
  const rows = await handle.db
131
500
  .select({ id: sessions.id, parentId: sessions.parentId })
132
501
  .from(sessions)
133
- .where(lt(sessions.deletedAt, cutoff));
502
+ .where(projectId
503
+ ? and(gt(sessions.deletedAt, new Date(0)), eq(sessions.projectId, projectId))
504
+ : gt(sessions.deletedAt, new Date(0)));
134
505
  if (rows.length === 0)
135
506
  return 0;
136
- // 拓扑序:无子会话的先删
137
507
  const remaining = new Set(rows.map((r) => r.id));
138
508
  let deleted = 0;
139
509
  while (remaining.size > 0) {
@@ -142,7 +512,6 @@ async function purgeDeletedSessions(handle, retentionMs = 30 * 24 * 60 * 60 * 10
142
512
  .filter((r) => remaining.has(r.id) && !hasChildParent.has(r.id))
143
513
  .map((r) => r.id);
144
514
  if (leaves.length === 0) {
145
- // 循环引用兜底:强制按 id 逐个删除(自引用环数据异常场景)
146
515
  for (const id of Array.from(remaining)) {
147
516
  await handle.db.delete(sessions).where(eq(sessions.id, id));
148
517
  remaining.delete(id);
@@ -158,9 +527,44 @@ async function purgeDeletedSessions(handle, retentionMs = 30 * 24 * 60 * 60 * 10
158
527
  }
159
528
  return deleted;
160
529
  }
161
- /** Update a session's title. */
162
- async function updateSessionTitle(handle, id, title) {
163
- await handle.db.update(sessions).set({ title, updatedAt: new Date() }).where(eq(sessions.id, id));
530
+ /**
531
+ * 清理过期临时会话(P2 → P1 收紧:仅处理显式标记的临时会话)。
532
+ * - agentType='print':CLI 一次性问答(c0de chat,非 --continue)创建的会话。
533
+ * - agentType='workflow':工作流运行产生的会话。
534
+ * 普通 CLI 会话(ACP)与 --continue 续接的会话(续接时已升级)永不清理——
535
+ * 此前按 source='cli' 全删会把用户显式续接的历史静默物理删除。
536
+ * 保留期默认 30 天。
537
+ *
538
+ * P1(本轮):清理方式从物理删除改为**移入回收站**(softDeleteSession)——
539
+ * 工作流/print 会话的子 agent 会话在 Web 树中可见,此前「30 天到点即物理清除」
540
+ * 让可见节点无回收站保护地凭空消失,与「60 天可恢复」的产品承诺冲突。
541
+ * 软删除级联同批次后代;随后由 purgeDeletedSessions 的两阶段回收站机制
542
+ * (trashSeenAt 60 天 / 绝对上限 365 天 + 7 天宽限)统一兜底。
543
+ * 返回本次移入回收站的会话数。
544
+ */
545
+ async function purgeTemporarySessions(handle, retentionMs = 30 * 24 * 60 * 60 * 1000) {
546
+ const cutoff = new Date(Date.now() - retentionMs);
547
+ const rows = await handle.db
548
+ .select({ id: sessions.id, agentType: sessions.agentType, updatedAt: sessions.updatedAt })
549
+ .from(sessions)
550
+ .where(isNull(sessions.deletedAt));
551
+ const roots = rows.filter((r) => (r.agentType === 'print' || r.agentType === 'workflow') &&
552
+ r.updatedAt != null &&
553
+ r.updatedAt.getTime() < cutoff.getTime());
554
+ let trashed = 0;
555
+ for (const r of roots) {
556
+ // softDeleteSession 级联标记其后代(同 deletedBatchId),恢复父即恢复子树。
557
+ if (await softDeleteSession(handle, r.id))
558
+ trashed += 1;
559
+ }
560
+ return trashed;
561
+ }
562
+ /**
563
+ * 把临时会话升级为持久会话(--continue 续接时调用):
564
+ * 清除 print 标记,30 天临时清理不再触及。
565
+ */
566
+ async function upgradeTemporarySession(handle, id) {
567
+ await handle.db.update(sessions).set({ agentType: null }).where(eq(sessions.id, id));
164
568
  }
165
569
  /** Bump updatedAt to now (used after appending messages). */
166
570
  async function touchSession(handle, id) {
@@ -264,11 +668,151 @@ async function updateSessionLastRun(handle, id, lastRun) {
264
668
  .set({ metadata: { ...meta, lastRun }, updatedAt: new Date() })
265
669
  .where(eq(sessions.id, id));
266
670
  }
671
+ /**
672
+ * P2-3:预算超支暂停时把原因写入 metadata.budgetPauseReason。
673
+ * 热更新/重启后 run 重建时由 consumeBudgetPauseMarker 消费——恢复内存
674
+ * budgetPauseTriggered 标记,避免同一超支原因二次暂停。
675
+ */
676
+ async function markBudgetPause(handle, id, reason) {
677
+ const [row] = await handle.db.select().from(sessions).where(eq(sessions.id, id));
678
+ if (!row)
679
+ return;
680
+ const meta = (row.metadata ?? {});
681
+ await handle.db
682
+ .update(sessions)
683
+ .set({ metadata: { ...meta, budgetPauseReason: reason }, updatedAt: new Date() })
684
+ .where(eq(sessions.id, id));
685
+ }
686
+ /**
687
+ * P2-3:消费预算暂停标记(新 run 启动时调用一次)。返回上次暂停原因并删除字段;
688
+ * 无标记返回 null。消费即删除——单次语义,之后 run 恢复常规预算检查。
689
+ */
690
+ async function consumeBudgetPauseMarker(handle, id) {
691
+ const [row] = await handle.db
692
+ .select({ metadata: sessions.metadata })
693
+ .from(sessions)
694
+ .where(eq(sessions.id, id));
695
+ if (!row)
696
+ return null;
697
+ const meta = (row.metadata ?? {});
698
+ const reason = typeof meta.budgetPauseReason === 'string' ? meta.budgetPauseReason : null;
699
+ if (reason === null)
700
+ return null;
701
+ const { budgetPauseReason: _omit, ...rest } = meta;
702
+ await handle.db
703
+ .update(sessions)
704
+ .set({ metadata: rest, updatedAt: new Date() })
705
+ .where(eq(sessions.id, id));
706
+ return reason;
707
+ }
267
708
  async function listSessionsByProject(handle, projectId) {
268
709
  const rows = await handle.db
269
710
  .select()
270
711
  .from(sessions)
271
- .where(and(eq(sessions.projectId, projectId), isNull(sessions.deletedAt), or(isNull(sessions.source), ne(sessions.source, 'cli'))));
712
+ .where(and(eq(sessions.projectId, projectId), isNull(sessions.deletedAt), webVisibleSessionCondition()));
272
713
  return rows.map(rowToSession);
273
714
  }
274
- export { createSession, getSession, listAllSessions, listDeletedSessions, listSessions, listSessionsByProject, purgeDeletedSessions, restoreSession, softDeleteSession, touchLastOpened, touchSession, updateSessionLastRun, updateSessionTitle, };
715
+ /**
716
+ * 跨会话搜索(P2-6):标题 + 消息内容子串匹配。
717
+ * 默认仅搜索未软删除的非 CLI 会话(与 Web 会话树一致);projectId 提供时限定项目。
718
+ * opts.includeDeleted=true 时搜索回收站(P3:回收站无搜索,删后找内容只能逐行翻)。
719
+ * 返回 { session, matchedBy: 'title' | 'content' }。
720
+ */
721
+ async function searchSessions(handle, query, projectId, opts = {}) {
722
+ const needle = query.trim();
723
+ if (!needle)
724
+ return [];
725
+ const pattern = `%${needle.replace(/[%_\\]/g, '\\$&')}%`;
726
+ const baseWhere = and(opts.includeDeleted ? gt(sessions.deletedAt, new Date(0)) : isNull(sessions.deletedAt),
727
+ // 回收站搜索与回收站列表(listDeletedSessions)一致:不过滤 source。
728
+ ...(opts.includeDeleted ? [] : [webVisibleSessionCondition()]), ...(projectId ? [eq(sessions.projectId, projectId)] : []));
729
+ // 标题命中(ILIKE 转义后按字面子串)
730
+ const byTitle = await handle.db
731
+ .select()
732
+ .from(sessions)
733
+ .where(and(baseWhere, ilike(sessions.title, pattern)));
734
+ const byContent = await handle.db
735
+ .selectDistinctOn([sessions.id], {
736
+ id: sessions.id,
737
+ title: sessions.title,
738
+ parentId: sessions.parentId,
739
+ projectId: sessions.projectId,
740
+ branchPoint: sessions.branchPoint,
741
+ metadata: sessions.metadata,
742
+ agentType: sessions.agentType,
743
+ worktreePath: sessions.worktreePath,
744
+ source: sessions.source,
745
+ deletedAt: sessions.deletedAt,
746
+ deletedBatchId: sessions.deletedBatchId,
747
+ createdAt: sessions.createdAt,
748
+ updatedAt: sessions.updatedAt,
749
+ })
750
+ .from(sessionEntries)
751
+ .innerJoin(sessions, eq(sessionEntries.sessionId, sessions.id))
752
+ .where(and(baseWhere, eq(sessionEntries.tag, 'message'), sql `${sessionEntries.content}::text ILIKE ${pattern}`))
753
+ .orderBy(sessions.id, sessions.updatedAt);
754
+ const result = [];
755
+ const seen = new Set();
756
+ for (const row of byTitle) {
757
+ const s = rowToSession(row);
758
+ result.push({ session: s, matchedBy: 'title' });
759
+ seen.add(s.id);
760
+ }
761
+ for (const row of byContent) {
762
+ const s = rowToSession(row);
763
+ if (!seen.has(s.id)) {
764
+ result.push({ session: s, matchedBy: 'content' });
765
+ seen.add(s.id);
766
+ }
767
+ }
768
+ return result;
769
+ }
770
+ /**
771
+ * M3:把中断 run 的半截轮次标记为「未完成」——写入 metadata 的
772
+ * (unfinishedSinceEntryId, unfinishedUntilEntryId] 区间。
773
+ * 检测条件:lastRun.status 仍为 running/paused(进程崩溃/重启或热更新交接
774
+ * 未能写入 completed;deliberate abort 由 SSE finally 正常写 completed,不误标)。
775
+ * 边界 = 最后一条含文本的 user 消息;其后的条目全部属中断轮次——
776
+ * 上下文构建剔除(context.ts),前端时间线置灰。
777
+ * 边界已是最后一条 → 无半截内容,跳过标记。
778
+ * 重复中断:区间起点不变(重发不追加 user 消息),终点推进到最新末尾,天然合并。
779
+ */
780
+ async function markUnfinishedTurn(handle, sessionId) {
781
+ const [row] = await handle.db
782
+ .select({ metadata: sessions.metadata })
783
+ .from(sessions)
784
+ .where(eq(sessions.id, sessionId));
785
+ if (!row)
786
+ return;
787
+ const meta = (row.metadata ?? {});
788
+ const status = meta.lastRun?.status;
789
+ if (status !== 'running' && status !== 'paused')
790
+ return;
791
+ const entries = await getEntries(handle, sessionId);
792
+ let lastUserIdx = -1;
793
+ for (let i = entries.length - 1; i >= 0; i--) {
794
+ const e = entries[i];
795
+ // P1-2:边界取「含任意内容(text 或 image)的 user 消息」——此前仅认 text,
796
+ // 纯图片消息不在边界判定内,中断轮次起点会错误回退到更早的文本消息。
797
+ if (e &&
798
+ !('_tag' in e) &&
799
+ e.role === 'user' &&
800
+ e.content.some((p) => p._tag === 'text' || p._tag === 'image')) {
801
+ lastUserIdx = i;
802
+ break;
803
+ }
804
+ }
805
+ if (lastUserIdx < 0 || lastUserIdx === entries.length - 1)
806
+ return;
807
+ const sinceId = entries[lastUserIdx]?.id;
808
+ const untilId = entries[entries.length - 1]?.id;
809
+ if (!sinceId || !untilId)
810
+ return;
811
+ await handle.db
812
+ .update(sessions)
813
+ .set({
814
+ metadata: { ...meta, unfinishedSinceEntryId: sinceId, unfinishedUntilEntryId: untilId },
815
+ })
816
+ .where(eq(sessions.id, sessionId));
817
+ }
818
+ export { clearTrashMarks, consumeBudgetPauseMarker, createSession, emptyTrash, getSession, listAllSessions, listDeletedSessions, listOrphanDeletedSessions, listSessions, listSessionsByProject, markBudgetPause, markUnfinishedTurn, permanentlyDeleteSession, purgeDeletedSessions, purgeEmptySession, purgeEmptySessions, purgeTemporarySessions, rebindSession, restoreSession, restoreSessionCore, searchSessions, softDeleteSession, touchLastOpened, touchSession, touchTrashSeen, updateSessionLastRun, updateSessionTitle, upgradeTemporarySession, };