aws-runtime-bridge 1.9.58 → 1.9.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/routes/sub-agents.js +14 -6
- package/package/acode/dist/fs-utils.d.ts +21 -0
- package/package/acode/dist/fs-utils.d.ts.map +1 -0
- package/package/acode/dist/fs-utils.js +79 -0
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +67 -8
- package/package/acode/dist/sub-agent-manager.d.ts +36 -0
- package/package/acode/dist/sub-agent-manager.d.ts.map +1 -1
- package/package/acode/dist/sub-agent-manager.js +184 -45
- package/package/acode/dist/sub-agent-store.d.ts +44 -5
- package/package/acode/dist/sub-agent-store.d.ts.map +1 -1
- package/package/acode/dist/sub-agent-store.js +88 -16
- package/package/acode/dist/todo-store.d.ts +1 -0
- package/package/acode/dist/todo-store.d.ts.map +1 -1
- package/package/acode/dist/todo-store.js +6 -1
- package/package/acode/dist/types.d.ts +11 -0
- package/package/acode/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js +5 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/logger.d.ts +12 -2
- package/package/aws-client-agent-mcp/dist/logger.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/logger.js +53 -12
- package/package/aws-client-agent-mcp/dist/logger.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/logger.test.js +5 -8
- package/package/aws-client-agent-mcp/dist/logger.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +21 -0
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +105 -7
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +4 -2
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +26 -0
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -14,13 +14,37 @@ const SUBAGENT_PENDING_ZOMBIE_MS = 2 * 60 * 1000;
|
|
|
14
14
|
* 正常 LLM 子任务即便较长也应有上界;卡死多因 runtime.start()/submit() 挂起
|
|
15
15
|
* (如复用的父 MCP 连接异常、模型调用无响应),需回收以释放资源并解除锁。
|
|
16
16
|
*
|
|
17
|
-
* ★
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
17
|
+
* ★ 设为 12min(> SUBAGENT_SUBMIT_TIMEOUT_MS=10min):
|
|
18
|
+
* - 子 agent 卡死时,submit() 10min 超时会先触发,给出明确的错误信息
|
|
19
|
+
* (如 "runtime.submit() turn=1 timed out after 600000ms")
|
|
20
|
+
* - zombie 兜底覆盖 submit 内部无超时保护的步骤(如 hookBus.dispatch)
|
|
21
|
+
* - 之前 5min 比 submit 10min 更早触发,导致用户永远看不到真正的 submit 超时错误
|
|
22
|
+
* - task 工具显式调用的 sub-agent 不走此自动路径,不受影响
|
|
22
23
|
*/
|
|
23
|
-
const SUBAGENT_RUNNING_ZOMBIE_MS =
|
|
24
|
+
const SUBAGENT_RUNNING_ZOMBIE_MS = 12 * 60 * 1000;
|
|
25
|
+
/**
|
|
26
|
+
* store 写入超时兜底(毫秒)。
|
|
27
|
+
* SubAgentStore.write 使用 writeChain 串行化所有文件写入,若前一个 write 卡住
|
|
28
|
+
* (磁盘 IO 慢、tmp 文件 rename 卡住、文件系统问题等),后续所有 write 都会
|
|
29
|
+
* 被永久阻塞,导致 sub-agent 卡在 store.updateStatus 处无法继续 start()。
|
|
30
|
+
* 5s 超时足够覆盖正常文件 IO(通常 < 100ms),超时后放弃落盘(不影响内存态)。
|
|
31
|
+
*
|
|
32
|
+
* ★ 仅用于 running 态写入(非阻塞路径不需要超时)。
|
|
33
|
+
* 终态写入走 persistFinalStatus 的指数退避重试,不再使用 withTimeout。
|
|
34
|
+
*/
|
|
35
|
+
const STORE_WRITE_TIMEOUT_MS = 5_000;
|
|
36
|
+
/**
|
|
37
|
+
* 终态落盘的指数退避序列(毫秒)。
|
|
38
|
+
*
|
|
39
|
+
* 终态写入失败时按此序列重试,覆盖以下瞬时错误场景:
|
|
40
|
+
* - Windows fs.rename EPERM(reader 持有 fd,退避后释放即可成功)
|
|
41
|
+
* - writeChain 临时拥堵(前一个 write 慢,退避后队列消化)
|
|
42
|
+
* - 磁盘瞬时不可用
|
|
43
|
+
*
|
|
44
|
+
* 总耗时 ≈ 100+500+2000+8000 = 10.6s,远大于 EPERM 退避窗口和 writeChain 排队时长,
|
|
45
|
+
* 同时不至过长影响 bridge 关闭时的 dispose 流程。
|
|
46
|
+
*/
|
|
47
|
+
const PERSIST_BACKOFF_MS = [100, 500, 2_000, 8_000];
|
|
24
48
|
/**
|
|
25
49
|
* 子 agent runtime.start() 整体超时兜底(毫秒)。
|
|
26
50
|
* start() 内部正常路径秒级完成(loadSkills/resolveProvider/filterMcp/fetchProfile/emitStatus),
|
|
@@ -78,6 +102,16 @@ export class SubAgentManager {
|
|
|
78
102
|
* null 表示当前没有等待者。
|
|
79
103
|
*/
|
|
80
104
|
this.completionWaiterResolve = null;
|
|
105
|
+
/**
|
|
106
|
+
* 正在执行的异步终态落盘 Promise 集合。
|
|
107
|
+
*
|
|
108
|
+
* 用于:
|
|
109
|
+
* - 测试等待异步落盘完成后再读取 store 验证状态
|
|
110
|
+
* - dispose 时确保所有落盘完成后再清理(避免文件句柄占用导致目录非空)
|
|
111
|
+
*
|
|
112
|
+
* persistFinalStatus 启动时入集,resolve/reject 后出集。
|
|
113
|
+
*/
|
|
114
|
+
this.pendingPersists = new Set();
|
|
81
115
|
this.store = opts.store;
|
|
82
116
|
this.maxConcurrent = opts.maxConcurrent ?? 5;
|
|
83
117
|
// 启动定期清理已完成 entry 的定时器(每 5 分钟扫一次)
|
|
@@ -112,6 +146,10 @@ export class SubAgentManager {
|
|
|
112
146
|
initialPrompt: undefined,
|
|
113
147
|
// sub-agent 不允许再 spawn 自己的子 agent(禁止递归嵌套)
|
|
114
148
|
subAgentManager: undefined,
|
|
149
|
+
// ★ 注入 subagentType:runtime.start() 据此过滤内建写入工具。
|
|
150
|
+
// explore 类型仅暴露只读工具(read_file/grep/symbols 等),
|
|
151
|
+
// 防止 explore 子 agent 改写主工作区文件,违背只读语义。
|
|
152
|
+
subagentType: opts.subagentType,
|
|
115
153
|
// Sub Agent 模型/强度覆盖:有值则覆盖继承的 model/effort
|
|
116
154
|
...(opts.parentConfig.subAgentModel
|
|
117
155
|
? { model: opts.parentConfig.subAgentModel }
|
|
@@ -334,10 +372,24 @@ export class SubAgentManager {
|
|
|
334
372
|
}
|
|
335
373
|
};
|
|
336
374
|
entry.runtime.on("event", onEvent);
|
|
337
|
-
// 状态 -> running
|
|
375
|
+
// ★ 状态 -> running:先同步设置 + emit + currentTool 标记,再异步落盘。
|
|
376
|
+
// 之前顺序是 status=running → await store.updateStatus → emit running,
|
|
377
|
+
// 若 store.updateStatus 因 writeChain 串行化卡住(前一个 write 卡死),
|
|
378
|
+
// emit running 不会执行,start() 也不会被调用,前端 modal 永远显示
|
|
379
|
+
// 初始 snapshot(0消息 0tokens 当前工具-),与"从未运行"无法区分。
|
|
380
|
+
// 调整顺序后即使落盘卡住,前端也能立即看到 [starting] 标记。
|
|
338
381
|
entry.status = "running";
|
|
339
|
-
|
|
340
|
-
|
|
382
|
+
entry.contextSnapshot.currentTool = "[starting]";
|
|
383
|
+
entry.contextSnapshot.updatedAt = new Date().toISOString();
|
|
384
|
+
this.emitEvent(entry.subId, "running", { snapshot: { ...entry.contextSnapshot } });
|
|
385
|
+
// 落盘带超时保护:writeChain 串行化可能卡住,超时后放弃落盘(不影响内存态)
|
|
386
|
+
try {
|
|
387
|
+
await withTimeout(this.store.updateStatus(entry.parentAgentId, entry.subId, { status: "running" }), STORE_WRITE_TIMEOUT_MS, `sub-agent ${entry.subId} store.updateStatus(running)`);
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
console.error(`[SubAgentManager] store.updateStatus(running) failed for subId=${entry.subId}: ` +
|
|
391
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
392
|
+
}
|
|
341
393
|
try {
|
|
342
394
|
await entry.runtime.start();
|
|
343
395
|
// 记录启动后的可用工具列表
|
|
@@ -353,16 +405,26 @@ export class SubAgentManager {
|
|
|
353
405
|
entry.result = output;
|
|
354
406
|
entry.endedAt = Date.now();
|
|
355
407
|
entry.durationMs = entry.endedAt - entry.startedAt;
|
|
356
|
-
|
|
408
|
+
// ★ emit 前置:先把 completed 事件发出,让前端 modal 立即看到状态变化
|
|
409
|
+
// (subscribers 同步执行,SSE 路由立即推送)。
|
|
410
|
+
this.emitEvent(entry.subId, "completed", { result: output });
|
|
411
|
+
// ★ 终态落盘改为非阻塞:内存已置 completed,立即 settle completionPromise
|
|
412
|
+
// + 入队通知,磁盘写入走 persistFinalStatus 的指数退避重试,不阻塞主流程。
|
|
413
|
+
// 即使落盘最终失败,前端看到的仍是真实 completed 状态(内存态是真相源),
|
|
414
|
+
// 不会触发列表接口的 "status mismatch" 兜底(兜底逻辑已移除)。
|
|
415
|
+
entry.completionResolve?.({ output });
|
|
416
|
+
this.enqueueCompletion(entry, output, undefined);
|
|
417
|
+
void this.persistFinalStatus(entry, {
|
|
357
418
|
status: "completed",
|
|
358
419
|
result: output,
|
|
359
420
|
endedAt: entry.endedAt,
|
|
360
421
|
durationMs: entry.durationMs,
|
|
422
|
+
// ★ 持久化 availableTools:start() 后才填充,必须随终态落盘,
|
|
423
|
+
// 否则 entry 从内存清理后 store 永远是 spawn 时的空数组,
|
|
424
|
+
// 前端 modal "可用工具" tab 始终显示"暂无工具信息"。
|
|
425
|
+
availableTools: entry.availableTools,
|
|
361
426
|
contextSnapshot: undefined, // 完成后清空快照
|
|
362
427
|
});
|
|
363
|
-
this.emitEvent(entry.subId, "completed", { result: output });
|
|
364
|
-
entry.completionResolve?.({ output });
|
|
365
|
-
this.enqueueCompletion(entry, output, undefined);
|
|
366
428
|
}
|
|
367
429
|
catch (err) {
|
|
368
430
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -370,16 +432,18 @@ export class SubAgentManager {
|
|
|
370
432
|
entry.errorMessage = errorMessage;
|
|
371
433
|
entry.endedAt = Date.now();
|
|
372
434
|
entry.durationMs = entry.endedAt - entry.startedAt;
|
|
373
|
-
|
|
435
|
+
// ★ emit 前置 + 终态非阻塞落盘(同 completed 路径的设计理由)
|
|
436
|
+
this.emitEvent(entry.subId, "failed", { error: errorMessage });
|
|
437
|
+
entry.completionResolve?.({ output: "", error: errorMessage });
|
|
438
|
+
this.enqueueCompletion(entry, "", errorMessage);
|
|
439
|
+
void this.persistFinalStatus(entry, {
|
|
374
440
|
status: "failed",
|
|
375
441
|
errorMessage,
|
|
376
442
|
endedAt: entry.endedAt,
|
|
377
443
|
durationMs: entry.durationMs,
|
|
444
|
+
availableTools: entry.availableTools,
|
|
378
445
|
contextSnapshot: undefined,
|
|
379
446
|
});
|
|
380
|
-
this.emitEvent(entry.subId, "failed", { error: errorMessage });
|
|
381
|
-
entry.completionResolve?.({ output: "", error: errorMessage });
|
|
382
|
-
this.enqueueCompletion(entry, "", errorMessage);
|
|
383
447
|
}
|
|
384
448
|
finally {
|
|
385
449
|
// 移除临时监听器
|
|
@@ -436,10 +500,24 @@ export class SubAgentManager {
|
|
|
436
500
|
}
|
|
437
501
|
};
|
|
438
502
|
entry.runtime.on("event", onEvent);
|
|
439
|
-
// 状态 -> running
|
|
503
|
+
// ★ 状态 -> running:先同步设置 + emit + currentTool 标记,再异步落盘。
|
|
504
|
+
// 之前顺序是 status=running → await store.updateStatus → emit running,
|
|
505
|
+
// 若 store.updateStatus 因 writeChain 串行化卡住(前一个 write 卡死),
|
|
506
|
+
// emit running 不会执行,start() 也不会被调用,前端 modal 永远显示
|
|
507
|
+
// 初始 snapshot(0消息 0tokens 当前工具-),与"从未运行"无法区分。
|
|
508
|
+
// 调整顺序后即使落盘卡住,前端也能立即看到 [starting] 标记。
|
|
440
509
|
entry.status = "running";
|
|
441
|
-
|
|
442
|
-
|
|
510
|
+
entry.contextSnapshot.currentTool = "[starting]";
|
|
511
|
+
entry.contextSnapshot.updatedAt = new Date().toISOString();
|
|
512
|
+
this.emitEvent(entry.subId, "running", { snapshot: { ...entry.contextSnapshot } });
|
|
513
|
+
// 落盘带超时保护:writeChain 串行化可能卡住,超时后放弃落盘(不影响内存态)
|
|
514
|
+
try {
|
|
515
|
+
await withTimeout(this.store.updateStatus(entry.parentAgentId, entry.subId, { status: "running" }), STORE_WRITE_TIMEOUT_MS, `sub-agent ${entry.subId} store.updateStatus(running)`);
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
console.error(`[SubAgentManager] store.updateStatus(running) failed for subId=${entry.subId}: ` +
|
|
519
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
520
|
+
}
|
|
443
521
|
try {
|
|
444
522
|
// ★ 超时兜底:start() 通常秒级完成,超时说明卡在 hookBus/fetchProfile/loadCommands
|
|
445
523
|
// 等无超时保护的步骤。抛错后被外层 catch 标记为 failed,前端可见。
|
|
@@ -498,16 +576,19 @@ export class SubAgentManager {
|
|
|
498
576
|
entry.result = output;
|
|
499
577
|
entry.endedAt = Date.now();
|
|
500
578
|
entry.durationMs = entry.endedAt - entry.startedAt;
|
|
501
|
-
|
|
579
|
+
// ★ emit 前置 + 终态非阻塞落盘 + 立即 settle completionPromise
|
|
580
|
+
// (设计理由同 runEntryToCompletion:内存态是真相源,磁盘写入不阻塞主流程)
|
|
581
|
+
this.emitEvent(entry.subId, "completed", { result: output });
|
|
582
|
+
entry.completionResolve?.({ output });
|
|
583
|
+
this.enqueueCompletion(entry, output, undefined);
|
|
584
|
+
void this.persistFinalStatus(entry, {
|
|
502
585
|
status: "completed",
|
|
503
586
|
result: output,
|
|
504
587
|
endedAt: entry.endedAt,
|
|
505
588
|
durationMs: entry.durationMs,
|
|
589
|
+
availableTools: entry.availableTools,
|
|
506
590
|
contextSnapshot: undefined,
|
|
507
591
|
});
|
|
508
|
-
this.emitEvent(entry.subId, "completed", { result: output });
|
|
509
|
-
entry.completionResolve?.({ output });
|
|
510
|
-
this.enqueueCompletion(entry, output, undefined);
|
|
511
592
|
}
|
|
512
593
|
catch (err) {
|
|
513
594
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -515,16 +596,18 @@ export class SubAgentManager {
|
|
|
515
596
|
entry.errorMessage = errorMessage;
|
|
516
597
|
entry.endedAt = Date.now();
|
|
517
598
|
entry.durationMs = entry.endedAt - entry.startedAt;
|
|
518
|
-
|
|
599
|
+
// ★ emit 前置 + 终态非阻塞落盘(同 completed 路径的设计理由)
|
|
600
|
+
this.emitEvent(entry.subId, "failed", { error: errorMessage });
|
|
601
|
+
entry.completionResolve?.({ output: "", error: errorMessage });
|
|
602
|
+
this.enqueueCompletion(entry, "", errorMessage);
|
|
603
|
+
void this.persistFinalStatus(entry, {
|
|
519
604
|
status: "failed",
|
|
520
605
|
errorMessage,
|
|
521
606
|
endedAt: entry.endedAt,
|
|
522
607
|
durationMs: entry.durationMs,
|
|
608
|
+
availableTools: entry.availableTools,
|
|
523
609
|
contextSnapshot: undefined,
|
|
524
610
|
});
|
|
525
|
-
this.emitEvent(entry.subId, "failed", { error: errorMessage });
|
|
526
|
-
entry.completionResolve?.({ output: "", error: errorMessage });
|
|
527
|
-
this.enqueueCompletion(entry, "", errorMessage);
|
|
528
611
|
}
|
|
529
612
|
finally {
|
|
530
613
|
// 移除临时监听器
|
|
@@ -580,16 +663,18 @@ export class SubAgentManager {
|
|
|
580
663
|
entry.status = "cancelled";
|
|
581
664
|
entry.endedAt = Date.now();
|
|
582
665
|
entry.durationMs = entry.endedAt - entry.startedAt;
|
|
583
|
-
|
|
666
|
+
// ★ emit 前置 + 终态非阻塞落盘 + 立即 settle completionPromise
|
|
667
|
+
// (设计理由同 completed 路径:内存态是真相源,磁盘写入不阻塞主流程)
|
|
668
|
+
this.emitEvent(subId, "cancelled", {});
|
|
669
|
+
entry.completionResolve?.({ output: "", error: "aborted" });
|
|
670
|
+
this.enqueueCompletion(entry, "", "aborted");
|
|
671
|
+
void this.persistFinalStatus(entry, {
|
|
584
672
|
status: "cancelled",
|
|
585
673
|
endedAt: entry.endedAt,
|
|
586
674
|
durationMs: entry.durationMs,
|
|
675
|
+
availableTools: entry.availableTools,
|
|
587
676
|
contextSnapshot: undefined,
|
|
588
677
|
});
|
|
589
|
-
this.emitEvent(subId, "cancelled", {});
|
|
590
|
-
// settle completionPromise(cancelled 视为 error)
|
|
591
|
-
entry.completionResolve?.({ output: "", error: "aborted" });
|
|
592
|
-
this.enqueueCompletion(entry, "", "aborted");
|
|
593
678
|
}
|
|
594
679
|
/** 获取内存中的 entry */
|
|
595
680
|
getSubAgent(subId) {
|
|
@@ -681,11 +766,66 @@ export class SubAgentManager {
|
|
|
681
766
|
}
|
|
682
767
|
const entries = Array.from(this.subAgents.values());
|
|
683
768
|
await Promise.allSettled(entries.map((e) => this.abort(e.subId)));
|
|
769
|
+
// 等待所有异步终态落盘完成(abort 触发的 cancelled 落盘是异步的),
|
|
770
|
+
// 避免文件句柄占用导致测试 cleanup 时目录非空
|
|
771
|
+
await this.flushPendingPersists();
|
|
684
772
|
this.subAgents.clear();
|
|
685
773
|
this.parentToSubs.clear();
|
|
686
774
|
this.subscribers.clear();
|
|
687
775
|
}
|
|
688
776
|
// ---------- 内部方法 ----------
|
|
777
|
+
/**
|
|
778
|
+
* 异步落盘终态记录(指数退避重试),不阻塞主流程。
|
|
779
|
+
*
|
|
780
|
+
* 设计要点:
|
|
781
|
+
* - 内存态已是终态(completed/failed/cancelled),落盘只做历史归档
|
|
782
|
+
* - 落盘失败不回滚内存态,前端看到的是真实终态
|
|
783
|
+
* - 重试覆盖瞬时错误:Windows fs.rename EPERM、writeChain 拥堵、磁盘瞬时不可用
|
|
784
|
+
* - 全部重试失败后仅打日志,下次 bridge 启动时 reconcileZombies 会清理残留
|
|
785
|
+
*
|
|
786
|
+
* 调用方约定:必须用 `void this.persistFinalStatus(...)` 触发,不 await。
|
|
787
|
+
* entry.completionResolve 与 enqueueCompletion 应在此调用之前完成,
|
|
788
|
+
* 保证主流程不被落盘 IO 阻塞。
|
|
789
|
+
*
|
|
790
|
+
* Promise 加入 pendingPersists 集合,供 flushPendingPersists / dispose 等待。
|
|
791
|
+
*/
|
|
792
|
+
persistFinalStatus(entry, patch) {
|
|
793
|
+
const run = (async () => {
|
|
794
|
+
let lastErr;
|
|
795
|
+
for (let attempt = 0; attempt <= PERSIST_BACKOFF_MS.length; attempt++) {
|
|
796
|
+
try {
|
|
797
|
+
await this.store.updateStatus(entry.parentAgentId, entry.subId, patch);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
catch (err) {
|
|
801
|
+
lastErr = err;
|
|
802
|
+
if (attempt < PERSIST_BACKOFF_MS.length) {
|
|
803
|
+
await new Promise((r) => setTimeout(r, PERSIST_BACKOFF_MS[attempt]));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
console.error(`[SubAgentManager] persistFinalStatus(${patch.status}) failed after ${PERSIST_BACKOFF_MS.length + 1} attempts ` +
|
|
808
|
+
`for subId=${entry.subId}: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`);
|
|
809
|
+
})();
|
|
810
|
+
// 入集供测试/dispose 等待
|
|
811
|
+
this.pendingPersists.add(run);
|
|
812
|
+
run.finally(() => this.pendingPersists.delete(run));
|
|
813
|
+
return run;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* 等待所有正在执行的异步终态落盘完成。
|
|
817
|
+
*
|
|
818
|
+
* 主要用途:
|
|
819
|
+
* - 测试在终态后读取 store 前调用,确保落盘已完成
|
|
820
|
+
* - dispose 前调用,避免文件句柄占用导致目录清理失败
|
|
821
|
+
*
|
|
822
|
+
* 生产环境一般不需要调用——落盘失败不影响内存态正确性。
|
|
823
|
+
*/
|
|
824
|
+
async flushPendingPersists() {
|
|
825
|
+
if (this.pendingPersists.size === 0)
|
|
826
|
+
return;
|
|
827
|
+
await Promise.allSettled(Array.from(this.pendingPersists));
|
|
828
|
+
}
|
|
689
829
|
/**
|
|
690
830
|
* 将子 agent 终态事件入队并通知等待者。
|
|
691
831
|
* 主流程:构建通知对象 -> 入队 completionQueue -> 若有 waitForAnyCompletion 等待者则立即 resolve。
|
|
@@ -782,21 +922,20 @@ export class SubAgentManager {
|
|
|
782
922
|
entry.errorMessage = errorMessage;
|
|
783
923
|
entry.endedAt = Date.now();
|
|
784
924
|
entry.durationMs = entry.endedAt - entry.startedAt;
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
errorMessage,
|
|
789
|
-
endedAt: entry.endedAt,
|
|
790
|
-
durationMs: entry.durationMs,
|
|
791
|
-
contextSnapshot: undefined,
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
catch {
|
|
795
|
-
/* 落盘失败不影响内存态与事件通知 */
|
|
796
|
-
}
|
|
925
|
+
// ★ emit 前置 + 终态非阻塞落盘 + 立即 settle completionPromise
|
|
926
|
+
// (与其他终态路径保持一致,zombie 回收路径若 writeChain 卡住,
|
|
927
|
+
// 也应保证 runtime 收到 failed 事件从而释放 pendingMemorySubAgents URI 锁)
|
|
797
928
|
this.emitEvent(entry.subId, "failed", { error: errorMessage });
|
|
798
929
|
entry.completionResolve?.({ output: "", error: errorMessage });
|
|
799
930
|
this.enqueueCompletion(entry, "", errorMessage);
|
|
931
|
+
void this.persistFinalStatus(entry, {
|
|
932
|
+
status: "failed",
|
|
933
|
+
errorMessage,
|
|
934
|
+
endedAt: entry.endedAt,
|
|
935
|
+
durationMs: entry.durationMs,
|
|
936
|
+
availableTools: entry.availableTools,
|
|
937
|
+
contextSnapshot: undefined,
|
|
938
|
+
});
|
|
800
939
|
try {
|
|
801
940
|
await entry.runtime.stop();
|
|
802
941
|
}
|
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import type { SubAgentDocument, SubAgentEvent, SubAgentRecord } from "./sub-agent-types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Sub Agent JSON 文件存储,按 agentId 隔离。
|
|
4
|
-
* 文件路径:{homedir}/.acode/sub-agents/{agentId}.json
|
|
5
4
|
*
|
|
6
|
-
*
|
|
5
|
+
* 文件布局:
|
|
6
|
+
* - 主记录文件:{homedir}/.acode/sub-agents/{agentId}.json
|
|
7
|
+
* 存放 SubAgentRecord[](不含 events 字段,避免全量读写阻塞终态写入)
|
|
8
|
+
* - 事件日志文件:{homedir}/.acode/sub-agents/{agentId}.{subId}.events.jsonl
|
|
9
|
+
* 每行一个 SubAgentEvent,append-only,O(1) 追加,不进主 writeChain
|
|
10
|
+
*
|
|
11
|
+
* 设计要点:
|
|
7
12
|
* - 不依赖 agent 工作目录,避免 workspacePath 错误时读取失败
|
|
8
13
|
* - sub-agent 跟随 agent(agentId),不跟随项目工作区
|
|
9
|
-
* -
|
|
14
|
+
* - 主记录写操作通过 Promise 链串行化,tmp + rename 原子替换
|
|
15
|
+
* (rename 在 Windows 上目标被打开时会 EPERM,使用 renameWithRetry 退避重试,
|
|
16
|
+
* 重试仍失败时 fallback 到 copyFile + unlink,彻底规避 EPERM 持续场景)
|
|
17
|
+
* - events 走独立文件,避免 tool_start/tool_end 高频落盘阻塞终态 updateStatus
|
|
18
|
+
* (历史教训:events 与 updateStatus 共用 writeChain 时,memory delegate
|
|
19
|
+
* 一次跑数十个工具调用,appendEvent 排队导致 updateStatus 超 5s 超时,
|
|
20
|
+
* 内存已翻终态但 store 仍 running,触发 "status mismatch" 兜底误报)
|
|
21
|
+
*
|
|
22
|
+
* ★ 终态一致性架构(治本方案):
|
|
23
|
+
* - 内存态(SubAgentManager)是运行态的唯一真相源
|
|
24
|
+
* - 终态写入通过 persistFinalStatus 异步重试落盘,不阻塞 completionResolve
|
|
25
|
+
* - 落盘失败不回滚内存态,前端看到的是真实终态
|
|
26
|
+
* - 列表接口不再做"磁盘 running/pending + 内存无 → 改写 failed"的兜底
|
|
27
|
+
* (该兜底是 "status mismatch" 误报的直接来源,已移除)
|
|
28
|
+
* - bridge 启动时 reconcileZombies 一次性清理上次进程残留的僵尸记录
|
|
10
29
|
*
|
|
11
30
|
* 历史保留策略:全部 sub-agent 记录都持久化(含 completed/failed/cancelled),
|
|
12
31
|
* 按 startedAt 倒序排列。dashboard 按需分页读取。
|
|
@@ -15,8 +34,10 @@ export declare class SubAgentStore {
|
|
|
15
34
|
private readonly dir;
|
|
16
35
|
private writeChain;
|
|
17
36
|
constructor();
|
|
18
|
-
/**
|
|
37
|
+
/** 主记录文件路径(按 agentId 隔离,防路径穿越) */
|
|
19
38
|
private filePath;
|
|
39
|
+
/** 事件日志文件路径(按 agentId + subId 隔离,防路径穿越) */
|
|
40
|
+
private eventsFilePath;
|
|
20
41
|
/** 读取该 agentId 的完整文档。文件不存在或损坏时返回空文档。 */
|
|
21
42
|
read(agentId: string): Promise<SubAgentDocument>;
|
|
22
43
|
/** 读取单个 sub-agent 记录 */
|
|
@@ -29,8 +50,26 @@ export declare class SubAgentStore {
|
|
|
29
50
|
updateStatus(agentId: string, subId: string, patch: Partial<SubAgentRecord>): Promise<void>;
|
|
30
51
|
/** 列出该 agent 的全部 sub-agent(已按 startedAt 倒序) */
|
|
31
52
|
list(agentId: string): Promise<SubAgentRecord[]>;
|
|
32
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* 追加一条事件到指定 sub-agent 的事件日志文件。
|
|
55
|
+
*
|
|
56
|
+
* 走独立 jsonl 文件(每行一个事件,append-only),不进主 writeChain:
|
|
57
|
+
* - O(1) 追加,无需 read-modify-write 全量主文件
|
|
58
|
+
* - 不阻塞终态 updateStatus(避免 status mismatch 兜底误报)
|
|
59
|
+
* - 高频 tool_start/tool_end 落盘成本恒定,与历史记录数无关
|
|
60
|
+
*
|
|
61
|
+
* 失败时仅打日志(fire-and-forget 语义),不阻断 sub-agent 运行。
|
|
62
|
+
* 调用方(sub-agent-manager.emitEvent)已用 void + catch 包装。
|
|
63
|
+
*/
|
|
33
64
|
appendEvent(agentId: string, subId: string, event: SubAgentEvent): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* 读取指定 sub-agent 的全部历史事件(按时间顺序)。
|
|
67
|
+
*
|
|
68
|
+
* 优先读独立 jsonl 文件;不存在时回退到主记录文件中的 events 字段
|
|
69
|
+
* (向后兼容旧版本数据:旧版把 events 内嵌在 SubAgentRecord 中)。
|
|
70
|
+
* 供 SSE 路由在客户端重连时补发历史 tool 事件。
|
|
71
|
+
*/
|
|
72
|
+
readEvents(agentId: string, subId: string): Promise<SubAgentEvent[]>;
|
|
34
73
|
/**
|
|
35
74
|
* 扫描所有 agent 文件,把 status="running"/"pending" 的记录修正为 "failed"。
|
|
36
75
|
* 在 bridge 启动时调用,清理上次进程异常退出留下的僵尸记录。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sub-agent-store.d.ts","sourceRoot":"","sources":["../src/sub-agent-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"sub-agent-store.d.ts","sourceRoot":"","sources":["../src/sub-agent-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG5F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,UAAU,CAAoC;;IAMtD,kCAAkC;IAClC,OAAO,CAAC,QAAQ;IAKhB,2CAA2C;IAC3C,OAAO,CAAC,cAAc;IAMtB,wCAAwC;IAClC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAoBtD,wBAAwB;IAClB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAKzE,oCAAoC;IAC9B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBxE,wBAAwB;IAClB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAWpE,eAAe;IACT,YAAY,CAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,OAAO,CAAC,cAAc,CAAC,GAC7B,OAAO,CAAC,IAAI,CAAC;IAQhB,+CAA+C;IACzC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAItD;;;;;;;;;;OAUG;IACG,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAOtF;;;;;;OAMG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC;IAyB1E;;;OAGG;IACG,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC;CAmC1C"}
|
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { renameWithRetry } from "./fs-utils.js";
|
|
4
5
|
/**
|
|
5
6
|
* Sub Agent JSON 文件存储,按 agentId 隔离。
|
|
6
|
-
* 文件路径:{homedir}/.acode/sub-agents/{agentId}.json
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* 文件布局:
|
|
9
|
+
* - 主记录文件:{homedir}/.acode/sub-agents/{agentId}.json
|
|
10
|
+
* 存放 SubAgentRecord[](不含 events 字段,避免全量读写阻塞终态写入)
|
|
11
|
+
* - 事件日志文件:{homedir}/.acode/sub-agents/{agentId}.{subId}.events.jsonl
|
|
12
|
+
* 每行一个 SubAgentEvent,append-only,O(1) 追加,不进主 writeChain
|
|
13
|
+
*
|
|
14
|
+
* 设计要点:
|
|
9
15
|
* - 不依赖 agent 工作目录,避免 workspacePath 错误时读取失败
|
|
10
16
|
* - sub-agent 跟随 agent(agentId),不跟随项目工作区
|
|
11
|
-
* -
|
|
17
|
+
* - 主记录写操作通过 Promise 链串行化,tmp + rename 原子替换
|
|
18
|
+
* (rename 在 Windows 上目标被打开时会 EPERM,使用 renameWithRetry 退避重试,
|
|
19
|
+
* 重试仍失败时 fallback 到 copyFile + unlink,彻底规避 EPERM 持续场景)
|
|
20
|
+
* - events 走独立文件,避免 tool_start/tool_end 高频落盘阻塞终态 updateStatus
|
|
21
|
+
* (历史教训:events 与 updateStatus 共用 writeChain 时,memory delegate
|
|
22
|
+
* 一次跑数十个工具调用,appendEvent 排队导致 updateStatus 超 5s 超时,
|
|
23
|
+
* 内存已翻终态但 store 仍 running,触发 "status mismatch" 兜底误报)
|
|
24
|
+
*
|
|
25
|
+
* ★ 终态一致性架构(治本方案):
|
|
26
|
+
* - 内存态(SubAgentManager)是运行态的唯一真相源
|
|
27
|
+
* - 终态写入通过 persistFinalStatus 异步重试落盘,不阻塞 completionResolve
|
|
28
|
+
* - 落盘失败不回滚内存态,前端看到的是真实终态
|
|
29
|
+
* - 列表接口不再做"磁盘 running/pending + 内存无 → 改写 failed"的兜底
|
|
30
|
+
* (该兜底是 "status mismatch" 误报的直接来源,已移除)
|
|
31
|
+
* - bridge 启动时 reconcileZombies 一次性清理上次进程残留的僵尸记录
|
|
12
32
|
*
|
|
13
33
|
* 历史保留策略:全部 sub-agent 记录都持久化(含 completed/failed/cancelled),
|
|
14
34
|
* 按 startedAt 倒序排列。dashboard 按需分页读取。
|
|
@@ -18,11 +38,17 @@ export class SubAgentStore {
|
|
|
18
38
|
this.writeChain = Promise.resolve();
|
|
19
39
|
this.dir = path.join(os.homedir(), ".acode", "sub-agents");
|
|
20
40
|
}
|
|
21
|
-
/**
|
|
41
|
+
/** 主记录文件路径(按 agentId 隔离,防路径穿越) */
|
|
22
42
|
filePath(agentId) {
|
|
23
43
|
const safe = String(agentId).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
24
44
|
return path.join(this.dir, `${safe}.json`);
|
|
25
45
|
}
|
|
46
|
+
/** 事件日志文件路径(按 agentId + subId 隔离,防路径穿越) */
|
|
47
|
+
eventsFilePath(agentId, subId) {
|
|
48
|
+
const safeAgent = String(agentId).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
49
|
+
const safeSub = String(subId).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
50
|
+
return path.join(this.dir, `${safeAgent}.${safeSub}.events.jsonl`);
|
|
51
|
+
}
|
|
26
52
|
/** 读取该 agentId 的完整文档。文件不存在或损坏时返回空文档。 */
|
|
27
53
|
async read(agentId) {
|
|
28
54
|
const empty = { agentId, updatedAt: "", subAgents: [] };
|
|
@@ -59,11 +85,19 @@ export class SubAgentStore {
|
|
|
59
85
|
const doc = {
|
|
60
86
|
agentId,
|
|
61
87
|
updatedAt: new Date().toISOString(),
|
|
62
|
-
|
|
88
|
+
// ★ 主记录文件不再存 events 字段(events 走独立 jsonl 文件)。
|
|
89
|
+
// 剥离 events 避免每次 appendEvent 都全量重写主文件,
|
|
90
|
+
// 也能显著减小主文件体积(events 是高频大字段)。
|
|
91
|
+
subAgents: subAgents
|
|
92
|
+
.map(({ events: _events, ...rest }) => rest)
|
|
93
|
+
.sort((a, b) => b.startedAt - a.startedAt),
|
|
63
94
|
};
|
|
64
95
|
const tmp = `${target}.tmp`;
|
|
65
96
|
await fs.writeFile(tmp, JSON.stringify(doc, null, 2), "utf-8");
|
|
66
|
-
|
|
97
|
+
// ★ Windows 上 fs.rename 目标被 reader 打开时会 EPERM/EBUSY/EACCES,
|
|
98
|
+
// 瞬时错误,短退避后重试通常即可成功(reader 已释放 fd)。
|
|
99
|
+
// POSIX 上 rename 原子,不会触发此类错误。
|
|
100
|
+
await renameWithRetry(tmp, target);
|
|
67
101
|
});
|
|
68
102
|
this.writeChain = run.catch(() => { });
|
|
69
103
|
await run;
|
|
@@ -93,18 +127,56 @@ export class SubAgentStore {
|
|
|
93
127
|
async list(agentId) {
|
|
94
128
|
return (await this.read(agentId)).subAgents;
|
|
95
129
|
}
|
|
96
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* 追加一条事件到指定 sub-agent 的事件日志文件。
|
|
132
|
+
*
|
|
133
|
+
* 走独立 jsonl 文件(每行一个事件,append-only),不进主 writeChain:
|
|
134
|
+
* - O(1) 追加,无需 read-modify-write 全量主文件
|
|
135
|
+
* - 不阻塞终态 updateStatus(避免 status mismatch 兜底误报)
|
|
136
|
+
* - 高频 tool_start/tool_end 落盘成本恒定,与历史记录数无关
|
|
137
|
+
*
|
|
138
|
+
* 失败时仅打日志(fire-and-forget 语义),不阻断 sub-agent 运行。
|
|
139
|
+
* 调用方(sub-agent-manager.emitEvent)已用 void + catch 包装。
|
|
140
|
+
*/
|
|
97
141
|
async appendEvent(agentId, subId, event) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
142
|
+
const target = this.eventsFilePath(agentId, subId);
|
|
143
|
+
await fs.mkdir(this.dir, { recursive: true });
|
|
144
|
+
// 每行一个 JSON 事件,以 \n 结尾,便于流式读取和解析
|
|
145
|
+
await fs.appendFile(target, JSON.stringify(event) + "\n", "utf-8");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 读取指定 sub-agent 的全部历史事件(按时间顺序)。
|
|
149
|
+
*
|
|
150
|
+
* 优先读独立 jsonl 文件;不存在时回退到主记录文件中的 events 字段
|
|
151
|
+
* (向后兼容旧版本数据:旧版把 events 内嵌在 SubAgentRecord 中)。
|
|
152
|
+
* 供 SSE 路由在客户端重连时补发历史 tool 事件。
|
|
153
|
+
*/
|
|
154
|
+
async readEvents(agentId, subId) {
|
|
155
|
+
const jsonlPath = this.eventsFilePath(agentId, subId);
|
|
156
|
+
try {
|
|
157
|
+
const raw = await fs.readFile(jsonlPath, "utf-8");
|
|
158
|
+
const events = [];
|
|
159
|
+
for (const line of raw.split("\n")) {
|
|
160
|
+
const trimmed = line.trim();
|
|
161
|
+
if (!trimmed)
|
|
162
|
+
continue;
|
|
163
|
+
try {
|
|
164
|
+
events.push(JSON.parse(trimmed));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
/* 跳过损坏行 */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return events;
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
if (err && err.code === "ENOENT") {
|
|
174
|
+
// jsonl 不存在:回退到主记录文件中的 events 字段(旧版本兼容)
|
|
175
|
+
const record = await this.get(agentId, subId);
|
|
176
|
+
return Array.isArray(record?.events) ? record.events : [];
|
|
177
|
+
}
|
|
178
|
+
throw err;
|
|
105
179
|
}
|
|
106
|
-
record.events.push(event);
|
|
107
|
-
await this.write(agentId, doc.subAgents);
|
|
108
180
|
}
|
|
109
181
|
/**
|
|
110
182
|
* 扫描所有 agent 文件,把 status="running"/"pending" 的记录修正为 "failed"。
|
|
@@ -27,6 +27,7 @@ export interface TodoDocument {
|
|
|
27
27
|
* 并发安全:同一 TodoStore 实例的写操作通过 Promise 链串行化,
|
|
28
28
|
* 避免同一 agentId 的并发 todo_write 互相覆盖。
|
|
29
29
|
* 写入采用 tmp + rename 原子替换,防止半写损坏。
|
|
30
|
+
* (rename 在 Windows 上目标被打开时会 EPERM,使用 renameWithRetry 短退避重试)
|
|
30
31
|
*/
|
|
31
32
|
export declare class TodoStore {
|
|
32
33
|
private readonly todosDir;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"todo-store.d.ts","sourceRoot":"","sources":["../src/todo-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"todo-store.d.ts","sourceRoot":"","sources":["../src/todo-store.ts"],"names":[],"mappings":"AAKA,gBAAgB;AAChB,MAAM,WAAW,QAAQ;IACvB,iBAAiB;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,WAAW,CAAC;IAC9D,QAAQ,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;CACrC;AAED,qBAAqB;AACrB,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,UAAU,CAAoC;;IAMtD,yBAAyB;IACzB,OAAO,CAAC,QAAQ;IAMhB,4CAA4C;IACtC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAmBlD,8DAA8D;IACxD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;CAqBnF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { renameWithRetry } from "./fs-utils.js";
|
|
4
5
|
/**
|
|
5
6
|
* Todo JSON 文件存储,按 agentId 隔离。
|
|
6
7
|
* 文件路径:{homedir}/.acode/todos/{agentId}.json
|
|
@@ -12,6 +13,7 @@ import path from "node:path";
|
|
|
12
13
|
* 并发安全:同一 TodoStore 实例的写操作通过 Promise 链串行化,
|
|
13
14
|
* 避免同一 agentId 的并发 todo_write 互相覆盖。
|
|
14
15
|
* 写入采用 tmp + rename 原子替换,防止半写损坏。
|
|
16
|
+
* (rename 在 Windows 上目标被打开时会 EPERM,使用 renameWithRetry 短退避重试)
|
|
15
17
|
*/
|
|
16
18
|
export class TodoStore {
|
|
17
19
|
constructor() {
|
|
@@ -59,7 +61,10 @@ export class TodoStore {
|
|
|
59
61
|
};
|
|
60
62
|
const tmp = `${target}.tmp`;
|
|
61
63
|
await fs.writeFile(tmp, JSON.stringify(doc, null, 2), "utf-8");
|
|
62
|
-
|
|
64
|
+
// ★ Windows 上 fs.rename 目标被 reader 打开时会 EPERM/EBUSY/EACCES,
|
|
65
|
+
// 瞬时错误,短退避后重试通常即可成功(reader 已释放 fd)。
|
|
66
|
+
// POSIX 上 rename 原子,不会触发此类错误。
|
|
67
|
+
await renameWithRetry(tmp, target);
|
|
63
68
|
});
|
|
64
69
|
// 维持链式串行,但不让单次失败阻断后续写
|
|
65
70
|
this.writeChain = run.catch(() => { });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SubAgentManager } from "./sub-agent-manager.js";
|
|
2
|
+
import type { SubAgentType } from "./sub-agent-types.js";
|
|
2
3
|
import type { ACodeMcpConnection } from "./runtime.js";
|
|
3
4
|
export interface ACodeMcpServerSpec {
|
|
4
5
|
type?: "stdio" | "sse" | "http";
|
|
@@ -132,6 +133,16 @@ export interface ACodeRuntimeConfig {
|
|
|
132
133
|
subAgentModel?: string;
|
|
133
134
|
/** Sub Agent 思考强度覆盖(空值=继承主 Agent 强度) */
|
|
134
135
|
subAgentEffort?: string;
|
|
136
|
+
/**
|
|
137
|
+
* Sub Agent 类型(general | explore)。
|
|
138
|
+
* - general:完整工具权限(可读写工作区文件)
|
|
139
|
+
* - explore:只读模式,过滤掉会修改工作区文件的内建工具
|
|
140
|
+
* (write_file/edit_file/delete_file/Bash),仅保留 read_file/grep/symbols
|
|
141
|
+
* 等只读工具 + memory MCP 工具。MCP 工具白名单由
|
|
142
|
+
* SUB_AGENT_ALLOWED_MCP_TOOLS 独立施加,此处不重复。
|
|
143
|
+
* 未设置时视为 general(向后兼容)。
|
|
144
|
+
*/
|
|
145
|
+
subagentType?: SubAgentType;
|
|
135
146
|
/**
|
|
136
147
|
* 可选:Agent 文件操作变更回调。
|
|
137
148
|
* 当 Agent 调用 write_file/edit_file/delete_file 工具时,
|