chatccc 0.2.270 → 0.2.276

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 (42) hide show
  1. package/README.md +16 -10
  2. package/config.sample.json +4 -3
  3. package/deepccc-agent/README.md +147 -61
  4. package/deepccc-agent/package.json +5 -2
  5. package/dist/deepccc-agent/src/attachments.js +192 -0
  6. package/dist/deepccc-agent/src/cli.js +59 -13
  7. package/dist/deepccc-agent/src/config.js +57 -4
  8. package/dist/deepccc-agent/src/context.js +299 -16
  9. package/dist/deepccc-agent/src/file-tools.js +33 -0
  10. package/dist/deepccc-agent/src/index.js +68 -21
  11. package/dist/deepccc-agent/src/tool-protocol.js +14 -3
  12. package/dist/deepccc-agent/src/web-entry.js +72 -0
  13. package/dist/deepccc-agent/src/web-page.js +414 -0
  14. package/dist/deepccc-agent/src/web-runtime.js +331 -0
  15. package/dist/deepccc-agent/src/web-server.js +476 -0
  16. package/dist/deepccc-agent/src/web-session-store.js +162 -0
  17. package/dist/deepccc-agent/src/web-tool-presentation.js +123 -0
  18. package/dist/src/adapters/ccc-adapter.js +5 -1
  19. package/dist/src/agent-capability-grants.js +26 -0
  20. package/dist/src/agent-delegate-task.js +5 -2
  21. package/dist/src/agent-file-rpc.js +6 -1
  22. package/dist/src/agent-image-rpc.js +6 -1
  23. package/dist/src/agent-team/application/task-execution-service.js +330 -97
  24. package/dist/src/agent-team/domain/task-run.js +14 -1
  25. package/dist/src/agent-team/infrastructure/task-execution-runtime.js +7 -2
  26. package/dist/src/agent-team/main-agent-bootstrap.js +24 -1
  27. package/dist/src/agent-team/repositories/json-task-run-repository.js +22 -4
  28. package/dist/src/agent-team/web/agent-team-page.js +14 -7
  29. package/dist/src/cards.js +7 -4
  30. package/dist/src/config.js +12 -0
  31. package/dist/src/im-skills.js +9 -2
  32. package/dist/src/orchestrator.js +117 -29
  33. package/dist/src/safe-maintenance.js +4 -1
  34. package/dist/src/session-name.js +15 -0
  35. package/dist/src/session.js +54 -9
  36. package/dist/src/web-ui.js +76 -32
  37. package/im-skills/feishu-skill/receive-send-file.md +3 -2
  38. package/im-skills/feishu-skill/receive-send-image.md +3 -2
  39. package/im-skills/feishu-skill/send-file.mjs +6 -5
  40. package/im-skills/feishu-skill/send-image.mjs +6 -5
  41. package/im-skills/feishu-skill/skill.md +4 -2
  42. package/package.json +1 -1
@@ -1,17 +1,22 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { isActiveTaskRun } from "../domain/task-run.js";
2
+ import { isActiveTaskRun, } from "../domain/task-run.js";
3
3
  import { BoardStoreError } from "../repositories/board-repository.js";
4
4
  import { beginSafeMaintenanceTrackedWork, isSafeMaintenanceAdmissionClosed, } from "../../safe-maintenance.js";
5
5
  export class TaskExecutionService {
6
6
  options;
7
7
  now;
8
8
  idFactory;
9
+ stopTimeoutMs;
10
+ staleAfterMs;
9
11
  projectOperations = new Map();
10
12
  executions = new Map();
13
+ pendingReconciliationProjects = new Set();
11
14
  constructor(options) {
12
15
  this.options = options;
13
16
  this.now = options.now ?? (() => new Date());
14
17
  this.idFactory = options.idFactory ?? randomUUID;
18
+ this.stopTimeoutMs = options.stopTimeoutMs ?? 30_000;
19
+ this.staleAfterMs = options.staleAfterMs ?? 5 * 60_000;
15
20
  }
16
21
  async listRuns(projectId) {
17
22
  const runs = await this.options.repository.listByProject(projectId);
@@ -22,85 +27,101 @@ export class TaskExecutionService {
22
27
  if (!run || run.projectId !== projectId) {
23
28
  throw new BoardStoreError("task_run_not_found", "找不到这次任务执行记录", 404);
24
29
  }
25
- if (!isActiveTaskRun(run) || !this.options.runtime.getTranscript)
30
+ if (!isActiveTaskRun(run) || !this.options.runtime.getSnapshot)
26
31
  return run;
27
- const live = await this.options.runtime.getTranscript(run.sessionId).catch(() => []);
28
- return live.length ? { ...run, transcript: mergeTranscript(run.transcript, live) } : run;
32
+ const snapshot = await this.options.runtime.getSnapshot(run.sessionId).catch(() => null);
33
+ return snapshot?.transcript.length
34
+ ? { ...run, transcript: combineTranscript(run.transcript, snapshot.transcript) }
35
+ : run;
29
36
  }
30
37
  async startTask(projectId, taskId, expectedRevision) {
31
38
  if (isSafeMaintenanceAdmissionClosed()) {
32
39
  throw new BoardStoreError("safe_maintenance_draining", "ChatCCC 正在等待安全维护,暂不接受新的 Agent Team 任务。", 409);
33
40
  }
34
- const release = beginSafeMaintenanceTrackedWork("agent-team-task-start");
35
- return this.exclusive(projectId, async () => {
36
- let board = await this.options.boardService.getBoard(projectId);
37
- const active = (await this.options.repository.listByProject(projectId)).find(isActiveTaskRun);
38
- if (active) {
39
- if (active.taskId === taskId)
40
- return { board, run: active };
41
- throw new BoardStoreError("task_run_busy", "该项目已有任务正在由主 Agent 执行", 409);
42
- }
43
- if (board.revision !== expectedRevision) {
44
- throw new BoardStoreError("revision_conflict", `Board changed in another page (expected revision ${expectedRevision}, current ${board.revision})`, 409);
45
- }
46
- const task = board.tasks.find((candidate) => candidate.id === taskId && !candidate.deletedAt);
47
- if (!task)
48
- throw new BoardStoreError("not_found", `Task not found: ${taskId}`, 404);
49
- const binding = await this.options.getBinding(projectId);
50
- if (!binding || binding.status !== "ready" || !binding.chatId || !binding.sessionId) {
51
- throw new BoardStoreError("main_agent_unavailable", "请先为项目设置可用的主 Agent", 409);
52
- }
53
- if (this.options.runtime.isSessionRunning(binding.sessionId)) {
54
- throw new BoardStoreError("task_run_busy", "主 Agent 正在处理其他消息,请稍后再试", 409);
55
- }
56
- const priorRuns = await this.options.repository.listByProject(projectId);
57
- const timestamp = this.now().toISOString();
58
- let run = {
59
- schemaVersion: 1,
60
- runId: this.idFactory(),
61
- projectId,
62
- taskId,
63
- taskTitle: task.title,
64
- taskDescription: task.description,
65
- attempt: priorRuns.filter((candidate) => candidate.taskId === taskId).length + 1,
66
- state: "queued",
67
- agentId: binding.agentId,
68
- chatId: binding.chatId,
69
- sessionId: binding.sessionId,
70
- createdAt: timestamp,
71
- updatedAt: timestamp,
72
- };
73
- run = {
74
- ...run,
75
- transcript: [{ type: "prompt", at: timestamp, text: taskPrompt(run) }],
76
- };
77
- await this.options.repository.save(run);
78
- try {
79
- if (task.columnId !== "doing") {
80
- const doingCount = board.tasks.filter((candidate) => !candidate.deletedAt && candidate.columnId === "doing").length;
81
- board = await this.options.boardService.moveTask(projectId, taskId, {
82
- expectedRevision: board.revision,
83
- columnId: "doing",
84
- index: doingCount,
85
- });
41
+ const release = beginSafeMaintenanceTrackedWork("agent-team-task-run");
42
+ let releaseOwnedByExecution = false;
43
+ try {
44
+ const result = await this.exclusive(projectId, async () => {
45
+ let board = await this.options.boardService.getBoard(projectId);
46
+ const active = (await this.options.repository.listByProject(projectId)).find(isActiveTaskRun);
47
+ if (active) {
48
+ if (active.taskId === taskId)
49
+ return { board, run: active };
50
+ throw new BoardStoreError("task_run_busy", "该项目已有任务正在由主 Agent 执行", 409);
86
51
  }
87
- }
88
- catch (err) {
89
- run = await this.finishRun(run, "failed", { error: err.message });
90
- throw err;
91
- }
92
- const startedAt = this.now().toISOString();
93
- run = { ...run, state: "running", startedAt, updatedAt: startedAt };
94
- await this.options.repository.save(run);
95
- const execution = this.execute(run).finally(() => {
96
- this.executions.delete(run.runId);
97
- });
98
- this.executions.set(run.runId, execution);
99
- void execution.catch((err) => {
100
- console.error(`[Agent Team] Task run ${run.runId} failed to persist its terminal state: ${err.message}`);
52
+ if (board.revision !== expectedRevision) {
53
+ throw new BoardStoreError("revision_conflict", `Board changed in another page (expected revision ${expectedRevision}, current ${board.revision})`, 409);
54
+ }
55
+ const task = board.tasks.find((candidate) => candidate.id === taskId && !candidate.deletedAt);
56
+ if (!task)
57
+ throw new BoardStoreError("not_found", `Task not found: ${taskId}`, 404);
58
+ const binding = await this.options.getBinding(projectId);
59
+ if (!binding || binding.status !== "ready" || !binding.chatId || !binding.sessionId) {
60
+ throw new BoardStoreError("main_agent_unavailable", "请先为项目设置可用的主 Agent", 409);
61
+ }
62
+ if (this.options.runtime.isSessionRunning(binding.sessionId)) {
63
+ throw new BoardStoreError("task_run_busy", "主 Agent 正在处理其他消息,请稍后再试", 409);
64
+ }
65
+ const priorRuns = await this.options.repository.listByProject(projectId);
66
+ const timestamp = this.now().toISOString();
67
+ const runId = this.idFactory();
68
+ let run = {
69
+ schemaVersion: 1,
70
+ runId,
71
+ projectId,
72
+ taskId,
73
+ taskTitle: task.title,
74
+ taskDescription: task.description,
75
+ attempt: priorRuns.filter((candidate) => candidate.taskId === taskId).length + 1,
76
+ state: "queued",
77
+ agentId: binding.agentId,
78
+ chatId: binding.chatId,
79
+ sessionId: binding.sessionId,
80
+ createdAt: timestamp,
81
+ updatedAt: timestamp,
82
+ traceId: `agent-team-${runId}`,
83
+ lastProgressAt: timestamp,
84
+ };
85
+ run = {
86
+ ...run,
87
+ transcript: [{ type: "prompt", at: timestamp, text: taskPrompt(run) }],
88
+ };
89
+ await this.options.repository.save(run);
90
+ try {
91
+ if (task.columnId !== "doing") {
92
+ const doingCount = board.tasks.filter((candidate) => !candidate.deletedAt && candidate.columnId === "doing").length;
93
+ board = await this.options.boardService.moveTask(projectId, taskId, {
94
+ expectedRevision: board.revision,
95
+ columnId: "doing",
96
+ index: doingCount,
97
+ });
98
+ }
99
+ }
100
+ catch (err) {
101
+ run = await this.finishRun(run, "failed", { error: err.message });
102
+ throw err;
103
+ }
104
+ const startedAt = this.now().toISOString();
105
+ run = { ...run, state: "running", startedAt, updatedAt: startedAt };
106
+ await this.options.repository.save(run);
107
+ logTaskRunEvent("started", run);
108
+ const execution = this.execute(run).finally(() => {
109
+ this.executions.delete(run.runId);
110
+ release();
111
+ });
112
+ releaseOwnedByExecution = true;
113
+ this.executions.set(run.runId, execution);
114
+ void execution.catch((err) => {
115
+ console.error(`[Agent Team] Task run ${run.runId} failed to persist its terminal state: ${err.message}`);
116
+ });
117
+ return { board, run };
101
118
  });
102
- return { board, run };
103
- }).finally(release);
119
+ return result;
120
+ }
121
+ finally {
122
+ if (!releaseOwnedByExecution)
123
+ release();
124
+ }
104
125
  }
105
126
  async stopRun(projectId, runId) {
106
127
  return this.exclusive(projectId, async () => {
@@ -110,11 +131,19 @@ export class TaskExecutionService {
110
131
  if (!isActiveTaskRun(run))
111
132
  return run;
112
133
  const now = this.now().toISOString();
113
- const updated = { ...run, stopRequestedAt: run.stopRequestedAt ?? now, updatedAt: now };
134
+ const updated = {
135
+ ...run,
136
+ stopRequestedAt: run.stopRequestedAt ?? now,
137
+ stopDeadlineAt: run.stopDeadlineAt ?? new Date(Date.parse(now) + this.stopTimeoutMs).toISOString(),
138
+ updatedAt: now,
139
+ };
114
140
  await this.options.repository.save(updated);
115
141
  const stopped = this.options.runtime.stop(run.sessionId);
116
142
  if (!stopped && !this.options.runtime.isSessionRunning(run.sessionId)) {
117
- return this.finishRun(updated, "interrupted", { error: "Task process was no longer running" });
143
+ return this.finishRun(updated, "interrupted", {
144
+ error: "Task process was no longer running",
145
+ failureCode: "process_missing",
146
+ });
118
147
  }
119
148
  return updated;
120
149
  });
@@ -130,35 +159,104 @@ export class TaskExecutionService {
130
159
  }
131
160
  async recoverInterruptedRuns() {
132
161
  const active = await this.options.repository.listActive();
133
- await Promise.all(active.map((run) => this.finishRun(run, "interrupted", {
134
- error: "ChatCCC restarted while this task was running",
135
- })));
162
+ await Promise.all(active.map(async (run) => {
163
+ const snapshot = await this.readSnapshot(run);
164
+ await this.finishRun(run, "interrupted", {
165
+ error: "ChatCCC restarted while this task was running",
166
+ failureCode: "chatccc_restart",
167
+ transcript: snapshot?.transcript,
168
+ });
169
+ }));
170
+ const workspaces = await this.options.boardService.listWorkspaces().catch(() => []);
171
+ await Promise.all(workspaces.map((workspace) => this.reconcileProject(workspace.boardId)));
172
+ return active.length;
173
+ }
174
+ /** Persist live execution state and enforce stop deadlines. Safe to call repeatedly from a timer. */
175
+ async checkpointActiveRuns() {
176
+ const active = await this.options.repository.listActive();
177
+ await Promise.all(active.map((run) => this.exclusive(run.projectId, () => this.checkpointRun(run.runId))));
178
+ const pending = [...this.pendingReconciliationProjects];
179
+ await Promise.all(pending.map((projectId) => this.reconcileProject(projectId)));
136
180
  return active.length;
137
181
  }
182
+ async reconcileProject(projectId) {
183
+ return this.exclusive(projectId, async () => {
184
+ const runs = await this.options.repository.listByProject(projectId);
185
+ const latestByTask = new Map();
186
+ for (const run of runs) {
187
+ if (!latestByTask.has(run.taskId))
188
+ latestByTask.set(run.taskId, run);
189
+ }
190
+ let reconciled = 0;
191
+ let failed = false;
192
+ for (const run of latestByTask.values()) {
193
+ // Only repair a transition that this service previously recorded as partial.
194
+ // A user may intentionally move an already-synced card after completion.
195
+ if (isActiveTaskRun(run) || run.boardSyncPending !== true)
196
+ continue;
197
+ try {
198
+ await this.moveFinishedTask(run);
199
+ const now = this.now().toISOString();
200
+ await this.options.repository.save({
201
+ ...run,
202
+ boardSyncPending: false,
203
+ reconciledAt: now,
204
+ updatedAt: now,
205
+ syncError: undefined,
206
+ });
207
+ reconciled++;
208
+ }
209
+ catch (err) {
210
+ await this.options.repository.save({
211
+ ...run,
212
+ boardSyncPending: true,
213
+ syncError: err.message,
214
+ });
215
+ this.pendingReconciliationProjects.add(projectId);
216
+ logTaskRunEvent("board_sync_failed", run, { error: err.message });
217
+ failed = true;
218
+ continue;
219
+ }
220
+ }
221
+ if (!failed)
222
+ this.pendingReconciliationProjects.delete(projectId);
223
+ return reconciled;
224
+ });
225
+ }
138
226
  async execute(run) {
139
227
  try {
140
228
  const result = await this.options.runtime.run({
141
229
  sessionId: run.sessionId,
142
230
  chatId: run.chatId,
143
231
  agentId: run.agentId,
144
- traceId: `agent-team-${run.runId}`,
232
+ traceId: run.traceId ?? `agent-team-${run.runId}`,
145
233
  prompt: taskPrompt(run),
146
234
  });
147
235
  if (result.outcome === "done") {
148
236
  return this.finishRun(run, "succeeded", { result: result.result, transcript: result.transcript, moveTask: true });
149
237
  }
150
238
  if (result.outcome === "stopped") {
151
- return this.finishRun(run, "canceled", { error: result.error, transcript: result.transcript, moveTask: true });
239
+ return this.finishRun(run, "canceled", {
240
+ error: result.error,
241
+ failureCode: "user_stopped",
242
+ transcript: result.transcript,
243
+ moveTask: true,
244
+ });
152
245
  }
153
246
  return this.finishRun(run, "failed", {
154
247
  error: result.error || (result.outcome === "auto_ended" ? "Agent response timed out" : "Agent execution failed"),
248
+ failureCode: result.outcome === "auto_ended" ? "agent_timeout" : "agent_error",
155
249
  result: result.result,
156
250
  transcript: result.transcript,
157
251
  moveTask: true,
158
252
  });
159
253
  }
160
254
  catch (err) {
161
- return this.finishRun(run, "failed", { error: err.message, moveTask: true });
255
+ return this.finishRun(run, "failed", {
256
+ error: err.message,
257
+ failureCode: "agent_error",
258
+ moveTask: true,
259
+ });
162
260
  }
163
261
  }
164
262
  async finishRun(run, state, details = {}) {
@@ -171,35 +269,109 @@ export class TaskExecutionService {
171
269
  state,
172
270
  updatedAt: now,
173
271
  finishedAt: now,
174
- ...(details.transcript?.length ? { transcript: mergeTranscript(latest.transcript, details.transcript) } : {}),
272
+ stalledAt: undefined,
273
+ lastProgressAt: latestTranscriptAt(details.transcript) ?? latest.lastProgressAt ?? now,
274
+ ...(details.transcript?.length ? { transcript: combineTranscript(latest.transcript, details.transcript) } : {}),
175
275
  ...(details.result ? { result: details.result } : {}),
176
276
  ...(details.error ? { error: details.error } : {}),
277
+ ...(details.failureCode ? { failureCode: details.failureCode } : {}),
278
+ ...(details.moveTask !== false ? { boardSyncPending: true } : {}),
177
279
  };
178
280
  await this.options.repository.save(updated);
281
+ logTaskRunEvent("finished", updated);
179
282
  if (details.moveTask !== false) {
180
- await this.moveFinishedTask(updated).catch(async (err) => {
283
+ try {
284
+ await this.moveFinishedTask(updated);
285
+ const reconciledAt = this.now().toISOString();
181
286
  await this.options.repository.save({
182
287
  ...updated,
183
- error: updated.error || `Task completed but board update failed: ${err.message}`,
288
+ boardSyncPending: false,
289
+ reconciledAt,
290
+ updatedAt: reconciledAt,
291
+ syncError: undefined,
184
292
  });
185
- });
293
+ }
294
+ catch (err) {
295
+ await this.options.repository.save({
296
+ ...updated,
297
+ boardSyncPending: true,
298
+ syncError: err.message,
299
+ });
300
+ this.pendingReconciliationProjects.add(run.projectId);
301
+ logTaskRunEvent("board_sync_failed", updated, { error: err.message });
302
+ }
186
303
  }
187
304
  return await this.options.repository.get(run.runId) ?? updated;
188
305
  }
189
306
  async moveFinishedTask(run) {
190
- const board = await this.options.boardService.getBoard(run.projectId);
191
- const task = board.tasks.find((candidate) => candidate.id === run.taskId && !candidate.deletedAt);
192
- if (!task)
307
+ for (let attempt = 0; attempt < 3; attempt++) {
308
+ const board = await this.options.boardService.getBoard(run.projectId);
309
+ const task = board.tasks.find((candidate) => candidate.id === run.taskId && !candidate.deletedAt);
310
+ if (!task)
311
+ return;
312
+ const target = run.state === "succeeded" ? "done" : "on_hold";
313
+ if (task.columnId === target)
314
+ return;
315
+ const targetCount = board.tasks.filter((candidate) => !candidate.deletedAt && candidate.columnId === target).length;
316
+ try {
317
+ await this.options.boardService.moveTask(run.projectId, run.taskId, {
318
+ expectedRevision: board.revision,
319
+ columnId: target,
320
+ index: targetCount,
321
+ });
322
+ return;
323
+ }
324
+ catch (err) {
325
+ if (!(err instanceof BoardStoreError) || err.code !== "revision_conflict" || attempt === 2)
326
+ throw err;
327
+ }
328
+ }
329
+ }
330
+ async checkpointRun(runId) {
331
+ const run = await this.options.repository.get(runId);
332
+ if (!run || !isActiveTaskRun(run))
193
333
  return;
194
- const target = run.state === "succeeded" ? "done" : "on_hold";
195
- if (task.columnId === target)
334
+ const now = this.now();
335
+ const snapshot = await this.readSnapshot(run);
336
+ const transcript = snapshot?.transcript.length
337
+ ? combineTranscript(run.transcript, snapshot.transcript)
338
+ : run.transcript;
339
+ const snapshotProgressAt = latestIso(validIso(snapshot?.updatedAt), run.lastProgressAt ?? run.startedAt ?? run.createdAt);
340
+ const madeProgress = snapshotProgressAt > (run.lastProgressAt ?? "");
341
+ const stalled = now.getTime() - Date.parse(snapshotProgressAt) >= this.staleAfterMs;
342
+ const updated = {
343
+ ...run,
344
+ transcript,
345
+ lastProgressAt: snapshotProgressAt,
346
+ updatedAt: madeProgress ? now.toISOString() : run.updatedAt,
347
+ stalledAt: stalled ? (run.stalledAt ?? now.toISOString()) : undefined,
348
+ };
349
+ if (JSON.stringify(updated) !== JSON.stringify(run))
350
+ await this.options.repository.save(updated);
351
+ if (updated.stopDeadlineAt && Date.parse(updated.stopDeadlineAt) <= now.getTime()) {
352
+ this.options.runtime.stop(updated.sessionId);
353
+ await this.finishRun(updated, "canceled", {
354
+ error: "Agent did not stop before the requested deadline",
355
+ failureCode: "stop_timeout",
356
+ transcript: snapshot?.transcript,
357
+ moveTask: true,
358
+ });
196
359
  return;
197
- const targetCount = board.tasks.filter((candidate) => !candidate.deletedAt && candidate.columnId === target).length;
198
- await this.options.boardService.moveTask(run.projectId, run.taskId, {
199
- expectedRevision: board.revision,
200
- columnId: target,
201
- index: targetCount,
202
- });
360
+ }
361
+ const board = await this.options.boardService.getBoard(updated.projectId);
362
+ const taskExists = board.tasks.some((task) => task.id === updated.taskId && !task.deletedAt);
363
+ if (!taskExists) {
364
+ this.options.runtime.stop(updated.sessionId);
365
+ await this.finishRun(updated, "canceled", {
366
+ error: "Task was deleted while the Agent was running",
367
+ failureCode: "task_deleted",
368
+ transcript: snapshot?.transcript,
369
+ moveTask: false,
370
+ });
371
+ }
372
+ }
373
+ async readSnapshot(run) {
374
+ return this.options.runtime.getSnapshot?.(run.sessionId).catch(() => null) ?? null;
203
375
  }
204
376
  async exclusive(projectId, operation) {
205
377
  const previous = this.projectOperations.get(projectId) ?? Promise.resolve();
@@ -217,8 +389,69 @@ export class TaskExecutionService {
217
389
  }
218
390
  }
219
391
  }
220
- function mergeTranscript(existing, appended) {
221
- return [...(existing ?? []), ...appended];
392
+ function combineTranscript(existing, live) {
393
+ const prompts = (existing ?? []).filter((entry) => entry.type === "prompt");
394
+ return compactTranscript([...prompts, ...live]);
395
+ }
396
+ const MAX_TRANSCRIPT_ENTRIES = 2_000;
397
+ const MAX_TRANSCRIPT_CHARS = 2_000_000;
398
+ const MAX_TRANSCRIPT_FIELD_CHARS = 100_000;
399
+ function compactTranscript(entries) {
400
+ const prompt = entries.find((entry) => entry.type === "prompt");
401
+ const tail = entries.filter((entry) => entry !== prompt).slice(-(MAX_TRANSCRIPT_ENTRIES - (prompt ? 1 : 0)));
402
+ const kept = [...(prompt ? [prompt] : []), ...tail].map((entry) => {
403
+ const compacted = { ...entry };
404
+ for (const field of ["text", "input", "output"]) {
405
+ const value = compacted[field];
406
+ if (value && value.length > MAX_TRANSCRIPT_FIELD_CHARS) {
407
+ compacted[field] = `${value.slice(0, MAX_TRANSCRIPT_FIELD_CHARS)}\n…(内容已截断)`;
408
+ }
409
+ }
410
+ return compacted;
411
+ });
412
+ let remaining = MAX_TRANSCRIPT_CHARS;
413
+ const result = [];
414
+ for (let index = kept.length - 1; index >= 0; index--) {
415
+ const entry = kept[index];
416
+ const size = JSON.stringify(entry).length;
417
+ if (size > remaining && result.length)
418
+ break;
419
+ result.unshift(entry);
420
+ remaining -= size;
421
+ }
422
+ return result;
423
+ }
424
+ function validIso(value) {
425
+ if (!value || !Number.isFinite(Date.parse(value)))
426
+ return null;
427
+ return new Date(value).toISOString();
428
+ }
429
+ function latestIso(first, second) {
430
+ if (!first)
431
+ return second;
432
+ return Date.parse(first) >= Date.parse(second) ? first : second;
433
+ }
434
+ function latestTranscriptAt(entries) {
435
+ if (!entries?.length)
436
+ return null;
437
+ for (let index = entries.length - 1; index >= 0; index--) {
438
+ const at = validIso(entries[index]?.at);
439
+ if (at)
440
+ return at;
441
+ }
442
+ return null;
443
+ }
444
+ function logTaskRunEvent(event, run, details = {}) {
445
+ console.log(`[Agent Team] ${JSON.stringify({
446
+ event,
447
+ projectId: run.projectId,
448
+ taskId: run.taskId,
449
+ runId: run.runId,
450
+ traceId: run.traceId,
451
+ state: run.state,
452
+ attempt: run.attempt,
453
+ ...details,
454
+ })}`);
222
455
  }
223
456
  function withoutTranscript(run) {
224
457
  const { transcript: _transcript, ...summary } = run;
@@ -22,10 +22,23 @@ export function parseTaskRun(value) {
22
22
  }
23
23
  if (!isAgentTool(run.agentId))
24
24
  throw new Error("Invalid task run Agent");
25
- for (const field of ["startedAt", "stopRequestedAt", "finishedAt", "result", "error"]) {
25
+ for (const field of [
26
+ "traceId", "startedAt", "stopRequestedAt", "stopDeadlineAt", "finishedAt", "lastProgressAt",
27
+ "stalledAt", "result", "error", "reconciledAt", "syncError",
28
+ ]) {
26
29
  if (run[field] !== undefined && typeof run[field] !== "string")
27
30
  throw new Error(`Invalid task run ${field}`);
28
31
  }
32
+ const failureCodes = [
33
+ "agent_error", "agent_timeout", "user_stopped", "stop_timeout", "process_missing",
34
+ "chatccc_restart", "task_deleted",
35
+ ];
36
+ if (run.failureCode !== undefined && !failureCodes.includes(run.failureCode)) {
37
+ throw new Error("Invalid task run failureCode");
38
+ }
39
+ if (run.boardSyncPending !== undefined && typeof run.boardSyncPending !== "boolean") {
40
+ throw new Error("Invalid task run boardSyncPending");
41
+ }
29
42
  if (run.transcript !== undefined && (!Array.isArray(run.transcript) || !run.transcript.every(isExecutionTranscriptEntry))) {
30
43
  throw new Error("Invalid task run transcript");
31
44
  }
@@ -16,8 +16,13 @@ export function createTaskExecutionRuntime(platform) {
16
16
  ...(stream?.terminalError?.message ? { error: stream.terminalError.message } : {}),
17
17
  };
18
18
  },
19
- async getTranscript(sessionId) {
20
- return (await readStreamState(sessionId))?.transcript ?? [];
19
+ async getSnapshot(sessionId) {
20
+ const state = await readStreamState(sessionId);
21
+ return {
22
+ transcript: state?.transcript ?? [],
23
+ ...(state?.updatedAt ? { updatedAt: new Date(state.updatedAt).toISOString() } : {}),
24
+ ...(state?.status ? { status: state.status } : {}),
25
+ };
21
26
  },
22
27
  stop: stopSession,
23
28
  isSessionRunning,
@@ -6,8 +6,13 @@ import { createTaskExecutionRuntime } from "./infrastructure/task-execution-runt
6
6
  import { feishuP2pContactStore } from "./repositories/feishu-p2p-contact-store.js";
7
7
  import { JsonMainAgentBindingRepository } from "./repositories/main-agent-binding-repository.js";
8
8
  import { JsonTaskRunRepository } from "./repositories/json-task-run-repository.js";
9
+ const TASK_RUN_CHECKPOINT_INTERVAL_MS = 3_000;
10
+ let taskRunMonitor = null;
11
+ let taskRunMonitorBusy = false;
12
+ let taskRunMonitorGeneration = 0;
9
13
  /** Wire runtime dependencies only from index.ts, keeping the standalone Web UI import side-effect free. */
10
14
  export function configureAgentTeamMainAgent(platform) {
15
+ const monitorGeneration = ++taskRunMonitorGeneration;
11
16
  const bindingRepository = new JsonMainAgentBindingRepository();
12
17
  const mainAgentService = new MainAgentService({
13
18
  boardService: defaultAgentTeamBoardService,
@@ -24,7 +29,25 @@ export function configureAgentTeamMainAgent(platform) {
24
29
  });
25
30
  setDefaultAgentTeamMainAgentService(mainAgentService);
26
31
  setDefaultAgentTeamTaskExecutionService(taskExecutionService);
27
- void taskExecutionService.recoverInterruptedRuns().catch((err) => {
32
+ if (taskRunMonitor)
33
+ clearInterval(taskRunMonitor);
34
+ void taskExecutionService.recoverInterruptedRuns()
35
+ .catch((err) => {
28
36
  console.error(`[Agent Team] Failed to recover interrupted task runs: ${err.message}`);
37
+ })
38
+ .finally(() => {
39
+ if (monitorGeneration !== taskRunMonitorGeneration)
40
+ return;
41
+ taskRunMonitor = setInterval(() => {
42
+ if (taskRunMonitorBusy)
43
+ return;
44
+ taskRunMonitorBusy = true;
45
+ void taskExecutionService.checkpointActiveRuns()
46
+ .catch((err) => {
47
+ console.error(`[Agent Team] Failed to checkpoint task runs: ${err.message}`);
48
+ })
49
+ .finally(() => { taskRunMonitorBusy = false; });
50
+ }, TASK_RUN_CHECKPOINT_INTERVAL_MS);
51
+ taskRunMonitor.unref?.();
29
52
  });
30
53
  }
@@ -16,8 +16,10 @@ export class JsonTaskRunRepository {
16
16
  return parseTaskRun(JSON.parse(await readFile(this.pathFor(projectId, runId), "utf8")));
17
17
  }
18
18
  catch (err) {
19
- if (err.code !== "ENOENT")
20
- throw err;
19
+ if (err.code === "ENOENT")
20
+ continue;
21
+ await quarantineCorruptRun(this.pathFor(projectId, runId), err);
22
+ return null;
21
23
  }
22
24
  }
23
25
  return null;
@@ -41,8 +43,19 @@ export class JsonTaskRunRepository {
41
43
  }
42
44
  const runs = await Promise.all(entries
43
45
  .filter((entry) => entry.endsWith(".json"))
44
- .map(async (entry) => parseTaskRun(JSON.parse(await readFile(join(directory, entry), "utf8")))));
45
- return runs.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.runId.localeCompare(a.runId));
46
+ .map(async (entry) => {
47
+ const path = join(directory, entry);
48
+ try {
49
+ return parseTaskRun(JSON.parse(await readFile(path, "utf8")));
50
+ }
51
+ catch (err) {
52
+ await quarantineCorruptRun(path, err);
53
+ return null;
54
+ }
55
+ }));
56
+ return runs
57
+ .filter((run) => run !== null)
58
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.runId.localeCompare(a.runId));
46
59
  }
47
60
  async listActive() {
48
61
  const projectIds = await this.projectDirectories();
@@ -96,3 +109,8 @@ async function writeJsonAtomic(path, value) {
96
109
  throw err;
97
110
  }
98
111
  }
112
+ async function quarantineCorruptRun(path, err) {
113
+ const quarantinePath = `${path}.corrupt-${Date.now()}`;
114
+ await rename(path, quarantinePath).catch(() => { });
115
+ console.error(`[Agent Team] Quarantined corrupt task run ${path}: ${err.message}`);
116
+ }