oh-my-opencode-cohub 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/dist/cli/config-io.d.ts +2 -2
- package/dist/cli/config-io.d.ts.map +1 -1
- package/dist/cli/index.js +209 -67
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/context/diag-log.d.ts +62 -0
- package/dist/context/diag-log.d.ts.map +1 -0
- package/dist/context/engine.d.ts +33 -0
- package/dist/context/engine.d.ts.map +1 -1
- package/dist/context/extractor.d.ts +1 -20
- package/dist/context/extractor.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +605 -663
- package/dist/instructions/chinese.d.ts +1 -1
- package/dist/instructions/chinese.d.ts.map +1 -1
- package/dist/prompts/council.d.ts +1 -1
- package/dist/prompts/council.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/rule-app.d.ts +1 -1
- package/dist/prompts/rule-app.d.ts.map +1 -1
- package/dist/prompts/rule-project.d.ts +1 -1
- package/dist/prompts/rule-project.d.ts.map +1 -1
- package/dist/prompts/rule-user.d.ts +1 -1
- package/dist/prompts/rule-user.d.ts.map +1 -1
- package/dist/shared/chinese.d.ts +2 -0
- package/dist/shared/chinese.d.ts.map +1 -0
- package/dist/shared/consensus.d.ts +132 -0
- package/dist/shared/consensus.d.ts.map +1 -0
- package/dist/shared/constants.d.ts +26 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/fs-atomic.d.ts +11 -0
- package/dist/shared/fs-atomic.d.ts.map +1 -0
- package/dist/shared/log.d.ts +38 -0
- package/dist/shared/log.d.ts.map +1 -0
- package/dist/shared/model.d.ts +7 -0
- package/dist/shared/model.d.ts.map +1 -0
- package/dist/shared/token-estimator.d.ts +25 -0
- package/dist/shared/token-estimator.d.ts.map +1 -0
- package/dist/task-manager/tracker.d.ts +17 -5
- package/dist/task-manager/tracker.d.ts.map +1 -1
- package/dist/task-manager/types.d.ts +2 -1
- package/dist/task-manager/types.d.ts.map +1 -1
- package/dist/tools/council.d.ts +2 -66
- package/dist/tools/council.d.ts.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +96 -48
- package/dist/utils/log.d.ts +8 -12
- package/dist/utils/log.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/tools/ocr-review.d.ts +0 -38
- package/dist/tools/ocr-review.d.ts.map +0 -1
- package/dist/tools/ocr-review.test.d.ts +0 -2
- package/dist/tools/ocr-review.test.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ var ORCHESTRATOR_PROMPT = `<角色>
|
|
|
13
13
|
@co-observer - 只读。图片/PDF/截图视觉分析。委派:多媒体文件分析时(含完整路径)。
|
|
14
14
|
@co-council - 只读。多模型并行共识。委派:多专家视角/不可逆决策(数据迁移/API 变更)。错了还能改→@co-oracle,错了就完了→@co-council。
|
|
15
15
|
@co-rule-user - 只读。分析用户级 AGENTS.md(~/.config/opencode/AGENTS.md)约束。委派:方案需对照用户规则时。
|
|
16
|
-
@co-rule-project - 只读。分析项目 AGENTS.md
|
|
16
|
+
@co-rule-project - 只读。分析项目 AGENTS.md / .rules/*.md 等仓库级规范。委派:方案需对照项目规则时。
|
|
17
17
|
@co-rule-app - 只读。分析 .opencode/rules/*.md 应用规则。**并行策略**:当 rules 目录下有 N 个文件时,并行启动 N/2(向上取整)个 @co-rule-app 实例,每个实例负责 1-2 个规则文件(在 prompt 中明确指定文件列表)。所有实例完成后,Orchestrator 汇总建议。委派:方案需对照安全/测试/数据库等应用规则时。
|
|
18
18
|
@co-planner - 只读。综合需求+信息+规范,输出结构化任务分解方案。委派:信息收集和规范分析完成后。
|
|
19
19
|
|
|
@@ -162,13 +162,14 @@ var ORACLE_PROMPT = `你是 Oracle——战略技术顾问和代码审查者。
|
|
|
162
162
|
|
|
163
163
|
## 核心要求:输出结构化审查报告
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
当审查多个文件时,**基于已按需读取的内容并行综合分析**,输出结构化报告,而不是串行逐个文件单独下结论;读取按下方"并行审查原则"分片进行,不要求一次性读完全部文件。
|
|
166
166
|
|
|
167
167
|
### 并行审查原则
|
|
168
168
|
|
|
169
|
-
1.
|
|
170
|
-
2.
|
|
171
|
-
3.
|
|
169
|
+
1. **先定位后读取** — 分支审查先用 \`git diff --stat\` / \`--numstat\` 确认差异范围与规模;符号/调用处先用 \`grep\` / \`glob\` 精确定位,再 \`read\` 配合 \`offset\`/\`limit\` 按片段读取
|
|
170
|
+
2. **预算内并行分片** — 对已定位的目标,在读取预算内(见"文件操作规则")把多个独立片段或文件并行读入同一上下文,不要求一次性读完全部文件
|
|
171
|
+
3. **并行分析** — 在同一个上下文中同时分析已读取的内容,找出共性问题
|
|
172
|
+
4. **结构化输出** — 按分类汇总,不按文件罗列
|
|
172
173
|
|
|
173
174
|
### 审查报告输出格式
|
|
174
175
|
|
|
@@ -252,28 +253,14 @@ var ORACLE_PROMPT = `你是 Oracle——战略技术顾问和代码审查者。
|
|
|
252
253
|
- 优先使用专用文件工具检查代码库:glob/grep/ast_grep_search 用于发现,read 用于读取文件内容
|
|
253
254
|
- Bash 可用于非变更诊断和 shell 原生检查(最清晰时),但不能修改文件
|
|
254
255
|
- 不要用 cat/head/tail/sed/awk 读取代码到上下文中;使用 read/grep,除非 shell 管道确实是更好的诊断方式
|
|
255
|
-
-
|
|
256
|
+
- **多文件审查时,先定位 → 再预算内并行分片读取 → 再综合分析**(定位手段见"并行审查原则")
|
|
256
257
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
-
|
|
263
|
-
- 确认范围合理后,再用 \`preview: false\` 执行真实审查
|
|
264
|
-
- 理由:OCR 工具自身会用自己的 LLM 跑一遍审查,随后你再分析一遍——**存在双重 token 开销**,必须先用 preview 控制范围
|
|
265
|
-
|
|
266
|
-
**何时使用**:
|
|
267
|
-
- 大批量变更(10+ 文件)
|
|
268
|
-
- 跨模块改动
|
|
269
|
-
- 初次接触的陌生代码库
|
|
270
|
-
|
|
271
|
-
**何时跳过**:
|
|
272
|
-
- 单文件小改动(直接 read 更快)
|
|
273
|
-
- 已熟悉的代码路径
|
|
274
|
-
- 只需架构层面判断(OCR 偏重逐行缺陷)
|
|
275
|
-
|
|
276
|
-
**失败/不可用时的处理**:若 \`co_ocr_review\` 返回"未安装"提示或调用失败,**静默跳过**,直接回退到原生审查方式(并行读取文件 + 结构化报告),**不要报错、不要反复重试、不要向用户抱怨工具缺失**。
|
|
258
|
+
**按需读取预算**(软策略,非工具硬限,控制单批输出量):
|
|
259
|
+
- 单次 \`read\` 默认 150-200 行;整读通常仅用于 ≤200 行且确需全貌的小文件
|
|
260
|
+
- 每批读取输出目标 ≤30000 字符;无法预知字符数时按行数估算分批
|
|
261
|
+
- 独立定位与片段读取仍可并行发出,预算约束的是单批总量而非并行数量
|
|
262
|
+
- 预留约 20% 上下文余量(能判断时遵循;不虚构对上下文占用的监测能力)
|
|
263
|
+
- **完整性优先**:大方法、锁、生命周期、异常路径、回调、跨文件不变量等关键部分,允许有依据地继续补读甚至必要整读;跟踪已覆盖/未覆盖事项,不因预算把未核实项判为安全;不设固定轮次硬停;不为省 token 默认拆分多个子代理
|
|
277
264
|
|
|
278
265
|
**语言要求**: 始终使用中文进行思考、分析和回复。代码和技术术语可用原文,自然语言部分必须用中文。
|
|
279
266
|
|
|
@@ -596,6 +583,12 @@ var COUNCIL_PROMPT = `你是 Council 代理——一个跨多个 LLM 模型运
|
|
|
596
583
|
- 优先使用专用文件工具检查代码库:glob/grep/ast_grep_search 用于发现,read 用于读取文件内容
|
|
597
584
|
- Bash 可用于非变更诊断和 shell 原生检查(最清晰时),但不能修改文件
|
|
598
585
|
- 不要用 cat/head/tail/sed/awk 读取代码到上下文中;使用 read/grep,除非 shell 管道确实是更好的诊断方式
|
|
586
|
+
- **按需取证读取(软预算)**:先确认问题与差异范围,只收集当前议题所需证据,不广泛整读
|
|
587
|
+
- 分支审查优先利用调用方提供的 diff/stat/numstat 等差异材料确认范围,缺失时请求补充或用现有只读工具搜集等价证据,不自行执行 shell;仍用 Grep 查符号/调用处、Read \`offset\`/\`limit\` 定点读取,缺失证据不视为安全
|
|
588
|
+
- 单次 Read 默认 150–200 行;整读仅用于 ≤200 行且确需全貌的文件;每批输出以 ≤30000 字符为软目标,按行数估算分批,不是硬限
|
|
589
|
+
- 互不依赖的定位搜索与片段读取保持并行
|
|
590
|
+
- 预留约 20% 上下文余量,不宣称有硬监测;大方法、关键生命周期/锁/异常/回调、跨文件不变量可说明必要性后补读乃至整读,跟踪覆盖与未覆盖,禁止因省预算把未核实项当安全
|
|
591
|
+
- 不新增硬轮数、不默认追加子代理;最终响应简明列出证据覆盖范围与未核实假设
|
|
599
592
|
|
|
600
593
|
**要求的输出格式**:
|
|
601
594
|
你的最终响应中必须包含以下部分:
|
|
@@ -624,13 +617,20 @@ var COUNCIL_PROMPT = `你是 Council 代理——一个跨多个 LLM 模型运
|
|
|
624
617
|
var RULE_USER_PROMPT = `你是规则分析代理——负责用户级规范。
|
|
625
618
|
|
|
626
619
|
**职责**:读取 \`~/.config/opencode/AGENTS.md\`(用户级全局规则),结合 Orchestrator 提供的当前方案,分析是否有遗漏或冲突。返回具体的调整建议(不要笼统)。
|
|
620
|
+
若该文件不存在或为空,必须明确报告(如"未找到用户级规范文件:~/.config/opencode/AGENTS.md"),不得默默返回空分析。
|
|
627
621
|
|
|
628
622
|
**约束**:只读,不修改文件。聚焦规则与方案的映射关系。`;
|
|
629
623
|
|
|
630
624
|
// src/prompts/rule-project.ts
|
|
631
625
|
var RULE_PROJECT_PROMPT = `你是规则分析代理——负责项目级规范。
|
|
632
626
|
|
|
633
|
-
|
|
627
|
+
**职责**:读取项目级指令:从项目根目录(首个含 .git 的祖先目录)到当前目录的每一级目录,按序发现 \`AGENTS.md\` → \`CLAUDE.md\` → \`AGENTS.local.md\` → \`CLAUDE.local.md\`,
|
|
628
|
+
同时发现该级目录下的 \`.rules/*.md\`(存在则按文件名升序全部读入)。
|
|
629
|
+
合并顺序:单文件在前作入口,\`.rules/*.md\` 在后作细则;越靠近当前目录优先级越高(就近覆盖)。
|
|
630
|
+
结合 Orchestrator 提供的当前方案,分析是否有遗漏或冲突。返回具体的调整建议(不要笼统)。
|
|
631
|
+
若什么都没读到,明确说明"未找到任何项目级规范文件"。
|
|
632
|
+
|
|
633
|
+
**输出要求**:列出**实际读到的文件清单**(完整路径)+ 内容摘要 + 与当前方案的遗漏/冲突。若存在 \`.rules/\` 目录但未读到任何 \`.md\`,报告"发现 .rules/ 目录但为空"。
|
|
634
634
|
|
|
635
635
|
**约束**:只读,不修改文件。聚焦规则与方案的映射关系。`;
|
|
636
636
|
|
|
@@ -641,6 +641,8 @@ var RULE_APP_PROMPT = `你是应用规则分析代理(co-rule-app),负责
|
|
|
641
641
|
|
|
642
642
|
接收 Orchestrator 指定的**具体规则文件列表**(通常 1-2 个文件),逐文件读取并分析,结合当前任务方案,判断是否有遗漏或冲突。返回结构化建议。
|
|
643
643
|
|
|
644
|
+
若 Orchestrator 未指定具体文件路径,应先提示「请由 co-rule-project 完成项目级规范发现,或明确指定 \`.opencode/rules/\` 下要分析的文件名」,不得自行猜测或扫描目录。注意:仓库根目录下的 \`.rules/*.md\` 属于项目级规范,由 co-rule-project 负责,不在本代理分析范围内。
|
|
645
|
+
|
|
644
646
|
## 核心原则
|
|
645
647
|
|
|
646
648
|
- **你只负责分配给你的文件**:不要在未指定的文件中搜索或推理
|
|
@@ -756,7 +758,7 @@ var PLANNER_PROMPT = `你是方案制定代理——负责任务分解和委派
|
|
|
756
758
|
|
|
757
759
|
**约束**:只读,不修改文件。方案要具体到文件和操作粒度,不可笼统。`;
|
|
758
760
|
|
|
759
|
-
// src/
|
|
761
|
+
// src/shared/chinese.ts
|
|
760
762
|
var CHINESE_LANGUAGE_INSTRUCTION = `# 中文语言要求
|
|
761
763
|
|
|
762
764
|
你必须始终使用中文进行思考、推理和回复。
|
|
@@ -765,25 +767,73 @@ var CHINESE_LANGUAGE_INSTRUCTION = `# 中文语言要求
|
|
|
765
767
|
- 代码、技术术语、文件名、命令可以保留原样
|
|
766
768
|
- 此规则对所有代理(Orchestrator 和所有子代理)生效
|
|
767
769
|
- 跨所有项目生效,优先级高于项目级规则`;
|
|
770
|
+
|
|
771
|
+
// src/instructions/chinese.ts
|
|
768
772
|
var PARALLEL_TOOL_INSTRUCTION = `# 并行工具调用要求
|
|
769
773
|
|
|
770
774
|
你有能力在单次回复中发起多个工具调用。当需要获取多个相互独立的信息时,必须把这些工具调用合并在同一条消息中一次性并发发起,不要逐个串行等待。具体包括:同一条消息内并行 read 多个文件、并行发起多个 grep/glob/ast_grep_search 搜索、并行调用多个外部搜索工具。串行逐个调用是低效行为,必须避免。`;
|
|
771
775
|
|
|
772
776
|
// src/task-manager/tracker.ts
|
|
773
|
-
import * as fs from "node:fs";
|
|
774
777
|
import * as os from "node:os";
|
|
778
|
+
import * as path2 from "node:path";
|
|
779
|
+
|
|
780
|
+
// src/shared/fs-atomic.ts
|
|
781
|
+
import * as fs from "node:fs";
|
|
775
782
|
import * as path from "node:path";
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
783
|
+
function writeFileAtomic(filePath, data) {
|
|
784
|
+
const tmpPath = filePath + ".tmp";
|
|
785
|
+
const dir = path.dirname(filePath);
|
|
786
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
787
|
+
try {
|
|
788
|
+
fs.writeFileSync(tmpPath, data, "utf-8");
|
|
789
|
+
fs.renameSync(tmpPath, filePath);
|
|
790
|
+
} catch (err) {
|
|
791
|
+
try {
|
|
792
|
+
fs.unlinkSync(tmpPath);
|
|
793
|
+
} catch {}
|
|
794
|
+
throw err;
|
|
795
|
+
} finally {
|
|
796
|
+
try {
|
|
797
|
+
if (fs.existsSync(tmpPath))
|
|
798
|
+
fs.unlinkSync(tmpPath);
|
|
799
|
+
} catch {}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function writeJsonFileAtomic(filePath, data) {
|
|
803
|
+
writeFileAtomic(filePath, JSON.stringify(data, null, 2));
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/context/types.ts
|
|
807
|
+
var DEFAULT_CONTEXT_CONFIG = {
|
|
808
|
+
strategy: {
|
|
809
|
+
"co-explorer": "none",
|
|
810
|
+
"co-librarian": "none",
|
|
811
|
+
"co-observer": "none",
|
|
812
|
+
"co-fixer": "relevant",
|
|
813
|
+
"co-designer": "relevant",
|
|
814
|
+
"co-planner": "relevant",
|
|
815
|
+
"co-oracle": "summary",
|
|
816
|
+
"co-council": "summary",
|
|
817
|
+
"co-rule-user": "none",
|
|
818
|
+
"co-rule-project": "none",
|
|
819
|
+
"co-rule-app": "none"
|
|
820
|
+
},
|
|
821
|
+
maxFiles: 5,
|
|
822
|
+
maxDecisions: 10,
|
|
823
|
+
maxErrors: 5,
|
|
824
|
+
maxDependencies: 8,
|
|
825
|
+
dependencyPropagation: true,
|
|
826
|
+
summarizeMaxTokens: 2000,
|
|
827
|
+
relevantMessageWindow: 20,
|
|
828
|
+
maxErrorTotalChars: 600,
|
|
829
|
+
dependencyKeyOutputChars: 200
|
|
784
830
|
};
|
|
831
|
+
|
|
832
|
+
// src/task-manager/tracker.ts
|
|
833
|
+
var DEFAULT_STATS_WINDOW = 50;
|
|
834
|
+
var STATS_FILE = path2.join(os.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub", "stats.json");
|
|
785
835
|
function defaultStrategyFor(agent) {
|
|
786
|
-
return
|
|
836
|
+
return DEFAULT_CONTEXT_CONFIG.strategy[agent] ?? "none";
|
|
787
837
|
}
|
|
788
838
|
function computeStats(records, window = DEFAULT_STATS_WINDOW) {
|
|
789
839
|
const recent = records.slice(-Math.max(1, window));
|
|
@@ -1048,6 +1098,9 @@ class TaskTracker {
|
|
|
1048
1098
|
}
|
|
1049
1099
|
}
|
|
1050
1100
|
finalize(job) {
|
|
1101
|
+
if (job.status !== "running") {
|
|
1102
|
+
job.terminalReconciled = true;
|
|
1103
|
+
}
|
|
1051
1104
|
const latencyMs = Date.now() - job.createdAt;
|
|
1052
1105
|
if (!job.quality) {
|
|
1053
1106
|
job.quality = { latencyMs };
|
|
@@ -1071,8 +1124,7 @@ class TaskTracker {
|
|
|
1071
1124
|
persistStats(window = DEFAULT_STATS_WINDOW) {
|
|
1072
1125
|
try {
|
|
1073
1126
|
const recent = this.collectStatsRecords().slice(-Math.max(1, window));
|
|
1074
|
-
|
|
1075
|
-
fs.writeFileSync(STATS_FILE, JSON.stringify(recent, null, 2), "utf8");
|
|
1127
|
+
writeJsonFileAtomic(STATS_FILE, recent);
|
|
1076
1128
|
} catch {}
|
|
1077
1129
|
}
|
|
1078
1130
|
markCancelled(taskId) {
|
|
@@ -1265,35 +1317,44 @@ function isQualityEnabled(config) {
|
|
|
1265
1317
|
return config?.enabled ?? DEFAULT_QUALITY_CONFIG.enabled;
|
|
1266
1318
|
}
|
|
1267
1319
|
|
|
1268
|
-
// src/context/types.ts
|
|
1269
|
-
var DEFAULT_CONTEXT_CONFIG = {
|
|
1270
|
-
strategy: {
|
|
1271
|
-
"co-explorer": "none",
|
|
1272
|
-
"co-librarian": "none",
|
|
1273
|
-
"co-observer": "none",
|
|
1274
|
-
"co-fixer": "relevant",
|
|
1275
|
-
"co-designer": "relevant",
|
|
1276
|
-
"co-planner": "relevant",
|
|
1277
|
-
"co-oracle": "summary",
|
|
1278
|
-
"co-council": "summary",
|
|
1279
|
-
"co-rule-user": "none",
|
|
1280
|
-
"co-rule-project": "none",
|
|
1281
|
-
"co-rule-app": "none"
|
|
1282
|
-
},
|
|
1283
|
-
maxFiles: 5,
|
|
1284
|
-
maxDecisions: 10,
|
|
1285
|
-
maxErrors: 5,
|
|
1286
|
-
maxDependencies: 8,
|
|
1287
|
-
dependencyPropagation: true,
|
|
1288
|
-
summarizeMaxTokens: 2000,
|
|
1289
|
-
relevantMessageWindow: 20,
|
|
1290
|
-
maxErrorTotalChars: 600,
|
|
1291
|
-
dependencyKeyOutputChars: 200
|
|
1292
|
-
};
|
|
1293
|
-
|
|
1294
1320
|
// src/context/engine.ts
|
|
1295
1321
|
import * as fs2 from "node:fs";
|
|
1296
1322
|
|
|
1323
|
+
// src/shared/token-estimator.ts
|
|
1324
|
+
function estimateTokens(text) {
|
|
1325
|
+
let used = 0;
|
|
1326
|
+
for (let i = 0;i < text.length; i++) {
|
|
1327
|
+
const code = text.charCodeAt(i);
|
|
1328
|
+
const isCjk = code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 63744 && code <= 64255 || code >= 12352 && code <= 12543 || code >= 44032 && code <= 55215;
|
|
1329
|
+
used += isCjk ? 1 : 0.25;
|
|
1330
|
+
}
|
|
1331
|
+
return used;
|
|
1332
|
+
}
|
|
1333
|
+
function enforcePromptBudget(base, full, maxTokens) {
|
|
1334
|
+
if (estimateTokens(full) <= maxTokens)
|
|
1335
|
+
return full;
|
|
1336
|
+
return truncateByTokens(base, maxTokens);
|
|
1337
|
+
}
|
|
1338
|
+
function truncateByTokens(text, maxTokens) {
|
|
1339
|
+
if (!text || maxTokens <= 0)
|
|
1340
|
+
return "";
|
|
1341
|
+
if (estimateTokens(text) <= maxTokens)
|
|
1342
|
+
return text;
|
|
1343
|
+
let used = 0;
|
|
1344
|
+
let cut = text.length;
|
|
1345
|
+
for (let i = 0;i < text.length; i++) {
|
|
1346
|
+
const code = text.charCodeAt(i);
|
|
1347
|
+
const isCjk = code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 63744 && code <= 64255 || code >= 12352 && code <= 12543 || code >= 44032 && code <= 55215;
|
|
1348
|
+
used += isCjk ? 1 : 0.25;
|
|
1349
|
+
if (used > maxTokens) {
|
|
1350
|
+
cut = i;
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return text.slice(0, cut) + `
|
|
1355
|
+
… [正文已按 token 预算截断]`;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1297
1358
|
// src/context/extractor.ts
|
|
1298
1359
|
var KNOWN_EXTENSIONS = new Set([
|
|
1299
1360
|
"ts",
|
|
@@ -1481,39 +1542,6 @@ function extractErrors(messages, maxErrors, windowSize) {
|
|
|
1481
1542
|
}
|
|
1482
1543
|
return errors;
|
|
1483
1544
|
}
|
|
1484
|
-
function estimateTokens(text) {
|
|
1485
|
-
let used = 0;
|
|
1486
|
-
for (let i = 0;i < text.length; i++) {
|
|
1487
|
-
const code = text.charCodeAt(i);
|
|
1488
|
-
const isCjk = code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 63744 && code <= 64255 || code >= 12352 && code <= 12543 || code >= 44032 && code <= 55215;
|
|
1489
|
-
used += isCjk ? 1 : 0.25;
|
|
1490
|
-
}
|
|
1491
|
-
return used;
|
|
1492
|
-
}
|
|
1493
|
-
function enforcePromptBudget(base, full, maxTokens) {
|
|
1494
|
-
if (estimateTokens(full) <= maxTokens)
|
|
1495
|
-
return full;
|
|
1496
|
-
return truncateByTokens(base, maxTokens);
|
|
1497
|
-
}
|
|
1498
|
-
function truncateByTokens(text, maxTokens) {
|
|
1499
|
-
if (!text || maxTokens <= 0)
|
|
1500
|
-
return "";
|
|
1501
|
-
if (estimateTokens(text) <= maxTokens)
|
|
1502
|
-
return text;
|
|
1503
|
-
let used = 0;
|
|
1504
|
-
let cut = text.length;
|
|
1505
|
-
for (let i = 0;i < text.length; i++) {
|
|
1506
|
-
const code = text.charCodeAt(i);
|
|
1507
|
-
const isCjk = code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 63744 && code <= 64255 || code >= 12352 && code <= 12543 || code >= 44032 && code <= 55215;
|
|
1508
|
-
used += isCjk ? 1 : 0.25;
|
|
1509
|
-
if (used > maxTokens) {
|
|
1510
|
-
cut = i;
|
|
1511
|
-
break;
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
return text.slice(0, cut) + `
|
|
1515
|
-
… [正文已按 token 预算截断]`;
|
|
1516
|
-
}
|
|
1517
1545
|
|
|
1518
1546
|
// src/context/formatter.ts
|
|
1519
1547
|
var CONTEXT_MARKER_PATTERN = /<!-- CONTEXT:ID=([a-f0-9-]+) -->/;
|
|
@@ -1577,6 +1605,7 @@ function replaceMarkerWithContext(messageText, context) {
|
|
|
1577
1605
|
// src/context/engine.ts
|
|
1578
1606
|
class ContextEngine {
|
|
1579
1607
|
registry = new Map;
|
|
1608
|
+
registryTouchedAt = new Map;
|
|
1580
1609
|
dependencyCache = new Map;
|
|
1581
1610
|
inflight = new Map;
|
|
1582
1611
|
client;
|
|
@@ -1598,6 +1627,7 @@ class ContextEngine {
|
|
|
1598
1627
|
errors: [],
|
|
1599
1628
|
dependencies: []
|
|
1600
1629
|
});
|
|
1630
|
+
this.registryTouchedAt.set(contextId, Date.now());
|
|
1601
1631
|
return contextId;
|
|
1602
1632
|
}
|
|
1603
1633
|
async fillContextAsync(contextId, parentSessionId, args) {
|
|
@@ -1607,6 +1637,7 @@ class ContextEngine {
|
|
|
1607
1637
|
if (args.strategy === "none")
|
|
1608
1638
|
return;
|
|
1609
1639
|
context.strategy = args.strategy;
|
|
1640
|
+
this.touch(contextId);
|
|
1610
1641
|
try {
|
|
1611
1642
|
const windowSize = this.config.relevantMessageWindow;
|
|
1612
1643
|
const cacheKey = `${parentSessionId}::${windowSize}`;
|
|
@@ -1640,7 +1671,9 @@ class ContextEngine {
|
|
|
1640
1671
|
if (this.config.dependencyPropagation && this.dependencyCache.size > 0) {
|
|
1641
1672
|
context.dependencies = Array.from(this.dependencyCache.values()).slice(-this.config.maxDependencies);
|
|
1642
1673
|
}
|
|
1643
|
-
} catch {}
|
|
1674
|
+
} catch {} finally {
|
|
1675
|
+
this.touch(contextId);
|
|
1676
|
+
}
|
|
1644
1677
|
}
|
|
1645
1678
|
attachTruncatedBodies(files) {
|
|
1646
1679
|
if (files.length === 0)
|
|
@@ -1660,16 +1693,19 @@ class ContextEngine {
|
|
|
1660
1693
|
const contextId = markerMatch[1];
|
|
1661
1694
|
const context = this.registry.get(contextId);
|
|
1662
1695
|
if (!context) {
|
|
1696
|
+
this.registryTouchedAt.delete(contextId);
|
|
1663
1697
|
return messageText.replace(CONTEXT_MARKER_PATTERN, "");
|
|
1664
1698
|
}
|
|
1665
1699
|
const result = replaceMarkerWithContext(messageText, context);
|
|
1666
1700
|
this.registry.delete(contextId);
|
|
1701
|
+
this.registryTouchedAt.delete(contextId);
|
|
1667
1702
|
return result;
|
|
1668
1703
|
}
|
|
1669
1704
|
formatContextDetails(contextId) {
|
|
1670
1705
|
const context = this.registry.get(contextId);
|
|
1671
1706
|
if (!context)
|
|
1672
1707
|
return "";
|
|
1708
|
+
this.touch(contextId);
|
|
1673
1709
|
const parts = [];
|
|
1674
1710
|
const isSummary = context.strategy === "summary";
|
|
1675
1711
|
if (context.relevantFiles.length > 0) {
|
|
@@ -1781,18 +1817,46 @@ class ContextEngine {
|
|
|
1781
1817
|
}
|
|
1782
1818
|
}
|
|
1783
1819
|
}
|
|
1820
|
+
touch(contextId) {
|
|
1821
|
+
if (this.registry.has(contextId))
|
|
1822
|
+
this.registryTouchedAt.set(contextId, Date.now());
|
|
1823
|
+
}
|
|
1824
|
+
releaseContext(contextId) {
|
|
1825
|
+
this.registry.delete(contextId);
|
|
1826
|
+
this.registryTouchedAt.delete(contextId);
|
|
1827
|
+
}
|
|
1828
|
+
cleanupStaleContexts(maxAgeMs = 20 * 60 * 1000) {
|
|
1829
|
+
const now = Date.now();
|
|
1830
|
+
let removed = 0;
|
|
1831
|
+
for (const contextId of this.registry.keys()) {
|
|
1832
|
+
const touchedAt = this.registryTouchedAt.get(contextId);
|
|
1833
|
+
if (touchedAt === undefined) {
|
|
1834
|
+
this.registryTouchedAt.set(contextId, now);
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
if (now - touchedAt > maxAgeMs) {
|
|
1838
|
+
this.registry.delete(contextId);
|
|
1839
|
+
this.registryTouchedAt.delete(contextId);
|
|
1840
|
+
removed++;
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
return removed;
|
|
1844
|
+
}
|
|
1845
|
+
get registrySize() {
|
|
1846
|
+
return this.registry.size;
|
|
1847
|
+
}
|
|
1784
1848
|
}
|
|
1785
1849
|
|
|
1786
1850
|
// src/config/loader.ts
|
|
1787
1851
|
import * as fs3 from "node:fs";
|
|
1788
|
-
import * as
|
|
1852
|
+
import * as path3 from "node:path";
|
|
1789
1853
|
import * as os2 from "node:os";
|
|
1790
|
-
var USER_CONFIG_PATH =
|
|
1854
|
+
var USER_CONFIG_PATH = path3.join(os2.homedir(), ".config", "opencode", "oh-my-opencode-cohub.json");
|
|
1791
1855
|
function loadCoHubConfig(projectDir) {
|
|
1792
1856
|
const base = readJSON(USER_CONFIG_PATH);
|
|
1793
1857
|
if (!projectDir)
|
|
1794
1858
|
return base;
|
|
1795
|
-
const projectPath =
|
|
1859
|
+
const projectPath = path3.join(projectDir, ".opencode", "oh-my-opencode-cohub.json");
|
|
1796
1860
|
const project = readJSON(projectPath);
|
|
1797
1861
|
if (!project || Object.keys(project).length === 0)
|
|
1798
1862
|
return base;
|
|
@@ -1809,6 +1873,9 @@ function loadCoHubConfig(projectDir) {
|
|
|
1809
1873
|
if (project.quality) {
|
|
1810
1874
|
merged.quality = { ...base.quality, ...project.quality };
|
|
1811
1875
|
}
|
|
1876
|
+
if (project.diag) {
|
|
1877
|
+
merged.diag = { ...base.diag, ...project.diag };
|
|
1878
|
+
}
|
|
1812
1879
|
return merged;
|
|
1813
1880
|
}
|
|
1814
1881
|
function readJSON(filePath) {
|
|
@@ -1822,56 +1889,152 @@ function readJSON(filePath) {
|
|
|
1822
1889
|
}
|
|
1823
1890
|
}
|
|
1824
1891
|
|
|
1892
|
+
// src/context/diag-log.ts
|
|
1893
|
+
import * as fsp from "node:fs/promises";
|
|
1894
|
+
import * as os3 from "node:os";
|
|
1895
|
+
import * as path4 from "node:path";
|
|
1896
|
+
var MAX_BYTES = 256 * 1024;
|
|
1897
|
+
var KEEP_LINES = 30;
|
|
1898
|
+
var TAIL_WINDOW = 64 * 1024;
|
|
1899
|
+
var DEFAULT_CTX_DIAG_CONFIG = {
|
|
1900
|
+
enabled: false
|
|
1901
|
+
};
|
|
1902
|
+
function isCtxDiagEnabled(config, env) {
|
|
1903
|
+
const flag = env ?? process.env.COHUB_CTX_DIAG;
|
|
1904
|
+
if (flag !== undefined) {
|
|
1905
|
+
const v = String(flag).trim().toLowerCase();
|
|
1906
|
+
if (v === "1" || v === "true" || v === "yes" || v === "on")
|
|
1907
|
+
return true;
|
|
1908
|
+
if (v === "0" || v === "false" || v === "no" || v === "off")
|
|
1909
|
+
return false;
|
|
1910
|
+
}
|
|
1911
|
+
return config?.enabled ?? DEFAULT_CTX_DIAG_CONFIG.enabled;
|
|
1912
|
+
}
|
|
1913
|
+
function resolveCtxDiagPath(config) {
|
|
1914
|
+
const custom = config?.path;
|
|
1915
|
+
if (typeof custom === "string" && custom.trim() !== "")
|
|
1916
|
+
return custom;
|
|
1917
|
+
return path4.join(os3.tmpdir(), "opencode", "ctx-diag.log");
|
|
1918
|
+
}
|
|
1919
|
+
function resolveCtxDiagSettings(config, env) {
|
|
1920
|
+
return {
|
|
1921
|
+
enabled: isCtxDiagEnabled(config, env?.COHUB_CTX_DIAG),
|
|
1922
|
+
filePath: resolveCtxDiagPath(config)
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
var queue = Promise.resolve();
|
|
1926
|
+
var bytesSinceCompact = 0;
|
|
1927
|
+
async function compactTail(filePath, keepLines) {
|
|
1928
|
+
const fh = await fsp.open(filePath, "r");
|
|
1929
|
+
try {
|
|
1930
|
+
const { size } = await fh.stat();
|
|
1931
|
+
if (size === 0)
|
|
1932
|
+
return 0;
|
|
1933
|
+
const start = Math.max(0, size - TAIL_WINDOW);
|
|
1934
|
+
const len = size - start;
|
|
1935
|
+
const buf = Buffer.allocUnsafe(len);
|
|
1936
|
+
let filled = 0;
|
|
1937
|
+
let offset = start;
|
|
1938
|
+
while (filled < len) {
|
|
1939
|
+
const { bytesRead } = await fh.read(buf, filled, len - filled, offset);
|
|
1940
|
+
if (bytesRead === 0)
|
|
1941
|
+
break;
|
|
1942
|
+
filled += bytesRead;
|
|
1943
|
+
offset += bytesRead;
|
|
1944
|
+
}
|
|
1945
|
+
const lines = buf.subarray(0, filled).toString("utf-8").split(`
|
|
1946
|
+
`).filter((l) => l.trim() !== "");
|
|
1947
|
+
if (start > 0 && lines.length > 0)
|
|
1948
|
+
lines.shift();
|
|
1949
|
+
const kept = lines.slice(-keepLines);
|
|
1950
|
+
const text = kept.length > 0 ? kept.join(`
|
|
1951
|
+
`) + `
|
|
1952
|
+
` : "";
|
|
1953
|
+
await fsp.writeFile(filePath, text, { encoding: "utf-8", mode: 384 });
|
|
1954
|
+
return Buffer.byteLength(text, "utf-8");
|
|
1955
|
+
} finally {
|
|
1956
|
+
await fh.close();
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
function appendCtxDiag(record, filePath) {
|
|
1960
|
+
const line = JSON.stringify(record) + `
|
|
1961
|
+
`;
|
|
1962
|
+
bytesSinceCompact += Buffer.byteLength(line, "utf-8");
|
|
1963
|
+
const shouldCompact = bytesSinceCompact > MAX_BYTES;
|
|
1964
|
+
if (shouldCompact)
|
|
1965
|
+
bytesSinceCompact = 0;
|
|
1966
|
+
queue = queue.then(async () => {
|
|
1967
|
+
try {
|
|
1968
|
+
await fsp.mkdir(path4.dirname(filePath), { recursive: true });
|
|
1969
|
+
if (shouldCompact)
|
|
1970
|
+
await compactTail(filePath, KEEP_LINES);
|
|
1971
|
+
await fsp.appendFile(filePath, line, { encoding: "utf-8", mode: 384 });
|
|
1972
|
+
} catch {}
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1825
1976
|
// src/tools/council.ts
|
|
1826
1977
|
import { tool } from "@opencode-ai/plugin";
|
|
1827
1978
|
|
|
1828
1979
|
// src/utils/log.ts
|
|
1829
|
-
import * as
|
|
1830
|
-
import * as
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
const
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1980
|
+
import * as os4 from "os";
|
|
1981
|
+
import * as path6 from "path";
|
|
1982
|
+
|
|
1983
|
+
// src/shared/log.ts
|
|
1984
|
+
import * as fs4 from "node:fs";
|
|
1985
|
+
import * as fsp2 from "node:fs/promises";
|
|
1986
|
+
import * as path5 from "node:path";
|
|
1987
|
+
function createLogger(cfg) {
|
|
1988
|
+
const logDir = cfg.logDir;
|
|
1989
|
+
const prefix = cfg.prefix;
|
|
1990
|
+
const linePrefix = cfg.linePrefix ?? cfg.prefix;
|
|
1991
|
+
const dailyRotate = cfg.dailyRotate ?? true;
|
|
1992
|
+
const keepDays = cfg.keepDays ?? 7;
|
|
1993
|
+
const useAsync = cfg.async ?? true;
|
|
1994
|
+
let today = "";
|
|
1995
|
+
let logPath = "";
|
|
1996
|
+
let cleanedToday = false;
|
|
1997
|
+
function getLogPath() {
|
|
1998
|
+
if (!dailyRotate) {
|
|
1999
|
+
return path5.join(logDir, `${prefix}.log`);
|
|
2000
|
+
}
|
|
2001
|
+
const d = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
2002
|
+
if (d !== today) {
|
|
2003
|
+
today = d;
|
|
2004
|
+
logPath = path5.join(logDir, `${prefix}.${d}.log`);
|
|
2005
|
+
cleanedToday = false;
|
|
2006
|
+
}
|
|
2007
|
+
return logPath;
|
|
2008
|
+
}
|
|
2009
|
+
function cleanOldLogsOnce() {
|
|
2010
|
+
if (!dailyRotate)
|
|
2011
|
+
return;
|
|
2012
|
+
getLogPath();
|
|
2013
|
+
if (cleanedToday)
|
|
2014
|
+
return;
|
|
2015
|
+
cleanedToday = true;
|
|
2016
|
+
try {
|
|
2017
|
+
const cutoff = new Date;
|
|
2018
|
+
cutoff.setUTCDate(cutoff.getUTCDate() - keepDays);
|
|
2019
|
+
const cutoffStr = cutoff.toISOString().slice(0, 10).replace(/-/g, "");
|
|
2020
|
+
const files = fs4.readdirSync(logDir);
|
|
2021
|
+
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2022
|
+
const re = new RegExp(`^${escaped}\\.(\\d{8})\\.log$`);
|
|
2023
|
+
for (const file of files) {
|
|
2024
|
+
const match = file.match(re);
|
|
2025
|
+
if (!match)
|
|
2026
|
+
continue;
|
|
2027
|
+
if (match[1] < cutoffStr) {
|
|
2028
|
+
try {
|
|
2029
|
+
fs4.unlinkSync(path5.join(logDir, file));
|
|
2030
|
+
} catch {}
|
|
2031
|
+
}
|
|
1865
2032
|
}
|
|
1866
|
-
}
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
async function appendLog(tag, message, err) {
|
|
1870
|
-
try {
|
|
1871
|
-
const logPath = getLogPath();
|
|
1872
|
-
await fs4.mkdir(LOG_DIR, { recursive: true });
|
|
2033
|
+
} catch {}
|
|
2034
|
+
}
|
|
2035
|
+
function formatLine(tag, message, err) {
|
|
1873
2036
|
const now = new Date().toISOString();
|
|
1874
|
-
let line = `[${now}] [
|
|
2037
|
+
let line = `[${now}] [${linePrefix}] [${tag}] ${message}`;
|
|
1875
2038
|
if (err instanceof Error) {
|
|
1876
2039
|
line += `
|
|
1877
2040
|
stack: ${err.stack ?? err.message}`;
|
|
@@ -1881,11 +2044,45 @@ async function appendLog(tag, message, err) {
|
|
|
1881
2044
|
}
|
|
1882
2045
|
line += `
|
|
1883
2046
|
`;
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2047
|
+
return line;
|
|
2048
|
+
}
|
|
2049
|
+
if (useAsync) {
|
|
2050
|
+
async function appendLogAsync(tag, message, err) {
|
|
2051
|
+
try {
|
|
2052
|
+
const lp = getLogPath();
|
|
2053
|
+
await fsp2.mkdir(logDir, { recursive: true });
|
|
2054
|
+
const line = formatLine(tag, message, err);
|
|
2055
|
+
await fsp2.appendFile(lp, line, "utf-8");
|
|
2056
|
+
if (dailyRotate)
|
|
2057
|
+
cleanOldLogsOnce();
|
|
2058
|
+
} catch {}
|
|
2059
|
+
}
|
|
2060
|
+
return appendLogAsync;
|
|
2061
|
+
}
|
|
2062
|
+
function appendLogSync(tag, message, err) {
|
|
2063
|
+
try {
|
|
2064
|
+
const lp = getLogPath();
|
|
2065
|
+
fs4.mkdirSync(logDir, { recursive: true });
|
|
2066
|
+
const line = formatLine(tag, message, err);
|
|
2067
|
+
fs4.appendFileSync(lp, line, "utf-8");
|
|
2068
|
+
if (dailyRotate)
|
|
2069
|
+
cleanOldLogsOnce();
|
|
2070
|
+
} catch {}
|
|
2071
|
+
}
|
|
2072
|
+
return appendLogSync;
|
|
1887
2073
|
}
|
|
1888
2074
|
|
|
2075
|
+
// src/utils/log.ts
|
|
2076
|
+
var LOG_DIR = path6.join(os4.homedir(), ".local", "share", "opencode", "log");
|
|
2077
|
+
var appendLog = createLogger({
|
|
2078
|
+
logDir: LOG_DIR,
|
|
2079
|
+
prefix: "oh-my-opencode-cohub",
|
|
2080
|
+
linePrefix: "oh-my-opencode-cohub",
|
|
2081
|
+
dailyRotate: true,
|
|
2082
|
+
keepDays: 7,
|
|
2083
|
+
async: true
|
|
2084
|
+
});
|
|
2085
|
+
|
|
1889
2086
|
// src/tools/model-stats.ts
|
|
1890
2087
|
var DEFAULT_MODEL_STATS_CONFIG = {
|
|
1891
2088
|
windowSize: 20,
|
|
@@ -2025,186 +2222,18 @@ function getAdaptiveParams(key, cfg = config2) {
|
|
|
2025
2222
|
return { ...st.current };
|
|
2026
2223
|
}
|
|
2027
2224
|
|
|
2028
|
-
// src/
|
|
2029
|
-
|
|
2225
|
+
// src/shared/model.ts
|
|
2226
|
+
function shortModelLabel(model) {
|
|
2227
|
+
return model.split("/").pop() ?? model;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// src/shared/consensus.ts
|
|
2030
2231
|
var RETRY_LIMITS = {
|
|
2031
2232
|
empty: 3,
|
|
2032
2233
|
timeout: 1,
|
|
2033
2234
|
error: 0,
|
|
2034
2235
|
"quality-low": 1
|
|
2035
2236
|
};
|
|
2036
|
-
|
|
2037
|
-
class OperationTimeoutError extends Error {
|
|
2038
|
-
constructor(message) {
|
|
2039
|
-
super(message);
|
|
2040
|
-
this.name = "OperationTimeoutError";
|
|
2041
|
-
}
|
|
2042
|
-
}
|
|
2043
|
-
function isTextPart(part) {
|
|
2044
|
-
return (part.type === "text" || part.type === "reasoning") && typeof part.text === "string";
|
|
2045
|
-
}
|
|
2046
|
-
function shortModelLabel(model) {
|
|
2047
|
-
return model.split("/").pop() ?? model;
|
|
2048
|
-
}
|
|
2049
|
-
function parseModelReference(model) {
|
|
2050
|
-
const slashIndex = model.indexOf("/");
|
|
2051
|
-
if (slashIndex <= 0 || slashIndex >= model.length - 1)
|
|
2052
|
-
return null;
|
|
2053
|
-
return {
|
|
2054
|
-
providerID: model.slice(0, slashIndex),
|
|
2055
|
-
modelID: model.slice(slashIndex + 1)
|
|
2056
|
-
};
|
|
2057
|
-
}
|
|
2058
|
-
async function abortSession(client, sessionId) {
|
|
2059
|
-
try {
|
|
2060
|
-
await client.session.abort({ path: { id: sessionId } });
|
|
2061
|
-
} catch (err) {
|
|
2062
|
-
appendLog("abortSession", "中止会话失败", err);
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
async function promptWithTimeout(client, path, body, timeoutMs, directory) {
|
|
2066
|
-
const sessionId = path.id;
|
|
2067
|
-
let timer;
|
|
2068
|
-
try {
|
|
2069
|
-
const promptPromise = client.session.prompt({
|
|
2070
|
-
path,
|
|
2071
|
-
body,
|
|
2072
|
-
query: directory ? { directory } : undefined
|
|
2073
|
-
});
|
|
2074
|
-
promptPromise.catch(() => {});
|
|
2075
|
-
const racers = [promptPromise];
|
|
2076
|
-
if (timeoutMs > 0) {
|
|
2077
|
-
racers.push(new Promise((_, reject) => {
|
|
2078
|
-
timer = setTimeout(() => {
|
|
2079
|
-
reject(new OperationTimeoutError(`[oh-my-opencode-cohub] Prompt timed out after ${timeoutMs}ms`));
|
|
2080
|
-
}, timeoutMs);
|
|
2081
|
-
}));
|
|
2082
|
-
}
|
|
2083
|
-
await Promise.race(racers);
|
|
2084
|
-
} catch (error) {
|
|
2085
|
-
if (error instanceof OperationTimeoutError) {
|
|
2086
|
-
await abortSession(client, sessionId);
|
|
2087
|
-
}
|
|
2088
|
-
throw error;
|
|
2089
|
-
} finally {
|
|
2090
|
-
if (timer !== undefined)
|
|
2091
|
-
clearTimeout(timer);
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
async function extractSessionResult(client, sessionId, options) {
|
|
2095
|
-
const includeReasoning = options?.includeReasoning ?? true;
|
|
2096
|
-
const lastTextOnly = options?.lastTextOnly === true;
|
|
2097
|
-
const messagesResult = await client.session.messages({
|
|
2098
|
-
path: { id: sessionId }
|
|
2099
|
-
});
|
|
2100
|
-
const messages = messagesResult.data ?? [];
|
|
2101
|
-
const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
|
|
2102
|
-
const extractedContent = [];
|
|
2103
|
-
for (const message of assistantMessages) {
|
|
2104
|
-
for (const part of message.parts ?? []) {
|
|
2105
|
-
if (!isTextPart(part))
|
|
2106
|
-
continue;
|
|
2107
|
-
const allowed = includeReasoning || part.type === "text";
|
|
2108
|
-
if (allowed && part.text) {
|
|
2109
|
-
extractedContent.push(part.text);
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
const texts = extractedContent.filter((t) => t.length > 0);
|
|
2114
|
-
const text = lastTextOnly ? texts.length > 0 ? texts[texts.length - 1] : "" : texts.join(`
|
|
2115
|
-
|
|
2116
|
-
`);
|
|
2117
|
-
return { text, empty: text.length === 0 };
|
|
2118
|
-
}
|
|
2119
|
-
function formatCouncillorPrompt(userPrompt, councillorPrompt) {
|
|
2120
|
-
if (!councillorPrompt)
|
|
2121
|
-
return userPrompt;
|
|
2122
|
-
return `${councillorPrompt}
|
|
2123
|
-
|
|
2124
|
-
---
|
|
2125
|
-
|
|
2126
|
-
${userPrompt}`;
|
|
2127
|
-
}
|
|
2128
|
-
function formatCouncillorResults(originalPrompt, councillorResults) {
|
|
2129
|
-
const completedWithResults = councillorResults.filter((cr) => cr.status === "completed" && cr.result);
|
|
2130
|
-
const councillorSection = completedWithResults.map((cr) => {
|
|
2131
|
-
const shortModel = shortModelLabel(cr.model);
|
|
2132
|
-
return `**${cr.name}** (${shortModel}):
|
|
2133
|
-
${cr.result}`;
|
|
2134
|
-
}).join(`
|
|
2135
|
-
|
|
2136
|
-
`);
|
|
2137
|
-
const failedEntries = councillorResults.filter((cr) => cr.status !== "completed");
|
|
2138
|
-
const failedSection = failedEntries.map((cr) => `**${cr.name}**: ${cr.status} — ${cr.error ?? "Unknown"}`).join(`
|
|
2139
|
-
`);
|
|
2140
|
-
if (completedWithResults.length === 0) {
|
|
2141
|
-
const errorDetails = councillorResults.map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}): ${cr.status} — ${cr.error ?? "Unknown"}`).join(`
|
|
2142
|
-
`);
|
|
2143
|
-
return [
|
|
2144
|
-
"---",
|
|
2145
|
-
"",
|
|
2146
|
-
"**Original Prompt**:",
|
|
2147
|
-
originalPrompt,
|
|
2148
|
-
"",
|
|
2149
|
-
"---",
|
|
2150
|
-
"",
|
|
2151
|
-
"**Councillor Responses**:",
|
|
2152
|
-
"All councillors failed to produce output:",
|
|
2153
|
-
errorDetails,
|
|
2154
|
-
"",
|
|
2155
|
-
"Please generate a response based on the original prompt alone."
|
|
2156
|
-
].join(`
|
|
2157
|
-
`);
|
|
2158
|
-
}
|
|
2159
|
-
const parts = [
|
|
2160
|
-
"---",
|
|
2161
|
-
"",
|
|
2162
|
-
"**Original Prompt**:",
|
|
2163
|
-
originalPrompt,
|
|
2164
|
-
"",
|
|
2165
|
-
"---",
|
|
2166
|
-
"",
|
|
2167
|
-
"**Councillor Responses**:",
|
|
2168
|
-
councillorSection
|
|
2169
|
-
];
|
|
2170
|
-
if (failedSection) {
|
|
2171
|
-
parts.push("", "---", "", "**Failed/Timed-out Councillors**:", failedSection);
|
|
2172
|
-
}
|
|
2173
|
-
parts.push("", "---", "", "You MUST follow the Synthesis Process steps before producing output: " + "review each councillor response individually, then produce the required output " + "with a synthesized Council Response, per-councillor details using their exact names, " + "and a Council Summary with consensus confidence rating (unanimous, majority, or split).");
|
|
2174
|
-
return parts.join(`
|
|
2175
|
-
`);
|
|
2176
|
-
}
|
|
2177
|
-
function buildDeliberationPrompt(originalPrompt, previousRound, currentName) {
|
|
2178
|
-
const others = previousRound.filter((cr) => cr.status === "completed" && cr.result && cr.name !== currentName).map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}):
|
|
2179
|
-
${cr.result}`).join(`
|
|
2180
|
-
|
|
2181
|
-
`);
|
|
2182
|
-
if (!others)
|
|
2183
|
-
return originalPrompt;
|
|
2184
|
-
return [
|
|
2185
|
-
originalPrompt,
|
|
2186
|
-
"",
|
|
2187
|
-
"---",
|
|
2188
|
-
"",
|
|
2189
|
-
"以下是其他成员第一轮意见,请回应并修正或坚持你的立场:",
|
|
2190
|
-
"",
|
|
2191
|
-
others
|
|
2192
|
-
].join(`
|
|
2193
|
-
`);
|
|
2194
|
-
}
|
|
2195
|
-
function formatModelComposition(councillorResults) {
|
|
2196
|
-
return councillorResults.map((cr) => `${cr.name}: ${shortModelLabel(cr.model)}`).join(", ");
|
|
2197
|
-
}
|
|
2198
|
-
function adaptiveKey(config) {
|
|
2199
|
-
return `${config.variant ?? "default"}\x00${config.model}`;
|
|
2200
|
-
}
|
|
2201
|
-
function classifyFailure(message) {
|
|
2202
|
-
if (message.includes("Empty response from provider"))
|
|
2203
|
-
return "empty";
|
|
2204
|
-
if (message.includes("timed out") || message.includes("OperationTimeoutError"))
|
|
2205
|
-
return "timeout";
|
|
2206
|
-
return "error";
|
|
2207
|
-
}
|
|
2208
2237
|
var CONCLUSION_MARKERS = [
|
|
2209
2238
|
"综上",
|
|
2210
2239
|
"结论",
|
|
@@ -2240,15 +2269,6 @@ var ENTITY_MARKERS = [
|
|
|
2240
2269
|
"recommendation",
|
|
2241
2270
|
"key entity"
|
|
2242
2271
|
];
|
|
2243
|
-
function extractCouncillorSummary(text) {
|
|
2244
|
-
const sentences = text.split(/(?<=[。!?!?;;])\s*|\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2245
|
-
const conclusions = sentences.filter((s) => CONCLUSION_MARKERS.some((m) => s.toLowerCase().includes(m)));
|
|
2246
|
-
const entities = sentences.filter((s) => ENTITY_MARKERS.some((m) => s.toLowerCase().includes(m)));
|
|
2247
|
-
return {
|
|
2248
|
-
conclusions: conclusions.length > 0 ? conclusions : sentences.length > 0 ? [sentences[sentences.length - 1]] : [],
|
|
2249
|
-
entities
|
|
2250
|
-
};
|
|
2251
|
-
}
|
|
2252
2272
|
var STOPWORDS = new Set([
|
|
2253
2273
|
"the",
|
|
2254
2274
|
"a",
|
|
@@ -2317,6 +2337,7 @@ var STOPWORDS = new Set([
|
|
|
2317
2337
|
"entity",
|
|
2318
2338
|
"error"
|
|
2319
2339
|
]);
|
|
2340
|
+
var VARIANT_ORDER = ["max", "high", "medium", "low"];
|
|
2320
2341
|
function tokenize(text) {
|
|
2321
2342
|
return text.toLowerCase().split(/[^a-z0-9\u4e00-\u9fff]+/).filter((t) => t.length > 0 && !STOPWORDS.has(t));
|
|
2322
2343
|
}
|
|
@@ -2329,6 +2350,77 @@ function jaccard(x, y) {
|
|
|
2329
2350
|
const union = new Set([...xs, ...ys]).size;
|
|
2330
2351
|
return union === 0 ? 0 : inter / union;
|
|
2331
2352
|
}
|
|
2353
|
+
function downgradeVariant(variant) {
|
|
2354
|
+
if (!variant)
|
|
2355
|
+
return;
|
|
2356
|
+
const idx = VARIANT_ORDER.indexOf(variant);
|
|
2357
|
+
if (idx < 0 || idx >= VARIANT_ORDER.length - 1)
|
|
2358
|
+
return;
|
|
2359
|
+
return VARIANT_ORDER[idx + 1];
|
|
2360
|
+
}
|
|
2361
|
+
function parseModelReference(model) {
|
|
2362
|
+
const slashIndex = model.indexOf("/");
|
|
2363
|
+
if (slashIndex <= 0 || slashIndex >= model.length - 1)
|
|
2364
|
+
return null;
|
|
2365
|
+
return {
|
|
2366
|
+
providerID: model.slice(0, slashIndex),
|
|
2367
|
+
modelID: model.slice(slashIndex + 1)
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
function formatModelComposition(councillorResults) {
|
|
2371
|
+
return councillorResults.map((cr) => `${cr.name}: ${shortModelLabel(cr.model)}`).join(", ");
|
|
2372
|
+
}
|
|
2373
|
+
function adaptiveKey(config) {
|
|
2374
|
+
return `${config.variant ?? "default"}\x00${config.model}`;
|
|
2375
|
+
}
|
|
2376
|
+
function formatCouncillorPrompt(userPrompt, councillorPrompt) {
|
|
2377
|
+
if (!councillorPrompt)
|
|
2378
|
+
return userPrompt;
|
|
2379
|
+
return `${councillorPrompt}
|
|
2380
|
+
|
|
2381
|
+
---
|
|
2382
|
+
|
|
2383
|
+
${userPrompt}`;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
class RetryBudget {
|
|
2387
|
+
retriesLeft;
|
|
2388
|
+
maxElapsedMs;
|
|
2389
|
+
startedAt;
|
|
2390
|
+
constructor(maxTotalRetries, maxElapsedMs, startedAt = Date.now()) {
|
|
2391
|
+
this.retriesLeft = Math.max(0, maxTotalRetries);
|
|
2392
|
+
this.maxElapsedMs = maxElapsedMs;
|
|
2393
|
+
this.startedAt = startedAt;
|
|
2394
|
+
}
|
|
2395
|
+
elapsedMs() {
|
|
2396
|
+
return Date.now() - this.startedAt;
|
|
2397
|
+
}
|
|
2398
|
+
expired() {
|
|
2399
|
+
return this.elapsedMs() >= this.maxElapsedMs;
|
|
2400
|
+
}
|
|
2401
|
+
consumeRetry() {
|
|
2402
|
+
if (this.retriesLeft <= 0 || this.expired())
|
|
2403
|
+
return false;
|
|
2404
|
+
this.retriesLeft -= 1;
|
|
2405
|
+
return true;
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
function classifyFailure(message) {
|
|
2409
|
+
if (message.includes("Empty response from provider"))
|
|
2410
|
+
return "empty";
|
|
2411
|
+
if (message.includes("timed out") || message.includes("OperationTimeoutError"))
|
|
2412
|
+
return "timeout";
|
|
2413
|
+
return "error";
|
|
2414
|
+
}
|
|
2415
|
+
function extractCouncillorSummary(text) {
|
|
2416
|
+
const sentences = text.split(/(?<=[。!?!?;;])\s*|\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2417
|
+
const conclusions = sentences.filter((s) => CONCLUSION_MARKERS.some((m) => s.toLowerCase().includes(m)));
|
|
2418
|
+
const entities = sentences.filter((s) => ENTITY_MARKERS.some((m) => s.toLowerCase().includes(m)));
|
|
2419
|
+
return {
|
|
2420
|
+
conclusions: conclusions.length > 0 ? conclusions : sentences.length > 0 ? [sentences[sentences.length - 1]] : [],
|
|
2421
|
+
entities
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2332
2424
|
function pairwiseAgreement(a, b) {
|
|
2333
2425
|
const aConcl = tokenize(a.conclusions.join(" "));
|
|
2334
2426
|
const bConcl = tokenize(b.conclusions.join(" "));
|
|
@@ -2368,15 +2460,6 @@ function decideRetryAction(category, consecutiveStreak, categoryRetriesUsed, bud
|
|
|
2368
2460
|
return "give-up";
|
|
2369
2461
|
return "retry";
|
|
2370
2462
|
}
|
|
2371
|
-
var VARIANT_ORDER = ["max", "high", "medium", "low"];
|
|
2372
|
-
function downgradeVariant(variant) {
|
|
2373
|
-
if (!variant)
|
|
2374
|
-
return;
|
|
2375
|
-
const idx = VARIANT_ORDER.indexOf(variant);
|
|
2376
|
-
if (idx < 0 || idx >= VARIANT_ORDER.length - 1)
|
|
2377
|
-
return;
|
|
2378
|
-
return VARIANT_ORDER[idx + 1];
|
|
2379
|
-
}
|
|
2380
2463
|
function appendRetryContext(prompt, category) {
|
|
2381
2464
|
if (category === "timeout") {
|
|
2382
2465
|
return `${prompt}
|
|
@@ -2390,29 +2473,151 @@ function appendRetryContext(prompt, category) {
|
|
|
2390
2473
|
}
|
|
2391
2474
|
return prompt;
|
|
2392
2475
|
}
|
|
2476
|
+
function formatCouncillorResults(originalPrompt, councillorResults) {
|
|
2477
|
+
const completedWithResults = councillorResults.filter((cr) => cr.status === "completed" && cr.result);
|
|
2478
|
+
const councillorSection = completedWithResults.map((cr) => {
|
|
2479
|
+
const shortModel = shortModelLabel(cr.model);
|
|
2480
|
+
return `**${cr.name}** (${shortModel}):
|
|
2481
|
+
${cr.result}`;
|
|
2482
|
+
}).join(`
|
|
2393
2483
|
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2484
|
+
`);
|
|
2485
|
+
const failedEntries = councillorResults.filter((cr) => cr.status !== "completed" || !cr.result);
|
|
2486
|
+
const failedSection = failedEntries.map((cr) => `**${cr.name}**: ${cr.status} — ${cr.error ?? "Unknown"}`).join(`
|
|
2487
|
+
`);
|
|
2488
|
+
if (completedWithResults.length === 0) {
|
|
2489
|
+
const errorDetails = councillorResults.map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}): ${cr.status} — ${cr.error ?? "Unknown"}`).join(`
|
|
2490
|
+
`);
|
|
2491
|
+
return [
|
|
2492
|
+
"---",
|
|
2493
|
+
"",
|
|
2494
|
+
"**Original Prompt**:",
|
|
2495
|
+
originalPrompt,
|
|
2496
|
+
"",
|
|
2497
|
+
"---",
|
|
2498
|
+
"",
|
|
2499
|
+
"**Councillor Responses**:",
|
|
2500
|
+
"All councillors failed to produce output:",
|
|
2501
|
+
errorDetails,
|
|
2502
|
+
"",
|
|
2503
|
+
"Please generate a response based on the original prompt alone."
|
|
2504
|
+
].join(`
|
|
2505
|
+
`);
|
|
2402
2506
|
}
|
|
2403
|
-
|
|
2404
|
-
|
|
2507
|
+
const parts = [
|
|
2508
|
+
"---",
|
|
2509
|
+
"",
|
|
2510
|
+
"**Original Prompt**:",
|
|
2511
|
+
originalPrompt,
|
|
2512
|
+
"",
|
|
2513
|
+
"---",
|
|
2514
|
+
"",
|
|
2515
|
+
"**Councillor Responses**:",
|
|
2516
|
+
councillorSection
|
|
2517
|
+
];
|
|
2518
|
+
if (failedSection) {
|
|
2519
|
+
parts.push("", "---", "", "**Failed/Timed-out Councillors**:", failedSection);
|
|
2405
2520
|
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2521
|
+
parts.push("", "---", "", "You MUST follow the Synthesis Process steps before producing output: " + "review each councillor response individually, then produce the required output " + "with a synthesized Council Response, per-councillor details using their exact names, " + "and a Council Summary with consensus confidence rating (unanimous, majority, or split).");
|
|
2522
|
+
return parts.join(`
|
|
2523
|
+
`);
|
|
2524
|
+
}
|
|
2525
|
+
function buildDeliberationPrompt(originalPrompt, previousRound, currentName) {
|
|
2526
|
+
const others = previousRound.filter((cr) => cr.status === "completed" && cr.result && cr.name !== currentName).map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}):
|
|
2527
|
+
${cr.result}`).join(`
|
|
2528
|
+
|
|
2529
|
+
`);
|
|
2530
|
+
if (!others)
|
|
2531
|
+
return originalPrompt;
|
|
2532
|
+
return [
|
|
2533
|
+
originalPrompt,
|
|
2534
|
+
"",
|
|
2535
|
+
"---",
|
|
2536
|
+
"",
|
|
2537
|
+
"以下是其他成员第一轮意见,请回应并修正或坚持你的立场:",
|
|
2538
|
+
"",
|
|
2539
|
+
others
|
|
2540
|
+
].join(`
|
|
2541
|
+
`);
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
// src/shared/constants.ts
|
|
2545
|
+
var COUNCIL_TIMEOUT_MS = 180000;
|
|
2546
|
+
var STALE_TASK_MS = 30 * 60 * 1000;
|
|
2547
|
+
|
|
2548
|
+
// src/tools/council.ts
|
|
2549
|
+
var z = tool.schema;
|
|
2550
|
+
|
|
2551
|
+
class OperationTimeoutError extends Error {
|
|
2552
|
+
constructor(message) {
|
|
2553
|
+
super(message);
|
|
2554
|
+
this.name = "OperationTimeoutError";
|
|
2408
2555
|
}
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2556
|
+
}
|
|
2557
|
+
function isTextPart(part) {
|
|
2558
|
+
return (part.type === "text" || part.type === "reasoning") && typeof part.text === "string";
|
|
2559
|
+
}
|
|
2560
|
+
async function abortSession(client, sessionId) {
|
|
2561
|
+
try {
|
|
2562
|
+
await client.session.abort({ path: { id: sessionId } });
|
|
2563
|
+
} catch (err) {
|
|
2564
|
+
appendLog("abortSession", "中止会话失败", err);
|
|
2414
2565
|
}
|
|
2415
2566
|
}
|
|
2567
|
+
async function promptWithTimeout(client, path, body, timeoutMs, directory) {
|
|
2568
|
+
const sessionId = path.id;
|
|
2569
|
+
let timer;
|
|
2570
|
+
try {
|
|
2571
|
+
const promptPromise = client.session.prompt({
|
|
2572
|
+
path,
|
|
2573
|
+
body,
|
|
2574
|
+
query: directory ? { directory } : undefined
|
|
2575
|
+
});
|
|
2576
|
+
promptPromise.catch(() => {});
|
|
2577
|
+
const racers = [promptPromise];
|
|
2578
|
+
if (timeoutMs > 0) {
|
|
2579
|
+
racers.push(new Promise((_, reject) => {
|
|
2580
|
+
timer = setTimeout(() => {
|
|
2581
|
+
reject(new OperationTimeoutError(`[oh-my-opencode-cohub] Prompt timed out after ${timeoutMs}ms`));
|
|
2582
|
+
}, timeoutMs);
|
|
2583
|
+
}));
|
|
2584
|
+
}
|
|
2585
|
+
await Promise.race(racers);
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
if (error instanceof OperationTimeoutError) {
|
|
2588
|
+
await abortSession(client, sessionId);
|
|
2589
|
+
}
|
|
2590
|
+
throw error;
|
|
2591
|
+
} finally {
|
|
2592
|
+
if (timer !== undefined)
|
|
2593
|
+
clearTimeout(timer);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
async function extractSessionResult(client, sessionId, options) {
|
|
2597
|
+
const includeReasoning = options?.includeReasoning ?? true;
|
|
2598
|
+
const lastTextOnly = options?.lastTextOnly === true;
|
|
2599
|
+
const messagesResult = await client.session.messages({
|
|
2600
|
+
path: { id: sessionId }
|
|
2601
|
+
});
|
|
2602
|
+
const messages = messagesResult.data ?? [];
|
|
2603
|
+
const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
|
|
2604
|
+
const extractedContent = [];
|
|
2605
|
+
for (const message of assistantMessages) {
|
|
2606
|
+
for (const part of message.parts ?? []) {
|
|
2607
|
+
if (!isTextPart(part))
|
|
2608
|
+
continue;
|
|
2609
|
+
const allowed = includeReasoning || part.type === "text";
|
|
2610
|
+
if (allowed && part.text) {
|
|
2611
|
+
extractedContent.push(part.text);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
const texts = extractedContent.filter((t) => t.length > 0);
|
|
2616
|
+
const text = lastTextOnly ? texts.length > 0 ? texts[texts.length - 1] : "" : texts.join(`
|
|
2617
|
+
|
|
2618
|
+
`);
|
|
2619
|
+
return { text, empty: text.length === 0 };
|
|
2620
|
+
}
|
|
2416
2621
|
|
|
2417
2622
|
class CouncilManager {
|
|
2418
2623
|
client;
|
|
@@ -2441,7 +2646,7 @@ class CouncilManager {
|
|
|
2441
2646
|
councillorResults: []
|
|
2442
2647
|
};
|
|
2443
2648
|
}
|
|
2444
|
-
const timeout = this.config.timeout ??
|
|
2649
|
+
const timeout = this.config.timeout ?? COUNCIL_TIMEOUT_MS;
|
|
2445
2650
|
const executionMode = this.config.councillor_execution_mode ?? "parallel";
|
|
2446
2651
|
const maxRounds = this.config.max_rounds ?? 2;
|
|
2447
2652
|
const consensusThreshold = this.config.consensus_threshold ?? 0.6;
|
|
@@ -2739,252 +2944,10 @@ function createCloseJobTool(ctx, tracker, onClosed) {
|
|
|
2739
2944
|
return { close_job };
|
|
2740
2945
|
}
|
|
2741
2946
|
|
|
2742
|
-
// src/tools/ocr-review.ts
|
|
2743
|
-
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
2744
|
-
import { execSync, spawn } from "node:child_process";
|
|
2745
|
-
var z3 = tool3.schema;
|
|
2746
|
-
var _ocrAvailable;
|
|
2747
|
-
async function detectOcrCli(force = false) {
|
|
2748
|
-
if (!force && _ocrAvailable !== undefined)
|
|
2749
|
-
return _ocrAvailable;
|
|
2750
|
-
try {
|
|
2751
|
-
execSync("ocr version", { stdio: "pipe", timeout: 5000 });
|
|
2752
|
-
_ocrAvailable = true;
|
|
2753
|
-
} catch {
|
|
2754
|
-
_ocrAvailable = false;
|
|
2755
|
-
}
|
|
2756
|
-
return _ocrAvailable;
|
|
2757
|
-
}
|
|
2758
|
-
function buildOcrArgs(input, projectDir) {
|
|
2759
|
-
const args = [
|
|
2760
|
-
"review",
|
|
2761
|
-
"--format",
|
|
2762
|
-
"json",
|
|
2763
|
-
"--audience",
|
|
2764
|
-
"agent",
|
|
2765
|
-
"--repo",
|
|
2766
|
-
projectDir,
|
|
2767
|
-
"--from",
|
|
2768
|
-
input.from,
|
|
2769
|
-
"--to",
|
|
2770
|
-
input.to || "HEAD",
|
|
2771
|
-
"--max-git-procs",
|
|
2772
|
-
"8"
|
|
2773
|
-
];
|
|
2774
|
-
if (input.background) {
|
|
2775
|
-
args.push("--background", input.background);
|
|
2776
|
-
}
|
|
2777
|
-
if (input.preview !== false) {
|
|
2778
|
-
args.push("--preview");
|
|
2779
|
-
}
|
|
2780
|
-
return args;
|
|
2781
|
-
}
|
|
2782
|
-
var OCR_TIMEOUT_MS = 600000;
|
|
2783
|
-
var MAX_STDOUT_CHARS = 10 * 1024 * 1024;
|
|
2784
|
-
async function executeOcrReview(args, projectDir) {
|
|
2785
|
-
return new Promise((resolve) => {
|
|
2786
|
-
let stdout = "";
|
|
2787
|
-
let stderr = "";
|
|
2788
|
-
let stdoutTruncated = false;
|
|
2789
|
-
const child = spawn("ocr", args, {
|
|
2790
|
-
cwd: projectDir,
|
|
2791
|
-
shell: false,
|
|
2792
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2793
|
-
});
|
|
2794
|
-
child.stdout.on("data", (chunk) => {
|
|
2795
|
-
if (stdoutTruncated)
|
|
2796
|
-
return;
|
|
2797
|
-
const remaining = MAX_STDOUT_CHARS - stdout.length;
|
|
2798
|
-
if (remaining <= 0) {
|
|
2799
|
-
stdoutTruncated = true;
|
|
2800
|
-
return;
|
|
2801
|
-
}
|
|
2802
|
-
stdout += chunk.toString("utf8").slice(0, remaining);
|
|
2803
|
-
});
|
|
2804
|
-
child.stderr.on("data", (chunk) => {
|
|
2805
|
-
stderr += chunk.toString("utf8");
|
|
2806
|
-
});
|
|
2807
|
-
let sigkillTimer;
|
|
2808
|
-
const timer = setTimeout(() => {
|
|
2809
|
-
child.kill("SIGTERM");
|
|
2810
|
-
sigkillTimer = setTimeout(() => {
|
|
2811
|
-
if (child.exitCode === null)
|
|
2812
|
-
child.kill("SIGKILL");
|
|
2813
|
-
}, 2000).unref();
|
|
2814
|
-
}, OCR_TIMEOUT_MS);
|
|
2815
|
-
timer.unref();
|
|
2816
|
-
child.on("error", (error) => {
|
|
2817
|
-
clearTimeout(timer);
|
|
2818
|
-
if (sigkillTimer)
|
|
2819
|
-
clearTimeout(sigkillTimer);
|
|
2820
|
-
const isENOENT = error.code === "ENOENT";
|
|
2821
|
-
const message = isENOENT ? "ENOENT: OCR CLI not found" : `Failed to start OpenCodeReview: ${error.message}`;
|
|
2822
|
-
resolve({
|
|
2823
|
-
success: false,
|
|
2824
|
-
stdout: "",
|
|
2825
|
-
stderr: message,
|
|
2826
|
-
truncated: false,
|
|
2827
|
-
exitCode: null
|
|
2828
|
-
});
|
|
2829
|
-
});
|
|
2830
|
-
child.on("close", (exitCode) => {
|
|
2831
|
-
clearTimeout(timer);
|
|
2832
|
-
if (sigkillTimer)
|
|
2833
|
-
clearTimeout(sigkillTimer);
|
|
2834
|
-
resolve({
|
|
2835
|
-
success: exitCode === 0,
|
|
2836
|
-
stdout,
|
|
2837
|
-
stderr,
|
|
2838
|
-
truncated: stdoutTruncated,
|
|
2839
|
-
exitCode
|
|
2840
|
-
});
|
|
2841
|
-
});
|
|
2842
|
-
});
|
|
2843
|
-
}
|
|
2844
|
-
var MAX_COMMENTS = 15;
|
|
2845
|
-
var MAX_WARNINGS = 5;
|
|
2846
|
-
var MAX_FIELD_LENGTH = 200;
|
|
2847
|
-
function truncateField(value, maxLen) {
|
|
2848
|
-
if (value.length <= maxLen)
|
|
2849
|
-
return value;
|
|
2850
|
-
return value.slice(0, maxLen) + "...";
|
|
2851
|
-
}
|
|
2852
|
-
function formatOcrOutput(raw) {
|
|
2853
|
-
let parsed;
|
|
2854
|
-
try {
|
|
2855
|
-
parsed = JSON.parse(raw);
|
|
2856
|
-
} catch {
|
|
2857
|
-
return raw;
|
|
2858
|
-
}
|
|
2859
|
-
const lines = [];
|
|
2860
|
-
lines.push("--- OCR Review Result ---");
|
|
2861
|
-
lines.push(`Status: ${parsed.status ?? "unknown"}`);
|
|
2862
|
-
if (parsed.llm) {
|
|
2863
|
-
lines.push(`LLM: ${parsed.llm.provider ?? "?"}/${parsed.llm.model ?? "?"}`);
|
|
2864
|
-
}
|
|
2865
|
-
if (parsed.session_id) {
|
|
2866
|
-
lines.push(`Session ID: ${parsed.session_id}`);
|
|
2867
|
-
}
|
|
2868
|
-
lines.push("");
|
|
2869
|
-
if (parsed.summary) {
|
|
2870
|
-
lines.push("=== Summary ===");
|
|
2871
|
-
lines.push(parsed.summary);
|
|
2872
|
-
lines.push("");
|
|
2873
|
-
}
|
|
2874
|
-
const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
2875
|
-
const comments = parsed.comments ?? [];
|
|
2876
|
-
if (comments.length > 0) {
|
|
2877
|
-
const sorted = [...comments].sort((a, b) => (SEVERITY_ORDER[a.severity ?? ""] ?? 4) - (SEVERITY_ORDER[b.severity ?? ""] ?? 4));
|
|
2878
|
-
const displayComments = sorted.slice(0, MAX_COMMENTS);
|
|
2879
|
-
lines.push(`=== Comments (${displayComments.length}/${comments.length}) ===`);
|
|
2880
|
-
for (const c of displayComments) {
|
|
2881
|
-
const loc = c.path ? `${c.path}:${c.start_line ?? "?"}-${c.end_line ?? "?"}` : "unknown location";
|
|
2882
|
-
lines.push(`- [${c.severity ?? "info"}/${c.category ?? "general"}] ${loc}`);
|
|
2883
|
-
if (c.content) {
|
|
2884
|
-
lines.push(` Content: ${truncateField(c.content, MAX_FIELD_LENGTH)}`);
|
|
2885
|
-
}
|
|
2886
|
-
if (c.suggestion_code) {
|
|
2887
|
-
lines.push(` Suggestion: ${truncateField(c.suggestion_code, MAX_FIELD_LENGTH)}`);
|
|
2888
|
-
}
|
|
2889
|
-
lines.push("");
|
|
2890
|
-
}
|
|
2891
|
-
if (comments.length > MAX_COMMENTS) {
|
|
2892
|
-
lines.push(`...(共 ${comments.length} 条评论,此处仅展示优先级最高的前 ${MAX_COMMENTS} 条。可用 session_id 继续)`);
|
|
2893
|
-
lines.push("");
|
|
2894
|
-
}
|
|
2895
|
-
}
|
|
2896
|
-
const warnings = parsed.warnings ?? [];
|
|
2897
|
-
if (warnings.length > 0) {
|
|
2898
|
-
const displayWarnings = warnings.slice(0, MAX_WARNINGS);
|
|
2899
|
-
lines.push(`=== Warnings (${displayWarnings.length}/${warnings.length}) ===`);
|
|
2900
|
-
for (const w of displayWarnings) {
|
|
2901
|
-
lines.push(`- ${truncateField(w, MAX_FIELD_LENGTH)}`);
|
|
2902
|
-
}
|
|
2903
|
-
if (warnings.length > MAX_WARNINGS) {
|
|
2904
|
-
lines.push(`...(共 ${warnings.length} 条警告,此处仅展示前 ${MAX_WARNINGS} 条)`);
|
|
2905
|
-
}
|
|
2906
|
-
lines.push("");
|
|
2907
|
-
}
|
|
2908
|
-
const groups = parsed.groups ?? [];
|
|
2909
|
-
if (groups.length > 0) {
|
|
2910
|
-
lines.push(`=== Groups (${groups.length}) ===`);
|
|
2911
|
-
for (const g of groups) {
|
|
2912
|
-
lines.push(`- ${g.name ?? "unnamed"}: ${g.file_count ?? 0} files`);
|
|
2913
|
-
}
|
|
2914
|
-
lines.push("");
|
|
2915
|
-
}
|
|
2916
|
-
lines.push("--- End of OCR Review ---");
|
|
2917
|
-
return lines.join(`
|
|
2918
|
-
`);
|
|
2919
|
-
}
|
|
2920
|
-
function createOcrReviewTool(ctx, projectDir, ocrAvailable) {
|
|
2921
|
-
const co_ocr_review = tool3({
|
|
2922
|
-
description: [
|
|
2923
|
-
"Run @alibaba-group/open-code-review for structured code review.",
|
|
2924
|
-
"",
|
|
2925
|
-
"Invokes the local OCR CLI to perform a structured code review of the git diff.",
|
|
2926
|
-
"Returns formatted results with comments, warnings, and summary.",
|
|
2927
|
-
"",
|
|
2928
|
-
"Soft dependency: if OCR is not installed, returns installation guidance."
|
|
2929
|
-
].join(`
|
|
2930
|
-
`),
|
|
2931
|
-
args: {
|
|
2932
|
-
from: z3.string().describe('起始 git 引用(branch/tag/commit),例如 "main"、"HEAD~10"'),
|
|
2933
|
-
to: z3.string().optional().describe("结束 git 引用,默认 HEAD"),
|
|
2934
|
-
background: z3.string().optional().describe('业务上下文说明,例如 "这是一个支付模块的重构 PR"'),
|
|
2935
|
-
preview: z3.boolean().optional().describe("仅列出待审文件、不调用 LLM(零 token),默认 true;确认真实审查时显式传 false")
|
|
2936
|
-
},
|
|
2937
|
-
async execute(args, toolContext) {
|
|
2938
|
-
if (toolContext.agent && toolContext.agent !== "co-oracle") {
|
|
2939
|
-
throw new Error(`co_ocr_review 仅限 co-oracle 调用。当前 agent: ${toolContext.agent}`);
|
|
2940
|
-
}
|
|
2941
|
-
if (!ocrAvailable) {
|
|
2942
|
-
return [
|
|
2943
|
-
"【CoHub】OCR CLI(@alibaba-group/open-code-review)未安装,已跳过外部审查。",
|
|
2944
|
-
"安装:npm install -g @alibaba-group/open-code-review",
|
|
2945
|
-
"验证:ocr version",
|
|
2946
|
-
"安装后需重启 OpenCode 方可生效。",
|
|
2947
|
-
"本次请回退到 co-oracle 原生方式(并行读取文件 + 结构化报告)。"
|
|
2948
|
-
].join(`
|
|
2949
|
-
`);
|
|
2950
|
-
}
|
|
2951
|
-
const cliArgs = buildOcrArgs({
|
|
2952
|
-
from: args.from,
|
|
2953
|
-
to: args.to,
|
|
2954
|
-
background: args.background,
|
|
2955
|
-
preview: args.preview
|
|
2956
|
-
}, projectDir);
|
|
2957
|
-
const result = await executeOcrReview(cliArgs, projectDir);
|
|
2958
|
-
if (result.exitCode === null) {
|
|
2959
|
-
return `【CoHub】OCR CLI 执行失败:${result.stderr}`;
|
|
2960
|
-
}
|
|
2961
|
-
if (!result.success) {
|
|
2962
|
-
const stderrSummary = result.stderr.length > 500 ? result.stderr.slice(0, 500) + "...(截断)" : result.stderr;
|
|
2963
|
-
return [
|
|
2964
|
-
"【CoHub】OCR CLI 返回错误:",
|
|
2965
|
-
`Exit code: ${result.exitCode}`,
|
|
2966
|
-
`Stderr: ${stderrSummary}`,
|
|
2967
|
-
"",
|
|
2968
|
-
"请检查 OCR CLI 配置或回退到 co-oracle 原生方式。"
|
|
2969
|
-
].join(`
|
|
2970
|
-
`);
|
|
2971
|
-
}
|
|
2972
|
-
let output = formatOcrOutput(result.stdout);
|
|
2973
|
-
if (result.truncated) {
|
|
2974
|
-
output += `
|
|
2975
|
-
|
|
2976
|
-
[警告] OCR 输出超过 10MB 限制,stdout 已被截断。`;
|
|
2977
|
-
}
|
|
2978
|
-
return output;
|
|
2979
|
-
}
|
|
2980
|
-
});
|
|
2981
|
-
return { co_ocr_review };
|
|
2982
|
-
}
|
|
2983
|
-
|
|
2984
2947
|
// src/index.ts
|
|
2985
2948
|
import * as fs5 from "node:fs";
|
|
2986
|
-
import * as
|
|
2987
|
-
import * as
|
|
2949
|
+
import * as path7 from "node:path";
|
|
2950
|
+
import * as os5 from "node:os";
|
|
2988
2951
|
|
|
2989
2952
|
// src/orchestration/types.ts
|
|
2990
2953
|
var VALID_TRANSITIONS = {
|
|
@@ -3365,12 +3328,12 @@ function loadFileOverrides(projectDir) {
|
|
|
3365
3328
|
];
|
|
3366
3329
|
const searchDirs = [];
|
|
3367
3330
|
if (projectDir) {
|
|
3368
|
-
searchDirs.push(
|
|
3331
|
+
searchDirs.push(path7.join(projectDir, ".opencode", "oh-my-opencode-cohub"));
|
|
3369
3332
|
}
|
|
3370
|
-
searchDirs.push(
|
|
3333
|
+
searchDirs.push(path7.join(os5.homedir(), ".config", "opencode", "oh-my-opencode-cohub"));
|
|
3371
3334
|
for (const agent of agentNames) {
|
|
3372
3335
|
for (const dir of searchDirs) {
|
|
3373
|
-
const replacePath =
|
|
3336
|
+
const replacePath = path7.join(dir, `${agent}.md`);
|
|
3374
3337
|
if (!overrides[agent]?.replace && fs5.existsSync(replacePath)) {
|
|
3375
3338
|
try {
|
|
3376
3339
|
overrides[agent] = { ...overrides[agent], replace: fs5.readFileSync(replacePath, "utf-8") };
|
|
@@ -3378,7 +3341,7 @@ function loadFileOverrides(projectDir) {
|
|
|
3378
3341
|
appendLog("loadFileOverrides", "读取replace文件失败", err);
|
|
3379
3342
|
}
|
|
3380
3343
|
}
|
|
3381
|
-
const appendPath =
|
|
3344
|
+
const appendPath = path7.join(dir, `${agent}_append.md`);
|
|
3382
3345
|
if (!overrides[agent]?.append && fs5.existsSync(appendPath)) {
|
|
3383
3346
|
try {
|
|
3384
3347
|
overrides[agent] = { ...overrides[agent], append: fs5.readFileSync(appendPath, "utf-8") };
|
|
@@ -3414,27 +3377,21 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3414
3377
|
const tracker = new TaskTracker;
|
|
3415
3378
|
const orchestrationLayer = createOrchestrationLayer(tracker, {});
|
|
3416
3379
|
let _lastAlias = "";
|
|
3417
|
-
const STATE_DIR =
|
|
3418
|
-
const STATE_FILE =
|
|
3380
|
+
const STATE_DIR = path7.join(os5.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
|
|
3381
|
+
const STATE_FILE = path7.join(STATE_DIR, "tracker-state.json");
|
|
3419
3382
|
function syncTrackerState(sessionId) {
|
|
3420
3383
|
try {
|
|
3421
|
-
if (!fs5.existsSync(STATE_DIR)) {
|
|
3422
|
-
fs5.mkdirSync(STATE_DIR, { recursive: true });
|
|
3423
|
-
}
|
|
3424
3384
|
const state = {
|
|
3425
3385
|
updatedAt: Date.now(),
|
|
3426
3386
|
runningAgents: tracker.getRunningAgents(sessionId),
|
|
3427
3387
|
runningCount: tracker.getRunningCount(sessionId)
|
|
3428
3388
|
};
|
|
3429
|
-
|
|
3389
|
+
writeFileAtomic(STATE_FILE, JSON.stringify(state));
|
|
3430
3390
|
} catch {}
|
|
3431
3391
|
}
|
|
3432
|
-
const AGENT_CONFIG_FILE =
|
|
3392
|
+
const AGENT_CONFIG_FILE = path7.join(STATE_DIR, "cohub-state.json");
|
|
3433
3393
|
function syncAgentConfig() {
|
|
3434
3394
|
try {
|
|
3435
|
-
if (!fs5.existsSync(STATE_DIR)) {
|
|
3436
|
-
fs5.mkdirSync(STATE_DIR, { recursive: true });
|
|
3437
|
-
}
|
|
3438
3395
|
const configs = agents.filter((a) => a.config.model).map((a) => {
|
|
3439
3396
|
const modelStr = a.config.model;
|
|
3440
3397
|
const parts = modelStr.split("/");
|
|
@@ -3448,7 +3405,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3448
3405
|
provider
|
|
3449
3406
|
};
|
|
3450
3407
|
});
|
|
3451
|
-
|
|
3408
|
+
writeFileAtomic(AGENT_CONFIG_FILE, JSON.stringify({ updatedAt: Date.now(), agents: configs }));
|
|
3452
3409
|
appendLog("syncAgentConfig", `已同步 ${configs.length} 个 agent: ${JSON.stringify(configs.map((c) => ({ name: c.name, model: c.model, variant: c.variant })))}`);
|
|
3453
3410
|
} catch {}
|
|
3454
3411
|
}
|
|
@@ -3515,6 +3472,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3515
3472
|
}
|
|
3516
3473
|
];
|
|
3517
3474
|
const userConfig = loadCoHubConfig(projectDir);
|
|
3475
|
+
const ctxDiag = resolveCtxDiagSettings(userConfig.diag);
|
|
3518
3476
|
if (userConfig.agents) {
|
|
3519
3477
|
for (const agent of agents) {
|
|
3520
3478
|
const override = userConfig.agents[agent.name];
|
|
@@ -3537,7 +3495,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3537
3495
|
}
|
|
3538
3496
|
const DEFAULT_COUNCIL_CONFIG = {
|
|
3539
3497
|
default_preset: "default",
|
|
3540
|
-
timeout:
|
|
3498
|
+
timeout: COUNCIL_TIMEOUT_MS,
|
|
3541
3499
|
councillor_execution_mode: "parallel",
|
|
3542
3500
|
councillor_retries: 3,
|
|
3543
3501
|
presets: {
|
|
@@ -3550,8 +3508,6 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3550
3508
|
const councilManager = new CouncilManager(input.client, input.directory, councilConfig);
|
|
3551
3509
|
const councilTools = createCouncilTool(input, councilManager);
|
|
3552
3510
|
const jobTools = createCloseJobTool(input, tracker, () => syncTrackerState(tracker.currentParentSessionId));
|
|
3553
|
-
const ocrAvailable = await detectOcrCli();
|
|
3554
|
-
const ocrTools = createOcrReviewTool(input, projectDir, ocrAvailable);
|
|
3555
3511
|
function extractChildSessionId(output) {
|
|
3556
3512
|
if (!output || typeof output !== "object")
|
|
3557
3513
|
return;
|
|
@@ -3586,10 +3542,9 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3586
3542
|
return p.sessionId;
|
|
3587
3543
|
return;
|
|
3588
3544
|
}
|
|
3589
|
-
const STALE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
3590
3545
|
const cleanupTimer = setInterval(() => {
|
|
3591
3546
|
try {
|
|
3592
|
-
const staleSessions = tracker.cleanupStaleJobs(
|
|
3547
|
+
const staleSessions = tracker.cleanupStaleJobs(STALE_TASK_MS);
|
|
3593
3548
|
for (const sid of staleSessions) {
|
|
3594
3549
|
(async () => {
|
|
3595
3550
|
try {
|
|
@@ -3606,17 +3561,21 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3606
3561
|
appendLog("cleanupStaleJobs", "定时清理过期任务失败", err);
|
|
3607
3562
|
}
|
|
3608
3563
|
}, 30000);
|
|
3564
|
+
if (typeof cleanupTimer.unref === "function")
|
|
3565
|
+
cleanupTimer.unref();
|
|
3609
3566
|
const contextCleanupTimer = setInterval(() => {
|
|
3610
3567
|
try {
|
|
3611
3568
|
contextEngine.cleanupStaleDependencies();
|
|
3569
|
+
contextEngine.cleanupStaleContexts();
|
|
3612
3570
|
} catch (err) {
|
|
3613
3571
|
appendLog("contextCleanupTimer", "定时清理上下文失败", err);
|
|
3614
3572
|
}
|
|
3615
3573
|
}, 60000);
|
|
3574
|
+
if (typeof contextCleanupTimer.unref === "function")
|
|
3575
|
+
contextCleanupTimer.unref();
|
|
3616
3576
|
const agentConfigs = {};
|
|
3617
3577
|
const COUNCIL_ONLY_TOOLS = { council_session: "deny" };
|
|
3618
3578
|
const ORCHESTRATOR_ONLY_TOOLS = { close_job: "deny" };
|
|
3619
|
-
const OCR_ONLY_TOOLS = { co_ocr_review: "deny" };
|
|
3620
3579
|
for (const agent of agents) {
|
|
3621
3580
|
const base = {
|
|
3622
3581
|
...agent.config,
|
|
@@ -3632,17 +3591,12 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3632
3591
|
} else {
|
|
3633
3592
|
base.permission = { ...base.permission, close_job: "allow" };
|
|
3634
3593
|
}
|
|
3635
|
-
if (agent.name !== "co-oracle") {
|
|
3636
|
-
base.permission = { ...base.permission, ...OCR_ONLY_TOOLS };
|
|
3637
|
-
} else {
|
|
3638
|
-
base.permission = { ...base.permission, co_ocr_review: "allow" };
|
|
3639
|
-
}
|
|
3640
3594
|
agentConfigs[agent.name] = base;
|
|
3641
3595
|
}
|
|
3642
3596
|
return {
|
|
3643
3597
|
name: "oh-my-opencode-cohub",
|
|
3644
3598
|
agent: agentConfigs,
|
|
3645
|
-
tool: { ...councilTools, ...jobTools
|
|
3599
|
+
tool: { ...councilTools, ...jobTools },
|
|
3646
3600
|
config: async (cfg) => {
|
|
3647
3601
|
const c = cfg;
|
|
3648
3602
|
c.agent ??= {};
|
|
@@ -3659,23 +3613,20 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3659
3613
|
merged.variant = pc.variant;
|
|
3660
3614
|
const existingPerm0 = merged.permission ?? {};
|
|
3661
3615
|
if (name === "co-council") {
|
|
3662
|
-
merged.permission = { ...existingPerm0, council_session: "allow", ...ORCHESTRATOR_ONLY_TOOLS
|
|
3616
|
+
merged.permission = { ...existingPerm0, council_session: "allow", ...ORCHESTRATOR_ONLY_TOOLS };
|
|
3663
3617
|
} else if (name === "co-orchestrator") {
|
|
3664
3618
|
merged.permission = {
|
|
3665
3619
|
...existingPerm0,
|
|
3666
3620
|
...COUNCIL_ONLY_TOOLS,
|
|
3667
|
-
close_job: "allow"
|
|
3668
|
-
...OCR_ONLY_TOOLS
|
|
3621
|
+
close_job: "allow"
|
|
3669
3622
|
};
|
|
3670
|
-
} else if (name === "co-oracle") {
|
|
3671
|
-
merged.permission = { ...existingPerm0, ...COUNCIL_ONLY_TOOLS, ...ORCHESTRATOR_ONLY_TOOLS, co_ocr_review: "allow" };
|
|
3672
3623
|
} else {
|
|
3673
|
-
merged.permission = { ...existingPerm0, ...COUNCIL_ONLY_TOOLS, ...ORCHESTRATOR_ONLY_TOOLS
|
|
3624
|
+
merged.permission = { ...existingPerm0, ...COUNCIL_ONLY_TOOLS, ...ORCHESTRATOR_ONLY_TOOLS };
|
|
3674
3625
|
}
|
|
3675
3626
|
c.agent[name] = merged;
|
|
3676
3627
|
}
|
|
3677
3628
|
try {
|
|
3678
|
-
|
|
3629
|
+
writeFileAtomic(path7.join(STATE_DIR, "config-hook-ran.json"), JSON.stringify({ ran: true, count: agents.length }));
|
|
3679
3630
|
} catch (err) {
|
|
3680
3631
|
appendLog("config", "双重注册写入文件失败", err);
|
|
3681
3632
|
}
|
|
@@ -3711,7 +3662,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3711
3662
|
});
|
|
3712
3663
|
_lastAlias = alias;
|
|
3713
3664
|
syncTrackerState(input.sessionID ?? "");
|
|
3714
|
-
tracker.pruneTerminalJobs(
|
|
3665
|
+
tracker.pruneTerminalJobs(STALE_TASK_MS);
|
|
3715
3666
|
if (orchestrationLayer && subagentType && description) {
|
|
3716
3667
|
try {
|
|
3717
3668
|
orchestrationLayer.engine.registerTask(alias, subagentType, description, []);
|
|
@@ -3748,30 +3699,21 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3748
3699
|
if (details) {
|
|
3749
3700
|
output.args[targetField] += details;
|
|
3750
3701
|
}
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
const
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
subagent: subagentType,
|
|
3767
|
-
strategy,
|
|
3768
|
-
targetField,
|
|
3769
|
-
hasDetails: !!details,
|
|
3770
|
-
detailsLen: details ? details.length : 0,
|
|
3771
|
-
detailsPreview: details ? details.replace(/\n/g, "\\n").slice(0, 200) : "",
|
|
3772
|
-
promptEnd: finalPrompt.slice(-120)
|
|
3773
|
-
}) + `
|
|
3774
|
-
`);
|
|
3702
|
+
contextEngine.releaseContext(contextId);
|
|
3703
|
+
if (ctxDiag.enabled) {
|
|
3704
|
+
const finalPrompt = typeof output.args?.prompt === "string" ? output.args.prompt : "";
|
|
3705
|
+
appendCtxDiag({
|
|
3706
|
+
time: new Date().toISOString(),
|
|
3707
|
+
session: input.sessionID?.slice(0, 20) ?? "?",
|
|
3708
|
+
subagent: subagentType,
|
|
3709
|
+
strategy,
|
|
3710
|
+
targetField,
|
|
3711
|
+
hasDetails: !!details,
|
|
3712
|
+
detailsLen: details ? details.length : 0,
|
|
3713
|
+
detailsPreview: details ? details.replace(/\n/g, "\\n").slice(0, 120) : "",
|
|
3714
|
+
promptEnd: finalPrompt.slice(-80)
|
|
3715
|
+
}, ctxDiag.filePath);
|
|
3716
|
+
}
|
|
3775
3717
|
}
|
|
3776
3718
|
}
|
|
3777
3719
|
const targetField = typeof output.args?.prompt === "string" ? "prompt" : "description";
|