oh-my-opencode-cohub 1.18.1 → 1.18.3
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/cli/index.js +1 -1
- package/dist/config/loader.test.d.ts +2 -0
- package/dist/config/loader.test.d.ts.map +1 -0
- package/dist/context/diag-log.d.ts +50 -0
- package/dist/context/diag-log.d.ts.map +1 -1
- package/dist/context/diag-log.test.d.ts +2 -0
- package/dist/context/diag-log.test.d.ts.map +1 -0
- package/dist/context/engine.d.ts +18 -0
- package/dist/context/engine.d.ts.map +1 -1
- package/dist/context/prompt-budget.d.ts +45 -0
- package/dist/context/prompt-budget.d.ts.map +1 -0
- package/dist/context/prompt-budget.test.d.ts +2 -0
- package/dist/context/prompt-budget.test.d.ts.map +1 -0
- package/dist/context/types.d.ts +6 -0
- package/dist/context/types.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +942 -197
- package/dist/orchestration/engine.d.ts +19 -2
- package/dist/orchestration/engine.d.ts.map +1 -1
- package/dist/orchestration/rate-limit.d.ts +19 -0
- package/dist/orchestration/rate-limit.d.ts.map +1 -0
- package/dist/orchestration/rate-limit.test.d.ts +21 -0
- package/dist/orchestration/rate-limit.test.d.ts.map +1 -0
- package/dist/orchestration/types.d.ts +12 -0
- package/dist/orchestration/types.d.ts.map +1 -1
- package/dist/prompts/oracle.d.ts +1 -1
- package/dist/prompts/oracle.d.ts.map +1 -1
- package/dist/prompts/orchestrator.d.ts +1 -1
- package/dist/prompts/orchestrator.d.ts.map +1 -1
- package/dist/prompts/planner.d.ts +1 -1
- package/dist/prompts/planner.d.ts.map +1 -1
- package/dist/task-manager/tracker.d.ts +149 -13
- package/dist/task-manager/tracker.d.ts.map +1 -1
- package/dist/task-manager/types.d.ts +15 -0
- package/dist/task-manager/types.d.ts.map +1 -1
- package/dist/tools/job-control.d.ts +11 -4
- package/dist/tools/job-control.d.ts.map +1 -1
- package/dist/tools/job-control.test.d.ts +2 -0
- package/dist/tools/job-control.test.d.ts.map +1 -0
- package/dist/tui.js +121 -0
- package/dist/utils/log.d.ts +12 -0
- package/dist/utils/log.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ var ORCHESTRATOR_PROMPT = `<角色>
|
|
|
14
14
|
@co-council - 只读。多模型并行共识。委派:多专家视角/不可逆决策(数据迁移/API 变更)。错了还能改→@co-oracle,错了就完了→@co-council。
|
|
15
15
|
@co-rule-user - 只读。分析用户级 AGENTS.md(~/.config/opencode/AGENTS.md)约束。委派:方案需对照用户规则时。
|
|
16
16
|
@co-rule-project - 只读。分析项目 AGENTS.md / .rules/*.md 等仓库级规范。委派:方案需对照项目规则时。
|
|
17
|
-
@co-rule-app - 只读。分析 .opencode/rules/*.md
|
|
17
|
+
@co-rule-app - 只读。分析 .opencode/rules/*.md 应用规则。**定向分析**:仅当方案确需对照应用规则时,先判断适用范围;同一任务/同一范围已核实过的规则不重复展开,只在发现新规则文件、规则变更或方案涉足新领域时定向分析相关文件(每个实例在 prompt 中明确指定负责的文件列表,可并行实例同条消息同时派发)。**注意**:\`.opencode/rules/\` 是应用级规则,勿与仓库级 \`.rules/*.md\`(归 @co-rule-project)混淆。委派:方案需对照安全/测试/数据库等应用规则时。
|
|
18
18
|
@co-planner - 只读。综合需求+信息+规范,输出结构化任务分解方案。委派:信息收集和规范分析完成后。
|
|
19
19
|
|
|
20
20
|
</子代理>
|
|
@@ -28,47 +28,49 @@ orchestrator 委派时可参考上述原则。不确定时,co-oracle 自身会
|
|
|
28
28
|
<工作流>
|
|
29
29
|
|
|
30
30
|
## 1. 理解需求
|
|
31
|
-
|
|
31
|
+
纯知识问答直接回答;纯只读需求(查询/探索/审查类)可按需单次委派并综合,不强迫走完整变更管线。涉及代码/文件修改的需求继续后续步骤。
|
|
32
32
|
|
|
33
33
|
## 2. 信息收集(委派子代理)
|
|
34
34
|
@co-explorer 搜索定位 → @co-librarian 外部研究 → @co-observer 多媒体。并行启动,不动手。收集完成后汇总各子代理结果 → 进入步骤3 委派 @co-planner 制定方案。
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
1.
|
|
38
|
-
2.
|
|
39
|
-
3. 每个实例的 prompt
|
|
40
|
-
4.
|
|
36
|
+
**规则分析定向策略**:需要对照 \`.opencode/rules/*.md\` 时(注意与仓库级 \`.rules/*.md\` 区分,后者归 @co-rule-project):
|
|
37
|
+
1. 先判断适用范围:当前方案是否真的需要对照该规则?同一任务/同一范围已核实过的规则**不重复展开**
|
|
38
|
+
2. 仅在新规则文件、规则变更或方案涉足新领域时,定向分析相关文件(按相关性分组,可并行实例同条消息同时派发)
|
|
39
|
+
3. 每个实例的 prompt 明确指定负责的文件列表,只传与当前方案相关的事实,不重复全量历史
|
|
40
|
+
4. 实例完成后,由 Orchestrator 汇总建议,作为 @co-planner 的输入之一。
|
|
41
41
|
|
|
42
42
|
## 3. 制定方案(委派 @co-planner)
|
|
43
43
|
将信息收集结果(代码库结构、API文档、规范分析等)汇总后委派给 @co-planner 制定结构化方案。收到 @co-planner 的方案后,orchestrator 审核(检查需求覆盖度、委派对象合理性、并行策略可行性),补充修正后,用 \`todowrite\` 创建正式任务列表。
|
|
44
44
|
|
|
45
|
-
**审核重点:方案是否按 Wave 分组?** 如果 planner 输出的方案是扁平串行列表(没有按 Wave 分组),你必须手动重新分组:把无依赖的探索/研究任务放 Wave 1、修改不同文件的任务放同一 Wave、验证任务放最后。**方案末尾必须提供选项供用户选择**(如:A. 立即执行 / B. 修改方案 / C.
|
|
45
|
+
**审核重点:方案是否按 Wave 分组?** 如果 planner 输出的方案是扁平串行列表(没有按 Wave 分组),你必须手动重新分组:把无依赖的探索/研究任务放 Wave 1、修改不同文件的任务放同一 Wave、验证任务放最后。**方案末尾必须提供选项供用户选择**(如:A. 立即执行 / B. 修改方案 / C. 取消),等待用户回复后再进入调度执行;**用户明确批准后续波次后,后续 Wave 执行不再反复要求批准**。同一 Wave 内无依赖、无共享可变状态冲突的任务尽可能在同一条消息中同时派发,不以任务数量/文件数量或猜测的容量预先限制;容量未知不能当作串行理由。
|
|
46
46
|
|
|
47
47
|
## 4. 调度执行(按波次并行)
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
**插件职责边界**(准确区分三层,不夸大插件能力):
|
|
50
|
+
- **原生 \`task()\`**:任务由 OpenCode 原生 task 执行;task 返回结果与生命周期通知来自宿主工具本身
|
|
51
|
+
- **插件观测**:TaskTracker 仅记录任务生命周期(pending→running→completed/failed)供 TUI 展示与统计,**不自动派发、不自动重试、不自动取消**
|
|
52
|
+
- **主代理显式处理**:依赖推进(仅按任务的实际前置结果推进——任务前置依赖已完成且无共享可变状态冲突即可派发,不等无关任务)、失败重派、取消后继任务,都由你(orchestrator)根据 task 返回结果显式执行
|
|
53
|
+
|
|
54
|
+
**你需自行显式处理**:
|
|
55
|
+
- 失败重派:先分析根因与重试预算,确认先前副作用(已写入文件/已启动后台任务)后再决定重派、降级或换代理;**后台已启动的任务不能重复启动**
|
|
56
|
+
- 取消后继:需要取消时显式执行取消,无自动级联取消
|
|
57
|
+
- **未知状态不能当作成功,无活动不能断言"仍在计算"**——以 task 返回结果与宿主通知为准,不臆测状态
|
|
55
58
|
|
|
56
59
|
**orchestrator 执行方法:按波次(Wave)执行**
|
|
57
60
|
|
|
58
61
|
planner 的方案已按 Wave 分组(或你自己审核时重新分组),执行时:
|
|
59
62
|
|
|
60
|
-
1. **从 Wave 1 开始** — 同一 Wave
|
|
61
|
-
2.
|
|
62
|
-
3.
|
|
63
|
-
4.
|
|
63
|
+
1. **从 Wave 1 开始** — 同一 Wave 内无依赖、无共享可变状态冲突的任务**在同一条消息中同时派发**(不按数量/容量预先分批)
|
|
64
|
+
2. **按实际前置结果推进** — 一个任务的**全部实际前置依赖已完成且无共享可变状态冲突,即可派发**,不必等本 Wave / 其他 Wave 的无关任务;同次所有已就绪任务同一条消息同时启动
|
|
65
|
+
3. **进入下一批已就绪任务** — 按同样的方式派发,重复直到所有任务完成
|
|
66
|
+
4. **Wave 只展示依赖层次,不构成整体等待屏障** — 若 Wave 1 的 B 已完成、C 仍慢,Wave 2 中只依赖 B 的任务可立即派发,不等 C
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
**关键:可并行任务必须同时启动,绝不逐个串行;等待仅因任务自身的真实前置依赖与共享状态冲突,不因 Wave 序号或默认批次人为制造等待。**
|
|
66
69
|
|
|
67
|
-
✅ 正确示例(
|
|
70
|
+
✅ 正确示例(3 个独立探索 + 2 个依赖修改):
|
|
68
71
|
\`\`\`
|
|
69
72
|
→ 同时发起:task(@explorer 搜索A) + task(@explorer 搜索B) + task(@librarian 查文档)
|
|
70
|
-
→
|
|
71
|
-
→ 同时发起:task(@fixer 修改A) + task(@fixer 修改B) // Wave 2
|
|
73
|
+
→ 搜索A、搜索B 已返回(查文档仍慢)→ 立即发起:task(@fixer 修改A) + task(@fixer 修改B) // 只等实际前置,不等无关的查文档
|
|
72
74
|
\`\`\`
|
|
73
75
|
|
|
74
76
|
❌ 错误示例(串行):
|
|
@@ -80,13 +82,14 @@ planner 的方案已按 Wave 分组(或你自己审核时重新分组),执
|
|
|
80
82
|
|
|
81
83
|
**⚠️ 执行前并行检查清单——每次准备派发 task 前,必须逐条确认(不可跳过):**
|
|
82
84
|
|
|
83
|
-
□
|
|
84
|
-
□
|
|
85
|
-
□ **识别同文件的任务**:涉及同一文件?→
|
|
85
|
+
□ **找出当前已就绪的任务**:从 planner 方案中找出**实际前置依赖已完成且无共享可变状态冲突**的任务(不等本 Wave / 其他 Wave 的无关任务)
|
|
86
|
+
□ **识别不同文件的任务**:涉及不同文件且无依赖?→ **同一条消息中同时派发**;独立任务不按数量/猜测容量预先限制,容量未知不能当串行理由
|
|
87
|
+
□ **识别同文件的任务**:涉及同一文件?→ **独立只读可并行**(多个 reader 同时读同文件无冲突);**同文件多写、或要求特定文件版本/读取结果的读写任务**按实际冲突与依赖串行(后一任务等前一任务完成后再派发,不把单文件任务拆给多个 writer),**其他任务不陪等**
|
|
88
|
+
□ **检查背压信号**:收到 429/TPM 限流等背压 → **暂停新增同类委派**,尊重宿主提供的重试时间(retry.next / Retry-After);不无限重派/轮询,不取消他人任务;插件未实现自动退避
|
|
86
89
|
□ **区分修改 Wave 与验证 Wave**:当前 Wave 是修改 Wave(只改文件不编译)?→ **委派 fixer 时必须注明"本轮只修改不验证,编译/测试统一在后续 Wave 执行"**。当前 Wave 是验证 Wave?→ 正常委派 fixer 执行编译/测试。
|
|
87
|
-
□ **确认派发方式**:以上确认完成后 →
|
|
90
|
+
□ **确认派发方式**:以上确认完成后 → **发起所有已就绪任务的 task 调用,同条消息同时启动,绝不逐个串行**
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
委派指令用中文,子代理 prompt 只传与当前子任务相关的事实/接口/文件,不重复全历史和无关依赖结果。后台启动使用 OpenCode 原生 \`task.background\`,不虚构自定义参数(如 run_in_background)。轻量任务走已有低成本路由,不私改 provider/variant 配置。
|
|
90
93
|
|
|
91
94
|
## 5. 验证(全部委派)
|
|
92
95
|
@co-fixer 编译测试 →(编译通过后)@co-oracle 代码审查 与 @co-designer UI审查 并行。发现问题重新委派。
|
|
@@ -102,9 +105,9 @@ planner 的方案已按 Wave 分组(或你自己审核时重新分组),执
|
|
|
102
105
|
<rule priority="1" name="先方案后执行">
|
|
103
106
|
### 规则 1:理解需求后必须先输出方案
|
|
104
107
|
|
|
105
|
-
**⚠️
|
|
108
|
+
**⚠️ 增量流程边界(避免重复全量分析):这是最容易被遗忘的规则。涉及代码/文件修改的需求,禁止未经方案直接执行;但分析按增量进行——同一任务/同一范围的已核实事实(代码位置、API 行为、规则结论)与已获批准的计划**可直接复用**,无需重新探索;仅当需求变化、代码基线变化或关键文件变化时,定向补充缺失信息,不重复全量规则分析。仍必须走:分析需求 →(必要时定向信息收集)→ 委派 @co-planner 制定方案 → 审核 → todowrite → 提供选项供用户选择 → 委派执行。**
|
|
106
109
|
|
|
107
|
-
收到需求后(涉及代码或文件修改时),**禁止立即执行**。必须先分析需求,委派 @co-planner 制定方案,orchestrator 审核后输出可验证的任务分解方案,**末尾提供选项(如"立即执行 / 修改方案"
|
|
110
|
+
收到需求后(涉及代码或文件修改时),**禁止立即执行**。必须先分析需求,委派 @co-planner 制定方案,orchestrator 审核后输出可验证的任务分解方案,**末尾提供选项(如"立即执行 / 修改方案")供用户决定**。单文件小改仍输出简化但可验证的计划并等待批准;用户明确批准后续波次后,后续 Wave 执行不再反复要求批准。方案包含:
|
|
108
111
|
(纯信息性问题可直接回答,无需方案。)
|
|
109
112
|
- 子任务列表及其依赖关系
|
|
110
113
|
- 每个子任务的委派对象(@co-explorer / @co-librarian / @co-fixer / @co-designer / @co-oracle / @co-observer)
|
|
@@ -127,16 +130,19 @@ planner 的方案已按 Wave 分组(或你自己审核时重新分组),执
|
|
|
127
130
|
</rule>
|
|
128
131
|
|
|
129
132
|
<rule priority="3" name="并行优先">
|
|
130
|
-
### 规则 3
|
|
131
|
-
|
|
133
|
+
### 规则 3:并行优先(真实依赖与共享状态为界)
|
|
134
|
+
分析任务依赖后,最大化并行收益——无依赖、无共享可变状态冲突的独立任务**同一条消息中同时派发**,不按数量/文件数量/猜测容量预先限制(容量未知不能当作串行理由);不确定是否独立时,先确认依赖与文件冲突,而不是盲目串行。
|
|
132
135
|
|
|
133
136
|
**并行决策框架**:
|
|
134
|
-
- 信息收集阶段:@co-explorer + @co-librarian + @co-observer
|
|
135
|
-
- 规则分析阶段:多个 @co-rule-app
|
|
136
|
-
- 执行阶段:修改不同文件的 @co-fixer
|
|
137
|
+
- 信息收集阶段:@co-explorer + @co-librarian + @co-observer 按需并行(定向补充,不重复全量)
|
|
138
|
+
- 规则分析阶段:多个 @co-rule-app 实例按需定向并行(只分析适用规则,已核实事实复用)
|
|
139
|
+
- 执行阶段:修改不同文件的 @co-fixer 任务可并行;同文件独立只读可并行;同文件多写或要求特定版本/读取结果的读写任务按实际冲突与依赖串行(其他任务不陪等)
|
|
137
140
|
- 验证阶段:编译通过后,@co-oracle 代码审查 与 @co-designer UI审查 可并行
|
|
141
|
+
- **背压信号**:收到 429/TPM 限流等背压 → 暂停新增同类委派,尊重宿主提供的重试时间;不无限重派/轮询,不取消他人任务
|
|
138
142
|
|
|
139
143
|
**⚠️ 并行退火警告**:长会话中,模型易陷入"一次只做一件事"的串行惯性。**每当你准备只发起一个 task 调用时,必须先自问:"还有没有其他可以同时完成的独立任务?"** 如果有——无论多小——必须立即找到并同时发起。单个 task 调用是最后手段,不是默认行为。
|
|
144
|
+
|
|
145
|
+
**收敛重复审查**:审查类委派先交付具体文件与验收标准,一次集中验证后按风险一次主审;仅当存在未解决问题或新增变更时复审,不对同一事实让多代理反复全量调查。
|
|
140
146
|
</rule>
|
|
141
147
|
|
|
142
148
|
</critical_rules>
|
|
@@ -149,7 +155,7 @@ planner 的方案已按 Wave 分组(或你自己审核时重新分组),执
|
|
|
149
155
|
→ 需要修改代码或文件 → **必须先输出方案 → 提供选项 → 等用户选择后才可委派执行**
|
|
150
156
|
|
|
151
157
|
□ **本轮需要同时发起多个独立操作吗?**
|
|
152
|
-
→ 有 2+
|
|
158
|
+
→ 有 2+ 个无依赖任务 → **同一条消息中同时派发,不得逐个串行**
|
|
153
159
|
→ 仅 1 个任务(确认无其他独立任务可并行) → 可以单个发起
|
|
154
160
|
|
|
155
161
|
</自检清单>
|
|
@@ -235,6 +241,7 @@ var ORACLE_PROMPT = `你是 Oracle——战略技术顾问和代码审查者。
|
|
|
235
241
|
- 简要解释推理
|
|
236
242
|
- 存在不确定性时承认
|
|
237
243
|
- 除非复杂度明确有收益,否则优先简单设计
|
|
244
|
+
- **收敛复审**:一次集中验证后按风险一次主审;仅当存在未解决问题或新增变更时复审,不对同一事实重复全量调查
|
|
238
245
|
|
|
239
246
|
**约束**:
|
|
240
247
|
- 只读:你提出建议,不实施
|
|
@@ -700,9 +707,9 @@ var PLANNER_PROMPT = `你是方案制定代理——负责任务分解和委派
|
|
|
700
707
|
|
|
701
708
|
| 波次 | 含义 | 执行方式 |
|
|
702
709
|
|------|------|---------|
|
|
703
|
-
| Wave 1 | 无前置依赖的任务 |
|
|
704
|
-
| Wave 2 | 依赖 Wave 1 的任务 |
|
|
705
|
-
| Wave N | 依赖 Wave N-1 的任务 |
|
|
710
|
+
| Wave 1 | 无前置依赖的任务 | **同一条消息同时派发**(真实无依赖、无共享可变状态冲突,不按数量/容量预先分批) |
|
|
711
|
+
| Wave 2 | 依赖 Wave 1 的任务 | **任务实际前置依赖完成后即派发**(不等本波无关任务;同次所有已就绪任务同一消息同时启动) |
|
|
712
|
+
| Wave N | 依赖 Wave N-1 的任务 | **任务实际前置依赖完成后即派发**(不等本波无关任务;同次所有已就绪任务同一消息同时启动) |
|
|
706
713
|
|
|
707
714
|
### 输出格式
|
|
708
715
|
|
|
@@ -712,7 +719,7 @@ var PLANNER_PROMPT = `你是方案制定代理——负责任务分解和委派
|
|
|
712
719
|
- 波次:M 个
|
|
713
720
|
- 涉及文件:文件列表
|
|
714
721
|
|
|
715
|
-
## Wave 1
|
|
722
|
+
## Wave 1(无依赖,同 Wave 并行)
|
|
716
723
|
- 任务 1.1:@explorer 搜索定位 xxx 文件中的关键代码
|
|
717
724
|
- 依赖:无
|
|
718
725
|
- 文件:xxx
|
|
@@ -750,11 +757,11 @@ var PLANNER_PROMPT = `你是方案制定代理——负责任务分解和委派
|
|
|
750
757
|
|
|
751
758
|
## 关键原则
|
|
752
759
|
|
|
753
|
-
1.
|
|
760
|
+
1. **同 Wave 并行** — 无依赖、无共享可变状态冲突的任务放同一 Wave,同一条消息同时派发;任务实际前置依赖完成即可派发(Wave 不构成整体等待屏障,不等无关任务);不按数量/文件数量/猜测容量预先分批(容量未知不能当串行理由),真实 429/TPM 背压按服务反馈处理
|
|
754
761
|
2. **不同文件的任务 → 同 Wave 并行** — 修改 a.ts 和 b.ts 无冲突,放同一 Wave
|
|
755
|
-
3.
|
|
756
|
-
4.
|
|
757
|
-
5. **验证任务放最后 Wave** —
|
|
762
|
+
3. **同文件冲突按实际粒度判定** — 同文件**独立只读可并行**(多个 reader 同文件无冲突);同文件**多写、或要求特定文件版本/读取结果的读写任务**按实际冲突与依赖串行(如先读后写必须等读完成),**其他任务不陪等**
|
|
763
|
+
4. **探索任务定向放 Wave 1** — 搜索/查文档无依赖,先并行跑;但只收集尚未核实、与需求直接相关的信息,同范围已核实信息直接复用,不重复全量扫描
|
|
764
|
+
5. **验证任务放最后 Wave** — 所有涉及修改完成后才编译/测试/审查(这是真实依赖:编译必须看到全部修改),但不等与验证无关的任务。修改 Wave 中的 fixer 任务仅做文件修改,不包含编译/测试步骤
|
|
758
765
|
|
|
759
766
|
**约束**:只读,不修改文件。方案要具体到文件和操作粒度,不可笼统。`;
|
|
760
767
|
|
|
@@ -824,6 +831,7 @@ var DEFAULT_CONTEXT_CONFIG = {
|
|
|
824
831
|
maxDependencies: 8,
|
|
825
832
|
dependencyPropagation: true,
|
|
826
833
|
summarizeMaxTokens: 2000,
|
|
834
|
+
summaryReadMode: "bounded",
|
|
827
835
|
relevantMessageWindow: 20,
|
|
828
836
|
maxErrorTotalChars: 600,
|
|
829
837
|
dependencyKeyOutputChars: 200
|
|
@@ -878,20 +886,29 @@ function computeStats(records, window = DEFAULT_STATS_WINDOW) {
|
|
|
878
886
|
|
|
879
887
|
class TaskTracker {
|
|
880
888
|
jobs = new Map;
|
|
889
|
+
callIndex = new Map;
|
|
881
890
|
counters = new Map;
|
|
882
891
|
_currentParentSessionId = "";
|
|
883
892
|
_reconciledForParent = "";
|
|
884
893
|
get currentParentSessionId() {
|
|
885
894
|
return this._currentParentSessionId;
|
|
886
895
|
}
|
|
896
|
+
callKey(parentSessionId, callID) {
|
|
897
|
+
return parentSessionId + "\x00" + callID;
|
|
898
|
+
}
|
|
887
899
|
alias(agentType) {
|
|
888
900
|
const short = agentType.slice(0, 4).replace(/[^a-z]/gi, "");
|
|
889
901
|
const n = (this.counters.get(agentType) ?? 0) + 1;
|
|
890
902
|
this.counters.set(agentType, n);
|
|
891
903
|
return `${short}-${n}`;
|
|
892
904
|
}
|
|
893
|
-
registerBeforeTask(parentSessionId, args) {
|
|
905
|
+
registerBeforeTask(parentSessionId, args, callID) {
|
|
894
906
|
this._currentParentSessionId = parentSessionId;
|
|
907
|
+
if (callID) {
|
|
908
|
+
const existing = this.getJobByCall(parentSessionId, callID);
|
|
909
|
+
if (existing)
|
|
910
|
+
return existing.alias;
|
|
911
|
+
}
|
|
895
912
|
if (this._reconciledForParent !== parentSessionId) {
|
|
896
913
|
this._reconciledForParent = parentSessionId;
|
|
897
914
|
for (const job of this.jobs.values()) {
|
|
@@ -903,7 +920,7 @@ class TaskTracker {
|
|
|
903
920
|
const agent = args.subagent_type ?? "unknown";
|
|
904
921
|
const alias = this.alias(agent);
|
|
905
922
|
const label = typeof args.description === "string" && args.description ? args.description : alias;
|
|
906
|
-
|
|
923
|
+
const job = {
|
|
907
924
|
alias,
|
|
908
925
|
sessionId: args.task_id ?? "",
|
|
909
926
|
parentSessionId,
|
|
@@ -914,40 +931,154 @@ class TaskTracker {
|
|
|
914
931
|
background: args.background ?? false,
|
|
915
932
|
terminalReconciled: false,
|
|
916
933
|
createdAt: Date.now()
|
|
917
|
-
}
|
|
934
|
+
};
|
|
935
|
+
if (callID)
|
|
936
|
+
job.callID = callID;
|
|
937
|
+
this.jobs.set(alias, job);
|
|
938
|
+
if (callID)
|
|
939
|
+
this.callIndex.set(this.callKey(parentSessionId, callID), alias);
|
|
918
940
|
return alias;
|
|
919
941
|
}
|
|
942
|
+
getJobByCall(parentSessionId, callID) {
|
|
943
|
+
const alias = this.callIndex.get(this.callKey(parentSessionId, callID));
|
|
944
|
+
return alias ? this.jobs.get(alias) : undefined;
|
|
945
|
+
}
|
|
946
|
+
updateAfterTaskByCall(parentSessionId, callID, status, sessionId) {
|
|
947
|
+
const job = this.getJobByCall(parentSessionId, callID);
|
|
948
|
+
if (!job)
|
|
949
|
+
return { ok: false, reason: "no-match" };
|
|
950
|
+
return this.applyCallUpdate(job, status, sessionId);
|
|
951
|
+
}
|
|
952
|
+
setBackgroundByCall(parentSessionId, callID, bg) {
|
|
953
|
+
const job = this.getJobByCall(parentSessionId, callID);
|
|
954
|
+
if (!job)
|
|
955
|
+
return { ok: false, reason: "no-match" };
|
|
956
|
+
if (job.status !== "running") {
|
|
957
|
+
return { ok: true, reason: "idempotent", alias: job.alias };
|
|
958
|
+
}
|
|
959
|
+
job.background = bg;
|
|
960
|
+
return { ok: true, reason: "ok", alias: job.alias };
|
|
961
|
+
}
|
|
962
|
+
getJobByAlias(alias) {
|
|
963
|
+
return this.jobs.get(alias);
|
|
964
|
+
}
|
|
965
|
+
updateByAlias(alias, status, sessionId) {
|
|
966
|
+
const job = this.jobs.get(alias);
|
|
967
|
+
if (!job)
|
|
968
|
+
return { ok: false, reason: "no-match" };
|
|
969
|
+
return this.applyCallUpdate(job, status, sessionId);
|
|
970
|
+
}
|
|
971
|
+
applyCallUpdate(job, status, sessionId) {
|
|
972
|
+
if (!job.background) {
|
|
973
|
+
if (job.status !== "running") {
|
|
974
|
+
return { ok: true, reason: "idempotent", alias: job.alias };
|
|
975
|
+
}
|
|
976
|
+
job.status = status;
|
|
977
|
+
job.terminalReconciled = true;
|
|
978
|
+
if (sessionId)
|
|
979
|
+
job.sessionId = sessionId;
|
|
980
|
+
this.finalize(job);
|
|
981
|
+
this.persistStats();
|
|
982
|
+
return { ok: true, reason: "ok", alias: job.alias };
|
|
983
|
+
}
|
|
984
|
+
if (sessionId)
|
|
985
|
+
job.sessionId = sessionId;
|
|
986
|
+
return { ok: true, reason: "ok", alias: job.alias };
|
|
987
|
+
}
|
|
920
988
|
updateAfterTask(parentSessionId, status, sessionId) {
|
|
921
989
|
this._currentParentSessionId = parentSessionId;
|
|
922
|
-
|
|
990
|
+
const running = [];
|
|
923
991
|
for (const job of this.jobs.values()) {
|
|
924
|
-
if (job.parentSessionId === parentSessionId && job.status === "running"
|
|
925
|
-
|
|
992
|
+
if (job.parentSessionId === parentSessionId && job.status === "running") {
|
|
993
|
+
running.push(job);
|
|
926
994
|
}
|
|
927
995
|
}
|
|
928
|
-
if (
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
996
|
+
if (running.length === 0)
|
|
997
|
+
return { ok: false, reason: "no-match" };
|
|
998
|
+
if (running.length > 1)
|
|
999
|
+
return { ok: false, reason: "ambiguous" };
|
|
1000
|
+
return this.applyCallUpdate(running[0], status, sessionId);
|
|
1001
|
+
}
|
|
1002
|
+
updateByChildSessionId(sessionId, status) {
|
|
1003
|
+
let allWithSession = 0;
|
|
1004
|
+
let runningBg;
|
|
1005
|
+
for (const job of this.jobs.values()) {
|
|
1006
|
+
if (job.sessionId === sessionId) {
|
|
1007
|
+
allWithSession++;
|
|
1008
|
+
if (job.background && job.status === "running")
|
|
1009
|
+
runningBg = job;
|
|
933
1010
|
}
|
|
934
|
-
latest.status = status;
|
|
935
|
-
latest.terminalReconciled = true;
|
|
936
|
-
if (sessionId)
|
|
937
|
-
latest.sessionId = sessionId;
|
|
938
|
-
this.finalize(latest);
|
|
939
|
-
this.persistStats();
|
|
940
1011
|
}
|
|
1012
|
+
if (allWithSession === 0)
|
|
1013
|
+
return { ok: false, reason: "no-match" };
|
|
1014
|
+
if (allWithSession > 1)
|
|
1015
|
+
return { ok: false, reason: "ambiguous" };
|
|
1016
|
+
if (!runningBg)
|
|
1017
|
+
return { ok: false, reason: "no-match" };
|
|
1018
|
+
runningBg.status = status;
|
|
1019
|
+
this.finalize(runningBg);
|
|
1020
|
+
this.persistStats();
|
|
1021
|
+
return { ok: true, reason: "ok", alias: runningBg.alias };
|
|
941
1022
|
}
|
|
942
|
-
|
|
1023
|
+
noteChildSessionIdle(sessionId) {
|
|
1024
|
+
let allWithSession = 0;
|
|
1025
|
+
let runningBg;
|
|
943
1026
|
for (const job of this.jobs.values()) {
|
|
944
|
-
if (job.sessionId === sessionId
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1027
|
+
if (job.sessionId === sessionId) {
|
|
1028
|
+
allWithSession++;
|
|
1029
|
+
if (job.background && job.status === "running")
|
|
1030
|
+
runningBg = job;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (allWithSession === 0)
|
|
1034
|
+
return { ok: false, reason: "no-match" };
|
|
1035
|
+
if (allWithSession > 1)
|
|
1036
|
+
return { ok: false, reason: "ambiguous" };
|
|
1037
|
+
if (!runningBg)
|
|
1038
|
+
return { ok: false, reason: "no-match" };
|
|
1039
|
+
return { ok: true, reason: "observed", alias: runningBg.alias };
|
|
1040
|
+
}
|
|
1041
|
+
findActiveByChildSessionId(sessionId) {
|
|
1042
|
+
let active;
|
|
1043
|
+
for (const job of this.jobs.values()) {
|
|
1044
|
+
if (job.sessionId === sessionId && job.status === "running") {
|
|
1045
|
+
if (active)
|
|
1046
|
+
return { ok: false, reason: "ambiguous" };
|
|
1047
|
+
active = job;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return active ? { ok: true, job: active } : { ok: false, reason: "no-match" };
|
|
1051
|
+
}
|
|
1052
|
+
finalizeByCall(parentSessionId, callID, expectedChildId, status) {
|
|
1053
|
+
const job = this.getJobByCall(parentSessionId, callID);
|
|
1054
|
+
if (!job)
|
|
1055
|
+
return { ok: false, reason: "no-match" };
|
|
1056
|
+
if (job.status !== "running") {
|
|
1057
|
+
return { ok: true, reason: "idempotent", alias: job.alias };
|
|
1058
|
+
}
|
|
1059
|
+
if (status === "running") {
|
|
1060
|
+
return { ok: false, reason: "no-match" };
|
|
1061
|
+
}
|
|
1062
|
+
if (expectedChildId !== undefined) {
|
|
1063
|
+
if (job.sessionId !== "" && job.sessionId !== expectedChildId) {
|
|
1064
|
+
return { ok: false, reason: "no-match" };
|
|
1065
|
+
}
|
|
1066
|
+
if (job.sessionId === "")
|
|
1067
|
+
job.sessionId = expectedChildId;
|
|
1068
|
+
let runningShare = 0;
|
|
1069
|
+
for (const j of this.jobs.values()) {
|
|
1070
|
+
if (j.sessionId === expectedChildId && j.status === "running")
|
|
1071
|
+
runningShare++;
|
|
949
1072
|
}
|
|
1073
|
+
if (runningShare === 0)
|
|
1074
|
+
return { ok: false, reason: "no-match" };
|
|
1075
|
+
if (runningShare > 1)
|
|
1076
|
+
return { ok: false, reason: "ambiguous" };
|
|
950
1077
|
}
|
|
1078
|
+
job.status = status;
|
|
1079
|
+
this.finalize(job);
|
|
1080
|
+
this.persistStats();
|
|
1081
|
+
return { ok: true, reason: "ok", alias: job.alias };
|
|
951
1082
|
}
|
|
952
1083
|
getBoardText(parentSessionId) {
|
|
953
1084
|
if (typeof parentSessionId !== "string" || parentSessionId === "")
|
|
@@ -1054,6 +1185,10 @@ class TaskTracker {
|
|
|
1054
1185
|
}
|
|
1055
1186
|
}
|
|
1056
1187
|
for (const alias of toDelete) {
|
|
1188
|
+
const job = this.jobs.get(alias);
|
|
1189
|
+
if (job?.callID) {
|
|
1190
|
+
this.callIndex.delete(this.callKey(job.parentSessionId, job.callID));
|
|
1191
|
+
}
|
|
1057
1192
|
this.jobs.delete(alias);
|
|
1058
1193
|
}
|
|
1059
1194
|
return toDelete.length;
|
|
@@ -1089,14 +1224,28 @@ class TaskTracker {
|
|
|
1089
1224
|
return;
|
|
1090
1225
|
}
|
|
1091
1226
|
updateQuality(sessionId, quality) {
|
|
1227
|
+
let hit;
|
|
1092
1228
|
for (const job of this.jobs.values()) {
|
|
1093
1229
|
if (job.sessionId === sessionId) {
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
return;
|
|
1230
|
+
if (hit)
|
|
1231
|
+
return;
|
|
1232
|
+
hit = job;
|
|
1098
1233
|
}
|
|
1099
1234
|
}
|
|
1235
|
+
if (hit) {
|
|
1236
|
+
hit.quality = quality;
|
|
1237
|
+
this.finalize(hit);
|
|
1238
|
+
this.persistStats();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
updateQualityByAlias(alias, quality) {
|
|
1242
|
+
const job = this.jobs.get(alias);
|
|
1243
|
+
if (!job)
|
|
1244
|
+
return { ok: false, reason: "no-match" };
|
|
1245
|
+
job.quality = quality;
|
|
1246
|
+
this.finalize(job);
|
|
1247
|
+
this.persistStats();
|
|
1248
|
+
return { ok: true, reason: "ok", alias };
|
|
1100
1249
|
}
|
|
1101
1250
|
finalize(job) {
|
|
1102
1251
|
if (job.status !== "running") {
|
|
@@ -1145,18 +1294,60 @@ class TaskTracker {
|
|
|
1145
1294
|
this.persistStats();
|
|
1146
1295
|
}
|
|
1147
1296
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
if (
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1297
|
+
resolveCancelTarget(taskId) {
|
|
1298
|
+
const byAlias = this.jobs.get(taskId);
|
|
1299
|
+
if (byAlias)
|
|
1300
|
+
return { ok: true, job: byAlias };
|
|
1301
|
+
let active;
|
|
1302
|
+
for (const job of this.jobs.values()) {
|
|
1303
|
+
if (job.sessionId === taskId && job.status === "running") {
|
|
1304
|
+
if (active)
|
|
1305
|
+
return { ok: false, reason: "ambiguous" };
|
|
1306
|
+
active = job;
|
|
1156
1307
|
}
|
|
1157
1308
|
}
|
|
1309
|
+
return active ? { ok: true, job: active } : { ok: false, reason: "no-match" };
|
|
1310
|
+
}
|
|
1311
|
+
markCancelledByAlias(alias) {
|
|
1312
|
+
const job = this.jobs.get(alias);
|
|
1158
1313
|
if (!job)
|
|
1159
|
-
return;
|
|
1314
|
+
return { ok: false, reason: "no-match" };
|
|
1315
|
+
if (job.status !== "running") {
|
|
1316
|
+
return { ok: true, reason: "idempotent", alias };
|
|
1317
|
+
}
|
|
1318
|
+
job.status = "cancelled";
|
|
1319
|
+
job.terminalReconciled = true;
|
|
1320
|
+
this.finalize(job);
|
|
1321
|
+
this.persistStats();
|
|
1322
|
+
return { ok: true, reason: "ok", alias };
|
|
1323
|
+
}
|
|
1324
|
+
abortJob(taskId) {
|
|
1325
|
+
const byAlias = this.jobs.get(taskId);
|
|
1326
|
+
if (byAlias)
|
|
1327
|
+
return this.abortFound(byAlias);
|
|
1328
|
+
let active;
|
|
1329
|
+
let firstHistory;
|
|
1330
|
+
let historyCount = 0;
|
|
1331
|
+
for (const j of this.jobs.values()) {
|
|
1332
|
+
if (j.sessionId !== taskId)
|
|
1333
|
+
continue;
|
|
1334
|
+
if (j.status === "running") {
|
|
1335
|
+
if (active)
|
|
1336
|
+
return;
|
|
1337
|
+
active = j;
|
|
1338
|
+
} else {
|
|
1339
|
+
if (!firstHistory)
|
|
1340
|
+
firstHistory = j;
|
|
1341
|
+
historyCount++;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (active)
|
|
1345
|
+
return this.abortFound(active);
|
|
1346
|
+
if (historyCount === 1)
|
|
1347
|
+
return this.abortFound(firstHistory);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
abortFound(job) {
|
|
1160
1351
|
if (job.status !== "running") {
|
|
1161
1352
|
return { sessionId: job.sessionId || undefined, job };
|
|
1162
1353
|
}
|
|
@@ -1603,7 +1794,44 @@ function replaceMarkerWithContext(messageText, context) {
|
|
|
1603
1794
|
return messageText.replace(CONTEXT_MARKER_PATTERN, formatted);
|
|
1604
1795
|
}
|
|
1605
1796
|
|
|
1797
|
+
// src/context/prompt-budget.ts
|
|
1798
|
+
var SUMMARY_READ_MAX_BYTES_PER_FILE = 1024 * 1024;
|
|
1799
|
+
var SUMMARY_READ_LOOKAHEAD_BYTES = 4;
|
|
1800
|
+
function tokenBudgetToByteLimit(budgetTokens) {
|
|
1801
|
+
const n = Number(budgetTokens);
|
|
1802
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
1803
|
+
return 4;
|
|
1804
|
+
const raw = Math.ceil(n * 4 * 2);
|
|
1805
|
+
return Math.min(SUMMARY_READ_MAX_BYTES_PER_FILE, Math.max(4, raw));
|
|
1806
|
+
}
|
|
1807
|
+
function utf8TrimIncomplete(buf) {
|
|
1808
|
+
if (buf.length === 0)
|
|
1809
|
+
return buf;
|
|
1810
|
+
const last = buf[buf.length - 1];
|
|
1811
|
+
if (last < 128)
|
|
1812
|
+
return buf;
|
|
1813
|
+
let i = buf.length - 1;
|
|
1814
|
+
while (i > 0 && (buf[i] & 192) === 128)
|
|
1815
|
+
i--;
|
|
1816
|
+
const head = buf[i];
|
|
1817
|
+
let seqLen = 0;
|
|
1818
|
+
if ((head & 224) === 192)
|
|
1819
|
+
seqLen = 2;
|
|
1820
|
+
else if ((head & 240) === 224)
|
|
1821
|
+
seqLen = 3;
|
|
1822
|
+
else if ((head & 248) === 240)
|
|
1823
|
+
seqLen = 4;
|
|
1824
|
+
else
|
|
1825
|
+
return buf;
|
|
1826
|
+
if (buf.length - i < seqLen)
|
|
1827
|
+
return buf.subarray(0, i);
|
|
1828
|
+
return buf;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1606
1831
|
// src/context/engine.ts
|
|
1832
|
+
var READ_LIMIT_TRUNCATION_SUFFIX = `
|
|
1833
|
+
… [正文已按读取上限截断]`;
|
|
1834
|
+
|
|
1607
1835
|
class ContextEngine {
|
|
1608
1836
|
registry = new Map;
|
|
1609
1837
|
registryTouchedAt = new Map;
|
|
@@ -1682,11 +1910,39 @@ class ContextEngine {
|
|
|
1682
1910
|
const budgetPerFile = Math.max(1, Math.floor(this.config.summarizeMaxTokens / files.length));
|
|
1683
1911
|
for (const file of files) {
|
|
1684
1912
|
try {
|
|
1685
|
-
|
|
1686
|
-
|
|
1913
|
+
if (this.config.summaryReadMode === "full") {
|
|
1914
|
+
const content = fs2.readFileSync(file.path, "utf-8");
|
|
1915
|
+
file.body = truncateByTokens(content, budgetPerFile);
|
|
1916
|
+
} else {
|
|
1917
|
+
const { text, truncatedByReadLimit } = this.readBoundedUtf8(file.path, tokenBudgetToByteLimit(budgetPerFile));
|
|
1918
|
+
file.body = truncateByTokens(text, budgetPerFile);
|
|
1919
|
+
if (truncatedByReadLimit && estimateTokens(file.body) <= budgetPerFile) {
|
|
1920
|
+
file.body += READ_LIMIT_TRUNCATION_SUFFIX;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1687
1923
|
} catch {}
|
|
1688
1924
|
}
|
|
1689
1925
|
}
|
|
1926
|
+
readBoundedUtf8(filePath, byteLimit) {
|
|
1927
|
+
const fd = fs2.openSync(filePath, "r");
|
|
1928
|
+
try {
|
|
1929
|
+
const { size } = fs2.fstatSync(fd);
|
|
1930
|
+
const toRead = Math.min(size, byteLimit + SUMMARY_READ_LOOKAHEAD_BYTES);
|
|
1931
|
+
const buf = Buffer.allocUnsafe(Math.max(0, toRead));
|
|
1932
|
+
let filled = 0;
|
|
1933
|
+
while (filled < toRead) {
|
|
1934
|
+
const n = fs2.readSync(fd, buf, filled, toRead - filled, filled);
|
|
1935
|
+
if (n <= 0)
|
|
1936
|
+
break;
|
|
1937
|
+
filled += n;
|
|
1938
|
+
}
|
|
1939
|
+
const truncatedByReadLimit = filled > byteLimit;
|
|
1940
|
+
const raw = buf.subarray(0, Math.min(filled, byteLimit));
|
|
1941
|
+
return { text: utf8TrimIncomplete(raw).toString("utf-8"), truncatedByReadLimit };
|
|
1942
|
+
} finally {
|
|
1943
|
+
fs2.closeSync(fd);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1690
1946
|
consumeMarkedContext(messageText) {
|
|
1691
1947
|
const markerMatch = messageText.match(CONTEXT_MARKER_PATTERN);
|
|
1692
1948
|
if (!markerMatch)
|
|
@@ -1973,6 +2229,47 @@ function appendCtxDiag(record, filePath) {
|
|
|
1973
2229
|
} catch {}
|
|
1974
2230
|
});
|
|
1975
2231
|
}
|
|
2232
|
+
var TASK_DIAG_STRING_MAX = 64;
|
|
2233
|
+
var TASK_DIAG_STRING_KEYS = ["callID", "session", "alias", "event", "status"];
|
|
2234
|
+
var TASK_DIAG_NUMBER_KEYS = [
|
|
2235
|
+
"durationMs",
|
|
2236
|
+
"lastActivityAt",
|
|
2237
|
+
"retryAttempt",
|
|
2238
|
+
"retryNextMs",
|
|
2239
|
+
"retryAfterMs",
|
|
2240
|
+
"budgetChars",
|
|
2241
|
+
"budgetTokens"
|
|
2242
|
+
];
|
|
2243
|
+
function sanitizeTaskDiagRecord(input) {
|
|
2244
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
2245
|
+
return null;
|
|
2246
|
+
const src = input;
|
|
2247
|
+
const out = { t: "task" };
|
|
2248
|
+
for (const key of TASK_DIAG_STRING_KEYS) {
|
|
2249
|
+
const v = src[key];
|
|
2250
|
+
if (typeof v === "string") {
|
|
2251
|
+
const trimmed = v.trim();
|
|
2252
|
+
if (trimmed !== "")
|
|
2253
|
+
out[key] = trimmed.slice(0, TASK_DIAG_STRING_MAX);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
for (const key of TASK_DIAG_NUMBER_KEYS) {
|
|
2257
|
+
const v = src[key];
|
|
2258
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
2259
|
+
out[key] = v;
|
|
2260
|
+
}
|
|
2261
|
+
const kind = src["rateLimitKind"];
|
|
2262
|
+
if (kind === "rate_limit" || kind === "retryable_network" || kind === "other") {
|
|
2263
|
+
out.rateLimitKind = kind;
|
|
2264
|
+
}
|
|
2265
|
+
return out;
|
|
2266
|
+
}
|
|
2267
|
+
function appendTaskDiag(record, filePath) {
|
|
2268
|
+
const safe = sanitizeTaskDiagRecord(record);
|
|
2269
|
+
if (!safe)
|
|
2270
|
+
return;
|
|
2271
|
+
appendCtxDiag(safe, filePath);
|
|
2272
|
+
}
|
|
1976
2273
|
|
|
1977
2274
|
// src/tools/council.ts
|
|
1978
2275
|
import { tool } from "@opencode-ai/plugin";
|
|
@@ -2073,6 +2370,119 @@ function createLogger(cfg) {
|
|
|
2073
2370
|
return appendLogSync;
|
|
2074
2371
|
}
|
|
2075
2372
|
|
|
2373
|
+
// src/orchestration/rate-limit.ts
|
|
2374
|
+
var RATE_LIMIT_TEXT = ["tokens per minute", "rate limit", "quota"];
|
|
2375
|
+
var NETWORK_TEXT = ["econnreset", "socket hang up", "timeout", "etimedout"];
|
|
2376
|
+
function isPlainObject(v) {
|
|
2377
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2378
|
+
}
|
|
2379
|
+
function normalizeError(error) {
|
|
2380
|
+
if (!isPlainObject(error))
|
|
2381
|
+
return null;
|
|
2382
|
+
const obj = error;
|
|
2383
|
+
const data = isPlainObject(obj["data"]) ? obj["data"] : undefined;
|
|
2384
|
+
const pick = (key) => {
|
|
2385
|
+
if (obj[key] !== undefined)
|
|
2386
|
+
return obj[key];
|
|
2387
|
+
if (data && data[key] !== undefined)
|
|
2388
|
+
return data[key];
|
|
2389
|
+
return;
|
|
2390
|
+
};
|
|
2391
|
+
const statusCode = pick("statusCode");
|
|
2392
|
+
const isRetryable = pick("isRetryable");
|
|
2393
|
+
const headers = pick("responseHeaders");
|
|
2394
|
+
const body = pick("responseBody");
|
|
2395
|
+
const message = pick("message");
|
|
2396
|
+
const cause = pick("cause");
|
|
2397
|
+
const headersOut = {};
|
|
2398
|
+
if (isPlainObject(headers)) {
|
|
2399
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
2400
|
+
if (typeof v === "string")
|
|
2401
|
+
headersOut[k] = v;
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
const causeCode = isPlainObject(cause) && typeof cause["code"] === "string" ? cause["code"] : undefined;
|
|
2405
|
+
return {
|
|
2406
|
+
statusCode: typeof statusCode === "number" && Number.isFinite(statusCode) ? statusCode : undefined,
|
|
2407
|
+
isRetryable: typeof isRetryable === "boolean" ? isRetryable : false,
|
|
2408
|
+
responseHeaders: headersOut,
|
|
2409
|
+
responseBody: typeof body === "string" ? body : undefined,
|
|
2410
|
+
message: typeof message === "string" ? message : undefined,
|
|
2411
|
+
causeCode
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2414
|
+
function findRetryAfter(headers) {
|
|
2415
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
2416
|
+
if (k.toLowerCase() === "retry-after" && v.trim() !== "")
|
|
2417
|
+
return v.trim();
|
|
2418
|
+
}
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
function parseRetryAfter(headers, nowMs) {
|
|
2422
|
+
const raw = findRetryAfter(headers);
|
|
2423
|
+
if (raw === undefined)
|
|
2424
|
+
return { retryAfterSource: "none" };
|
|
2425
|
+
if (/^\d+$/.test(raw)) {
|
|
2426
|
+
const secs = Number(raw);
|
|
2427
|
+
if (secs > 0)
|
|
2428
|
+
return { retryAfterMs: secs * 1000, retryAfterSource: "header-seconds" };
|
|
2429
|
+
return { retryAfterSource: "none" };
|
|
2430
|
+
}
|
|
2431
|
+
const dateMs = Date.parse(raw);
|
|
2432
|
+
if (Number.isNaN(dateMs))
|
|
2433
|
+
return { retryAfterSource: "none" };
|
|
2434
|
+
const waitMs = dateMs - nowMs;
|
|
2435
|
+
if (waitMs > 0)
|
|
2436
|
+
return { retryAfterMs: waitMs, retryAfterSource: "header-date" };
|
|
2437
|
+
return { retryAfterSource: "none" };
|
|
2438
|
+
}
|
|
2439
|
+
function classifyRateLimit(error, now) {
|
|
2440
|
+
const norm = normalizeError(error);
|
|
2441
|
+
if (!norm) {
|
|
2442
|
+
return {
|
|
2443
|
+
kind: "other",
|
|
2444
|
+
retryAfterSource: "none",
|
|
2445
|
+
isRetryable: false,
|
|
2446
|
+
maskedMessage: "non-object-input"
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
const nowMs = typeof now === "number" && Number.isFinite(now) ? now : Date.now();
|
|
2450
|
+
if (norm.statusCode === 429) {
|
|
2451
|
+
const ra = parseRetryAfter(norm.responseHeaders, nowMs);
|
|
2452
|
+
return {
|
|
2453
|
+
kind: "rate_limit",
|
|
2454
|
+
...ra,
|
|
2455
|
+
isRetryable: norm.isRetryable,
|
|
2456
|
+
maskedMessage: "rate-limit-classified (http-429)"
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
const text = [norm.message, norm.responseBody].filter((s) => typeof s === "string" && s !== "").join(`
|
|
2460
|
+
`).toLowerCase();
|
|
2461
|
+
if (RATE_LIMIT_TEXT.some((t) => text.includes(t))) {
|
|
2462
|
+
return {
|
|
2463
|
+
kind: "rate_limit",
|
|
2464
|
+
retryAfterSource: "none",
|
|
2465
|
+
isRetryable: norm.isRetryable,
|
|
2466
|
+
maskedMessage: "rate-limit-classified (text-signal)"
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
const causeText = norm.causeCode?.toLowerCase() ?? "";
|
|
2470
|
+
if (NETWORK_TEXT.some((t) => text.includes(t)) || NETWORK_TEXT.some((t) => causeText.includes(t))) {
|
|
2471
|
+
return {
|
|
2472
|
+
kind: "retryable_network",
|
|
2473
|
+
retryAfterSource: "none",
|
|
2474
|
+
isRetryable: norm.isRetryable,
|
|
2475
|
+
maskedMessage: "retryable-network-error (socket/timeout)"
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
return {
|
|
2479
|
+
kind: "other",
|
|
2480
|
+
retryAfterSource: "none",
|
|
2481
|
+
isRetryable: norm.isRetryable,
|
|
2482
|
+
maskedMessage: "unclassified-error"
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2076
2486
|
// src/utils/log.ts
|
|
2077
2487
|
var LOG_DIR = path6.join(os4.homedir(), ".local", "share", "opencode", "log");
|
|
2078
2488
|
var appendLog = createLogger({
|
|
@@ -2083,6 +2493,14 @@ var appendLog = createLogger({
|
|
|
2083
2493
|
keepDays: 7,
|
|
2084
2494
|
async: true
|
|
2085
2495
|
});
|
|
2496
|
+
function appendSafeLog(tag, message, err) {
|
|
2497
|
+
if (err === undefined) {
|
|
2498
|
+
appendLog(tag, message);
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
const { maskedMessage } = classifyRateLimit(err);
|
|
2502
|
+
appendLog(tag, `${message} — ${maskedMessage}`);
|
|
2503
|
+
}
|
|
2086
2504
|
|
|
2087
2505
|
// src/tools/model-stats.ts
|
|
2088
2506
|
var DEFAULT_MODEL_STATS_CONFIG = {
|
|
@@ -2907,7 +3325,7 @@ function createCouncilTool(ctx, councilManager) {
|
|
|
2907
3325
|
// src/tools/job-control.ts
|
|
2908
3326
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
2909
3327
|
var z2 = tool2.schema;
|
|
2910
|
-
function createCloseJobTool(ctx, tracker, onClosed) {
|
|
3328
|
+
function createCloseJobTool(ctx, tracker, onClosed, onCancelled) {
|
|
2911
3329
|
const close_job = tool2({
|
|
2912
3330
|
description: "Close (abort) a stuck or unnecessary subagent background task. " + "Accepts the child Session ID (e.g. ses_xxx, shown in the Background Job Board) or the task alias (e.g. coe-1). " + "The underlying subagent session is aborted via session.abort() and the task is marked cancelled. " + "Only the co-orchestrator (primary) agent may invoke this tool.",
|
|
2913
3331
|
args: {
|
|
@@ -2917,28 +3335,68 @@ function createCloseJobTool(ctx, tracker, onClosed) {
|
|
|
2917
3335
|
if (toolContext.agent !== "co-orchestrator") {
|
|
2918
3336
|
throw new Error(`[oh-my-opencode-cohub] close_job can only be invoked by co-orchestrator. Current agent: ${toolContext.agent}`);
|
|
2919
3337
|
}
|
|
2920
|
-
const
|
|
2921
|
-
if (!
|
|
2922
|
-
return JSON.stringify({ ok: false, task_id: args.task_id, reason:
|
|
3338
|
+
const target = tracker.resolveCancelTarget(args.task_id);
|
|
3339
|
+
if (!target.ok) {
|
|
3340
|
+
return JSON.stringify({ ok: false, task_id: args.task_id, reason: target.reason, session_aborted: false }, null, 2);
|
|
3341
|
+
}
|
|
3342
|
+
const job = target.job;
|
|
3343
|
+
const alias = job.alias;
|
|
3344
|
+
if (job.status !== "running") {
|
|
3345
|
+
return JSON.stringify({
|
|
3346
|
+
ok: true,
|
|
3347
|
+
task_id: args.task_id,
|
|
3348
|
+
alias,
|
|
3349
|
+
agent: job.agent,
|
|
3350
|
+
session_aborted: false,
|
|
3351
|
+
status: job.status,
|
|
3352
|
+
reason: "idempotent"
|
|
3353
|
+
}, null, 2);
|
|
2923
3354
|
}
|
|
2924
3355
|
let aborted = false;
|
|
2925
|
-
if (
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
3356
|
+
if (job.sessionId) {
|
|
3357
|
+
try {
|
|
3358
|
+
const res = await ctx.client.session.abort({ path: { id: job.sessionId } });
|
|
3359
|
+
aborted = !res.error;
|
|
3360
|
+
if (res.error) {
|
|
3361
|
+
appendLog("job-control", `abort session ${job.sessionId} 失败`, res.error);
|
|
3362
|
+
}
|
|
3363
|
+
} catch (err) {
|
|
3364
|
+
appendLog("job-control", `abort session ${job.sessionId} 抛出异常`, err);
|
|
2929
3365
|
aborted = false;
|
|
2930
|
-
}
|
|
2931
|
-
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
if (job.sessionId && !aborted) {
|
|
3369
|
+
return JSON.stringify({
|
|
3370
|
+
ok: false,
|
|
3371
|
+
task_id: args.task_id,
|
|
3372
|
+
alias,
|
|
3373
|
+
agent: job.agent,
|
|
3374
|
+
session_aborted: false,
|
|
3375
|
+
status: job.status,
|
|
3376
|
+
reason: "abort_failed"
|
|
3377
|
+
}, null, 2);
|
|
3378
|
+
}
|
|
3379
|
+
const up = tracker.markCancelledByAlias(alias);
|
|
3380
|
+
if (!up.ok) {
|
|
3381
|
+
return JSON.stringify({ ok: false, task_id: args.task_id, alias, session_aborted: aborted, status: job.status, reason: "stale" }, null, 2);
|
|
3382
|
+
}
|
|
3383
|
+
if (up.reason === "ok") {
|
|
3384
|
+
try {
|
|
3385
|
+
onCancelled?.(alias);
|
|
3386
|
+
} catch (err) {
|
|
3387
|
+
appendLog("job-control", `onCancelled 回调异常(不影响取消结果)`, err);
|
|
2932
3388
|
}
|
|
2933
3389
|
}
|
|
2934
3390
|
onClosed?.();
|
|
3391
|
+
const actualStatus = up.reason === "ok" ? "cancelled" : tracker.getJobByAlias(alias)?.status ?? job.status;
|
|
2935
3392
|
return JSON.stringify({
|
|
2936
|
-
ok:
|
|
3393
|
+
ok: up.reason === "ok" || up.reason === "idempotent",
|
|
2937
3394
|
task_id: args.task_id,
|
|
2938
|
-
alias
|
|
2939
|
-
agent:
|
|
3395
|
+
alias,
|
|
3396
|
+
agent: job.agent,
|
|
2940
3397
|
session_aborted: aborted,
|
|
2941
|
-
status:
|
|
3398
|
+
status: actualStatus,
|
|
3399
|
+
reason: up.reason
|
|
2942
3400
|
}, null, 2);
|
|
2943
3401
|
}
|
|
2944
3402
|
});
|
|
@@ -3047,10 +3505,27 @@ class OrchestrationEngine {
|
|
|
3047
3505
|
const task = this.dag.tasks.get(alias);
|
|
3048
3506
|
if (!task)
|
|
3049
3507
|
throw new Error(`Task ${alias} not found`);
|
|
3508
|
+
if (task.state === to)
|
|
3509
|
+
return;
|
|
3050
3510
|
const allowed = VALID_TRANSITIONS[task.state];
|
|
3051
3511
|
if (!allowed.includes(to)) {
|
|
3052
3512
|
throw new Error(`Invalid transition: ${task.state} → ${to} for ${alias}`);
|
|
3053
3513
|
}
|
|
3514
|
+
this.setState(task, to);
|
|
3515
|
+
}
|
|
3516
|
+
applyLifecycleEvent(alias, to) {
|
|
3517
|
+
const task = this.dag.tasks.get(alias);
|
|
3518
|
+
if (!task)
|
|
3519
|
+
return { ok: false, reason: "unknown-task" };
|
|
3520
|
+
if (task.state === to)
|
|
3521
|
+
return { ok: true, reason: "idempotent" };
|
|
3522
|
+
const allowed = VALID_TRANSITIONS[task.state];
|
|
3523
|
+
if (!allowed.includes(to))
|
|
3524
|
+
return { ok: false, reason: "invalid-transition" };
|
|
3525
|
+
this.setState(task, to);
|
|
3526
|
+
return { ok: true, reason: "applied" };
|
|
3527
|
+
}
|
|
3528
|
+
setState(task, to) {
|
|
3054
3529
|
task.state = to;
|
|
3055
3530
|
if (to === "completed") {
|
|
3056
3531
|
for (const depAlias of task.dependents) {
|
|
@@ -3058,7 +3533,7 @@ class OrchestrationEngine {
|
|
|
3058
3533
|
}
|
|
3059
3534
|
}
|
|
3060
3535
|
if (to === "cancelled") {
|
|
3061
|
-
this.cascadeCancel(alias);
|
|
3536
|
+
this.cascadeCancel(task.alias);
|
|
3062
3537
|
}
|
|
3063
3538
|
}
|
|
3064
3539
|
checkDependencies(alias) {
|
|
@@ -3368,6 +3843,7 @@ ${append}`;
|
|
|
3368
3843
|
return base;
|
|
3369
3844
|
}
|
|
3370
3845
|
var CoHubPlugin = async (input, options) => {
|
|
3846
|
+
const pluginClient = input.client;
|
|
3371
3847
|
const projectDir = input.directory || process.cwd();
|
|
3372
3848
|
const fileOverrides = loadFileOverrides(projectDir);
|
|
3373
3849
|
const configOverrides = {};
|
|
@@ -3377,7 +3853,6 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3377
3853
|
const promptOverrides = { ...configOverrides, ...fileOverrides };
|
|
3378
3854
|
const tracker = new TaskTracker;
|
|
3379
3855
|
const orchestrationLayer = createOrchestrationLayer(tracker, {});
|
|
3380
|
-
let _lastAlias = "";
|
|
3381
3856
|
const STATE_DIR = path7.join(os5.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
|
|
3382
3857
|
const STATE_FILE = path7.join(STATE_DIR, "tracker-state.json");
|
|
3383
3858
|
function syncTrackerState(sessionId) {
|
|
@@ -3508,26 +3983,29 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3508
3983
|
const councilConfig = userConfig.council ?? DEFAULT_COUNCIL_CONFIG;
|
|
3509
3984
|
const councilManager = new CouncilManager(input.client, input.directory, councilConfig);
|
|
3510
3985
|
const councilTools = createCouncilTool(input, councilManager);
|
|
3511
|
-
const jobTools = createCloseJobTool(input, tracker, () =>
|
|
3986
|
+
const jobTools = createCloseJobTool(input, tracker, undefined, (alias) => {
|
|
3987
|
+
const job = tracker.getJobByAlias(alias);
|
|
3988
|
+
if (job && orchestrationLayer) {
|
|
3989
|
+
orchestrationLayer.engine.applyLifecycleEvent(alias, "cancelled");
|
|
3990
|
+
syncTrackerState(job.parentSessionId);
|
|
3991
|
+
}
|
|
3992
|
+
});
|
|
3512
3993
|
function extractChildSessionId(output) {
|
|
3513
3994
|
if (!output || typeof output !== "object")
|
|
3514
3995
|
return;
|
|
3515
3996
|
const o = output;
|
|
3516
3997
|
const meta = o.metadata;
|
|
3517
3998
|
if (meta) {
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
return meta.task_id;
|
|
3524
|
-
if (typeof meta.id === "string")
|
|
3525
|
-
return meta.id;
|
|
3999
|
+
for (const key of ["sessionId", "taskId", "task_id", "id"]) {
|
|
4000
|
+
const v = meta[key];
|
|
4001
|
+
if (typeof v === "string" && v.startsWith("ses_"))
|
|
4002
|
+
return v;
|
|
4003
|
+
}
|
|
3526
4004
|
}
|
|
3527
4005
|
const text = typeof o.output === "string" ? o.output : "";
|
|
3528
|
-
const m = text.match(/\b(session|task)[_\s]?
|
|
4006
|
+
const m = text.match(/\b(?:session|task)[_\s]?id[:\s]+(ses_\S+)/i);
|
|
3529
4007
|
if (m)
|
|
3530
|
-
return m[
|
|
4008
|
+
return m[1];
|
|
3531
4009
|
return;
|
|
3532
4010
|
}
|
|
3533
4011
|
function extractSessionIdFromEvent(props) {
|
|
@@ -3543,6 +4021,130 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3543
4021
|
return p.sessionId;
|
|
3544
4022
|
return;
|
|
3545
4023
|
}
|
|
4024
|
+
async function verifyChildTerminal(client, sessionId, jobCreatedAt) {
|
|
4025
|
+
try {
|
|
4026
|
+
const res = await client.session.messages({ path: { id: sessionId }, query: { limit: 10 } });
|
|
4027
|
+
if (res.error)
|
|
4028
|
+
return "unknown";
|
|
4029
|
+
const msgs = res.data;
|
|
4030
|
+
if (!Array.isArray(msgs) || msgs.length === 0)
|
|
4031
|
+
return "unknown";
|
|
4032
|
+
let latest;
|
|
4033
|
+
let latestCreated = -1;
|
|
4034
|
+
for (const item of msgs) {
|
|
4035
|
+
const info = item?.info;
|
|
4036
|
+
if (!info || info.role !== "assistant")
|
|
4037
|
+
continue;
|
|
4038
|
+
const t = info.time;
|
|
4039
|
+
const created = typeof t?.created === "number" ? t.created : -1;
|
|
4040
|
+
if (created > latestCreated) {
|
|
4041
|
+
latestCreated = created;
|
|
4042
|
+
latest = item;
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
if (!latest)
|
|
4046
|
+
return "unknown";
|
|
4047
|
+
const info = latest.info;
|
|
4048
|
+
const time = info.time;
|
|
4049
|
+
const created = typeof time?.created === "number" ? time.created : undefined;
|
|
4050
|
+
if (created !== undefined && created < jobCreatedAt)
|
|
4051
|
+
return "unknown";
|
|
4052
|
+
if (info.error)
|
|
4053
|
+
return isAbortLikeError(info.error) ? "cancelled" : "errored";
|
|
4054
|
+
const parts = latest.parts ?? [];
|
|
4055
|
+
const hasOpenTool = parts.some((p) => {
|
|
4056
|
+
if (p.type !== "tool")
|
|
4057
|
+
return false;
|
|
4058
|
+
const st = p.state;
|
|
4059
|
+
return st?.status !== "completed" && st?.status !== "error";
|
|
4060
|
+
});
|
|
4061
|
+
if (hasOpenTool)
|
|
4062
|
+
return "unknown";
|
|
4063
|
+
const finish = info.finish;
|
|
4064
|
+
if (typeof finish === "string" && finish !== "") {
|
|
4065
|
+
const f = finish.toLowerCase();
|
|
4066
|
+
if (f === "tool-calls" || f === "tool_calls")
|
|
4067
|
+
return "unknown";
|
|
4068
|
+
if (f === "abort" || f === "cancel" || f === "cancelled")
|
|
4069
|
+
return "cancelled";
|
|
4070
|
+
if (f === "length" || f === "content-filter" || f === "content_filter")
|
|
4071
|
+
return "errored";
|
|
4072
|
+
if (KNOWN_SUCCESS_FINISH.some((m) => m === f))
|
|
4073
|
+
return "completed";
|
|
4074
|
+
return "unknown";
|
|
4075
|
+
}
|
|
4076
|
+
return "unknown";
|
|
4077
|
+
} catch {
|
|
4078
|
+
return "unknown";
|
|
4079
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
const KNOWN_SUCCESS_FINISH = ["finish", "stop", "end_turn", "end", "success", "done", "completed"];
|
|
4082
|
+
function isAbortLikeError(err) {
|
|
4083
|
+
if (!err || typeof err !== "object")
|
|
4084
|
+
return false;
|
|
4085
|
+
return err.name === "MessageAbortedError";
|
|
4086
|
+
}
|
|
4087
|
+
function taskDiag(record) {
|
|
4088
|
+
if (ctxDiag.enabled)
|
|
4089
|
+
appendTaskDiag(record, ctxDiag.filePath);
|
|
4090
|
+
}
|
|
4091
|
+
function classifyTaskAfter(output) {
|
|
4092
|
+
if (output && typeof output === "object") {
|
|
4093
|
+
const meta = output.metadata;
|
|
4094
|
+
if (meta && typeof meta === "object") {
|
|
4095
|
+
if (meta.status === "cancelled" || meta.cancelled === true)
|
|
4096
|
+
return { status: "cancelled" };
|
|
4097
|
+
if (meta.status === "failed" || meta.status === "error" || meta.status === "errored" || meta.error !== undefined) {
|
|
4098
|
+
return { status: "errored" };
|
|
4099
|
+
}
|
|
4100
|
+
if (meta.status === "running" || meta.status === "pending")
|
|
4101
|
+
return { status: "running" };
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
return { status: "unknown" };
|
|
4105
|
+
}
|
|
4106
|
+
async function reconcileJobTerminal(job) {
|
|
4107
|
+
const parentAtCall = job.parentSessionId;
|
|
4108
|
+
const callAtCall = job.callID;
|
|
4109
|
+
const aliasAtCall = job.alias;
|
|
4110
|
+
const childId = job.sessionId;
|
|
4111
|
+
if (!childId)
|
|
4112
|
+
return;
|
|
4113
|
+
const verdict = await verifyChildTerminal(pluginClient, childId, job.createdAt);
|
|
4114
|
+
const nowJob = callAtCall ? tracker.getJobByCall(parentAtCall, callAtCall) : tracker.getJobBySessionId(childId);
|
|
4115
|
+
if (!nowJob || nowJob.alias !== aliasAtCall || nowJob.sessionId !== childId || nowJob.status !== "running") {
|
|
4116
|
+
appendSafeLog("event", `收尾竞态:候选已变更,不更新新调用 (session=${childId.slice(0, 20)})`);
|
|
4117
|
+
return;
|
|
4118
|
+
}
|
|
4119
|
+
if (verdict === "completed" || verdict === "errored" || verdict === "cancelled") {
|
|
4120
|
+
const term = verdict === "completed" ? "completed" : verdict === "cancelled" ? "cancelled" : "errored";
|
|
4121
|
+
const up = callAtCall ? tracker.finalizeByCall(parentAtCall, callAtCall, childId, term) : tracker.updateByChildSessionId(childId, term);
|
|
4122
|
+
syncTrackerState(tracker.currentParentSessionId);
|
|
4123
|
+
const alias = up.ok ? up.alias : aliasAtCall;
|
|
4124
|
+
if (up.ok && up.alias && orchestrationLayer) {
|
|
4125
|
+
orchestrationLayer.engine.applyLifecycleEvent(up.alias, verdict === "completed" ? "completed" : verdict === "cancelled" ? "cancelled" : "failed");
|
|
4126
|
+
}
|
|
4127
|
+
taskDiag({
|
|
4128
|
+
t: "task",
|
|
4129
|
+
session: childId.slice(0, 20),
|
|
4130
|
+
alias,
|
|
4131
|
+
event: verdict === "completed" ? "task_completed" : verdict === "cancelled" ? "task_cancelled" : "task_failed",
|
|
4132
|
+
status: verdict === "completed" ? "completed" : verdict === "cancelled" ? "cancelled" : "failed",
|
|
4133
|
+
durationMs: Date.now() - job.createdAt,
|
|
4134
|
+
lastActivityAt: Date.now()
|
|
4135
|
+
});
|
|
4136
|
+
} else {
|
|
4137
|
+
appendSafeLog("event", `无终态证据,保持 running (session=${childId.slice(0, 20)})`);
|
|
4138
|
+
taskDiag({
|
|
4139
|
+
t: "task",
|
|
4140
|
+
session: childId.slice(0, 20),
|
|
4141
|
+
alias: aliasAtCall,
|
|
4142
|
+
event: "state_changed",
|
|
4143
|
+
status: "running",
|
|
4144
|
+
lastActivityAt: Date.now()
|
|
4145
|
+
});
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
3546
4148
|
const cleanupTimer = setInterval(() => {
|
|
3547
4149
|
try {
|
|
3548
4150
|
const staleSessions = tracker.cleanupStaleJobs(STALE_TASK_MS);
|
|
@@ -3660,13 +4262,14 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3660
4262
|
subagent_type: subagentType,
|
|
3661
4263
|
task_id: typeof output.args.task_id === "string" && output.args.task_id.startsWith("ses_") ? output.args.task_id : undefined,
|
|
3662
4264
|
background: typeof args.background === "boolean" ? args.background : undefined
|
|
3663
|
-
});
|
|
3664
|
-
_lastAlias = alias;
|
|
4265
|
+
}, input.callID);
|
|
3665
4266
|
syncTrackerState(input.sessionID ?? "");
|
|
3666
4267
|
tracker.pruneTerminalJobs(STALE_TASK_MS);
|
|
3667
4268
|
if (orchestrationLayer && subagentType && description) {
|
|
3668
4269
|
try {
|
|
3669
|
-
orchestrationLayer.engine.
|
|
4270
|
+
if (!orchestrationLayer.engine.getTask(alias)) {
|
|
4271
|
+
orchestrationLayer.engine.registerTask(alias, subagentType, description, []);
|
|
4272
|
+
}
|
|
3670
4273
|
const targetField = output.args?.prompt ? "prompt" : "description";
|
|
3671
4274
|
if (output.args?.[targetField] && typeof output.args[targetField] === "string") {
|
|
3672
4275
|
output.args[targetField] = stripExistingContracts(output.args[targetField]);
|
|
@@ -3677,8 +4280,31 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3677
4280
|
constraints: []
|
|
3678
4281
|
});
|
|
3679
4282
|
}
|
|
4283
|
+
const lr = orchestrationLayer.engine.applyLifecycleEvent(alias, "running");
|
|
4284
|
+
if (!lr.ok) {
|
|
4285
|
+
appendLog("orchestration", `before: applyLifecycleEvent(running) ${lr.reason} for ${alias}(状态来自实际原生生命周期,未接线 DAG 依赖不强制推进)`);
|
|
4286
|
+
taskDiag({
|
|
4287
|
+
t: "task",
|
|
4288
|
+
callID: input.callID,
|
|
4289
|
+
session: input.sessionID?.slice(0, 20),
|
|
4290
|
+
alias,
|
|
4291
|
+
event: "state_changed",
|
|
4292
|
+
status: orchestrationLayer.engine.getState(alias) ?? "pending",
|
|
4293
|
+
lastActivityAt: Date.now()
|
|
4294
|
+
});
|
|
4295
|
+
} else {
|
|
4296
|
+
taskDiag({
|
|
4297
|
+
t: "task",
|
|
4298
|
+
callID: input.callID,
|
|
4299
|
+
session: input.sessionID?.slice(0, 20),
|
|
4300
|
+
alias,
|
|
4301
|
+
event: "task_started",
|
|
4302
|
+
status: "running",
|
|
4303
|
+
lastActivityAt: Date.now()
|
|
4304
|
+
});
|
|
4305
|
+
}
|
|
3680
4306
|
} catch (e) {
|
|
3681
|
-
|
|
4307
|
+
appendSafeLog("orchestration", "registerTask error", e);
|
|
3682
4308
|
}
|
|
3683
4309
|
}
|
|
3684
4310
|
if (subagentType) {
|
|
@@ -3731,119 +4357,238 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3731
4357
|
if (input.tool === "cancel_task") {
|
|
3732
4358
|
const args = input.args;
|
|
3733
4359
|
const taskId = args?.task_id;
|
|
3734
|
-
if (typeof taskId === "string")
|
|
3735
|
-
tracker.
|
|
4360
|
+
if (typeof taskId === "string") {
|
|
4361
|
+
const aborted = tracker.abortJob(taskId);
|
|
4362
|
+
if (aborted && orchestrationLayer) {
|
|
4363
|
+
orchestrationLayer.engine.applyLifecycleEvent(aborted.job.alias, "cancelled");
|
|
4364
|
+
taskDiag({
|
|
4365
|
+
t: "task",
|
|
4366
|
+
session: input.sessionID?.slice(0, 20),
|
|
4367
|
+
alias: aborted.job.alias,
|
|
4368
|
+
event: "task_cancelled",
|
|
4369
|
+
status: "cancelled",
|
|
4370
|
+
lastActivityAt: Date.now()
|
|
4371
|
+
});
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
3736
4374
|
syncTrackerState(input.sessionID ?? "");
|
|
3737
4375
|
}
|
|
3738
4376
|
if (input.tool === "task") {
|
|
3739
4377
|
const childSessionId = extractChildSessionId(output);
|
|
3740
|
-
tracker.
|
|
4378
|
+
const job0 = tracker.getJobByCall(input.sessionID, input.callID);
|
|
4379
|
+
if (!job0) {
|
|
4380
|
+
appendSafeLog("tool.execute.after", `after 未知 callID,no-op (parent=${input.sessionID?.slice(0, 20)})`);
|
|
4381
|
+
syncTrackerState(input.sessionID ?? "");
|
|
4382
|
+
return;
|
|
4383
|
+
}
|
|
4384
|
+
if (output === undefined || output === null) {
|
|
4385
|
+
appendSafeLog("tool.execute.after", `after 无有效输出,不假成功 (parent=${input.sessionID?.slice(0, 20)})`);
|
|
4386
|
+
syncTrackerState(input.sessionID ?? "");
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4389
|
+
const meta = output?.metadata;
|
|
4390
|
+
const metaBg = meta && typeof meta === "object" ? meta.background : undefined;
|
|
4391
|
+
if (metaBg === true && !job0.background) {
|
|
4392
|
+
tracker.setBackgroundByCall(input.sessionID, input.callID, true);
|
|
4393
|
+
}
|
|
4394
|
+
const outcome = classifyTaskAfter(output);
|
|
4395
|
+
if (outcome.status === "cancelled" || outcome.status === "errored") {
|
|
4396
|
+
const term = outcome.status === "cancelled" ? "cancelled" : "errored";
|
|
4397
|
+
const up = tracker.finalizeByCall(input.sessionID, input.callID, (childSessionId ?? job0.sessionId) || undefined, term);
|
|
4398
|
+
syncTrackerState(input.sessionID ?? "");
|
|
4399
|
+
if (up.ok && up.alias && orchestrationLayer) {
|
|
4400
|
+
orchestrationLayer.engine.applyLifecycleEvent(up.alias, term === "cancelled" ? "cancelled" : "failed");
|
|
4401
|
+
}
|
|
4402
|
+
taskDiag({
|
|
4403
|
+
t: "task",
|
|
4404
|
+
callID: input.callID,
|
|
4405
|
+
session: input.sessionID?.slice(0, 20),
|
|
4406
|
+
alias: up.ok ? up.alias : job0.alias,
|
|
4407
|
+
event: term === "cancelled" ? "task_cancelled" : "task_failed",
|
|
4408
|
+
status: term === "cancelled" ? "cancelled" : "failed",
|
|
4409
|
+
durationMs: Date.now() - job0.createdAt,
|
|
4410
|
+
lastActivityAt: Date.now()
|
|
4411
|
+
});
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4414
|
+
const up = tracker.updateAfterTaskByCall(input.sessionID, input.callID, "completed", childSessionId);
|
|
3741
4415
|
syncTrackerState(input.sessionID ?? "");
|
|
3742
|
-
if (
|
|
4416
|
+
if (up.ok && orchestrationLayer) {
|
|
3743
4417
|
try {
|
|
3744
|
-
if (
|
|
3745
|
-
const
|
|
3746
|
-
if (
|
|
3747
|
-
|
|
4418
|
+
if (!job0.background) {
|
|
4419
|
+
const lr = orchestrationLayer.engine.applyLifecycleEvent(job0.alias, "completed");
|
|
4420
|
+
if (!lr.ok)
|
|
4421
|
+
appendLog("orchestration", `after: applyLifecycleEvent(completed) ${lr.reason} for ${job0.alias}`);
|
|
4422
|
+
const text = typeof output === "string" ? output : typeof output?.output === "string" ? output.output : "";
|
|
4423
|
+
if (text) {
|
|
4424
|
+
const contract = orchestrationLayer.contractMgr.extract(text);
|
|
4425
|
+
if (contract)
|
|
4426
|
+
orchestrationLayer.engine.setResult(job0.alias, contract.keyResult, contract);
|
|
3748
4427
|
}
|
|
4428
|
+
taskDiag({
|
|
4429
|
+
t: "task",
|
|
4430
|
+
callID: input.callID,
|
|
4431
|
+
session: input.sessionID?.slice(0, 20),
|
|
4432
|
+
alias: job0.alias,
|
|
4433
|
+
event: "task_completed",
|
|
4434
|
+
status: "completed",
|
|
4435
|
+
durationMs: Date.now() - job0.createdAt,
|
|
4436
|
+
lastActivityAt: Date.now()
|
|
4437
|
+
});
|
|
4438
|
+
} else {
|
|
4439
|
+
taskDiag({
|
|
4440
|
+
t: "task",
|
|
4441
|
+
callID: input.callID,
|
|
4442
|
+
session: input.sessionID?.slice(0, 20),
|
|
4443
|
+
alias: job0.alias,
|
|
4444
|
+
event: "state_changed",
|
|
4445
|
+
status: "running",
|
|
4446
|
+
lastActivityAt: Date.now()
|
|
4447
|
+
});
|
|
4448
|
+
await reconcileJobTerminal(job0);
|
|
3749
4449
|
}
|
|
3750
|
-
orchestrationLayer.scheduler.markCompleted(_lastAlias);
|
|
3751
4450
|
} catch (e) {
|
|
3752
|
-
|
|
4451
|
+
appendSafeLog("orchestration", "afterTask engine 同步失败", e);
|
|
3753
4452
|
}
|
|
3754
4453
|
}
|
|
3755
4454
|
}
|
|
3756
4455
|
} catch (err) {
|
|
3757
|
-
|
|
4456
|
+
appendSafeLog("tool.execute.after", "hook 失败", err);
|
|
3758
4457
|
}
|
|
3759
4458
|
},
|
|
3760
4459
|
event: async (input) => {
|
|
3761
4460
|
try {
|
|
3762
4461
|
const e = input.event;
|
|
3763
|
-
const
|
|
3764
|
-
|
|
3765
|
-
return;
|
|
4462
|
+
const props = e.properties;
|
|
4463
|
+
const sessionId = extractSessionIdFromEvent(props);
|
|
3766
4464
|
if (e.type === "session.idle") {
|
|
3767
|
-
|
|
4465
|
+
if (!sessionId)
|
|
4466
|
+
return;
|
|
4467
|
+
const active = tracker.findActiveByChildSessionId(sessionId);
|
|
3768
4468
|
syncTrackerState(tracker.currentParentSessionId);
|
|
3769
|
-
if (
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
if (job) {
|
|
3773
|
-
orchestrationLayer.engine.transition(job.alias, "completed");
|
|
3774
|
-
orchestrationLayer.scheduler.markCompleted(job.alias);
|
|
3775
|
-
}
|
|
3776
|
-
} catch (e) {
|
|
3777
|
-
appendLog("orchestration", "session.idle transition error", e);
|
|
3778
|
-
}
|
|
3779
|
-
}
|
|
3780
|
-
const job = tracker.getJobBySessionId(sessionId);
|
|
3781
|
-
if (job) {
|
|
3782
|
-
contextEngine.captureResult(sessionId, job.alias, job.agent).then((captured) => {
|
|
3783
|
-
if (!captured)
|
|
3784
|
-
return;
|
|
3785
|
-
if (!isQualityEnabled(userConfig.quality))
|
|
3786
|
-
return;
|
|
3787
|
-
const quality = assessQuality({
|
|
3788
|
-
output: captured.output,
|
|
3789
|
-
decisions: captured.decisions,
|
|
3790
|
-
latencyMs: Date.now() - job.createdAt,
|
|
3791
|
-
tokens: captured.tokens
|
|
3792
|
-
});
|
|
3793
|
-
tracker.updateQuality(sessionId, {
|
|
3794
|
-
score: quality.score,
|
|
3795
|
-
...quality.failureCategory ? { failureCategory: quality.failureCategory } : {},
|
|
3796
|
-
...quality.latencyMs !== undefined ? { latencyMs: quality.latencyMs } : {},
|
|
3797
|
-
...quality.tokens ? { tokens: quality.tokens } : {}
|
|
3798
|
-
});
|
|
3799
|
-
syncTrackerState(tracker.currentParentSessionId);
|
|
3800
|
-
}).catch((err) => appendLog("event", "质量回送失败", err));
|
|
4469
|
+
if (!active.ok) {
|
|
4470
|
+
appendSafeLog("event", `session.idle ${active.reason} 不操作 (session=${sessionId.slice(0, 20)})`);
|
|
4471
|
+
return;
|
|
3801
4472
|
}
|
|
4473
|
+
const job = active.job;
|
|
4474
|
+
await reconcileJobTerminal(job);
|
|
4475
|
+
const qAlias = job.alias;
|
|
4476
|
+
const qParent = job.parentSessionId;
|
|
4477
|
+
const qCall = job.callID;
|
|
4478
|
+
const qChild = sessionId;
|
|
4479
|
+
const qCreatedAt = job.createdAt;
|
|
4480
|
+
contextEngine.captureResult(sessionId, qAlias, job.agent).then((captured) => {
|
|
4481
|
+
if (!captured)
|
|
4482
|
+
return;
|
|
4483
|
+
if (!isQualityEnabled(userConfig.quality))
|
|
4484
|
+
return;
|
|
4485
|
+
const now = qCall ? tracker.getJobByCall(qParent, qCall) : tracker.getJobBySessionId(qChild);
|
|
4486
|
+
if (!now || now.alias !== qAlias || now.sessionId !== qChild) {
|
|
4487
|
+
appendSafeLog("event", "质量回送竞态:候选已变更,不写回复用/新调用");
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
4490
|
+
const quality = assessQuality({
|
|
4491
|
+
output: captured.output,
|
|
4492
|
+
decisions: captured.decisions,
|
|
4493
|
+
latencyMs: Date.now() - qCreatedAt,
|
|
4494
|
+
tokens: captured.tokens
|
|
4495
|
+
});
|
|
4496
|
+
tracker.updateQualityByAlias(qAlias, {
|
|
4497
|
+
score: quality.score,
|
|
4498
|
+
...quality.failureCategory ? { failureCategory: quality.failureCategory } : {},
|
|
4499
|
+
...quality.latencyMs !== undefined ? { latencyMs: quality.latencyMs } : {},
|
|
4500
|
+
...quality.tokens ? { tokens: quality.tokens } : {}
|
|
4501
|
+
});
|
|
4502
|
+
syncTrackerState(tracker.currentParentSessionId);
|
|
4503
|
+
}).catch((err) => appendSafeLog("event", "质量回送失败", err));
|
|
3802
4504
|
} else if (e.type === "session.deleted") {
|
|
3803
|
-
|
|
4505
|
+
if (!sessionId)
|
|
4506
|
+
return;
|
|
4507
|
+
const active = tracker.findActiveByChildSessionId(sessionId);
|
|
4508
|
+
const up = active.ok && active.job.callID ? tracker.finalizeByCall(active.job.parentSessionId, active.job.callID, sessionId, "errored") : tracker.updateByChildSessionId(sessionId, "errored");
|
|
3804
4509
|
syncTrackerState(tracker.currentParentSessionId);
|
|
3805
|
-
if (orchestrationLayer) {
|
|
3806
|
-
|
|
3807
|
-
const job = tracker.getJobBySessionId(sessionId);
|
|
3808
|
-
if (job) {
|
|
3809
|
-
orchestrationLayer.engine.transition(job.alias, "failed");
|
|
3810
|
-
orchestrationLayer.scheduler.markFailed(job.alias);
|
|
3811
|
-
const task = orchestrationLayer.engine.getTask(job.alias);
|
|
3812
|
-
if (task) {
|
|
3813
|
-
const decision = orchestrationLayer.retryMgr.decide(task, "session deleted");
|
|
3814
|
-
if (decision === "retry") {
|
|
3815
|
-
orchestrationLayer.engine.transition(job.alias, "pending");
|
|
3816
|
-
}
|
|
3817
|
-
}
|
|
3818
|
-
}
|
|
3819
|
-
} catch (e) {
|
|
3820
|
-
appendLog("orchestration", "session.deleted transition error", e);
|
|
3821
|
-
}
|
|
4510
|
+
if (up.ok && up.alias && orchestrationLayer) {
|
|
4511
|
+
orchestrationLayer.engine.applyLifecycleEvent(up.alias, "failed");
|
|
3822
4512
|
}
|
|
3823
|
-
|
|
3824
|
-
|
|
4513
|
+
taskDiag({
|
|
4514
|
+
t: "task",
|
|
4515
|
+
session: sessionId.slice(0, 20),
|
|
4516
|
+
alias: up.alias,
|
|
4517
|
+
event: "task_failed",
|
|
4518
|
+
status: "failed",
|
|
4519
|
+
lastActivityAt: Date.now()
|
|
4520
|
+
});
|
|
4521
|
+
} else if (e.type === "message.part.updated") {
|
|
4522
|
+
const part = props?.part;
|
|
4523
|
+
if (!part || part.type !== "tool" || part.tool !== "task")
|
|
4524
|
+
return;
|
|
4525
|
+
const pstate = part.state;
|
|
4526
|
+
const pstatus = pstate?.status;
|
|
4527
|
+
if (pstatus !== "error" && pstatus !== "cancelled")
|
|
4528
|
+
return;
|
|
4529
|
+
const parentSessionID = typeof part.sessionID === "string" ? part.sessionID : undefined;
|
|
4530
|
+
if (!parentSessionID)
|
|
4531
|
+
return;
|
|
4532
|
+
const partCallID = typeof part.callID === "string" ? part.callID : undefined;
|
|
4533
|
+
const partID = typeof part.id === "string" ? part.id : undefined;
|
|
4534
|
+
const byCall = partCallID ? tracker.getJobByCall(parentSessionID, partCallID) : undefined;
|
|
4535
|
+
const byId = partID ? tracker.getJobByCall(parentSessionID, partID) : undefined;
|
|
4536
|
+
if (byCall && byId && byCall.alias !== byId.alias)
|
|
4537
|
+
return;
|
|
4538
|
+
const job = byCall ?? byId;
|
|
4539
|
+
if (!job || job.status !== "running")
|
|
4540
|
+
return;
|
|
4541
|
+
const term = pstatus === "cancelled" ? "cancelled" : "errored";
|
|
4542
|
+
const up = tracker.finalizeByCall(parentSessionID, job.callID, job.sessionId || undefined, term);
|
|
3825
4543
|
syncTrackerState(tracker.currentParentSessionId);
|
|
3826
|
-
if (orchestrationLayer) {
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
4544
|
+
if (up.ok && up.alias && orchestrationLayer) {
|
|
4545
|
+
orchestrationLayer.engine.applyLifecycleEvent(up.alias, term === "cancelled" ? "cancelled" : "failed");
|
|
4546
|
+
}
|
|
4547
|
+
taskDiag({
|
|
4548
|
+
t: "task",
|
|
4549
|
+
session: parentSessionID.slice(0, 20),
|
|
4550
|
+
alias: up.ok ? up.alias : job.alias,
|
|
4551
|
+
event: term === "cancelled" ? "task_cancelled" : "task_failed",
|
|
4552
|
+
status: term === "cancelled" ? "cancelled" : "failed",
|
|
4553
|
+
lastActivityAt: Date.now()
|
|
4554
|
+
});
|
|
4555
|
+
} else if (e.type === "session.status") {
|
|
4556
|
+
if (!sessionId)
|
|
4557
|
+
return;
|
|
4558
|
+
const status = props?.status;
|
|
4559
|
+
if (status?.type === "retry") {
|
|
4560
|
+
taskDiag({
|
|
4561
|
+
t: "task",
|
|
4562
|
+
session: sessionId.slice(0, 20),
|
|
4563
|
+
event: "retry_scheduled",
|
|
4564
|
+
status: "running",
|
|
4565
|
+
retryAttempt: typeof status.attempt === "number" ? status.attempt : undefined,
|
|
4566
|
+
retryNextMs: typeof status.next === "number" ? status.next - Date.now() : undefined,
|
|
4567
|
+
lastActivityAt: Date.now()
|
|
4568
|
+
});
|
|
4569
|
+
appendLog("event", `session.status retry 观测 (session=${sessionId.slice(0, 20)} attempt=${String(status.attempt)})`);
|
|
4570
|
+
}
|
|
4571
|
+
} else if (e.type === "session.error") {
|
|
4572
|
+
if (!sessionId) {
|
|
4573
|
+
appendSafeLog("event", "session.error 缺少 sessionID,不关联任何任务", props?.error);
|
|
4574
|
+
return;
|
|
4575
|
+
}
|
|
4576
|
+
if (ctxDiag.enabled) {
|
|
4577
|
+
const info = classifyRateLimit(props?.error);
|
|
4578
|
+
taskDiag({
|
|
4579
|
+
t: "task",
|
|
4580
|
+
session: sessionId.slice(0, 20),
|
|
4581
|
+
event: "rate_limit_detected",
|
|
4582
|
+
status: "running",
|
|
4583
|
+
rateLimitKind: info.kind,
|
|
4584
|
+
retryAfterMs: info.retryAfterMs,
|
|
4585
|
+
lastActivityAt: Date.now()
|
|
4586
|
+
});
|
|
3843
4587
|
}
|
|
4588
|
+
appendSafeLog("event", `session.error 仅观测不终结 (session=${sessionId.slice(0, 20)})`, props?.error);
|
|
3844
4589
|
}
|
|
3845
4590
|
} catch (err) {
|
|
3846
|
-
|
|
4591
|
+
appendSafeLog("event", "事件处理失败", err);
|
|
3847
4592
|
}
|
|
3848
4593
|
},
|
|
3849
4594
|
"experimental.chat.messages.transform": async (_input, output) => {
|