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,10 +1,53 @@
1
- import { and, asc, eq, max } from 'drizzle-orm';
1
+ import { and, asc, eq, inArray, isNotNull, isNull, max, sql } from 'drizzle-orm';
2
2
  import { kanbanBoards, kanbanCards } from '../db/schema.js';
3
3
  import { DEFAULT_KANBAN_COLUMNS } from '../shared/types/kanban.js';
4
4
  /** Default column when none is specified. */
5
5
  const DEFAULT_COLUMN_ID = 'todo';
6
6
  /** Position increment — large gap avoids frequent re-indexing on reorder. */
7
7
  const POSITION_GAP = 1000;
8
+ /** 合法优先级枚举(运行时校验用——REST/tool 层输入未经 TS 约束)。 */
9
+ const VALID_PRIORITIES = new Set(['high', 'medium', 'low']);
10
+ /** 列内仍有卡片时禁止删除该列(否则卡片静默不可见)。 */
11
+ class KanbanColumnInUseError extends Error {
12
+ constructor(columnNames, cardCount, cardIds = []) {
13
+ const idHint = cardIds.length > 0
14
+ ? `(示例卡片 id:${cardIds.slice(0, 5).join('、')}${cardIds.length > 5 ? ' 等' : ''},可直接按 id 删除这些卡片)`
15
+ : '';
16
+ super(`列 [${columnNames.join(', ')}] 中仍有 ${cardCount} 张卡片,无法删除。` +
17
+ `请先移动或删除这些卡片。${idHint}`);
18
+ }
19
+ }
20
+ /**
21
+ * 目标列不存在(add/move 引用未配置的列)——与 import/删列的悬空列防护同口径:
22
+ * 此前运行时 add/move 不校验,LLM 幻觉列名写入后卡片静默不可见,且幽灵卡片
23
+ * 会反过来冻结列配置删除(KanbanColumnInUseError 死锁)。
24
+ * 错误信息回传可用列清单,供 LLM 自纠而非再猜。
25
+ */
26
+ class KanbanColumnNotFoundError extends Error {
27
+ columnId;
28
+ constructor(columnId, availableColumns, op) {
29
+ const avail = availableColumns.length > 0 ? availableColumns.join('、') : '(看板无列)';
30
+ super(`列 "${columnId}" 不存在,无法${op === 'add' ? '创建' : '移动'}卡片。` +
31
+ `可用列:${avail}。请改用其中一个列 id。`);
32
+ this.columnId = columnId;
33
+ }
34
+ }
35
+ /** 非法优先级(REST PATCH / kanbanTool 透传未校验值)。 */
36
+ class KanbanInvalidPriorityError extends Error {
37
+ value;
38
+ constructor(value) {
39
+ super(`无效优先级 "${value}",允许值:high、medium、low`);
40
+ this.value = value;
41
+ }
42
+ }
43
+ /** 卡片不存在(update/move 目标 id 无效)。 */
44
+ class KanbanCardNotFoundError extends Error {
45
+ cardId;
46
+ constructor(cardId) {
47
+ super(`Kanban card not found: ${cardId}`);
48
+ this.cardId = cardId;
49
+ }
50
+ }
8
51
  // ── Row → API mappers ──────────────────────────────────────
9
52
  function rowToBoard(row) {
10
53
  return {
@@ -50,9 +93,14 @@ function createKanbanStore(handle, projectId) {
50
93
  const [row] = await db
51
94
  .select({ id: kanbanBoards.id })
52
95
  .from(kanbanBoards)
53
- .where(eq(kanbanBoards.projectId, projectId))
96
+ .where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
54
97
  .limit(1);
55
- // 行一定存在:上面 insert + onConflictDoNothing 保证了 projectId 对应的行已创建
98
+ // 正常路径行一定存在(insert + onConflictDoNothing 已保证)。防御性兜底:
99
+ // 唯一 projectId 槽位被软删行占用等异常态下不再返回 undefined id
100
+ //(后续查询会静默查空、上层误判「板不存在」),显式抛错暴露问题。
101
+ if (!row) {
102
+ throw new Error(`Kanban board for project ${projectId} missing after ensure-insert (unique slot likely held by a soft-deleted row)`);
103
+ }
56
104
  return row.id;
57
105
  }
58
106
  /** Max position in a column (0 if empty). */
@@ -63,7 +111,45 @@ function createKanbanStore(handle, projectId) {
63
111
  .where(and(eq(kanbanCards.boardId, boardId), eq(kanbanCards.columnId, columnId)));
64
112
  return row?.m ?? 0;
65
113
  }
114
+ /** 当前板的列配置(add/move 的悬空列校验用)。 */
115
+ async function boardColumns(boardId) {
116
+ const [row] = await db
117
+ .select({ columns: kanbanBoards.columns })
118
+ .from(kanbanBoards)
119
+ .where(eq(kanbanBoards.id, boardId))
120
+ .limit(1);
121
+ return (row?.columns ?? []);
122
+ }
123
+ /** 校验列存在;不存在抛 KanbanColumnNotFoundError(消息含可用列清单)。 */
124
+ function assertColumn(columns, columnId, op) {
125
+ if (!columns.some((c) => c.id === columnId)) {
126
+ throw new KanbanColumnNotFoundError(columnId, columns.map((c) => c.id), op);
127
+ }
128
+ }
129
+ /** 校验优先级合法;非法抛 KanbanInvalidPriorityError。 */
130
+ function assertPriority(priority) {
131
+ if (priority !== undefined && !VALID_PRIORITIES.has(priority)) {
132
+ throw new KanbanInvalidPriorityError(String(priority));
133
+ }
134
+ }
66
135
  return {
136
+ /** 只读获取板(不创建):导出等纯读路径用——此前导出经 getBoard 懒建板,
137
+ * 「导出」一个从未打开过看板的项目会凭空产生空板行。无板返回 null。 */
138
+ async peekBoard() {
139
+ const [boardRow] = await db
140
+ .select()
141
+ .from(kanbanBoards)
142
+ .where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
143
+ .limit(1);
144
+ if (!boardRow)
145
+ return null;
146
+ const cards = await db
147
+ .select()
148
+ .from(kanbanCards)
149
+ .where(eq(kanbanCards.boardId, boardRow.id))
150
+ .orderBy(asc(kanbanCards.columnId), asc(kanbanCards.position));
151
+ return { ...rowToBoard(boardRow), cards: cards.map(rowToCard) };
152
+ },
67
153
  async getBoard() {
68
154
  const boardId = await getOrCreateBoardId();
69
155
  const [boardRow] = await db
@@ -81,7 +167,12 @@ function createKanbanStore(handle, projectId) {
81
167
  },
82
168
  async addCard(input) {
83
169
  const boardId = await getOrCreateBoardId();
84
- const columnId = input.columnId ?? DEFAULT_COLUMN_ID;
170
+ const columns = await boardColumns(boardId);
171
+ // 默认列 = 板上第一列(而非硬编码 'todo'):todo 列被删除/重排后
172
+ // 硬编码默认会让无参 addCard 静默落入不存在的列(卡片不可见)。
173
+ const columnId = input.columnId ?? columns[0]?.id ?? DEFAULT_COLUMN_ID;
174
+ assertColumn(columns, columnId, 'add');
175
+ assertPriority(input.priority);
85
176
  const position = (await maxPos(boardId, columnId)) + POSITION_GAP;
86
177
  const [cardRow] = await db
87
178
  .insert(kanbanCards)
@@ -99,6 +190,7 @@ function createKanbanStore(handle, projectId) {
99
190
  return rowToCard(row);
100
191
  },
101
192
  async updateCard(id, patch) {
193
+ assertPriority(patch.priority);
102
194
  const [row] = await db
103
195
  .update(kanbanCards)
104
196
  .set({
@@ -111,36 +203,69 @@ function createKanbanStore(handle, projectId) {
111
203
  .where(eq(kanbanCards.id, id))
112
204
  .returning();
113
205
  if (!row)
114
- throw new Error(`Kanban card not found: ${id}`);
206
+ throw new KanbanCardNotFoundError(id);
115
207
  return rowToCard(row);
116
208
  },
117
209
  async moveCard(id, columnId, position) {
210
+ const [card] = await db
211
+ .select({ boardId: kanbanCards.boardId })
212
+ .from(kanbanCards)
213
+ .where(eq(kanbanCards.id, id))
214
+ .limit(1);
215
+ if (!card)
216
+ throw new KanbanCardNotFoundError(id);
217
+ // 悬空列校验:目标列必须存在于当前列配置,否则卡片静默不可见。
218
+ const columns = await boardColumns(card.boardId);
219
+ assertColumn(columns, columnId, 'move');
118
220
  // If no explicit position, append to end of target column.
119
- let newPos = position;
120
- if (newPos === undefined) {
121
- const [card] = await db
122
- .select({ boardId: kanbanCards.boardId })
123
- .from(kanbanCards)
124
- .where(eq(kanbanCards.id, id))
125
- .limit(1);
126
- if (!card)
127
- throw new Error(`Kanban card not found: ${id}`);
128
- newPos = (await maxPos(card.boardId, columnId)) + POSITION_GAP;
129
- }
221
+ const newPos = position ?? (await maxPos(card.boardId, columnId)) + POSITION_GAP;
130
222
  const [row] = await db
131
223
  .update(kanbanCards)
132
224
  .set({ columnId, position: newPos, updatedAt: new Date() })
133
225
  .where(eq(kanbanCards.id, id))
134
226
  .returning();
135
- if (!row)
136
- throw new Error(`Kanban card not found: ${id}`);
137
- return rowToCard(row);
227
+ const moved = row;
228
+ return rowToCard(moved);
138
229
  },
139
230
  async deleteCard(id) {
140
231
  await db.delete(kanbanCards).where(eq(kanbanCards.id, id));
141
232
  },
142
233
  async updateBoard(patch) {
143
- await getOrCreateBoardId();
234
+ const boardId = await getOrCreateBoardId();
235
+ // P1-4:删除列前校验该列内是否有卡片;删除标签前把悬空 labelId 从卡片上清掉。
236
+ if (patch.columns !== undefined) {
237
+ const cards = await db
238
+ .select({ id: kanbanCards.id, columnId: kanbanCards.columnId })
239
+ .from(kanbanCards)
240
+ .where(eq(kanbanCards.boardId, boardId));
241
+ const newColumnIds = new Set(patch.columns.map((c) => c.id));
242
+ const removedWithCards = new Map();
243
+ const orphanCardIds = [];
244
+ for (const card of cards) {
245
+ if (!newColumnIds.has(card.columnId)) {
246
+ removedWithCards.set(card.columnId, (removedWithCards.get(card.columnId) ?? 0) + 1);
247
+ orphanCardIds.push(card.id);
248
+ }
249
+ }
250
+ if (removedWithCards.size > 0) {
251
+ const total = Array.from(removedWithCards.values()).reduce((a, b) => a + b, 0);
252
+ throw new KanbanColumnInUseError(Array.from(removedWithCards.keys()), total, orphanCardIds);
253
+ }
254
+ }
255
+ if (patch.labels !== undefined) {
256
+ const labelIds = new Set(patch.labels.map((l) => l.id));
257
+ const cards = await db
258
+ .select({ id: kanbanCards.id, labels: kanbanCards.labels })
259
+ .from(kanbanCards)
260
+ .where(eq(kanbanCards.boardId, boardId));
261
+ for (const card of cards) {
262
+ const current = (card.labels ?? []);
263
+ const kept = current.filter((lid) => labelIds.has(lid));
264
+ if (kept.length !== current.length) {
265
+ await db.update(kanbanCards).set({ labels: kept }).where(eq(kanbanCards.id, card.id));
266
+ }
267
+ }
268
+ }
144
269
  const [boardRow] = await db
145
270
  .update(kanbanBoards)
146
271
  .set({
@@ -148,11 +273,293 @@ function createKanbanStore(handle, projectId) {
148
273
  ...(patch.labels !== undefined && { labels: patch.labels }),
149
274
  updatedAt: new Date(),
150
275
  })
151
- .where(eq(kanbanBoards.projectId, projectId))
276
+ .where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
152
277
  .returning();
153
278
  const row = boardRow;
154
279
  return rowToBoard(row);
155
280
  },
281
+ /** 整板替换(导入):board 配置 + 卡片在单事务内重建;中途失败整体回滚,
282
+ * 不残留「配置已换、卡片半新半旧」的混合状态。 */
283
+ async replaceBoard(input) {
284
+ const boardId = await getOrCreateBoardId();
285
+ await db.transaction(async (tx) => {
286
+ await tx.delete(kanbanCards).where(eq(kanbanCards.boardId, boardId));
287
+ await tx
288
+ .update(kanbanBoards)
289
+ .set({ columns: input.columns, labels: input.labels, updatedAt: new Date() })
290
+ .where(eq(kanbanBoards.id, boardId));
291
+ if (input.cards.length > 0) {
292
+ await tx.insert(kanbanCards).values(input.cards.map((c) => ({
293
+ boardId,
294
+ title: c.title,
295
+ description: c.description ?? null,
296
+ columnId: c.columnId,
297
+ priority: c.priority,
298
+ position: c.position,
299
+ labels: c.labels,
300
+ })));
301
+ }
302
+ });
303
+ const boardRow = await db
304
+ .select()
305
+ .from(kanbanBoards)
306
+ .where(eq(kanbanBoards.id, boardId))
307
+ .limit(1);
308
+ const board = boardRow[0];
309
+ const cards = await db
310
+ .select()
311
+ .from(kanbanCards)
312
+ .where(eq(kanbanCards.boardId, boardId))
313
+ .orderBy(asc(kanbanCards.columnId), asc(kanbanCards.position));
314
+ return { ...rowToBoard(board), cards: cards.map(rowToCard) };
315
+ },
316
+ };
317
+ }
318
+ export { createKanbanStore, KanbanCardNotFoundError, KanbanColumnInUseError, KanbanColumnNotFoundError, KanbanInvalidPriorityError, };
319
+ /** 把活动看板软删除进回收站(记录原项目名与工作目录)。项目行删除后 FK 将 projectId 置 null。 */
320
+ export async function softDeleteKanbanBoard(handle, projectId, projectName, worktree) {
321
+ const rows = await handle.db
322
+ .update(kanbanBoards)
323
+ .set({
324
+ deletedAt: new Date(),
325
+ deletedProjectName: projectName ?? undefined,
326
+ deletedProjectWorktree: worktree ?? undefined,
327
+ // 恢复后重删的看板须重新经历「到期 → 宽限」周期,宽限标记不跨删除周期生效。
328
+ purgePendingAt: null,
329
+ updatedAt: new Date(),
330
+ })
331
+ .where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
332
+ .returning({ id: kanbanBoards.id });
333
+ return rows.length > 0;
334
+ }
335
+ /** 行 + 卡片数 → 回收站条目(时间戳统一转 ms,兼容 Date/null/字符串)。 */
336
+ function toDeletedBoard(b, cardCount) {
337
+ const toMs = (d) => {
338
+ if (d == null)
339
+ return null;
340
+ const t = d instanceof Date ? d.getTime() : new Date(d).getTime();
341
+ return Number.isFinite(t) ? t : null;
342
+ };
343
+ return {
344
+ id: b.id,
345
+ projectName: b.deletedProjectName ?? '未知项目',
346
+ cardCount,
347
+ deletedAt: toMs(b.deletedAt) ?? 0,
348
+ purgePendingAt: toMs(b.purgePendingAt),
349
+ deletedProjectWorktree: b.deletedProjectWorktree ?? null,
156
350
  };
157
351
  }
158
- export { createKanbanStore };
352
+ /** 回收站看板列表(含卡片数)。 */
353
+ export async function listDeletedKanbanBoards(handle) {
354
+ const boards = await handle.db
355
+ .select()
356
+ .from(kanbanBoards)
357
+ .where(isNotNull(kanbanBoards.deletedAt))
358
+ .orderBy(asc(kanbanBoards.deletedAt));
359
+ if (boards.length === 0)
360
+ return [];
361
+ const counts = await handle.db
362
+ .select({ boardId: kanbanCards.boardId, n: sql `count(*)::int` })
363
+ .from(kanbanCards)
364
+ .where(inArray(kanbanCards.boardId, boards.map((b) => b.id)))
365
+ .groupBy(kanbanCards.boardId);
366
+ const countMap = new Map(counts.map((c) => [c.boardId, c.n]));
367
+ return boards.map((b) => toDeletedBoard(b, countMap.get(b.id) ?? 0));
368
+ }
369
+ /** 按 id 取单个回收站看板(供「重建原项目」恢复读取原工作目录)。 */
370
+ export async function getDeletedKanbanBoard(handle, boardId) {
371
+ const [b] = await handle.db
372
+ .select()
373
+ .from(kanbanBoards)
374
+ .where(and(eq(kanbanBoards.id, boardId), isNotNull(kanbanBoards.deletedAt)))
375
+ .limit(1);
376
+ if (!b)
377
+ return null;
378
+ const counts = await handle.db
379
+ .select({ n: sql `count(*)::int` })
380
+ .from(kanbanCards)
381
+ .where(eq(kanbanCards.boardId, boardId));
382
+ return toDeletedBoard(b, counts[0]?.n ?? 0);
383
+ }
384
+ /** 恢复看板到指定项目:目标项目已有活动看板 → 409 语义(不覆盖用户现有看板)。
385
+ * P2 修复:检查与更新放进单事务,并发下撞 uq_kanban_boards_project 唯一索引时
386
+ * 降级为 TARGET_HAS_BOARD(此前抛 PG 错误 500)。 */
387
+ export async function restoreKanbanBoard(handle, boardId, projectId) {
388
+ try {
389
+ return await handle.db.transaction(async (tx) => {
390
+ const [active] = await tx
391
+ .select({ id: kanbanBoards.id })
392
+ .from(kanbanBoards)
393
+ .where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
394
+ .limit(1);
395
+ if (active)
396
+ return { ok: false, reason: 'TARGET_HAS_BOARD' };
397
+ const rows = await tx
398
+ .update(kanbanBoards)
399
+ .set({
400
+ projectId,
401
+ deletedAt: null,
402
+ deletedProjectName: null,
403
+ deletedProjectWorktree: null,
404
+ purgePendingAt: null,
405
+ updatedAt: new Date(),
406
+ })
407
+ .where(and(eq(kanbanBoards.id, boardId), isNotNull(kanbanBoards.deletedAt)))
408
+ .returning({ id: kanbanBoards.id });
409
+ if (rows.length === 0)
410
+ return { ok: false, reason: 'BOARD_NOT_FOUND' };
411
+ return { ok: true };
412
+ });
413
+ }
414
+ catch (err) {
415
+ // PG 23505 = unique_violation:并发窗口内目标板被创建。
416
+ if (err.code === '23505') {
417
+ return { ok: false, reason: 'TARGET_HAS_BOARD' };
418
+ }
419
+ throw err;
420
+ }
421
+ }
422
+ /**
423
+ * P1 修复:把回收站看板合并进目标项目的活动看板(TARGET_HAS_BOARD 死胡同的出口)。
424
+ * 此前「目标项目已有看板」时恢复 409,而产品没有删除活动看板的能力,用户无法
425
+ * 把旧项目卡片拿回现有项目——现在 merge 模式下:
426
+ * - 目标无活动看板 → 等价普通恢复(重新归属);
427
+ * - 目标有活动看板 → 追加缺失列,卡片并入对应列末尾(position 重新计算避免
428
+ * 与目标卡片重叠);源卡片引用源板已不存在列时落到目标第一列(保证可见);
429
+ * - 合并成功后源看板物理删除(卡片已迁移,FK cascade 清源卡)。
430
+ * 全程单事务:失败整体回滚,不残留半合并状态。
431
+ */
432
+ export async function mergeKanbanBoard(handle, boardId, projectId) {
433
+ return await handle.db.transaction(async (tx) => {
434
+ const [src] = await tx
435
+ .select({ id: kanbanBoards.id, columns: kanbanBoards.columns })
436
+ .from(kanbanBoards)
437
+ .where(and(eq(kanbanBoards.id, boardId), isNotNull(kanbanBoards.deletedAt)))
438
+ .limit(1);
439
+ if (!src)
440
+ return { ok: false, reason: 'BOARD_NOT_FOUND' };
441
+ const [target] = await tx
442
+ .select({ id: kanbanBoards.id, columns: kanbanBoards.columns })
443
+ .from(kanbanBoards)
444
+ .where(and(eq(kanbanBoards.projectId, projectId), isNull(kanbanBoards.deletedAt)))
445
+ .limit(1);
446
+ // 目标无活动看板 → 重新归属(等价普通恢复,但保持 merge 调用幂等语义)。
447
+ if (!target) {
448
+ const rows = await tx
449
+ .update(kanbanBoards)
450
+ .set({
451
+ projectId,
452
+ deletedAt: null,
453
+ deletedProjectName: null,
454
+ deletedProjectWorktree: null,
455
+ purgePendingAt: null,
456
+ updatedAt: new Date(),
457
+ })
458
+ .where(and(eq(kanbanBoards.id, boardId), isNotNull(kanbanBoards.deletedAt)))
459
+ .returning({ id: kanbanBoards.id });
460
+ if (rows.length === 0)
461
+ return { ok: false, reason: 'BOARD_NOT_FOUND' };
462
+ return { ok: true, mergedColumns: 0, mergedCards: 0 };
463
+ }
464
+ // —— 合并路径 ——
465
+ const srcCols = (src.columns ?? []);
466
+ const targetCols = (target.columns ?? []);
467
+ const targetIds = new Set(targetCols.map((c) => c.id));
468
+ const newCols = srcCols.filter((c) => !targetIds.has(c.id));
469
+ if (newCols.length > 0) {
470
+ await tx
471
+ .update(kanbanBoards)
472
+ .set({ columns: [...targetCols, ...newCols], updatedAt: new Date() })
473
+ .where(eq(kanbanBoards.id, target.id));
474
+ }
475
+ const mergedCols = [...targetCols, ...newCols];
476
+ const fallbackCol = mergedCols[0]?.id ?? DEFAULT_COLUMN_ID;
477
+ const srcCards = await tx
478
+ .select()
479
+ .from(kanbanCards)
480
+ .where(eq(kanbanCards.boardId, boardId))
481
+ .orderBy(asc(kanbanCards.position));
482
+ // 目标各列现有 max(position):合并卡片追加到末尾,避免 position 重叠。
483
+ const targetMax = await tx
484
+ .select({ columnId: kanbanCards.columnId, m: max(kanbanCards.position) })
485
+ .from(kanbanCards)
486
+ .where(eq(kanbanCards.boardId, target.id))
487
+ .groupBy(kanbanCards.columnId);
488
+ const maxMap = new Map(targetMax.map((r) => [r.columnId, r.m ?? 0]));
489
+ const counters = new Map();
490
+ let mergedCards = 0;
491
+ for (const card of srcCards) {
492
+ const colId = mergedCols.some((c) => c.id === card.columnId) ? card.columnId : fallbackCol;
493
+ const idx = counters.get(colId) ?? 0;
494
+ counters.set(colId, idx + 1);
495
+ await tx.insert(kanbanCards).values({
496
+ boardId: target.id,
497
+ title: card.title,
498
+ description: card.description,
499
+ columnId: colId,
500
+ priority: card.priority,
501
+ position: (maxMap.get(colId) ?? 0) + POSITION_GAP * (idx + 1),
502
+ labels: card.labels ?? [],
503
+ });
504
+ mergedCards += 1;
505
+ }
506
+ // 源看板物理删除(卡片已迁移;卡片行经 FK cascade 连带清除)。
507
+ await tx.delete(kanbanBoards).where(eq(kanbanBoards.id, boardId));
508
+ return { ok: true, mergedColumns: newCols.length, mergedCards };
509
+ });
510
+ }
511
+ /** 回收站内彻底删除看板(不可恢复)。 */
512
+ export async function permanentlyDeleteKanbanBoard(handle, boardId) {
513
+ const rows = await handle.db
514
+ .delete(kanbanBoards)
515
+ .where(and(eq(kanbanBoards.id, boardId), isNotNull(kanbanBoards.deletedAt)))
516
+ .returning({ id: kanbanBoards.id });
517
+ return rows.length;
518
+ }
519
+ /**
520
+ * 看板回收站两阶段清理(与会话 purgeDeletedSessions 同策略,杜绝「到期即静默清空」):
521
+ * - 阶段一:软删除超过 retentionMs 且尚未标记 → 写 purgePendingAt(进入宽限期,
522
+ * UI 显示「即将清除」仍可恢复);
523
+ * - 阶段二:purgePendingAt 早于 graceMs 截止 → 物理清除(卡片经 FK cascade 连带删除)。
524
+ * 看板回收站全局单一分组、无「首次查看才起算」语义,保留期自删除时刻起算,稳定可预期。
525
+ */
526
+ export async function purgeDeletedKanbanBoards(handle, retentionMs, graceMs) {
527
+ const retentionCutoff = new Date(Date.now() - retentionMs);
528
+ const graceCutoff = new Date(Date.now() - graceMs);
529
+ const rows = await handle.db
530
+ .select({
531
+ id: kanbanBoards.id,
532
+ deletedAt: kanbanBoards.deletedAt,
533
+ purgePendingAt: kanbanBoards.purgePendingAt,
534
+ })
535
+ .from(kanbanBoards)
536
+ .where(isNotNull(kanbanBoards.deletedAt));
537
+ // —— 阶段一:到期标记(先宽限,不直接清除)——
538
+ let marked = 0;
539
+ for (const r of rows) {
540
+ if (!r.deletedAt)
541
+ continue;
542
+ if (r.purgePendingAt)
543
+ continue;
544
+ if (r.deletedAt.getTime() >= retentionCutoff.getTime())
545
+ continue;
546
+ await handle.db
547
+ .update(kanbanBoards)
548
+ .set({ purgePendingAt: new Date(), updatedAt: new Date() })
549
+ .where(eq(kanbanBoards.id, r.id));
550
+ marked += 1;
551
+ }
552
+ // —— 阶段二:宽限期满物理清除 ——
553
+ const pendingIds = rows
554
+ .filter((r) => r.purgePendingAt && r.purgePendingAt.getTime() < graceCutoff.getTime())
555
+ .map((r) => r.id);
556
+ let deleted = 0;
557
+ if (pendingIds.length > 0) {
558
+ const removed = await handle.db
559
+ .delete(kanbanBoards)
560
+ .where(inArray(kanbanBoards.id, pendingIds))
561
+ .returning({ id: kanbanBoards.id });
562
+ deleted = removed.length;
563
+ }
564
+ return { marked, deleted };
565
+ }
@@ -8,6 +8,6 @@ export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overri
8
8
  export type { Retryable, RetryOptions } from './retry.js';
9
9
  export { delay, retryable, withRetry } from './retry.js';
10
10
  export type { FallbackChain } from './routing.js';
11
- export { runWithFallback, shouldFallOver } from './routing.js';
11
+ export { buildFallbackChain, runWithFallback, shouldFallOver } from './routing.js';
12
12
  export * from './schema/index.js';
13
13
  export { estimateTokens } from './token.js';
package/dist/llm/index.js CHANGED
@@ -3,6 +3,6 @@ export { buildInternalRequest, chat, chatStream, toInternalMessage, toStreamChun
3
3
  export { isContextOverflow, isContextOverflowFailure } from './provider-error.js';
4
4
  export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, registerProvider, resolveModelByRole, resolveRoute, setRole, } from './registry.js';
5
5
  export { delay, retryable, withRetry } from './retry.js';
6
- export { runWithFallback, shouldFallOver } from './routing.js';
6
+ export { buildFallbackChain, runWithFallback, shouldFallOver } from './routing.js';
7
7
  export * from './schema/index.js';
8
8
  export { estimateTokens } from './token.js';
@@ -135,7 +135,7 @@ const streamTarget = async function* (ctx, request, target, chain) {
135
135
  headers: { authorization: `Bearer ${authHeader}`, ...resolved.route.headers() },
136
136
  signal: ctx.signal,
137
137
  fetchImpl: ctx.fetchImpl,
138
- }), { maxRetries: chain.maxRetries, sleep: chain.sleep });
138
+ }), { maxRetries: chain.maxRetries, sleep: chain.sleep, initialDelayMs: chain.retryDelay });
139
139
  let state = initialStepState();
140
140
  for await (const frame of sseFraming(stream)) {
141
141
  const chunk = parseChunk(resolved.route.id, frame);
@@ -81,7 +81,13 @@ declare const setRole: (registry: Registry, role: ModelRole, provider: string, m
81
81
  * live request handlers (e.g. config hot-reload).
82
82
  */
83
83
  declare const rebuildRegistry: (registry: Registry, builder: (next: Registry) => void) => void;
84
+ /**
85
+ * 内置价目表版本(H3):记录 builtinCapabilities 最近一次人工核价的日期。
86
+ * LLM 价格频繁变动,前端在用量/成本面板展示此版本提醒「估算可能过期」;
87
+ * 发布新版本时同步核对 builtinCapabilities 并更新此常量(release checklist)。
88
+ */
89
+ declare const PRICE_CATALOG_VERSION = "2026-09-09";
84
90
  /** Default role + a starter catalog of well-known models. */
85
91
  declare const builtinCapabilities: Record<string, Record<string, ModelCapabilities>>;
86
92
  export type { ProviderInput, Registry, ResolveResult, RouteEntry, RouteTable };
87
- export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, rebuildRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
93
+ export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, PRICE_CATALOG_VERSION, rebuildRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
@@ -117,6 +117,12 @@ const rebuildRegistry = (registry, builder) => {
117
117
  builder(next);
118
118
  registry.table = next.table;
119
119
  };
120
+ /**
121
+ * 内置价目表版本(H3):记录 builtinCapabilities 最近一次人工核价的日期。
122
+ * LLM 价格频繁变动,前端在用量/成本面板展示此版本提醒「估算可能过期」;
123
+ * 发布新版本时同步核对 builtinCapabilities 并更新此常量(release checklist)。
124
+ */
125
+ const PRICE_CATALOG_VERSION = '2026-09-09';
120
126
  /** Default role + a starter catalog of well-known models. */
121
127
  const builtinCapabilities = {
122
128
  openai: {
@@ -151,4 +157,4 @@ const builtinCapabilities = {
151
157
  },
152
158
  },
153
159
  };
154
- export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, rebuildRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
160
+ export { builtinCapabilities, createRegistry, DEFAULT_MODEL_CAPABILITIES, overrideToCapabilities, PRICE_CATALOG_VERSION, rebuildRegistry, registerProvider, resolveModelByRole, resolveRoute, setRole, };
@@ -4,9 +4,10 @@ declare const RETRY_MAX_DELAY_NO_HEADERS = 30000;
4
4
  declare const RETRY_MAX_DELAY = 2147483647;
5
5
  /**
6
6
  * Compute the delay before the next retry attempt (ms).
7
- * Honors retry-after / retry-after-ms headers when present, else exponential backoff.
7
+ * Honors retry-after / retry-after-ms headers when present, else exponential backoff
8
+ * from the given base delay (defaults to RETRY_INITIAL_DELAY).
8
9
  */
9
- declare const delay: (attempt: number, error?: unknown) => number;
10
+ declare const delay: (attempt: number, error?: unknown, initialDelayMs?: number) => number;
10
11
  /** A normalized, retryable error descriptor for the session layer. */
11
12
  type Retryable = {
12
13
  message: string;
@@ -29,6 +30,8 @@ type RetryOptions = {
29
30
  delayMs: number;
30
31
  error: unknown;
31
32
  }) => void;
33
+ /** Base delay for exponential backoff (default RETRY_INITIAL_DELAY). */
34
+ initialDelayMs?: number;
32
35
  };
33
36
  /**
34
37
  * Run an async operation with retry. Retries only on retryable LLM errors.
package/dist/llm/retry.js CHANGED
@@ -17,9 +17,10 @@ const errorHeaders = (error) => {
17
17
  const capDelay = (ms) => Math.min(ms, RETRY_MAX_DELAY);
18
18
  /**
19
19
  * Compute the delay before the next retry attempt (ms).
20
- * Honors retry-after / retry-after-ms headers when present, else exponential backoff.
20
+ * Honors retry-after / retry-after-ms headers when present, else exponential backoff
21
+ * from the given base delay (defaults to RETRY_INITIAL_DELAY).
21
22
  */
22
- const delay = (attempt, error) => {
23
+ const delay = (attempt, error, initialDelayMs = RETRY_INITIAL_DELAY) => {
23
24
  const headers = errorHeaders(error);
24
25
  if (headers) {
25
26
  const retryAfterMs = headers['retry-after-ms'];
@@ -37,9 +38,9 @@ const delay = (attempt, error) => {
37
38
  if (!Number.isNaN(parsed) && parsed > 0)
38
39
  return capDelay(Math.ceil(parsed));
39
40
  }
40
- return capDelay(RETRY_INITIAL_DELAY * RETRY_BACKOFF_FACTOR ** (attempt - 1));
41
+ return capDelay(initialDelayMs * RETRY_BACKOFF_FACTOR ** (attempt - 1));
41
42
  }
42
- return capDelay(Math.min(RETRY_INITIAL_DELAY * RETRY_BACKOFF_FACTOR ** (attempt - 1), RETRY_MAX_DELAY_NO_HEADERS));
43
+ return capDelay(Math.min(initialDelayMs * RETRY_BACKOFF_FACTOR ** (attempt - 1), RETRY_MAX_DELAY_NO_HEADERS));
43
44
  };
44
45
  /**
45
46
  * Decide whether a thrown error is retryable. Returns undefined when not retryable
@@ -74,7 +75,7 @@ const withRetry = async (fn, options) => {
74
75
  const fallbackReason = isLLMError(error)
75
76
  ? error.reason
76
77
  : { _tag: 'InvalidRequest', message: '' };
77
- const delayMs = Math.min(reasonRetryAfterMs(fallbackReason) ?? delay(attempt, error), canRetry.policy.maxDelay);
78
+ const delayMs = Math.min(reasonRetryAfterMs(fallbackReason) ?? delay(attempt, error, options.initialDelayMs), canRetry.policy.maxDelay);
78
79
  options.onRetry?.({ attempt, delayMs, error });
79
80
  await sleep(delayMs);
80
81
  }
@@ -32,5 +32,28 @@ declare const runWithFallback: <T>(registry: Registry, chain: FallbackChain, run
32
32
  * errors propagate without trying fallbacks.
33
33
  */
34
34
  declare const shouldFallOver: (error: unknown) => boolean;
35
+ /** 回退目标所需的最小 provider 信息。 */
36
+ type FallbackSourceProvider = {
37
+ name?: string;
38
+ _tag?: string;
39
+ /** 声明了模型清单的 provider 只对清单内的模型回退;未声明视为不支持(保守)。 */
40
+ models?: Record<string, unknown>;
41
+ };
42
+ /**
43
+ * 从 config.fallback 构建回退链(主 chat 循环/压缩/标题等 LLM 调用的接线点)。
44
+ * - enabled=false → undefined(保持内置默认:3 次重试、无跨 provider 回退)。
45
+ * - enabled=true → 按配置 maxRetries/retryDelay 重试,并对「声明了同一模型」的
46
+ * 其他 provider 依次回退。无合格回退目标时 fallbacks 为空——重试参数仍然生效。
47
+ * 回退只接受声明了该模型的 provider:对未声明的 provider 盲目回退会在其 API 处
48
+ * 得到 InvalidRequest 并中断整条链(shouldFallOver 不放过非回退错误)。
49
+ */
50
+ declare function buildFallbackChain(config: {
51
+ providers: FallbackSourceProvider[];
52
+ fallback: {
53
+ enabled: boolean;
54
+ maxRetries: number;
55
+ retryDelay: number;
56
+ };
57
+ }, provider: string, model: string): FallbackChain | undefined;
35
58
  export type { FallbackChain };
36
- export { runWithFallback, shouldFallOver };
59
+ export { buildFallbackChain, runWithFallback, shouldFallOver };