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