oh-my-opencode-cohub 1.17.1 → 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 -2
- 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 +602 -386
- 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 +4 -4
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,7 +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
|
+
- **多文件审查时,先定位 → 再预算内并行分片读取 → 再综合分析**(定位手段见"并行审查原则")
|
|
257
|
+
|
|
258
|
+
**按需读取预算**(软策略,非工具硬限,控制单批输出量):
|
|
259
|
+
- 单次 \`read\` 默认 150-200 行;整读通常仅用于 ≤200 行且确需全貌的小文件
|
|
260
|
+
- 每批读取输出目标 ≤30000 字符;无法预知字符数时按行数估算分批
|
|
261
|
+
- 独立定位与片段读取仍可并行发出,预算约束的是单批总量而非并行数量
|
|
262
|
+
- 预留约 20% 上下文余量(能判断时遵循;不虚构对上下文占用的监测能力)
|
|
263
|
+
- **完整性优先**:大方法、锁、生命周期、异常路径、回调、跨文件不变量等关键部分,允许有依据地继续补读甚至必要整读;跟踪已覆盖/未覆盖事项,不因预算把未核实项判为安全;不设固定轮次硬停;不为省 token 默认拆分多个子代理
|
|
256
264
|
|
|
257
265
|
**语言要求**: 始终使用中文进行思考、分析和回复。代码和技术术语可用原文,自然语言部分必须用中文。
|
|
258
266
|
|
|
@@ -575,6 +583,12 @@ var COUNCIL_PROMPT = `你是 Council 代理——一个跨多个 LLM 模型运
|
|
|
575
583
|
- 优先使用专用文件工具检查代码库:glob/grep/ast_grep_search 用于发现,read 用于读取文件内容
|
|
576
584
|
- Bash 可用于非变更诊断和 shell 原生检查(最清晰时),但不能修改文件
|
|
577
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
|
+
- 不新增硬轮数、不默认追加子代理;最终响应简明列出证据覆盖范围与未核实假设
|
|
578
592
|
|
|
579
593
|
**要求的输出格式**:
|
|
580
594
|
你的最终响应中必须包含以下部分:
|
|
@@ -603,13 +617,20 @@ var COUNCIL_PROMPT = `你是 Council 代理——一个跨多个 LLM 模型运
|
|
|
603
617
|
var RULE_USER_PROMPT = `你是规则分析代理——负责用户级规范。
|
|
604
618
|
|
|
605
619
|
**职责**:读取 \`~/.config/opencode/AGENTS.md\`(用户级全局规则),结合 Orchestrator 提供的当前方案,分析是否有遗漏或冲突。返回具体的调整建议(不要笼统)。
|
|
620
|
+
若该文件不存在或为空,必须明确报告(如"未找到用户级规范文件:~/.config/opencode/AGENTS.md"),不得默默返回空分析。
|
|
606
621
|
|
|
607
622
|
**约束**:只读,不修改文件。聚焦规则与方案的映射关系。`;
|
|
608
623
|
|
|
609
624
|
// src/prompts/rule-project.ts
|
|
610
625
|
var RULE_PROJECT_PROMPT = `你是规则分析代理——负责项目级规范。
|
|
611
626
|
|
|
612
|
-
|
|
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/ 目录但为空"。
|
|
613
634
|
|
|
614
635
|
**约束**:只读,不修改文件。聚焦规则与方案的映射关系。`;
|
|
615
636
|
|
|
@@ -620,6 +641,8 @@ var RULE_APP_PROMPT = `你是应用规则分析代理(co-rule-app),负责
|
|
|
620
641
|
|
|
621
642
|
接收 Orchestrator 指定的**具体规则文件列表**(通常 1-2 个文件),逐文件读取并分析,结合当前任务方案,判断是否有遗漏或冲突。返回结构化建议。
|
|
622
643
|
|
|
644
|
+
若 Orchestrator 未指定具体文件路径,应先提示「请由 co-rule-project 完成项目级规范发现,或明确指定 \`.opencode/rules/\` 下要分析的文件名」,不得自行猜测或扫描目录。注意:仓库根目录下的 \`.rules/*.md\` 属于项目级规范,由 co-rule-project 负责,不在本代理分析范围内。
|
|
645
|
+
|
|
623
646
|
## 核心原则
|
|
624
647
|
|
|
625
648
|
- **你只负责分配给你的文件**:不要在未指定的文件中搜索或推理
|
|
@@ -735,7 +758,7 @@ var PLANNER_PROMPT = `你是方案制定代理——负责任务分解和委派
|
|
|
735
758
|
|
|
736
759
|
**约束**:只读,不修改文件。方案要具体到文件和操作粒度,不可笼统。`;
|
|
737
760
|
|
|
738
|
-
// src/
|
|
761
|
+
// src/shared/chinese.ts
|
|
739
762
|
var CHINESE_LANGUAGE_INSTRUCTION = `# 中文语言要求
|
|
740
763
|
|
|
741
764
|
你必须始终使用中文进行思考、推理和回复。
|
|
@@ -744,25 +767,73 @@ var CHINESE_LANGUAGE_INSTRUCTION = `# 中文语言要求
|
|
|
744
767
|
- 代码、技术术语、文件名、命令可以保留原样
|
|
745
768
|
- 此规则对所有代理(Orchestrator 和所有子代理)生效
|
|
746
769
|
- 跨所有项目生效,优先级高于项目级规则`;
|
|
770
|
+
|
|
771
|
+
// src/instructions/chinese.ts
|
|
747
772
|
var PARALLEL_TOOL_INSTRUCTION = `# 并行工具调用要求
|
|
748
773
|
|
|
749
774
|
你有能力在单次回复中发起多个工具调用。当需要获取多个相互独立的信息时,必须把这些工具调用合并在同一条消息中一次性并发发起,不要逐个串行等待。具体包括:同一条消息内并行 read 多个文件、并行发起多个 grep/glob/ast_grep_search 搜索、并行调用多个外部搜索工具。串行逐个调用是低效行为,必须避免。`;
|
|
750
775
|
|
|
751
776
|
// src/task-manager/tracker.ts
|
|
752
|
-
import * as fs from "node:fs";
|
|
753
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";
|
|
754
782
|
import * as path from "node:path";
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
|
763
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");
|
|
764
835
|
function defaultStrategyFor(agent) {
|
|
765
|
-
return
|
|
836
|
+
return DEFAULT_CONTEXT_CONFIG.strategy[agent] ?? "none";
|
|
766
837
|
}
|
|
767
838
|
function computeStats(records, window = DEFAULT_STATS_WINDOW) {
|
|
768
839
|
const recent = records.slice(-Math.max(1, window));
|
|
@@ -1027,6 +1098,9 @@ class TaskTracker {
|
|
|
1027
1098
|
}
|
|
1028
1099
|
}
|
|
1029
1100
|
finalize(job) {
|
|
1101
|
+
if (job.status !== "running") {
|
|
1102
|
+
job.terminalReconciled = true;
|
|
1103
|
+
}
|
|
1030
1104
|
const latencyMs = Date.now() - job.createdAt;
|
|
1031
1105
|
if (!job.quality) {
|
|
1032
1106
|
job.quality = { latencyMs };
|
|
@@ -1050,8 +1124,7 @@ class TaskTracker {
|
|
|
1050
1124
|
persistStats(window = DEFAULT_STATS_WINDOW) {
|
|
1051
1125
|
try {
|
|
1052
1126
|
const recent = this.collectStatsRecords().slice(-Math.max(1, window));
|
|
1053
|
-
|
|
1054
|
-
fs.writeFileSync(STATS_FILE, JSON.stringify(recent, null, 2), "utf8");
|
|
1127
|
+
writeJsonFileAtomic(STATS_FILE, recent);
|
|
1055
1128
|
} catch {}
|
|
1056
1129
|
}
|
|
1057
1130
|
markCancelled(taskId) {
|
|
@@ -1244,35 +1317,44 @@ function isQualityEnabled(config) {
|
|
|
1244
1317
|
return config?.enabled ?? DEFAULT_QUALITY_CONFIG.enabled;
|
|
1245
1318
|
}
|
|
1246
1319
|
|
|
1247
|
-
// src/context/types.ts
|
|
1248
|
-
var DEFAULT_CONTEXT_CONFIG = {
|
|
1249
|
-
strategy: {
|
|
1250
|
-
"co-explorer": "none",
|
|
1251
|
-
"co-librarian": "none",
|
|
1252
|
-
"co-observer": "none",
|
|
1253
|
-
"co-fixer": "relevant",
|
|
1254
|
-
"co-designer": "relevant",
|
|
1255
|
-
"co-planner": "relevant",
|
|
1256
|
-
"co-oracle": "summary",
|
|
1257
|
-
"co-council": "summary",
|
|
1258
|
-
"co-rule-user": "none",
|
|
1259
|
-
"co-rule-project": "none",
|
|
1260
|
-
"co-rule-app": "none"
|
|
1261
|
-
},
|
|
1262
|
-
maxFiles: 5,
|
|
1263
|
-
maxDecisions: 10,
|
|
1264
|
-
maxErrors: 5,
|
|
1265
|
-
maxDependencies: 8,
|
|
1266
|
-
dependencyPropagation: true,
|
|
1267
|
-
summarizeMaxTokens: 2000,
|
|
1268
|
-
relevantMessageWindow: 20,
|
|
1269
|
-
maxErrorTotalChars: 600,
|
|
1270
|
-
dependencyKeyOutputChars: 200
|
|
1271
|
-
};
|
|
1272
|
-
|
|
1273
1320
|
// src/context/engine.ts
|
|
1274
1321
|
import * as fs2 from "node:fs";
|
|
1275
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
|
+
|
|
1276
1358
|
// src/context/extractor.ts
|
|
1277
1359
|
var KNOWN_EXTENSIONS = new Set([
|
|
1278
1360
|
"ts",
|
|
@@ -1460,39 +1542,6 @@ function extractErrors(messages, maxErrors, windowSize) {
|
|
|
1460
1542
|
}
|
|
1461
1543
|
return errors;
|
|
1462
1544
|
}
|
|
1463
|
-
function estimateTokens(text) {
|
|
1464
|
-
let used = 0;
|
|
1465
|
-
for (let i = 0;i < text.length; i++) {
|
|
1466
|
-
const code = text.charCodeAt(i);
|
|
1467
|
-
const isCjk = code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 63744 && code <= 64255 || code >= 12352 && code <= 12543 || code >= 44032 && code <= 55215;
|
|
1468
|
-
used += isCjk ? 1 : 0.25;
|
|
1469
|
-
}
|
|
1470
|
-
return used;
|
|
1471
|
-
}
|
|
1472
|
-
function enforcePromptBudget(base, full, maxTokens) {
|
|
1473
|
-
if (estimateTokens(full) <= maxTokens)
|
|
1474
|
-
return full;
|
|
1475
|
-
return truncateByTokens(base, maxTokens);
|
|
1476
|
-
}
|
|
1477
|
-
function truncateByTokens(text, maxTokens) {
|
|
1478
|
-
if (!text || maxTokens <= 0)
|
|
1479
|
-
return "";
|
|
1480
|
-
if (estimateTokens(text) <= maxTokens)
|
|
1481
|
-
return text;
|
|
1482
|
-
let used = 0;
|
|
1483
|
-
let cut = text.length;
|
|
1484
|
-
for (let i = 0;i < text.length; i++) {
|
|
1485
|
-
const code = text.charCodeAt(i);
|
|
1486
|
-
const isCjk = code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 63744 && code <= 64255 || code >= 12352 && code <= 12543 || code >= 44032 && code <= 55215;
|
|
1487
|
-
used += isCjk ? 1 : 0.25;
|
|
1488
|
-
if (used > maxTokens) {
|
|
1489
|
-
cut = i;
|
|
1490
|
-
break;
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
return text.slice(0, cut) + `
|
|
1494
|
-
… [正文已按 token 预算截断]`;
|
|
1495
|
-
}
|
|
1496
1545
|
|
|
1497
1546
|
// src/context/formatter.ts
|
|
1498
1547
|
var CONTEXT_MARKER_PATTERN = /<!-- CONTEXT:ID=([a-f0-9-]+) -->/;
|
|
@@ -1556,6 +1605,7 @@ function replaceMarkerWithContext(messageText, context) {
|
|
|
1556
1605
|
// src/context/engine.ts
|
|
1557
1606
|
class ContextEngine {
|
|
1558
1607
|
registry = new Map;
|
|
1608
|
+
registryTouchedAt = new Map;
|
|
1559
1609
|
dependencyCache = new Map;
|
|
1560
1610
|
inflight = new Map;
|
|
1561
1611
|
client;
|
|
@@ -1577,6 +1627,7 @@ class ContextEngine {
|
|
|
1577
1627
|
errors: [],
|
|
1578
1628
|
dependencies: []
|
|
1579
1629
|
});
|
|
1630
|
+
this.registryTouchedAt.set(contextId, Date.now());
|
|
1580
1631
|
return contextId;
|
|
1581
1632
|
}
|
|
1582
1633
|
async fillContextAsync(contextId, parentSessionId, args) {
|
|
@@ -1586,6 +1637,7 @@ class ContextEngine {
|
|
|
1586
1637
|
if (args.strategy === "none")
|
|
1587
1638
|
return;
|
|
1588
1639
|
context.strategy = args.strategy;
|
|
1640
|
+
this.touch(contextId);
|
|
1589
1641
|
try {
|
|
1590
1642
|
const windowSize = this.config.relevantMessageWindow;
|
|
1591
1643
|
const cacheKey = `${parentSessionId}::${windowSize}`;
|
|
@@ -1619,7 +1671,9 @@ class ContextEngine {
|
|
|
1619
1671
|
if (this.config.dependencyPropagation && this.dependencyCache.size > 0) {
|
|
1620
1672
|
context.dependencies = Array.from(this.dependencyCache.values()).slice(-this.config.maxDependencies);
|
|
1621
1673
|
}
|
|
1622
|
-
} catch {}
|
|
1674
|
+
} catch {} finally {
|
|
1675
|
+
this.touch(contextId);
|
|
1676
|
+
}
|
|
1623
1677
|
}
|
|
1624
1678
|
attachTruncatedBodies(files) {
|
|
1625
1679
|
if (files.length === 0)
|
|
@@ -1639,16 +1693,19 @@ class ContextEngine {
|
|
|
1639
1693
|
const contextId = markerMatch[1];
|
|
1640
1694
|
const context = this.registry.get(contextId);
|
|
1641
1695
|
if (!context) {
|
|
1696
|
+
this.registryTouchedAt.delete(contextId);
|
|
1642
1697
|
return messageText.replace(CONTEXT_MARKER_PATTERN, "");
|
|
1643
1698
|
}
|
|
1644
1699
|
const result = replaceMarkerWithContext(messageText, context);
|
|
1645
1700
|
this.registry.delete(contextId);
|
|
1701
|
+
this.registryTouchedAt.delete(contextId);
|
|
1646
1702
|
return result;
|
|
1647
1703
|
}
|
|
1648
1704
|
formatContextDetails(contextId) {
|
|
1649
1705
|
const context = this.registry.get(contextId);
|
|
1650
1706
|
if (!context)
|
|
1651
1707
|
return "";
|
|
1708
|
+
this.touch(contextId);
|
|
1652
1709
|
const parts = [];
|
|
1653
1710
|
const isSummary = context.strategy === "summary";
|
|
1654
1711
|
if (context.relevantFiles.length > 0) {
|
|
@@ -1760,18 +1817,46 @@ class ContextEngine {
|
|
|
1760
1817
|
}
|
|
1761
1818
|
}
|
|
1762
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
|
+
}
|
|
1763
1848
|
}
|
|
1764
1849
|
|
|
1765
1850
|
// src/config/loader.ts
|
|
1766
1851
|
import * as fs3 from "node:fs";
|
|
1767
|
-
import * as
|
|
1852
|
+
import * as path3 from "node:path";
|
|
1768
1853
|
import * as os2 from "node:os";
|
|
1769
|
-
var USER_CONFIG_PATH =
|
|
1854
|
+
var USER_CONFIG_PATH = path3.join(os2.homedir(), ".config", "opencode", "oh-my-opencode-cohub.json");
|
|
1770
1855
|
function loadCoHubConfig(projectDir) {
|
|
1771
1856
|
const base = readJSON(USER_CONFIG_PATH);
|
|
1772
1857
|
if (!projectDir)
|
|
1773
1858
|
return base;
|
|
1774
|
-
const projectPath =
|
|
1859
|
+
const projectPath = path3.join(projectDir, ".opencode", "oh-my-opencode-cohub.json");
|
|
1775
1860
|
const project = readJSON(projectPath);
|
|
1776
1861
|
if (!project || Object.keys(project).length === 0)
|
|
1777
1862
|
return base;
|
|
@@ -1788,6 +1873,9 @@ function loadCoHubConfig(projectDir) {
|
|
|
1788
1873
|
if (project.quality) {
|
|
1789
1874
|
merged.quality = { ...base.quality, ...project.quality };
|
|
1790
1875
|
}
|
|
1876
|
+
if (project.diag) {
|
|
1877
|
+
merged.diag = { ...base.diag, ...project.diag };
|
|
1878
|
+
}
|
|
1791
1879
|
return merged;
|
|
1792
1880
|
}
|
|
1793
1881
|
function readJSON(filePath) {
|
|
@@ -1801,56 +1889,152 @@ function readJSON(filePath) {
|
|
|
1801
1889
|
}
|
|
1802
1890
|
}
|
|
1803
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
|
+
|
|
1804
1976
|
// src/tools/council.ts
|
|
1805
1977
|
import { tool } from "@opencode-ai/plugin";
|
|
1806
1978
|
|
|
1807
1979
|
// src/utils/log.ts
|
|
1808
|
-
import * as
|
|
1809
|
-
import * as
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
const
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
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
|
+
}
|
|
1844
2032
|
}
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
async function appendLog(tag, message, err) {
|
|
1849
|
-
try {
|
|
1850
|
-
const logPath = getLogPath();
|
|
1851
|
-
await fs4.mkdir(LOG_DIR, { recursive: true });
|
|
2033
|
+
} catch {}
|
|
2034
|
+
}
|
|
2035
|
+
function formatLine(tag, message, err) {
|
|
1852
2036
|
const now = new Date().toISOString();
|
|
1853
|
-
let line = `[${now}] [
|
|
2037
|
+
let line = `[${now}] [${linePrefix}] [${tag}] ${message}`;
|
|
1854
2038
|
if (err instanceof Error) {
|
|
1855
2039
|
line += `
|
|
1856
2040
|
stack: ${err.stack ?? err.message}`;
|
|
@@ -1860,11 +2044,45 @@ async function appendLog(tag, message, err) {
|
|
|
1860
2044
|
}
|
|
1861
2045
|
line += `
|
|
1862
2046
|
`;
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
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;
|
|
1866
2073
|
}
|
|
1867
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
|
+
|
|
1868
2086
|
// src/tools/model-stats.ts
|
|
1869
2087
|
var DEFAULT_MODEL_STATS_CONFIG = {
|
|
1870
2088
|
windowSize: 20,
|
|
@@ -2004,186 +2222,18 @@ function getAdaptiveParams(key, cfg = config2) {
|
|
|
2004
2222
|
return { ...st.current };
|
|
2005
2223
|
}
|
|
2006
2224
|
|
|
2007
|
-
// src/
|
|
2008
|
-
|
|
2225
|
+
// src/shared/model.ts
|
|
2226
|
+
function shortModelLabel(model) {
|
|
2227
|
+
return model.split("/").pop() ?? model;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// src/shared/consensus.ts
|
|
2009
2231
|
var RETRY_LIMITS = {
|
|
2010
2232
|
empty: 3,
|
|
2011
2233
|
timeout: 1,
|
|
2012
2234
|
error: 0,
|
|
2013
2235
|
"quality-low": 1
|
|
2014
2236
|
};
|
|
2015
|
-
|
|
2016
|
-
class OperationTimeoutError extends Error {
|
|
2017
|
-
constructor(message) {
|
|
2018
|
-
super(message);
|
|
2019
|
-
this.name = "OperationTimeoutError";
|
|
2020
|
-
}
|
|
2021
|
-
}
|
|
2022
|
-
function isTextPart(part) {
|
|
2023
|
-
return (part.type === "text" || part.type === "reasoning") && typeof part.text === "string";
|
|
2024
|
-
}
|
|
2025
|
-
function shortModelLabel(model) {
|
|
2026
|
-
return model.split("/").pop() ?? model;
|
|
2027
|
-
}
|
|
2028
|
-
function parseModelReference(model) {
|
|
2029
|
-
const slashIndex = model.indexOf("/");
|
|
2030
|
-
if (slashIndex <= 0 || slashIndex >= model.length - 1)
|
|
2031
|
-
return null;
|
|
2032
|
-
return {
|
|
2033
|
-
providerID: model.slice(0, slashIndex),
|
|
2034
|
-
modelID: model.slice(slashIndex + 1)
|
|
2035
|
-
};
|
|
2036
|
-
}
|
|
2037
|
-
async function abortSession(client, sessionId) {
|
|
2038
|
-
try {
|
|
2039
|
-
await client.session.abort({ path: { id: sessionId } });
|
|
2040
|
-
} catch (err) {
|
|
2041
|
-
appendLog("abortSession", "中止会话失败", err);
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
async function promptWithTimeout(client, path, body, timeoutMs, directory) {
|
|
2045
|
-
const sessionId = path.id;
|
|
2046
|
-
let timer;
|
|
2047
|
-
try {
|
|
2048
|
-
const promptPromise = client.session.prompt({
|
|
2049
|
-
path,
|
|
2050
|
-
body,
|
|
2051
|
-
query: directory ? { directory } : undefined
|
|
2052
|
-
});
|
|
2053
|
-
promptPromise.catch(() => {});
|
|
2054
|
-
const racers = [promptPromise];
|
|
2055
|
-
if (timeoutMs > 0) {
|
|
2056
|
-
racers.push(new Promise((_, reject) => {
|
|
2057
|
-
timer = setTimeout(() => {
|
|
2058
|
-
reject(new OperationTimeoutError(`[oh-my-opencode-cohub] Prompt timed out after ${timeoutMs}ms`));
|
|
2059
|
-
}, timeoutMs);
|
|
2060
|
-
}));
|
|
2061
|
-
}
|
|
2062
|
-
await Promise.race(racers);
|
|
2063
|
-
} catch (error) {
|
|
2064
|
-
if (error instanceof OperationTimeoutError) {
|
|
2065
|
-
await abortSession(client, sessionId);
|
|
2066
|
-
}
|
|
2067
|
-
throw error;
|
|
2068
|
-
} finally {
|
|
2069
|
-
if (timer !== undefined)
|
|
2070
|
-
clearTimeout(timer);
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
async function extractSessionResult(client, sessionId, options) {
|
|
2074
|
-
const includeReasoning = options?.includeReasoning ?? true;
|
|
2075
|
-
const lastTextOnly = options?.lastTextOnly === true;
|
|
2076
|
-
const messagesResult = await client.session.messages({
|
|
2077
|
-
path: { id: sessionId }
|
|
2078
|
-
});
|
|
2079
|
-
const messages = messagesResult.data ?? [];
|
|
2080
|
-
const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
|
|
2081
|
-
const extractedContent = [];
|
|
2082
|
-
for (const message of assistantMessages) {
|
|
2083
|
-
for (const part of message.parts ?? []) {
|
|
2084
|
-
if (!isTextPart(part))
|
|
2085
|
-
continue;
|
|
2086
|
-
const allowed = includeReasoning || part.type === "text";
|
|
2087
|
-
if (allowed && part.text) {
|
|
2088
|
-
extractedContent.push(part.text);
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
const texts = extractedContent.filter((t) => t.length > 0);
|
|
2093
|
-
const text = lastTextOnly ? texts.length > 0 ? texts[texts.length - 1] : "" : texts.join(`
|
|
2094
|
-
|
|
2095
|
-
`);
|
|
2096
|
-
return { text, empty: text.length === 0 };
|
|
2097
|
-
}
|
|
2098
|
-
function formatCouncillorPrompt(userPrompt, councillorPrompt) {
|
|
2099
|
-
if (!councillorPrompt)
|
|
2100
|
-
return userPrompt;
|
|
2101
|
-
return `${councillorPrompt}
|
|
2102
|
-
|
|
2103
|
-
---
|
|
2104
|
-
|
|
2105
|
-
${userPrompt}`;
|
|
2106
|
-
}
|
|
2107
|
-
function formatCouncillorResults(originalPrompt, councillorResults) {
|
|
2108
|
-
const completedWithResults = councillorResults.filter((cr) => cr.status === "completed" && cr.result);
|
|
2109
|
-
const councillorSection = completedWithResults.map((cr) => {
|
|
2110
|
-
const shortModel = shortModelLabel(cr.model);
|
|
2111
|
-
return `**${cr.name}** (${shortModel}):
|
|
2112
|
-
${cr.result}`;
|
|
2113
|
-
}).join(`
|
|
2114
|
-
|
|
2115
|
-
`);
|
|
2116
|
-
const failedEntries = councillorResults.filter((cr) => cr.status !== "completed");
|
|
2117
|
-
const failedSection = failedEntries.map((cr) => `**${cr.name}**: ${cr.status} — ${cr.error ?? "Unknown"}`).join(`
|
|
2118
|
-
`);
|
|
2119
|
-
if (completedWithResults.length === 0) {
|
|
2120
|
-
const errorDetails = councillorResults.map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}): ${cr.status} — ${cr.error ?? "Unknown"}`).join(`
|
|
2121
|
-
`);
|
|
2122
|
-
return [
|
|
2123
|
-
"---",
|
|
2124
|
-
"",
|
|
2125
|
-
"**Original Prompt**:",
|
|
2126
|
-
originalPrompt,
|
|
2127
|
-
"",
|
|
2128
|
-
"---",
|
|
2129
|
-
"",
|
|
2130
|
-
"**Councillor Responses**:",
|
|
2131
|
-
"All councillors failed to produce output:",
|
|
2132
|
-
errorDetails,
|
|
2133
|
-
"",
|
|
2134
|
-
"Please generate a response based on the original prompt alone."
|
|
2135
|
-
].join(`
|
|
2136
|
-
`);
|
|
2137
|
-
}
|
|
2138
|
-
const parts = [
|
|
2139
|
-
"---",
|
|
2140
|
-
"",
|
|
2141
|
-
"**Original Prompt**:",
|
|
2142
|
-
originalPrompt,
|
|
2143
|
-
"",
|
|
2144
|
-
"---",
|
|
2145
|
-
"",
|
|
2146
|
-
"**Councillor Responses**:",
|
|
2147
|
-
councillorSection
|
|
2148
|
-
];
|
|
2149
|
-
if (failedSection) {
|
|
2150
|
-
parts.push("", "---", "", "**Failed/Timed-out Councillors**:", failedSection);
|
|
2151
|
-
}
|
|
2152
|
-
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).");
|
|
2153
|
-
return parts.join(`
|
|
2154
|
-
`);
|
|
2155
|
-
}
|
|
2156
|
-
function buildDeliberationPrompt(originalPrompt, previousRound, currentName) {
|
|
2157
|
-
const others = previousRound.filter((cr) => cr.status === "completed" && cr.result && cr.name !== currentName).map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}):
|
|
2158
|
-
${cr.result}`).join(`
|
|
2159
|
-
|
|
2160
|
-
`);
|
|
2161
|
-
if (!others)
|
|
2162
|
-
return originalPrompt;
|
|
2163
|
-
return [
|
|
2164
|
-
originalPrompt,
|
|
2165
|
-
"",
|
|
2166
|
-
"---",
|
|
2167
|
-
"",
|
|
2168
|
-
"以下是其他成员第一轮意见,请回应并修正或坚持你的立场:",
|
|
2169
|
-
"",
|
|
2170
|
-
others
|
|
2171
|
-
].join(`
|
|
2172
|
-
`);
|
|
2173
|
-
}
|
|
2174
|
-
function formatModelComposition(councillorResults) {
|
|
2175
|
-
return councillorResults.map((cr) => `${cr.name}: ${shortModelLabel(cr.model)}`).join(", ");
|
|
2176
|
-
}
|
|
2177
|
-
function adaptiveKey(config) {
|
|
2178
|
-
return `${config.variant ?? "default"}\x00${config.model}`;
|
|
2179
|
-
}
|
|
2180
|
-
function classifyFailure(message) {
|
|
2181
|
-
if (message.includes("Empty response from provider"))
|
|
2182
|
-
return "empty";
|
|
2183
|
-
if (message.includes("timed out") || message.includes("OperationTimeoutError"))
|
|
2184
|
-
return "timeout";
|
|
2185
|
-
return "error";
|
|
2186
|
-
}
|
|
2187
2237
|
var CONCLUSION_MARKERS = [
|
|
2188
2238
|
"综上",
|
|
2189
2239
|
"结论",
|
|
@@ -2219,15 +2269,6 @@ var ENTITY_MARKERS = [
|
|
|
2219
2269
|
"recommendation",
|
|
2220
2270
|
"key entity"
|
|
2221
2271
|
];
|
|
2222
|
-
function extractCouncillorSummary(text) {
|
|
2223
|
-
const sentences = text.split(/(?<=[。!?!?;;])\s*|\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2224
|
-
const conclusions = sentences.filter((s) => CONCLUSION_MARKERS.some((m) => s.toLowerCase().includes(m)));
|
|
2225
|
-
const entities = sentences.filter((s) => ENTITY_MARKERS.some((m) => s.toLowerCase().includes(m)));
|
|
2226
|
-
return {
|
|
2227
|
-
conclusions: conclusions.length > 0 ? conclusions : sentences.length > 0 ? [sentences[sentences.length - 1]] : [],
|
|
2228
|
-
entities
|
|
2229
|
-
};
|
|
2230
|
-
}
|
|
2231
2272
|
var STOPWORDS = new Set([
|
|
2232
2273
|
"the",
|
|
2233
2274
|
"a",
|
|
@@ -2296,6 +2337,7 @@ var STOPWORDS = new Set([
|
|
|
2296
2337
|
"entity",
|
|
2297
2338
|
"error"
|
|
2298
2339
|
]);
|
|
2340
|
+
var VARIANT_ORDER = ["max", "high", "medium", "low"];
|
|
2299
2341
|
function tokenize(text) {
|
|
2300
2342
|
return text.toLowerCase().split(/[^a-z0-9\u4e00-\u9fff]+/).filter((t) => t.length > 0 && !STOPWORDS.has(t));
|
|
2301
2343
|
}
|
|
@@ -2308,6 +2350,77 @@ function jaccard(x, y) {
|
|
|
2308
2350
|
const union = new Set([...xs, ...ys]).size;
|
|
2309
2351
|
return union === 0 ? 0 : inter / union;
|
|
2310
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
|
+
}
|
|
2311
2424
|
function pairwiseAgreement(a, b) {
|
|
2312
2425
|
const aConcl = tokenize(a.conclusions.join(" "));
|
|
2313
2426
|
const bConcl = tokenize(b.conclusions.join(" "));
|
|
@@ -2347,15 +2460,6 @@ function decideRetryAction(category, consecutiveStreak, categoryRetriesUsed, bud
|
|
|
2347
2460
|
return "give-up";
|
|
2348
2461
|
return "retry";
|
|
2349
2462
|
}
|
|
2350
|
-
var VARIANT_ORDER = ["max", "high", "medium", "low"];
|
|
2351
|
-
function downgradeVariant(variant) {
|
|
2352
|
-
if (!variant)
|
|
2353
|
-
return;
|
|
2354
|
-
const idx = VARIANT_ORDER.indexOf(variant);
|
|
2355
|
-
if (idx < 0 || idx >= VARIANT_ORDER.length - 1)
|
|
2356
|
-
return;
|
|
2357
|
-
return VARIANT_ORDER[idx + 1];
|
|
2358
|
-
}
|
|
2359
2463
|
function appendRetryContext(prompt, category) {
|
|
2360
2464
|
if (category === "timeout") {
|
|
2361
2465
|
return `${prompt}
|
|
@@ -2369,29 +2473,151 @@ function appendRetryContext(prompt, category) {
|
|
|
2369
2473
|
}
|
|
2370
2474
|
return prompt;
|
|
2371
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(`
|
|
2372
2483
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
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
|
+
`);
|
|
2381
2506
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
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);
|
|
2384
2520
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
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";
|
|
2387
2555
|
}
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
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);
|
|
2393
2565
|
}
|
|
2394
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
|
+
}
|
|
2395
2621
|
|
|
2396
2622
|
class CouncilManager {
|
|
2397
2623
|
client;
|
|
@@ -2420,7 +2646,7 @@ class CouncilManager {
|
|
|
2420
2646
|
councillorResults: []
|
|
2421
2647
|
};
|
|
2422
2648
|
}
|
|
2423
|
-
const timeout = this.config.timeout ??
|
|
2649
|
+
const timeout = this.config.timeout ?? COUNCIL_TIMEOUT_MS;
|
|
2424
2650
|
const executionMode = this.config.councillor_execution_mode ?? "parallel";
|
|
2425
2651
|
const maxRounds = this.config.max_rounds ?? 2;
|
|
2426
2652
|
const consensusThreshold = this.config.consensus_threshold ?? 0.6;
|
|
@@ -2720,8 +2946,8 @@ function createCloseJobTool(ctx, tracker, onClosed) {
|
|
|
2720
2946
|
|
|
2721
2947
|
// src/index.ts
|
|
2722
2948
|
import * as fs5 from "node:fs";
|
|
2723
|
-
import * as
|
|
2724
|
-
import * as
|
|
2949
|
+
import * as path7 from "node:path";
|
|
2950
|
+
import * as os5 from "node:os";
|
|
2725
2951
|
|
|
2726
2952
|
// src/orchestration/types.ts
|
|
2727
2953
|
var VALID_TRANSITIONS = {
|
|
@@ -3102,12 +3328,12 @@ function loadFileOverrides(projectDir) {
|
|
|
3102
3328
|
];
|
|
3103
3329
|
const searchDirs = [];
|
|
3104
3330
|
if (projectDir) {
|
|
3105
|
-
searchDirs.push(
|
|
3331
|
+
searchDirs.push(path7.join(projectDir, ".opencode", "oh-my-opencode-cohub"));
|
|
3106
3332
|
}
|
|
3107
|
-
searchDirs.push(
|
|
3333
|
+
searchDirs.push(path7.join(os5.homedir(), ".config", "opencode", "oh-my-opencode-cohub"));
|
|
3108
3334
|
for (const agent of agentNames) {
|
|
3109
3335
|
for (const dir of searchDirs) {
|
|
3110
|
-
const replacePath =
|
|
3336
|
+
const replacePath = path7.join(dir, `${agent}.md`);
|
|
3111
3337
|
if (!overrides[agent]?.replace && fs5.existsSync(replacePath)) {
|
|
3112
3338
|
try {
|
|
3113
3339
|
overrides[agent] = { ...overrides[agent], replace: fs5.readFileSync(replacePath, "utf-8") };
|
|
@@ -3115,7 +3341,7 @@ function loadFileOverrides(projectDir) {
|
|
|
3115
3341
|
appendLog("loadFileOverrides", "读取replace文件失败", err);
|
|
3116
3342
|
}
|
|
3117
3343
|
}
|
|
3118
|
-
const appendPath =
|
|
3344
|
+
const appendPath = path7.join(dir, `${agent}_append.md`);
|
|
3119
3345
|
if (!overrides[agent]?.append && fs5.existsSync(appendPath)) {
|
|
3120
3346
|
try {
|
|
3121
3347
|
overrides[agent] = { ...overrides[agent], append: fs5.readFileSync(appendPath, "utf-8") };
|
|
@@ -3151,27 +3377,21 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3151
3377
|
const tracker = new TaskTracker;
|
|
3152
3378
|
const orchestrationLayer = createOrchestrationLayer(tracker, {});
|
|
3153
3379
|
let _lastAlias = "";
|
|
3154
|
-
const STATE_DIR =
|
|
3155
|
-
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");
|
|
3156
3382
|
function syncTrackerState(sessionId) {
|
|
3157
3383
|
try {
|
|
3158
|
-
if (!fs5.existsSync(STATE_DIR)) {
|
|
3159
|
-
fs5.mkdirSync(STATE_DIR, { recursive: true });
|
|
3160
|
-
}
|
|
3161
3384
|
const state = {
|
|
3162
3385
|
updatedAt: Date.now(),
|
|
3163
3386
|
runningAgents: tracker.getRunningAgents(sessionId),
|
|
3164
3387
|
runningCount: tracker.getRunningCount(sessionId)
|
|
3165
3388
|
};
|
|
3166
|
-
|
|
3389
|
+
writeFileAtomic(STATE_FILE, JSON.stringify(state));
|
|
3167
3390
|
} catch {}
|
|
3168
3391
|
}
|
|
3169
|
-
const AGENT_CONFIG_FILE =
|
|
3392
|
+
const AGENT_CONFIG_FILE = path7.join(STATE_DIR, "cohub-state.json");
|
|
3170
3393
|
function syncAgentConfig() {
|
|
3171
3394
|
try {
|
|
3172
|
-
if (!fs5.existsSync(STATE_DIR)) {
|
|
3173
|
-
fs5.mkdirSync(STATE_DIR, { recursive: true });
|
|
3174
|
-
}
|
|
3175
3395
|
const configs = agents.filter((a) => a.config.model).map((a) => {
|
|
3176
3396
|
const modelStr = a.config.model;
|
|
3177
3397
|
const parts = modelStr.split("/");
|
|
@@ -3185,7 +3405,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3185
3405
|
provider
|
|
3186
3406
|
};
|
|
3187
3407
|
});
|
|
3188
|
-
|
|
3408
|
+
writeFileAtomic(AGENT_CONFIG_FILE, JSON.stringify({ updatedAt: Date.now(), agents: configs }));
|
|
3189
3409
|
appendLog("syncAgentConfig", `已同步 ${configs.length} 个 agent: ${JSON.stringify(configs.map((c) => ({ name: c.name, model: c.model, variant: c.variant })))}`);
|
|
3190
3410
|
} catch {}
|
|
3191
3411
|
}
|
|
@@ -3252,6 +3472,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3252
3472
|
}
|
|
3253
3473
|
];
|
|
3254
3474
|
const userConfig = loadCoHubConfig(projectDir);
|
|
3475
|
+
const ctxDiag = resolveCtxDiagSettings(userConfig.diag);
|
|
3255
3476
|
if (userConfig.agents) {
|
|
3256
3477
|
for (const agent of agents) {
|
|
3257
3478
|
const override = userConfig.agents[agent.name];
|
|
@@ -3274,7 +3495,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3274
3495
|
}
|
|
3275
3496
|
const DEFAULT_COUNCIL_CONFIG = {
|
|
3276
3497
|
default_preset: "default",
|
|
3277
|
-
timeout:
|
|
3498
|
+
timeout: COUNCIL_TIMEOUT_MS,
|
|
3278
3499
|
councillor_execution_mode: "parallel",
|
|
3279
3500
|
councillor_retries: 3,
|
|
3280
3501
|
presets: {
|
|
@@ -3321,10 +3542,9 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3321
3542
|
return p.sessionId;
|
|
3322
3543
|
return;
|
|
3323
3544
|
}
|
|
3324
|
-
const STALE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
3325
3545
|
const cleanupTimer = setInterval(() => {
|
|
3326
3546
|
try {
|
|
3327
|
-
const staleSessions = tracker.cleanupStaleJobs(
|
|
3547
|
+
const staleSessions = tracker.cleanupStaleJobs(STALE_TASK_MS);
|
|
3328
3548
|
for (const sid of staleSessions) {
|
|
3329
3549
|
(async () => {
|
|
3330
3550
|
try {
|
|
@@ -3341,13 +3561,18 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3341
3561
|
appendLog("cleanupStaleJobs", "定时清理过期任务失败", err);
|
|
3342
3562
|
}
|
|
3343
3563
|
}, 30000);
|
|
3564
|
+
if (typeof cleanupTimer.unref === "function")
|
|
3565
|
+
cleanupTimer.unref();
|
|
3344
3566
|
const contextCleanupTimer = setInterval(() => {
|
|
3345
3567
|
try {
|
|
3346
3568
|
contextEngine.cleanupStaleDependencies();
|
|
3569
|
+
contextEngine.cleanupStaleContexts();
|
|
3347
3570
|
} catch (err) {
|
|
3348
3571
|
appendLog("contextCleanupTimer", "定时清理上下文失败", err);
|
|
3349
3572
|
}
|
|
3350
3573
|
}, 60000);
|
|
3574
|
+
if (typeof contextCleanupTimer.unref === "function")
|
|
3575
|
+
contextCleanupTimer.unref();
|
|
3351
3576
|
const agentConfigs = {};
|
|
3352
3577
|
const COUNCIL_ONLY_TOOLS = { council_session: "deny" };
|
|
3353
3578
|
const ORCHESTRATOR_ONLY_TOOLS = { close_job: "deny" };
|
|
@@ -3401,7 +3626,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3401
3626
|
c.agent[name] = merged;
|
|
3402
3627
|
}
|
|
3403
3628
|
try {
|
|
3404
|
-
|
|
3629
|
+
writeFileAtomic(path7.join(STATE_DIR, "config-hook-ran.json"), JSON.stringify({ ran: true, count: agents.length }));
|
|
3405
3630
|
} catch (err) {
|
|
3406
3631
|
appendLog("config", "双重注册写入文件失败", err);
|
|
3407
3632
|
}
|
|
@@ -3437,7 +3662,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3437
3662
|
});
|
|
3438
3663
|
_lastAlias = alias;
|
|
3439
3664
|
syncTrackerState(input.sessionID ?? "");
|
|
3440
|
-
tracker.pruneTerminalJobs(
|
|
3665
|
+
tracker.pruneTerminalJobs(STALE_TASK_MS);
|
|
3441
3666
|
if (orchestrationLayer && subagentType && description) {
|
|
3442
3667
|
try {
|
|
3443
3668
|
orchestrationLayer.engine.registerTask(alias, subagentType, description, []);
|
|
@@ -3474,30 +3699,21 @@ var CoHubPlugin = async (input, options) => {
|
|
|
3474
3699
|
if (details) {
|
|
3475
3700
|
output.args[targetField] += details;
|
|
3476
3701
|
}
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
const
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
subagent: subagentType,
|
|
3493
|
-
strategy,
|
|
3494
|
-
targetField,
|
|
3495
|
-
hasDetails: !!details,
|
|
3496
|
-
detailsLen: details ? details.length : 0,
|
|
3497
|
-
detailsPreview: details ? details.replace(/\n/g, "\\n").slice(0, 200) : "",
|
|
3498
|
-
promptEnd: finalPrompt.slice(-120)
|
|
3499
|
-
}) + `
|
|
3500
|
-
`);
|
|
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
|
+
}
|
|
3501
3717
|
}
|
|
3502
3718
|
}
|
|
3503
3719
|
const targetField = typeof output.args?.prompt === "string" ? "prompt" : "description";
|