oh-my-opencode-cohub 1.12.13 → 1.13.0-beta.1

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.
Files changed (49) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/index.js +115 -15
  3. package/dist/config/loader.d.ts +5 -16
  4. package/dist/config/loader.d.ts.map +1 -1
  5. package/dist/context/engine.d.ts +15 -1
  6. package/dist/context/engine.d.ts.map +1 -1
  7. package/dist/context/extractor.d.ts +8 -0
  8. package/dist/context/extractor.d.ts.map +1 -1
  9. package/dist/context/truncate.test.d.ts +2 -0
  10. package/dist/context/truncate.test.d.ts.map +1 -0
  11. package/dist/context/types.d.ts +2 -0
  12. package/dist/context/types.d.ts.map +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1033 -111
  15. package/dist/prompts/designer.d.ts +1 -1
  16. package/dist/prompts/designer.d.ts.map +1 -1
  17. package/dist/prompts/fixer.d.ts +1 -1
  18. package/dist/prompts/fixer.d.ts.map +1 -1
  19. package/dist/prompts/oracle.d.ts +1 -1
  20. package/dist/prompts/oracle.d.ts.map +1 -1
  21. package/dist/task-manager/quality.d.ts +51 -0
  22. package/dist/task-manager/quality.d.ts.map +1 -0
  23. package/dist/task-manager/quality.test.d.ts +2 -0
  24. package/dist/task-manager/quality.test.d.ts.map +1 -0
  25. package/dist/task-manager/stats.test.d.ts +2 -0
  26. package/dist/task-manager/stats.test.d.ts.map +1 -0
  27. package/dist/task-manager/tracker.d.ts +77 -8
  28. package/dist/task-manager/tracker.d.ts.map +1 -1
  29. package/dist/task-manager/tracker.test.d.ts +2 -0
  30. package/dist/task-manager/tracker.test.d.ts.map +1 -0
  31. package/dist/task-manager/types.d.ts +11 -0
  32. package/dist/task-manager/types.d.ts.map +1 -1
  33. package/dist/tools/adaptive-params.d.ts +63 -0
  34. package/dist/tools/adaptive-params.d.ts.map +1 -0
  35. package/dist/tools/adaptive-params.test.d.ts +2 -0
  36. package/dist/tools/adaptive-params.test.d.ts.map +1 -0
  37. package/dist/tools/council.d.ts +78 -1
  38. package/dist/tools/council.d.ts.map +1 -1
  39. package/dist/tools/council.test.d.ts +2 -0
  40. package/dist/tools/council.test.d.ts.map +1 -0
  41. package/dist/tools/job-control.d.ts +14 -0
  42. package/dist/tools/job-control.d.ts.map +1 -0
  43. package/dist/tools/model-stats.d.ts +56 -0
  44. package/dist/tools/model-stats.d.ts.map +1 -0
  45. package/dist/tools/model-stats.test.d.ts +2 -0
  46. package/dist/tools/model-stats.test.d.ts.map +1 -0
  47. package/dist/tui.d.ts.map +1 -1
  48. package/dist/tui.js +26 -5
  49. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -144,6 +144,12 @@ var ORACLE_PROMPT = `你是 Oracle——战略技术顾问和代码审查者。
144
144
  - 聚焦策略,不聚焦执行
145
145
  - 必要时指出具体文件/行号
146
146
 
147
+ **代码审查清单**:
148
+ - 正确性与安全性:逻辑错误、边界情况、注入/越权风险
149
+ - 性能与可维护性:明显的性能陷阱、不必要的复杂度
150
+ - 该删没删的代码:调试残留、被替代的旧实现、失效的 import、为未来预留却从未使用的抽象
151
+ - 过度工程检测:引入了超出任务需求的依赖、配置项、封装层
152
+
147
153
  **文件操作规则**:
148
154
  - 只读:检查并报告,不修改文件
149
155
  - 优先使用专用文件工具检查代码库:glob/grep/ast_grep_search 用于发现,read 用于读取文件内容
@@ -305,6 +311,11 @@ var DESIGNER_PROMPT = `你是 Designer——前端 UI/UX 专家,创造和审
305
311
  - 极简主义设计 → 克制、精准、精心处理间距和排版
306
312
  - 优雅来自完全执行所选愿景,而非半途而废
307
313
 
314
+ **最小实现自查**
315
+ - 实现代码时依次自查:能复用现有组件/样式模式吗?平台原生元素够用吗?
316
+ - 只写达成当前设计愿景所需的最小实现;未被要求的包装层与投机抽象不加
317
+ - ⚠️ 此原则约束的是代码实现的克制,不约束视觉表达——视觉上大胆丰富依然鼓励
318
+
308
319
  ## 约束
309
320
  - 有现有设计系统时尊重它
310
321
  - 有组件库时利用它
@@ -357,6 +368,12 @@ var FIXER_PROMPT = `你是 Fixer——快速、聚焦的实现专家。
357
368
  - 只询问真正无法自己获取的缺失信息
358
369
  - 不要充当主要审查者;实施请求的变更并简要指出明显问题
359
370
 
371
+ **最小实现自查阶梯**:
372
+ - 写代码前依次自问,停在第一级成立处:这个改动必须存在吗?代码库里已有可复用的实现吗?标准库/平台原生能力/已安装依赖能完成吗?
373
+ - 上述都不满足时,只写能工作的最小实现
374
+ - 未被要求的防御性代码、配置项、提前抽象——一律不加
375
+ - 验证、错误处理、安全相关代码永不在省略之列
376
+
360
377
  **输出格式**:
361
378
  <summary>
362
379
  简要总结实施内容
@@ -560,6 +577,62 @@ var CHINESE_LANGUAGE_INSTRUCTION = `# 中文语言要求
560
577
  - 跨所有项目生效,优先级高于项目级规则`;
561
578
 
562
579
  // src/task-manager/tracker.ts
580
+ import * as fs from "node:fs";
581
+ import * as os from "node:os";
582
+ import * as path from "node:path";
583
+ var DEFAULT_STATS_WINDOW = 50;
584
+ var STATS_FILE = path.join(os.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub", "stats.json");
585
+ var AGENT_DEFAULT_STRATEGY = {
586
+ "co-fixer": "relevant",
587
+ "co-designer": "relevant",
588
+ "co-planner": "relevant",
589
+ "co-oracle": "summary",
590
+ "co-council": "summary"
591
+ };
592
+ function defaultStrategyFor(agent) {
593
+ return AGENT_DEFAULT_STRATEGY[agent] ?? "none";
594
+ }
595
+ function computeStats(records, window = DEFAULT_STATS_WINDOW) {
596
+ const recent = records.slice(-Math.max(1, window));
597
+ const buckets = new Map;
598
+ for (const r of recent) {
599
+ const key = r.strategy + "\x00" + r.agent;
600
+ let bucket = buckets.get(key);
601
+ if (!bucket) {
602
+ bucket = {
603
+ strategy: r.strategy,
604
+ agent: r.agent,
605
+ count: 0,
606
+ successCount: 0,
607
+ successRate: 0,
608
+ avgLatencyMs: 0,
609
+ latencySamples: 0,
610
+ avgTokens: 0,
611
+ tokenSamples: 0
612
+ };
613
+ buckets.set(key, bucket);
614
+ }
615
+ bucket.count += 1;
616
+ if (r.status === "completed")
617
+ bucket.successCount += 1;
618
+ if (typeof r.latencyMs === "number") {
619
+ bucket.avgLatencyMs = (bucket.avgLatencyMs * bucket.latencySamples + r.latencyMs) / (bucket.latencySamples + 1);
620
+ bucket.latencySamples += 1;
621
+ }
622
+ if (r.tokens && typeof r.tokens.input === "number" && typeof r.tokens.output === "number") {
623
+ const total = r.tokens.input + r.tokens.output;
624
+ bucket.avgTokens = (bucket.avgTokens * bucket.tokenSamples + total) / (bucket.tokenSamples + 1);
625
+ bucket.tokenSamples += 1;
626
+ }
627
+ }
628
+ const result = Array.from(buckets.values());
629
+ for (const b of result) {
630
+ b.successRate = b.count > 0 ? b.successCount / b.count * 100 : 0;
631
+ }
632
+ result.sort((a, b) => a.strategy.localeCompare(b.strategy) || a.agent.localeCompare(b.agent));
633
+ return result;
634
+ }
635
+
563
636
  class TaskTracker {
564
637
  jobs = new Map;
565
638
  counters = new Map;
@@ -592,6 +665,7 @@ class TaskTracker {
592
665
  sessionId: args.task_id ?? "",
593
666
  parentSessionId,
594
667
  agent,
668
+ contextStrategy: defaultStrategyFor(agent),
595
669
  label,
596
670
  status: "running",
597
671
  background: args.background ?? false,
@@ -618,12 +692,16 @@ class TaskTracker {
618
692
  latest.terminalReconciled = true;
619
693
  if (sessionId)
620
694
  latest.sessionId = sessionId;
695
+ this.finalize(latest);
696
+ this.persistStats();
621
697
  }
622
698
  }
623
699
  updateByChildSessionId(sessionId, status) {
624
700
  for (const job of this.jobs.values()) {
625
701
  if (job.sessionId === sessionId && job.background && job.status === "running") {
626
702
  job.status = status;
703
+ this.finalize(job);
704
+ this.persistStats();
627
705
  return;
628
706
  }
629
707
  }
@@ -699,11 +777,17 @@ class TaskTracker {
699
777
  }
700
778
  cleanupStaleJobs(timeoutMs) {
701
779
  const now = Date.now();
780
+ const staleSessions = [];
702
781
  for (const job of this.jobs.values()) {
703
782
  if (job.background && job.status === "running" && now - job.createdAt > timeoutMs) {
704
783
  job.status = "errored";
784
+ this.finalize(job);
785
+ if (job.sessionId)
786
+ staleSessions.push(job.sessionId);
705
787
  }
706
788
  }
789
+ this.persistStats();
790
+ return staleSessions;
707
791
  }
708
792
  isReusable(alias) {
709
793
  const job = this.jobs.get(alias);
@@ -730,11 +814,49 @@ class TaskTracker {
730
814
  getJobBySessionId(sessionId) {
731
815
  for (const job of this.jobs.values()) {
732
816
  if (job.sessionId === sessionId) {
733
- return { alias: job.alias, agent: job.agent };
817
+ return job;
734
818
  }
735
819
  }
736
820
  return;
737
821
  }
822
+ updateQuality(sessionId, quality) {
823
+ for (const job of this.jobs.values()) {
824
+ if (job.sessionId === sessionId) {
825
+ job.quality = quality;
826
+ this.finalize(job);
827
+ this.persistStats();
828
+ return;
829
+ }
830
+ }
831
+ }
832
+ finalize(job) {
833
+ const latencyMs = Date.now() - job.createdAt;
834
+ if (!job.quality) {
835
+ job.quality = { latencyMs };
836
+ } else if (job.quality.latencyMs === undefined) {
837
+ job.quality.latencyMs = latencyMs;
838
+ }
839
+ }
840
+ collectStatsRecords() {
841
+ const jobs = Array.from(this.jobs.values()).filter((j) => j.status !== "running").sort((a, b) => a.createdAt - b.createdAt);
842
+ return jobs.map((job) => ({
843
+ agent: job.agent,
844
+ strategy: job.contextStrategy ?? defaultStrategyFor(job.agent),
845
+ status: job.status,
846
+ ...job.quality?.latencyMs !== undefined ? { latencyMs: job.quality.latencyMs } : {},
847
+ ...job.quality?.tokens ? { tokens: job.quality.tokens } : {}
848
+ }));
849
+ }
850
+ getStats(window = DEFAULT_STATS_WINDOW) {
851
+ return computeStats(this.collectStatsRecords(), window);
852
+ }
853
+ persistStats(window = DEFAULT_STATS_WINDOW) {
854
+ try {
855
+ const recent = this.collectStatsRecords().slice(-Math.max(1, window));
856
+ fs.mkdirSync(path.dirname(STATS_FILE), { recursive: true });
857
+ fs.writeFileSync(STATS_FILE, JSON.stringify(recent, null, 2), "utf8");
858
+ } catch {}
859
+ }
738
860
  markCancelled(taskId) {
739
861
  let job = this.jobs.get(taskId);
740
862
  if (!job) {
@@ -748,10 +870,183 @@ class TaskTracker {
748
870
  if (job) {
749
871
  job.status = "cancelled";
750
872
  job.terminalReconciled = true;
873
+ this.finalize(job);
874
+ this.persistStats();
875
+ }
876
+ }
877
+ abortJob(taskId) {
878
+ let job = this.jobs.get(taskId);
879
+ if (!job) {
880
+ for (const j of this.jobs.values()) {
881
+ if (j.sessionId === taskId) {
882
+ job = j;
883
+ break;
884
+ }
885
+ }
751
886
  }
887
+ if (!job)
888
+ return;
889
+ if (job.status !== "running") {
890
+ return { sessionId: job.sessionId || undefined, job };
891
+ }
892
+ job.status = "cancelled";
893
+ job.terminalReconciled = true;
894
+ this.finalize(job);
895
+ this.persistStats();
896
+ return { sessionId: job.sessionId || undefined, job };
752
897
  }
753
898
  }
754
899
 
900
+ // src/task-manager/quality.ts
901
+ var DEFAULT_QUALITY_CONFIG = {
902
+ enabled: true
903
+ };
904
+ var ERROR_KEYWORDS = [
905
+ "error",
906
+ "failed",
907
+ "failure",
908
+ "exception",
909
+ "traceback",
910
+ "unable to",
911
+ "失败",
912
+ "错误",
913
+ "异常",
914
+ "无法完成",
915
+ "未能"
916
+ ];
917
+ var BOUNDARY_KEYWORDS = new Set(["error", "failed", "failure", "exception"]);
918
+ var NEGATION_PHRASES = [
919
+ "no error",
920
+ "no errors",
921
+ "no failure",
922
+ "no failures",
923
+ "no exception",
924
+ "no exceptions",
925
+ "without error",
926
+ "without errors",
927
+ "without any error",
928
+ "without any errors",
929
+ "without failure",
930
+ "without failures",
931
+ "without exception",
932
+ "without exceptions",
933
+ "without any failure",
934
+ "without any failures",
935
+ "without any exception",
936
+ "without any exceptions",
937
+ "with no error",
938
+ "with no errors",
939
+ "with no failure",
940
+ "with no failures",
941
+ "not an error",
942
+ "not a failure",
943
+ "is not an error",
944
+ "never failed",
945
+ "never fails",
946
+ "did not fail",
947
+ "does not fail",
948
+ "not failed",
949
+ "无错误",
950
+ "没有错误",
951
+ "未失败",
952
+ "没有失败",
953
+ "未发现错误",
954
+ "未出现错误",
955
+ "未发生错误",
956
+ "不存在错误",
957
+ "无异常",
958
+ "没有异常",
959
+ "未发现异常",
960
+ "未出现异常",
961
+ "没有发现错误",
962
+ "没有发现任何错误",
963
+ "未发现任何错误",
964
+ "无任何错误",
965
+ "未出现任何错误",
966
+ "未发生任何错误",
967
+ "不存在任何错误",
968
+ "没有发现异常",
969
+ "未发现任何异常",
970
+ "无任何异常",
971
+ "未遇到错误",
972
+ "未产生错误",
973
+ "未遇到异常",
974
+ "未产生异常"
975
+ ];
976
+ var NEGATION_WINDOW = 16;
977
+ function isNegated(lower, hitStart, hitLength) {
978
+ const windowStart = Math.max(0, hitStart - NEGATION_WINDOW);
979
+ const window = lower.slice(windowStart, hitStart + hitLength);
980
+ return NEGATION_PHRASES.some((phrase) => window.includes(phrase));
981
+ }
982
+ function hasErrorKeywords(text) {
983
+ const lower = text.toLowerCase();
984
+ for (const kw of BOUNDARY_KEYWORDS) {
985
+ const re = new RegExp(`\\b${kw}\\b`, "g");
986
+ let m;
987
+ while ((m = re.exec(lower)) !== null) {
988
+ const before = m.index > 0 ? lower[m.index - 1] : "";
989
+ const after = lower[m.index + kw.length] ?? "";
990
+ if (before === "." || after === "(")
991
+ continue;
992
+ if (isNegated(lower, m.index, kw.length))
993
+ continue;
994
+ return true;
995
+ }
996
+ }
997
+ for (const kw of ERROR_KEYWORDS) {
998
+ if (BOUNDARY_KEYWORDS.has(kw))
999
+ continue;
1000
+ let idx = lower.indexOf(kw);
1001
+ while (idx !== -1) {
1002
+ if (!isNegated(lower, idx, kw.length))
1003
+ return true;
1004
+ idx = lower.indexOf(kw, idx + kw.length);
1005
+ }
1006
+ }
1007
+ return false;
1008
+ }
1009
+ function assessQuality(input) {
1010
+ const outputOk = input.output.trim().length > 0;
1011
+ const exitOk = input.exitCode === undefined || input.exitCode === 0;
1012
+ const keywordOk = !hasErrorKeywords(input.output);
1013
+ const decisionsOk = input.decisions >= 1;
1014
+ const timedOut = input.timedOut === true;
1015
+ let score = 0;
1016
+ if (outputOk)
1017
+ score += 0.25;
1018
+ if (exitOk && !timedOut)
1019
+ score += 0.25;
1020
+ if (keywordOk && !timedOut)
1021
+ score += 0.25;
1022
+ if (decisionsOk && !timedOut)
1023
+ score += 0.25;
1024
+ if (!outputOk)
1025
+ score = 0;
1026
+ const passed = !timedOut && outputOk && exitOk && keywordOk && decisionsOk;
1027
+ let failureCategory;
1028
+ if (!passed) {
1029
+ if (timedOut)
1030
+ failureCategory = "timeout";
1031
+ else if (!outputOk)
1032
+ failureCategory = "empty";
1033
+ else if (!exitOk || !keywordOk)
1034
+ failureCategory = "error";
1035
+ else
1036
+ failureCategory = "quality-low";
1037
+ }
1038
+ return {
1039
+ score,
1040
+ passed,
1041
+ ...failureCategory ? { failureCategory } : {},
1042
+ ...input.latencyMs !== undefined ? { latencyMs: input.latencyMs } : {},
1043
+ ...input.tokens ? { tokens: input.tokens } : {}
1044
+ };
1045
+ }
1046
+ function isQualityEnabled(config) {
1047
+ return config?.enabled ?? DEFAULT_QUALITY_CONFIG.enabled;
1048
+ }
1049
+
755
1050
  // src/context/types.ts
756
1051
  var DEFAULT_CONTEXT_CONFIG = {
757
1052
  strategy: {
@@ -776,6 +1071,9 @@ var DEFAULT_CONTEXT_CONFIG = {
776
1071
  relevantMessageWindow: 20
777
1072
  };
778
1073
 
1074
+ // src/context/engine.ts
1075
+ import * as fs2 from "node:fs";
1076
+
779
1077
  // src/context/extractor.ts
780
1078
  var KNOWN_EXTENSIONS = new Set([
781
1079
  "ts",
@@ -827,12 +1125,12 @@ function extractRelevantFiles(messages, maxFiles, windowSize) {
827
1125
  if (part.type === "tool" && part.state) {
828
1126
  const state = part.state;
829
1127
  const input = state.input ?? {};
830
- const path = extractPath(input);
831
- if (path) {
832
- if (!fileMap.has(path)) {
833
- fileMap.set(path, { path, summary: "" });
1128
+ const path2 = extractPath(input);
1129
+ if (path2) {
1130
+ if (!fileMap.has(path2)) {
1131
+ fileMap.set(path2, { path: path2, summary: "" });
834
1132
  }
835
- const existing = fileMap.get(path);
1133
+ const existing = fileMap.get(path2);
836
1134
  if (typeof input.offset === "number") {
837
1135
  const limit = typeof input.limit === "number" ? input.limit : 50;
838
1136
  existing.lines = `${input.offset}-${input.offset + limit}`;
@@ -952,6 +1250,25 @@ function extractErrors(messages, maxErrors, windowSize) {
952
1250
  }
953
1251
  return errors;
954
1252
  }
1253
+ function truncateByTokens(text, maxTokens) {
1254
+ if (!text || maxTokens <= 0)
1255
+ return "";
1256
+ let used = 0;
1257
+ let cut = text.length;
1258
+ for (let i = 0;i < text.length; i++) {
1259
+ const code = text.charCodeAt(i);
1260
+ const isCjk = code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 63744 && code <= 64255 || code >= 12352 && code <= 12543 || code >= 44032 && code <= 55215;
1261
+ used += isCjk ? 1 : 0.25;
1262
+ if (used > maxTokens) {
1263
+ cut = i;
1264
+ break;
1265
+ }
1266
+ }
1267
+ if (cut >= text.length)
1268
+ return text;
1269
+ return text.slice(0, cut) + `
1270
+ … [正文已按 token 预算截断]`;
1271
+ }
955
1272
 
956
1273
  // src/context/formatter.ts
957
1274
  var CONTEXT_MARKER_PATTERN = /<!-- CONTEXT:ID=([a-f0-9-]+) -->/;
@@ -1043,6 +1360,7 @@ class ContextEngine {
1043
1360
  return;
1044
1361
  if (args.strategy === "none")
1045
1362
  return;
1363
+ context.strategy = args.strategy;
1046
1364
  try {
1047
1365
  const windowSize = this.config.relevantMessageWindow;
1048
1366
  const messagesResult = await this.client.session.messages({
@@ -1055,11 +1373,25 @@ class ContextEngine {
1055
1373
  context.decisions = extractDecisions(messages, this.config.maxDecisions, windowSize);
1056
1374
  context.errors = extractErrors(messages, this.config.maxErrors, windowSize);
1057
1375
  }
1376
+ if (args.strategy === "summary") {
1377
+ this.attachTruncatedBodies(context.relevantFiles);
1378
+ }
1058
1379
  if (this.config.dependencyPropagation && this.dependencyCache.size > 0) {
1059
1380
  context.dependencies = Array.from(this.dependencyCache.values()).slice(-this.config.maxDependencies);
1060
1381
  }
1061
1382
  } catch {}
1062
1383
  }
1384
+ attachTruncatedBodies(files) {
1385
+ if (files.length === 0)
1386
+ return;
1387
+ const budgetPerFile = Math.max(1, Math.floor(this.config.summarizeMaxTokens / files.length));
1388
+ for (const file of files) {
1389
+ try {
1390
+ const content = fs2.readFileSync(file.path, "utf-8");
1391
+ file.body = truncateByTokens(content, budgetPerFile);
1392
+ } catch {}
1393
+ }
1394
+ }
1063
1395
  consumeMarkedContext(messageText) {
1064
1396
  const markerMatch = messageText.match(CONTEXT_MARKER_PATTERN);
1065
1397
  if (!markerMatch)
@@ -1078,10 +1410,23 @@ class ContextEngine {
1078
1410
  if (!context)
1079
1411
  return "";
1080
1412
  const parts = [];
1413
+ const isSummary = context.strategy === "summary";
1081
1414
  if (context.relevantFiles.length > 0) {
1082
- parts.push("### \uD83D\uDCC1 相关文件");
1083
- for (const file of context.relevantFiles) {
1084
- parts.push(`- \`${file.path}\`` + (file.summary ? ` — ${file.summary}` : ""));
1415
+ if (isSummary) {
1416
+ parts.push("### \uD83D\uDCC1 相关文件(正文按 token 预算截断)");
1417
+ for (const file of context.relevantFiles) {
1418
+ const loc = file.lines ? ":" + file.lines : "";
1419
+ if (file.body) {
1420
+ parts.push("- `" + file.path + loc + "`:\n```text\n" + file.body + "\n```");
1421
+ } else {
1422
+ parts.push("- `" + file.path + loc + "` — " + (file.summary || "(正文不可用)"));
1423
+ }
1424
+ }
1425
+ } else {
1426
+ parts.push("### \uD83D\uDCC1 相关文件");
1427
+ for (const file of context.relevantFiles) {
1428
+ parts.push("- `" + file.path + "`" + (file.summary ? " — " + file.summary : ""));
1429
+ }
1085
1430
  }
1086
1431
  }
1087
1432
  if (context.decisions.length > 0) {
@@ -1108,27 +1453,31 @@ class ContextEngine {
1108
1453
  ` : "";
1109
1454
  }
1110
1455
  async captureResult(childSessionId, alias, agent) {
1111
- if (!this.config.dependencyPropagation)
1112
- return;
1113
1456
  try {
1114
1457
  const messagesResult = await this.client.session.messages({
1115
1458
  path: { id: childSessionId }
1116
1459
  });
1117
1460
  const messages = messagesResult.data ?? [];
1118
1461
  let keyOutput = "";
1462
+ let tokens;
1119
1463
  for (let i = messages.length - 1;i >= 0; i--) {
1120
- if (messages[i].info?.role === "assistant") {
1121
- for (const part of messages[i].parts ?? []) {
1122
- if (part.type === "text" && part.text) {
1123
- keyOutput = part.text.slice(0, 500).replace(/\n/g, " ");
1124
- break;
1125
- }
1126
- }
1127
- if (keyOutput)
1464
+ const info = messages[i].info;
1465
+ if (!info || info.role !== "assistant")
1466
+ continue;
1467
+ if (typeof info.tokens?.input === "number" && typeof info.tokens?.output === "number") {
1468
+ tokens = { input: info.tokens.input, output: info.tokens.output };
1469
+ }
1470
+ for (const part of messages[i].parts ?? []) {
1471
+ if (part.type === "text" && part.text) {
1472
+ keyOutput = part.text.slice(0, 500).replace(/\n/g, " ");
1128
1473
  break;
1474
+ }
1129
1475
  }
1476
+ if (keyOutput)
1477
+ break;
1130
1478
  }
1131
- if (keyOutput) {
1479
+ const decisions = extractDecisions(messages, this.config.maxDecisions, this.config.relevantMessageWindow);
1480
+ if (keyOutput && this.config.dependencyPropagation) {
1132
1481
  this.dependencyCache.set(alias, {
1133
1482
  alias,
1134
1483
  agent,
@@ -1136,7 +1485,14 @@ class ContextEngine {
1136
1485
  capturedAt: Date.now()
1137
1486
  });
1138
1487
  }
1139
- } catch {}
1488
+ return {
1489
+ output: keyOutput,
1490
+ decisions: decisions.length,
1491
+ ...tokens ? { tokens } : {}
1492
+ };
1493
+ } catch {
1494
+ return null;
1495
+ }
1140
1496
  }
1141
1497
  formatMarker(contextId) {
1142
1498
  return formatContextMarker(contextId);
@@ -1155,15 +1511,15 @@ class ContextEngine {
1155
1511
  }
1156
1512
 
1157
1513
  // src/config/loader.ts
1158
- import * as fs from "node:fs";
1159
- import * as path from "node:path";
1160
- import * as os from "node:os";
1161
- var USER_CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "oh-my-opencode-cohub.json");
1514
+ import * as fs3 from "node:fs";
1515
+ import * as path2 from "node:path";
1516
+ import * as os2 from "node:os";
1517
+ var USER_CONFIG_PATH = path2.join(os2.homedir(), ".config", "opencode", "oh-my-opencode-cohub.json");
1162
1518
  function loadCoHubConfig(projectDir) {
1163
1519
  const base = readJSON(USER_CONFIG_PATH);
1164
1520
  if (!projectDir)
1165
1521
  return base;
1166
- const projectPath = path.join(projectDir, ".opencode", "oh-my-opencode-cohub.json");
1522
+ const projectPath = path2.join(projectDir, ".opencode", "oh-my-opencode-cohub.json");
1167
1523
  const project = readJSON(projectPath);
1168
1524
  if (!project || Object.keys(project).length === 0)
1169
1525
  return base;
@@ -1177,13 +1533,16 @@ function loadCoHubConfig(projectDir) {
1177
1533
  if (project.context) {
1178
1534
  merged.context = { ...base.context, ...project.context };
1179
1535
  }
1536
+ if (project.quality) {
1537
+ merged.quality = { ...base.quality, ...project.quality };
1538
+ }
1180
1539
  return merged;
1181
1540
  }
1182
1541
  function readJSON(filePath) {
1183
1542
  try {
1184
- if (!fs.existsSync(filePath))
1543
+ if (!fs3.existsSync(filePath))
1185
1544
  return {};
1186
- const raw = fs.readFileSync(filePath, "utf-8");
1545
+ const raw = fs3.readFileSync(filePath, "utf-8");
1187
1546
  return JSON.parse(raw);
1188
1547
  } catch {
1189
1548
  return {};
@@ -1194,10 +1553,10 @@ function readJSON(filePath) {
1194
1553
  import { tool } from "@opencode-ai/plugin";
1195
1554
 
1196
1555
  // src/utils/log.ts
1197
- import * as fs2 from "fs/promises";
1198
- import * as path2 from "path";
1199
- import * as os2 from "os";
1200
- var LOG_DIR = path2.join(os2.homedir(), ".local", "share", "opencode", "log");
1556
+ import * as fs4 from "fs/promises";
1557
+ import * as path3 from "path";
1558
+ import * as os3 from "os";
1559
+ var LOG_DIR = path3.join(os3.homedir(), ".local", "share", "opencode", "log");
1201
1560
  var LOG_PREFIX = "oh-my-opencode-cohub";
1202
1561
  var KEEP_DAYS = 7;
1203
1562
  var _today = "";
@@ -1207,7 +1566,7 @@ function getLogPath() {
1207
1566
  const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
1208
1567
  if (today !== _today) {
1209
1568
  _today = today;
1210
- _logPath = path2.join(LOG_DIR, `${LOG_PREFIX}.${today}.log`);
1569
+ _logPath = path3.join(LOG_DIR, `${LOG_PREFIX}.${today}.log`);
1211
1570
  _cleanedToday = false;
1212
1571
  }
1213
1572
  return _logPath;
@@ -1220,7 +1579,7 @@ async function cleanOldLogsOnce() {
1220
1579
  const cutoff = new Date;
1221
1580
  cutoff.setUTCDate(cutoff.getUTCDate() - KEEP_DAYS);
1222
1581
  const cutoffStr = cutoff.toISOString().slice(0, 10).replace(/-/g, "");
1223
- const files = await fs2.readdir(LOG_DIR);
1582
+ const files = await fs4.readdir(LOG_DIR);
1224
1583
  for (const file of files) {
1225
1584
  if (!file.startsWith(LOG_PREFIX + "."))
1226
1585
  continue;
@@ -1229,7 +1588,7 @@ async function cleanOldLogsOnce() {
1229
1588
  if (!match)
1230
1589
  continue;
1231
1590
  if (match[1] < cutoffStr) {
1232
- await fs2.unlink(path2.join(LOG_DIR, file)).catch(() => {});
1591
+ await fs4.unlink(path3.join(LOG_DIR, file)).catch(() => {});
1233
1592
  }
1234
1593
  }
1235
1594
  } catch {}
@@ -1237,7 +1596,7 @@ async function cleanOldLogsOnce() {
1237
1596
  async function appendLog(tag, message, err) {
1238
1597
  try {
1239
1598
  const logPath = getLogPath();
1240
- await fs2.mkdir(LOG_DIR, { recursive: true });
1599
+ await fs4.mkdir(LOG_DIR, { recursive: true });
1241
1600
  const now = new Date().toISOString();
1242
1601
  let line = `[${now}] [oh-my-opencode-cohub] [${tag}] ${message}`;
1243
1602
  if (err instanceof Error) {
@@ -1249,13 +1608,158 @@ async function appendLog(tag, message, err) {
1249
1608
  }
1250
1609
  line += `
1251
1610
  `;
1252
- await fs2.appendFile(logPath, line, "utf-8");
1611
+ await fs4.appendFile(logPath, line, "utf-8");
1253
1612
  await cleanOldLogsOnce();
1254
1613
  } catch {}
1255
1614
  }
1256
1615
 
1616
+ // src/tools/model-stats.ts
1617
+ var DEFAULT_MODEL_STATS_CONFIG = {
1618
+ windowSize: 20,
1619
+ highFailureRate: 0.5,
1620
+ minSamples: 5
1621
+ };
1622
+ var config = { ...DEFAULT_MODEL_STATS_CONFIG };
1623
+ var history = new Map;
1624
+ function recordModelResult(model, category) {
1625
+ const window = history.get(model) ?? [];
1626
+ window.push(category);
1627
+ if (window.length > config.windowSize) {
1628
+ window.splice(0, window.length - config.windowSize);
1629
+ }
1630
+ history.set(model, window);
1631
+ }
1632
+ function getModelStats(model) {
1633
+ const window = history.get(model) ?? [];
1634
+ let successes = 0;
1635
+ let empty = 0;
1636
+ let timeout = 0;
1637
+ let error = 0;
1638
+ let qualityLow = 0;
1639
+ for (const c of window) {
1640
+ if (c === "success") {
1641
+ successes += 1;
1642
+ } else if (c === "empty") {
1643
+ empty += 1;
1644
+ } else if (c === "timeout") {
1645
+ timeout += 1;
1646
+ } else if (c === "error") {
1647
+ error += 1;
1648
+ } else {
1649
+ qualityLow += 1;
1650
+ }
1651
+ }
1652
+ const total = window.length;
1653
+ const failures = total - successes;
1654
+ return {
1655
+ model,
1656
+ total,
1657
+ successes,
1658
+ failures,
1659
+ empty,
1660
+ timeout,
1661
+ error,
1662
+ qualityLow,
1663
+ failureRate: total === 0 ? 0 : failures / total,
1664
+ successRate: total === 0 ? 1 : successes / total
1665
+ };
1666
+ }
1667
+ function orderCouncillorsByFailure(entries, statsProvider = getModelStats, cfg = config) {
1668
+ const isHighRisk = ([, c]) => {
1669
+ const s = statsProvider(c.model);
1670
+ return s.total >= cfg.minSamples && s.failureRate > cfg.highFailureRate;
1671
+ };
1672
+ const preferred = entries.filter((entry) => !isHighRisk(entry));
1673
+ const chosen = preferred.length > 0 ? preferred : [...entries];
1674
+ return chosen.sort((a, b) => statsProvider(a[1].model).failureRate - statsProvider(b[1].model).failureRate);
1675
+ }
1676
+
1677
+ // src/tools/adaptive-params.ts
1678
+ var DEFAULT_ADAPTIVE_CONFIG = {
1679
+ highRate: 0.8,
1680
+ lowRate: 0.5,
1681
+ minSamples: 5,
1682
+ hysteresisSamples: 5,
1683
+ minRetries: 1,
1684
+ maxRetries: 5,
1685
+ minTimeoutMs: 60000,
1686
+ maxTimeoutMs: 300000,
1687
+ defaultRetries: 3,
1688
+ defaultTimeoutMs: 180000,
1689
+ timeoutStepMs: 30000,
1690
+ windowSize: 20
1691
+ };
1692
+ var config2 = { ...DEFAULT_ADAPTIVE_CONFIG };
1693
+ var samples = new Map;
1694
+ function evaluateAdjustment(successRate, sampleCount, cfg = config2) {
1695
+ if (sampleCount < cfg.minSamples)
1696
+ return 0;
1697
+ if (successRate > cfg.highRate)
1698
+ return -1;
1699
+ if (successRate < cfg.lowRate)
1700
+ return 1;
1701
+ return 0;
1702
+ }
1703
+ function clampAdaptiveParams(params, cfg = config2) {
1704
+ return {
1705
+ retries: Math.min(cfg.maxRetries, Math.max(cfg.minRetries, Math.round(params.retries))),
1706
+ timeoutMs: Math.min(cfg.maxTimeoutMs, Math.max(cfg.minTimeoutMs, Math.round(params.timeoutMs)))
1707
+ };
1708
+ }
1709
+ function adjustAdaptiveParams(current, direction, cfg = config2) {
1710
+ if (direction === 0)
1711
+ return clampAdaptiveParams(current, cfg);
1712
+ return clampAdaptiveParams({
1713
+ retries: current.retries + direction,
1714
+ timeoutMs: current.timeoutMs + direction * cfg.timeoutStepMs
1715
+ }, cfg);
1716
+ }
1717
+ function recordAdaptiveSample(key, success, cfg = config2) {
1718
+ let st = samples.get(key);
1719
+ if (!st) {
1720
+ st = {
1721
+ recent: [],
1722
+ current: { retries: cfg.defaultRetries, timeoutMs: cfg.defaultTimeoutMs },
1723
+ lastDirection: 0,
1724
+ streak: 0
1725
+ };
1726
+ samples.set(key, st);
1727
+ }
1728
+ st.recent.push(success);
1729
+ if (st.recent.length > cfg.windowSize) {
1730
+ st.recent.splice(0, st.recent.length - cfg.windowSize);
1731
+ }
1732
+ const total = st.recent.length;
1733
+ if (total < cfg.minSamples)
1734
+ return;
1735
+ const successes = st.recent.filter(Boolean).length;
1736
+ const direction = evaluateAdjustment(successes / total, total, cfg);
1737
+ if (direction === st.lastDirection) {
1738
+ st.streak += 1;
1739
+ } else {
1740
+ st.lastDirection = direction;
1741
+ st.streak = 1;
1742
+ }
1743
+ if (st.streak >= cfg.hysteresisSamples) {
1744
+ st.current = adjustAdaptiveParams(st.current, direction, cfg);
1745
+ st.streak = 0;
1746
+ }
1747
+ }
1748
+ function getAdaptiveParams(key, cfg = config2) {
1749
+ const st = samples.get(key);
1750
+ if (!st)
1751
+ return { retries: cfg.defaultRetries, timeoutMs: cfg.defaultTimeoutMs };
1752
+ return { ...st.current };
1753
+ }
1754
+
1257
1755
  // src/tools/council.ts
1258
1756
  var z = tool.schema;
1757
+ var RETRY_LIMITS = {
1758
+ empty: 3,
1759
+ timeout: 1,
1760
+ error: 0,
1761
+ "quality-low": 1
1762
+ };
1259
1763
 
1260
1764
  class OperationTimeoutError extends Error {
1261
1765
  constructor(message) {
@@ -1285,12 +1789,12 @@ async function abortSession(client, sessionId) {
1285
1789
  appendLog("abortSession", "中止会话失败", err);
1286
1790
  }
1287
1791
  }
1288
- async function promptWithTimeout(client, path3, body, timeoutMs, directory) {
1289
- const sessionId = path3.id;
1792
+ async function promptWithTimeout(client, path4, body, timeoutMs, directory) {
1793
+ const sessionId = path4.id;
1290
1794
  let timer;
1291
1795
  try {
1292
1796
  const promptPromise = client.session.prompt({
1293
- path: path3,
1797
+ path: path4,
1294
1798
  body,
1295
1799
  query: directory ? { directory } : undefined
1296
1800
  });
@@ -1316,6 +1820,7 @@ async function promptWithTimeout(client, path3, body, timeoutMs, directory) {
1316
1820
  }
1317
1821
  async function extractSessionResult(client, sessionId, options) {
1318
1822
  const includeReasoning = options?.includeReasoning ?? true;
1823
+ const lastTextOnly = options?.lastTextOnly === true;
1319
1824
  const messagesResult = await client.session.messages({
1320
1825
  path: { id: sessionId }
1321
1826
  });
@@ -1332,7 +1837,8 @@ async function extractSessionResult(client, sessionId, options) {
1332
1837
  }
1333
1838
  }
1334
1839
  }
1335
- const text = extractedContent.filter((t) => t.length > 0).join(`
1840
+ const texts = extractedContent.filter((t) => t.length > 0);
1841
+ const text = lastTextOnly ? texts.length > 0 ? texts[texts.length - 1] : "" : texts.join(`
1336
1842
 
1337
1843
  `);
1338
1844
  return { text, empty: text.length === 0 };
@@ -1395,18 +1901,254 @@ ${cr.result}`;
1395
1901
  return parts.join(`
1396
1902
  `);
1397
1903
  }
1904
+ function buildDeliberationPrompt(originalPrompt, previousRound, currentName) {
1905
+ const others = previousRound.filter((cr) => cr.status === "completed" && cr.result && cr.name !== currentName).map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}):
1906
+ ${cr.result}`).join(`
1907
+
1908
+ `);
1909
+ if (!others)
1910
+ return originalPrompt;
1911
+ return [
1912
+ originalPrompt,
1913
+ "",
1914
+ "---",
1915
+ "",
1916
+ "以下是其他成员第一轮意见,请回应并修正或坚持你的立场:",
1917
+ "",
1918
+ others
1919
+ ].join(`
1920
+ `);
1921
+ }
1398
1922
  function formatModelComposition(councillorResults) {
1399
1923
  return councillorResults.map((cr) => `${cr.name}: ${shortModelLabel(cr.model)}`).join(", ");
1400
1924
  }
1925
+ function adaptiveKey(config3) {
1926
+ return `${config3.variant ?? "default"}\x00${config3.model}`;
1927
+ }
1928
+ function classifyFailure(message) {
1929
+ if (message.includes("Empty response from provider"))
1930
+ return "empty";
1931
+ if (message.includes("timed out") || message.includes("OperationTimeoutError"))
1932
+ return "timeout";
1933
+ return "error";
1934
+ }
1935
+ var CONCLUSION_MARKERS = [
1936
+ "综上",
1937
+ "结论",
1938
+ "因此",
1939
+ "总之",
1940
+ "建议",
1941
+ "推荐",
1942
+ "我认为",
1943
+ "最终",
1944
+ "关键结论",
1945
+ "conclusion",
1946
+ "therefore",
1947
+ "in summary",
1948
+ "in conclusion",
1949
+ "recommend",
1950
+ "suggest",
1951
+ "overall"
1952
+ ];
1953
+ var ENTITY_MARKERS = [
1954
+ "决策",
1955
+ "决定",
1956
+ "方案",
1957
+ "采用",
1958
+ "选择",
1959
+ "关键",
1960
+ "实体",
1961
+ "要点",
1962
+ "关键决策",
1963
+ "decision",
1964
+ "entity",
1965
+ "key point",
1966
+ "option",
1967
+ "recommendation",
1968
+ "key entity"
1969
+ ];
1970
+ function extractCouncillorSummary(text) {
1971
+ const sentences = text.split(/(?<=[。!?!?;;])\s*|\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
1972
+ const conclusions = sentences.filter((s) => CONCLUSION_MARKERS.some((m) => s.toLowerCase().includes(m)));
1973
+ const entities = sentences.filter((s) => ENTITY_MARKERS.some((m) => s.toLowerCase().includes(m)));
1974
+ return {
1975
+ conclusions: conclusions.length > 0 ? conclusions : sentences.length > 0 ? [sentences[sentences.length - 1]] : [],
1976
+ entities
1977
+ };
1978
+ }
1979
+ var STOPWORDS = new Set([
1980
+ "the",
1981
+ "a",
1982
+ "an",
1983
+ "and",
1984
+ "or",
1985
+ "of",
1986
+ "to",
1987
+ "in",
1988
+ "on",
1989
+ "for",
1990
+ "with",
1991
+ "is",
1992
+ "are",
1993
+ "was",
1994
+ "were",
1995
+ "be",
1996
+ "been",
1997
+ "this",
1998
+ "that",
1999
+ "it",
2000
+ "as",
2001
+ "at",
2002
+ "by",
2003
+ "from",
2004
+ "我们",
2005
+ "的",
2006
+ "了",
2007
+ "是",
2008
+ "在",
2009
+ "和",
2010
+ "与",
2011
+ "及",
2012
+ "或",
2013
+ "一个",
2014
+ "这个",
2015
+ "进行",
2016
+ "以及",
2017
+ "结论",
2018
+ "综上",
2019
+ "总之",
2020
+ "因此",
2021
+ "建议",
2022
+ "推荐",
2023
+ "最终",
2024
+ "我认为",
2025
+ "关键",
2026
+ "要点",
2027
+ "采用",
2028
+ "方案",
2029
+ "决策",
2030
+ "决定",
2031
+ "选择",
2032
+ "实体",
2033
+ "conclusion",
2034
+ "therefore",
2035
+ "summary",
2036
+ "recommend",
2037
+ "suggest",
2038
+ "overall",
2039
+ "decision",
2040
+ "option",
2041
+ "recommendation",
2042
+ "key",
2043
+ "point",
2044
+ "entity",
2045
+ "error"
2046
+ ]);
2047
+ function tokenize(text) {
2048
+ return text.toLowerCase().split(/[^a-z0-9\u4e00-\u9fff]+/).filter((t) => t.length > 0 && !STOPWORDS.has(t));
2049
+ }
2050
+ function jaccard(x, y) {
2051
+ if (x.length === 0 || y.length === 0)
2052
+ return 0;
2053
+ const xs = new Set(x);
2054
+ const ys = new Set(y);
2055
+ const inter = [...xs].filter((t) => ys.has(t)).length;
2056
+ const union = new Set([...xs, ...ys]).size;
2057
+ return union === 0 ? 0 : inter / union;
2058
+ }
2059
+ function pairwiseAgreement(a, b) {
2060
+ const aConcl = tokenize(a.conclusions.join(" "));
2061
+ const bConcl = tokenize(b.conclusions.join(" "));
2062
+ const aEnt = tokenize(a.entities.join(" "));
2063
+ const bEnt = tokenize(b.entities.join(" "));
2064
+ return 0.5 * jaccard(aConcl, bConcl) + 0.5 * jaccard(aEnt, bEnt);
2065
+ }
2066
+ function computeConsensus(inputs, threshold) {
2067
+ const n = inputs.length;
2068
+ if (n === 0)
2069
+ return { consensus: false, agreementScores: {}, averageAgreement: 0 };
2070
+ if (n === 1) {
2071
+ return { consensus: true, agreementScores: { [inputs[0].name]: 1 }, averageAgreement: 1 };
2072
+ }
2073
+ const agreementScores = {};
2074
+ for (const a of inputs) {
2075
+ let sum = 0;
2076
+ for (const b of inputs) {
2077
+ if (a.name === b.name)
2078
+ continue;
2079
+ sum += pairwiseAgreement(a.summary, b.summary);
2080
+ }
2081
+ agreementScores[a.name] = sum / (n - 1);
2082
+ }
2083
+ const values = Object.values(agreementScores);
2084
+ const averageAgreement = values.reduce((acc, v) => acc + v, 0) / values.length;
2085
+ const agreeing = values.filter((v) => v >= threshold).length;
2086
+ const required = Math.ceil(2 * n / 3);
2087
+ return { consensus: agreeing >= required, agreementScores, averageAgreement };
2088
+ }
2089
+ function decideRetryAction(category, consecutiveStreak, categoryRetriesUsed, budgetAvailable, categoryLimit = RETRY_LIMITS[category]) {
2090
+ if (!budgetAvailable)
2091
+ return "budget-exhausted";
2092
+ if (consecutiveStreak >= 2)
2093
+ return "needs-human";
2094
+ if (categoryRetriesUsed >= categoryLimit)
2095
+ return "give-up";
2096
+ return "retry";
2097
+ }
2098
+ var VARIANT_ORDER = ["max", "high", "medium", "low"];
2099
+ function downgradeVariant(variant) {
2100
+ if (!variant)
2101
+ return;
2102
+ const idx = VARIANT_ORDER.indexOf(variant);
2103
+ if (idx < 0 || idx >= VARIANT_ORDER.length - 1)
2104
+ return;
2105
+ return VARIANT_ORDER[idx + 1];
2106
+ }
2107
+ function appendRetryContext(prompt, category) {
2108
+ if (category === "timeout") {
2109
+ return `${prompt}
2110
+
2111
+ [retry] 上次尝试超时。请精炼作答:先给出结论与关键决策,避免冗长过程。`;
2112
+ }
2113
+ if (category === "quality-low") {
2114
+ return `${prompt}
2115
+
2116
+ [retry] 上次回答缺少明确的结论与关键实体/决策。请以"结论:..."开头,并列出关键决策/实体要点。`;
2117
+ }
2118
+ return prompt;
2119
+ }
2120
+
2121
+ class RetryBudget {
2122
+ retriesLeft;
2123
+ maxElapsedMs;
2124
+ startedAt;
2125
+ constructor(maxTotalRetries, maxElapsedMs, startedAt = Date.now()) {
2126
+ this.retriesLeft = Math.max(0, maxTotalRetries);
2127
+ this.maxElapsedMs = maxElapsedMs;
2128
+ this.startedAt = startedAt;
2129
+ }
2130
+ elapsedMs() {
2131
+ return Date.now() - this.startedAt;
2132
+ }
2133
+ expired() {
2134
+ return this.elapsedMs() >= this.maxElapsedMs;
2135
+ }
2136
+ consumeRetry() {
2137
+ if (this.retriesLeft <= 0 || this.expired())
2138
+ return false;
2139
+ this.retriesLeft -= 1;
2140
+ return true;
2141
+ }
2142
+ }
1401
2143
 
1402
2144
  class CouncilManager {
1403
2145
  client;
1404
2146
  directory;
1405
2147
  config;
1406
- constructor(client, directory, config) {
2148
+ constructor(client, directory, config3) {
1407
2149
  this.client = client;
1408
2150
  this.directory = directory;
1409
- this.config = config;
2151
+ this.config = config3;
1410
2152
  }
1411
2153
  async runCouncil(prompt, presetName, parentSessionId) {
1412
2154
  const resolvedPreset = presetName ?? this.config.default_preset ?? "default";
@@ -1428,8 +2170,10 @@ class CouncilManager {
1428
2170
  }
1429
2171
  const timeout = this.config.timeout ?? 180000;
1430
2172
  const executionMode = this.config.councillor_execution_mode ?? "parallel";
1431
- const maxRetries = this.config.councillor_retries ?? 3;
1432
- const councillorResults = await this.runCouncillors(prompt, preset, parentSessionId, timeout, executionMode, maxRetries);
2173
+ const maxRounds = this.config.max_rounds ?? 2;
2174
+ const consensusThreshold = this.config.consensus_threshold ?? 0.6;
2175
+ const budget = new RetryBudget(this.config.max_total_retries ?? 6, this.config.max_elapsed_ms ?? 3 * timeout);
2176
+ const councillorResults = await this.runCouncillors(prompt, preset, parentSessionId, timeout, executionMode, budget, { maxRounds, consensusThreshold });
1433
2177
  const completedCount = councillorResults.filter((r) => r.status === "completed").length;
1434
2178
  if (completedCount === 0) {
1435
2179
  return {
@@ -1438,23 +2182,56 @@ class CouncilManager {
1438
2182
  councillorResults
1439
2183
  };
1440
2184
  }
2185
+ const consensusInputs = councillorResults.filter((r) => r.status === "completed" && !!r.summary).map((r) => ({ name: r.name, summary: r.summary }));
2186
+ const consensus = computeConsensus(consensusInputs, consensusThreshold);
1441
2187
  const formatted = formatCouncillorResults(prompt, councillorResults);
1442
2188
  return {
1443
2189
  success: true,
1444
2190
  result: formatted,
1445
- councillorResults
2191
+ councillorResults,
2192
+ consensus: consensus.consensus,
2193
+ agreement: consensus.averageAgreement
1446
2194
  };
1447
2195
  }
1448
- async runCouncillors(prompt, councillors, parentSessionId, timeout, executionMode, maxRetries) {
1449
- const entries = Object.entries(councillors);
2196
+ async runCouncillors(prompt, councillors, parentSessionId, timeout, executionMode, budget, options) {
2197
+ const entries = orderCouncillorsByFailure(Object.entries(councillors));
1450
2198
  const results = [];
2199
+ const explicitTimeout = this.config.timeout;
2200
+ const explicitRetries = this.config.councillor_retries;
2201
+ const resolveParams = (config3) => {
2202
+ const p = getAdaptiveParams(adaptiveKey(config3), {
2203
+ ...DEFAULT_ADAPTIVE_CONFIG,
2204
+ defaultTimeoutMs: explicitTimeout ?? timeout,
2205
+ defaultRetries: explicitRetries ?? 3
2206
+ });
2207
+ return {
2208
+ timeout: explicitTimeout ?? p.timeoutMs,
2209
+ retries: explicitRetries ?? p.retries
2210
+ };
2211
+ };
1451
2212
  if (executionMode === "serial") {
1452
- for (const [name, config] of entries) {
1453
- const r = await this.runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries);
1454
- results.push(r);
2213
+ let lastRound = [];
2214
+ for (let round = 1;round <= options.maxRounds; round++) {
2215
+ const roundResults = [];
2216
+ const firstAttemptIsExtra = round > 1;
2217
+ for (const [name, config3] of entries) {
2218
+ const params = resolveParams(config3);
2219
+ const roundPrompt = round > 1 ? buildDeliberationPrompt(prompt, lastRound, name) : prompt;
2220
+ const r = await this.runCouncillorWithRetry(name, config3, roundPrompt, parentSessionId, params.timeout, budget, { firstAttemptIsExtra, retries: params.retries });
2221
+ roundResults.push(r);
2222
+ }
2223
+ lastRound = roundResults;
2224
+ const consensusInputs = roundResults.filter((r) => r.status === "completed" && !!r.summary).map((r) => ({ name: r.name, summary: r.summary }));
2225
+ if (computeConsensus(consensusInputs, options.consensusThreshold).consensus) {
2226
+ break;
2227
+ }
1455
2228
  }
2229
+ results.push(...lastRound);
1456
2230
  } else {
1457
- const promises = entries.map(([name, config]) => this.runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries));
2231
+ const promises = entries.map(([name, config3]) => {
2232
+ const params = resolveParams(config3);
2233
+ return this.runCouncillorWithRetry(name, config3, prompt, parentSessionId, params.timeout, budget, { retries: params.retries });
2234
+ });
1458
2235
  const settled = await Promise.allSettled(promises);
1459
2236
  for (let i = 0;i < settled.length; i++) {
1460
2237
  const s = settled[i];
@@ -1473,39 +2250,80 @@ class CouncilManager {
1473
2250
  }
1474
2251
  return results;
1475
2252
  }
1476
- async runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries) {
1477
- const totalAttempts = 1 + maxRetries;
1478
- for (let attempt = 1;attempt <= totalAttempts; attempt++) {
2253
+ async runCouncillorWithRetry(name, config3, prompt, parentSessionId, timeout, budget, options) {
2254
+ let attemptPrompt = formatCouncillorPrompt(prompt, config3.prompt);
2255
+ let attemptVariant = config3.variant;
2256
+ let lastCategory = null;
2257
+ let streak = 0;
2258
+ const categoryRetries = {};
2259
+ let attempt = 1;
2260
+ for (;; ) {
2261
+ if (budget.expired()) {
2262
+ return { name, model: config3.model, status: "error", error: "budget-exhausted" };
2263
+ }
2264
+ const isExtraAttempt = attempt > 1 || options?.firstAttemptIsExtra === true;
2265
+ if (isExtraAttempt && !budget.consumeRetry()) {
2266
+ return { name, model: config3.model, status: "error", error: "budget-exhausted" };
2267
+ }
2268
+ let category;
2269
+ let errorInfo;
1479
2270
  try {
1480
- const result = await this.runAgentSession({
2271
+ const session = await this.runAgentSession({
1481
2272
  parentSessionId,
1482
- title: `Council ${name} (${shortModelLabel(config.model)})`,
1483
- model: config.model,
1484
- promptText: formatCouncillorPrompt(prompt, config.prompt),
1485
- variant: config.variant,
2273
+ title: `Council ${name} (${shortModelLabel(config3.model)})`,
2274
+ model: config3.model,
2275
+ promptText: attemptPrompt,
2276
+ variant: attemptVariant,
1486
2277
  timeout
1487
2278
  });
1488
- return { name, model: config.model, status: "completed", result };
2279
+ const quality = assessQuality({ output: session.text, decisions: session.summary.entities.length });
2280
+ if (quality.passed) {
2281
+ recordModelResult(config3.model, "success");
2282
+ recordAdaptiveSample(adaptiveKey(config3), true);
2283
+ return { name, model: config3.model, status: "completed", result: session.text, summary: session.summary };
2284
+ }
2285
+ category = quality.failureCategory ?? "quality-low";
2286
+ errorInfo = `Councillor "${name}": quality score ${quality.score} (${category})`;
1489
2287
  } catch (error) {
1490
2288
  const msg = error instanceof Error ? error.message : String(error);
1491
- const isEmptyResponse = msg.includes("Empty response from provider");
1492
- const canRetry = attempt < totalAttempts && isEmptyResponse;
1493
- if (!canRetry) {
1494
- return {
1495
- name,
1496
- model: config.model,
1497
- status: msg.includes("timed out") ? "timed_out" : "failed",
1498
- error: `Councillor "${name}": ${msg}`
1499
- };
1500
- }
2289
+ category = classifyFailure(msg);
2290
+ errorInfo = `Councillor "${name}": ${msg}`;
1501
2291
  }
2292
+ recordModelResult(config3.model, category);
2293
+ recordAdaptiveSample(adaptiveKey(config3), false);
2294
+ streak = lastCategory === category ? streak + 1 : 1;
2295
+ lastCategory = category;
2296
+ const used = categoryRetries[category] ?? 0;
2297
+ const categoryLimit = category === "empty" ? options?.retries ?? this.config.councillor_retries ?? 3 : RETRY_LIMITS[category];
2298
+ const action = decideRetryAction(category, streak, used, budget.retriesLeft > 0 && !budget.expired(), categoryLimit);
2299
+ if (action === "needs-human") {
2300
+ return {
2301
+ name,
2302
+ model: config3.model,
2303
+ status: "needs_human",
2304
+ error: `${errorInfo} — ${streak} consecutive ${category} failures, needs human review`
2305
+ };
2306
+ }
2307
+ if (action === "budget-exhausted") {
2308
+ return { name, model: config3.model, status: "error", error: "budget-exhausted" };
2309
+ }
2310
+ if (action === "give-up") {
2311
+ return {
2312
+ name,
2313
+ model: config3.model,
2314
+ status: category === "timeout" ? "timed_out" : "failed",
2315
+ error: errorInfo
2316
+ };
2317
+ }
2318
+ categoryRetries[category] = used + 1;
2319
+ if (category === "timeout") {
2320
+ attemptVariant = downgradeVariant(attemptVariant);
2321
+ attemptPrompt = appendRetryContext(attemptPrompt, "timeout");
2322
+ } else if (category === "quality-low") {
2323
+ attemptPrompt = appendRetryContext(attemptPrompt, "quality-low");
2324
+ }
2325
+ attempt += 1;
1502
2326
  }
1503
- return {
1504
- name,
1505
- model: config.model,
1506
- status: "failed",
1507
- error: `Councillor "${name}": max retries exhausted`
1508
- };
1509
2327
  }
1510
2328
  async runAgentSession(options) {
1511
2329
  const modelRef = parseModelReference(options.model);
@@ -1545,11 +2363,15 @@ class CouncilManager {
1545
2363
  promptBody.variant = options.variant;
1546
2364
  }
1547
2365
  await promptWithTimeout(this.client, { id: sessionId }, promptBody, options.timeout, this.directory);
1548
- const extraction = await extractSessionResult(this.client, sessionId);
2366
+ const extraction = await extractSessionResult(this.client, sessionId, {
2367
+ includeReasoning: false,
2368
+ lastTextOnly: true
2369
+ });
1549
2370
  if (extraction.empty) {
1550
2371
  throw new Error("[oh-my-opencode-cohub] Empty response from provider");
1551
2372
  }
1552
- return extraction.text;
2373
+ const summary = extractCouncillorSummary(extraction.text);
2374
+ return { text: extraction.text, summary };
1553
2375
  } finally {
1554
2376
  if (sessionId) {
1555
2377
  abortSession(this.client, sessionId).catch(() => {});
@@ -1593,16 +2415,61 @@ function createCouncilTool(ctx, councilManager) {
1593
2415
 
1594
2416
  ---
1595
2417
  *Council: ${completed}/${total} councillors responded (${composition})*`;
2418
+ const agreementNum = typeof result.agreement === "number" ? result.agreement : 0;
2419
+ const consensusLabel = result.consensus === true ? agreementNum >= 0.8 ? "unanimous" : "majority" : "split";
2420
+ output += `
2421
+ *Consensus: ${consensusLabel} (${agreementNum.toFixed(2)})*`;
1596
2422
  return output;
1597
2423
  }
1598
2424
  });
1599
2425
  return { council_session };
1600
2426
  }
1601
2427
 
2428
+ // src/tools/job-control.ts
2429
+ import { tool as tool2 } from "@opencode-ai/plugin";
2430
+ var z2 = tool2.schema;
2431
+ function createCloseJobTool(ctx, tracker, onClosed) {
2432
+ const close_job = tool2({
2433
+ description: "Close (abort) a stuck or unnecessary subagent background task. " + "Accepts the child Session ID (e.g. ses_xxx, shown in the Background Job Board) or the task alias (e.g. coe-1). " + "The underlying subagent session is aborted via session.abort() and the task is marked cancelled. " + "Only the co-orchestrator (primary) agent may invoke this tool.",
2434
+ args: {
2435
+ task_id: z2.string().describe("The child session ID (ses_xxx) or task alias (e.g. coe-1) of the task to close.")
2436
+ },
2437
+ async execute(args, toolContext) {
2438
+ if (toolContext.agent !== "co-orchestrator") {
2439
+ throw new Error(`[oh-my-opencode-cohub] close_job can only be invoked by co-orchestrator. Current agent: ${toolContext.agent}`);
2440
+ }
2441
+ const hit = tracker.abortJob(args.task_id);
2442
+ if (!hit) {
2443
+ return JSON.stringify({ ok: false, task_id: args.task_id, reason: "not_found" }, null, 2);
2444
+ }
2445
+ let aborted = false;
2446
+ if (hit.sessionId) {
2447
+ const res = await ctx.client.session.abort({ path: { id: hit.sessionId } });
2448
+ if (res.error) {
2449
+ appendLog("job-control", `abort session ${hit.sessionId} 失败`, res.error);
2450
+ aborted = false;
2451
+ } else {
2452
+ aborted = true;
2453
+ }
2454
+ }
2455
+ onClosed?.();
2456
+ return JSON.stringify({
2457
+ ok: true,
2458
+ task_id: args.task_id,
2459
+ alias: hit.job?.alias,
2460
+ agent: hit.job?.agent,
2461
+ session_aborted: aborted,
2462
+ status: "cancelled"
2463
+ }, null, 2);
2464
+ }
2465
+ });
2466
+ return { close_job };
2467
+ }
2468
+
1602
2469
  // src/index.ts
1603
- import * as fs3 from "node:fs";
1604
- import * as path3 from "node:path";
1605
- import * as os3 from "node:os";
2470
+ import * as fs5 from "node:fs";
2471
+ import * as path4 from "node:path";
2472
+ import * as os4 from "node:os";
1606
2473
  function loadFileOverrides(projectDir) {
1607
2474
  const overrides = {};
1608
2475
  const agentNames = [
@@ -1621,23 +2488,23 @@ function loadFileOverrides(projectDir) {
1621
2488
  ];
1622
2489
  const searchDirs = [];
1623
2490
  if (projectDir) {
1624
- searchDirs.push(path3.join(projectDir, ".opencode", "oh-my-opencode-cohub"));
2491
+ searchDirs.push(path4.join(projectDir, ".opencode", "oh-my-opencode-cohub"));
1625
2492
  }
1626
- searchDirs.push(path3.join(os3.homedir(), ".config", "opencode", "oh-my-opencode-cohub"));
2493
+ searchDirs.push(path4.join(os4.homedir(), ".config", "opencode", "oh-my-opencode-cohub"));
1627
2494
  for (const agent of agentNames) {
1628
2495
  for (const dir of searchDirs) {
1629
- const replacePath = path3.join(dir, `${agent}.md`);
1630
- if (!overrides[agent]?.replace && fs3.existsSync(replacePath)) {
2496
+ const replacePath = path4.join(dir, `${agent}.md`);
2497
+ if (!overrides[agent]?.replace && fs5.existsSync(replacePath)) {
1631
2498
  try {
1632
- overrides[agent] = { ...overrides[agent], replace: fs3.readFileSync(replacePath, "utf-8") };
2499
+ overrides[agent] = { ...overrides[agent], replace: fs5.readFileSync(replacePath, "utf-8") };
1633
2500
  } catch (err) {
1634
2501
  appendLog("loadFileOverrides", "读取replace文件失败", err);
1635
2502
  }
1636
2503
  }
1637
- const appendPath = path3.join(dir, `${agent}_append.md`);
1638
- if (!overrides[agent]?.append && fs3.existsSync(appendPath)) {
2504
+ const appendPath = path4.join(dir, `${agent}_append.md`);
2505
+ if (!overrides[agent]?.append && fs5.existsSync(appendPath)) {
1639
2506
  try {
1640
- overrides[agent] = { ...overrides[agent], append: fs3.readFileSync(appendPath, "utf-8") };
2507
+ overrides[agent] = { ...overrides[agent], append: fs5.readFileSync(appendPath, "utf-8") };
1641
2508
  } catch (err) {
1642
2509
  appendLog("loadFileOverrides", "读取append文件失败", err);
1643
2510
  }
@@ -1668,26 +2535,26 @@ var CoHubPlugin = async (input, options) => {
1668
2535
  }
1669
2536
  const promptOverrides = { ...configOverrides, ...fileOverrides };
1670
2537
  const tracker = new TaskTracker;
1671
- const STATE_DIR = path3.join(os3.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
1672
- const STATE_FILE = path3.join(STATE_DIR, "tracker-state.json");
2538
+ const STATE_DIR = path4.join(os4.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
2539
+ const STATE_FILE = path4.join(STATE_DIR, "tracker-state.json");
1673
2540
  function syncTrackerState(sessionId) {
1674
2541
  try {
1675
- if (!fs3.existsSync(STATE_DIR)) {
1676
- fs3.mkdirSync(STATE_DIR, { recursive: true });
2542
+ if (!fs5.existsSync(STATE_DIR)) {
2543
+ fs5.mkdirSync(STATE_DIR, { recursive: true });
1677
2544
  }
1678
2545
  const state = {
1679
2546
  updatedAt: Date.now(),
1680
2547
  runningAgents: tracker.getRunningAgents(sessionId),
1681
2548
  runningCount: tracker.getRunningCount(sessionId)
1682
2549
  };
1683
- fs3.writeFileSync(STATE_FILE, JSON.stringify(state), "utf-8");
2550
+ fs5.writeFileSync(STATE_FILE, JSON.stringify(state), "utf-8");
1684
2551
  } catch {}
1685
2552
  }
1686
- const AGENT_CONFIG_FILE = path3.join(STATE_DIR, "cohub-state.json");
2553
+ const AGENT_CONFIG_FILE = path4.join(STATE_DIR, "cohub-state.json");
1687
2554
  function syncAgentConfig() {
1688
2555
  try {
1689
- if (!fs3.existsSync(STATE_DIR)) {
1690
- fs3.mkdirSync(STATE_DIR, { recursive: true });
2556
+ if (!fs5.existsSync(STATE_DIR)) {
2557
+ fs5.mkdirSync(STATE_DIR, { recursive: true });
1691
2558
  }
1692
2559
  const configs = agents.filter((a) => a.config.model).map((a) => {
1693
2560
  const modelStr = a.config.model;
@@ -1702,7 +2569,7 @@ var CoHubPlugin = async (input, options) => {
1702
2569
  provider
1703
2570
  };
1704
2571
  });
1705
- fs3.writeFileSync(AGENT_CONFIG_FILE, JSON.stringify({ updatedAt: Date.now(), agents: configs }), "utf-8");
2572
+ fs5.writeFileSync(AGENT_CONFIG_FILE, JSON.stringify({ updatedAt: Date.now(), agents: configs }), "utf-8");
1706
2573
  appendLog("syncAgentConfig", `已同步 ${configs.length} 个 agent: ${JSON.stringify(configs.map((c) => ({ name: c.name, model: c.model, variant: c.variant })))}`);
1707
2574
  } catch {}
1708
2575
  }
@@ -1803,6 +2670,7 @@ var CoHubPlugin = async (input, options) => {
1803
2670
  const councilConfig = userConfig.council ?? DEFAULT_COUNCIL_CONFIG;
1804
2671
  const councilManager = new CouncilManager(input.client, input.directory, councilConfig);
1805
2672
  const councilTools = createCouncilTool(input, councilManager);
2673
+ const jobTools = createCloseJobTool(input, tracker, () => syncTrackerState(tracker.currentParentSessionId));
1806
2674
  function extractChildSessionId(output) {
1807
2675
  if (!output || typeof output !== "object")
1808
2676
  return;
@@ -1840,7 +2708,19 @@ var CoHubPlugin = async (input, options) => {
1840
2708
  const STALE_TIMEOUT_MS = 30 * 60 * 1000;
1841
2709
  const cleanupTimer = setInterval(() => {
1842
2710
  try {
1843
- tracker.cleanupStaleJobs(STALE_TIMEOUT_MS);
2711
+ const staleSessions = tracker.cleanupStaleJobs(STALE_TIMEOUT_MS);
2712
+ for (const sid of staleSessions) {
2713
+ (async () => {
2714
+ try {
2715
+ const res = await input.client.session.abort({ path: { id: sid } });
2716
+ if (res.error) {
2717
+ appendLog("cleanupStaleJobs", `abort stale session ${sid} 失败`, res.error);
2718
+ }
2719
+ } catch (err) {
2720
+ appendLog("cleanupStaleJobs", `abort stale session ${sid} 失败`, err);
2721
+ }
2722
+ })();
2723
+ }
1844
2724
  } catch (err) {
1845
2725
  appendLog("cleanupStaleJobs", "定时清理过期任务失败", err);
1846
2726
  }
@@ -1853,17 +2733,29 @@ var CoHubPlugin = async (input, options) => {
1853
2733
  }
1854
2734
  }, 60000);
1855
2735
  const agentConfigs = {};
2736
+ const COUNCIL_ONLY_TOOLS = { council_session: "deny" };
2737
+ const ORCHESTRATOR_ONLY_TOOLS = { close_job: "deny" };
1856
2738
  for (const agent of agents) {
1857
- agentConfigs[agent.name] = {
2739
+ const base = {
1858
2740
  ...agent.config,
1859
2741
  name: agent.name,
1860
2742
  description: agent.description
1861
2743
  };
2744
+ const existingPermission = base.permission ?? {};
2745
+ if (agent.name !== "co-council") {
2746
+ base.permission = { ...existingPermission, ...COUNCIL_ONLY_TOOLS };
2747
+ }
2748
+ if (agent.name !== "co-orchestrator") {
2749
+ base.permission = { ...base.permission, ...ORCHESTRATOR_ONLY_TOOLS };
2750
+ } else {
2751
+ base.permission = { ...base.permission, close_job: "allow" };
2752
+ }
2753
+ agentConfigs[agent.name] = base;
1862
2754
  }
1863
2755
  return {
1864
2756
  name: "oh-my-opencode-cohub",
1865
2757
  agent: agentConfigs,
1866
- tool: councilTools,
2758
+ tool: { ...councilTools, ...jobTools },
1867
2759
  config: async (cfg) => {
1868
2760
  const c = cfg;
1869
2761
  c.agent ??= {};
@@ -1878,10 +2770,22 @@ var CoHubPlugin = async (input, options) => {
1878
2770
  merged.model = pc.model;
1879
2771
  if (pc.variant)
1880
2772
  merged.variant = pc.variant;
2773
+ const existingPerm0 = merged.permission ?? {};
2774
+ if (name === "co-council") {
2775
+ merged.permission = { ...existingPerm0, council_session: "allow", ...ORCHESTRATOR_ONLY_TOOLS };
2776
+ } else if (name === "co-orchestrator") {
2777
+ merged.permission = {
2778
+ ...existingPerm0,
2779
+ ...COUNCIL_ONLY_TOOLS,
2780
+ close_job: "allow"
2781
+ };
2782
+ } else {
2783
+ merged.permission = { ...existingPerm0, ...COUNCIL_ONLY_TOOLS, ...ORCHESTRATOR_ONLY_TOOLS };
2784
+ }
1881
2785
  c.agent[name] = merged;
1882
2786
  }
1883
2787
  try {
1884
- fs3.writeFileSync(path3.join(STATE_DIR, "config-hook-ran.json"), JSON.stringify({ ran: true, count: agents.length }));
2788
+ fs5.writeFileSync(path4.join(STATE_DIR, "config-hook-ran.json"), JSON.stringify({ ran: true, count: agents.length }));
1885
2789
  } catch (err) {
1886
2790
  appendLog("config", "双重注册写入文件失败", err);
1887
2791
  }
@@ -1932,19 +2836,19 @@ var CoHubPlugin = async (input, options) => {
1932
2836
  if (details) {
1933
2837
  output.args[targetField] += details;
1934
2838
  }
1935
- const logPath = path3.join(os3.tmpdir(), "opencode", "ctx-diag.log");
2839
+ const logPath = path4.join(os4.tmpdir(), "opencode", "ctx-diag.log");
1936
2840
  try {
1937
- const stat = fs3.statSync(logPath);
2841
+ const stat = fs5.statSync(logPath);
1938
2842
  if (stat.size > 50 * 1024) {
1939
- const lines = fs3.readFileSync(logPath, "utf-8").trim().split(`
2843
+ const lines = fs5.readFileSync(logPath, "utf-8").trim().split(`
1940
2844
  `);
1941
- fs3.writeFileSync(logPath, lines.slice(-30).join(`
2845
+ fs5.writeFileSync(logPath, lines.slice(-30).join(`
1942
2846
  `) + `
1943
2847
  `);
1944
2848
  }
1945
2849
  } catch {}
1946
2850
  const finalPrompt = typeof output.args?.prompt === "string" ? output.args.prompt : "";
1947
- fs3.appendFileSync(logPath, JSON.stringify({
2851
+ fs5.appendFileSync(logPath, JSON.stringify({
1948
2852
  time: new Date().toISOString(),
1949
2853
  session: input2.sessionID?.slice(0, 20) ?? "?",
1950
2854
  subagent: subagentType,
@@ -1992,7 +2896,25 @@ var CoHubPlugin = async (input, options) => {
1992
2896
  syncTrackerState(tracker.currentParentSessionId);
1993
2897
  const job = tracker.getJobBySessionId(sessionId);
1994
2898
  if (job) {
1995
- contextEngine.captureResult(sessionId, job.alias, job.agent);
2899
+ contextEngine.captureResult(sessionId, job.alias, job.agent).then((captured) => {
2900
+ if (!captured)
2901
+ return;
2902
+ if (!isQualityEnabled(userConfig.quality))
2903
+ return;
2904
+ const quality = assessQuality({
2905
+ output: captured.output,
2906
+ decisions: captured.decisions,
2907
+ latencyMs: Date.now() - job.createdAt,
2908
+ tokens: captured.tokens
2909
+ });
2910
+ tracker.updateQuality(sessionId, {
2911
+ score: quality.score,
2912
+ ...quality.failureCategory ? { failureCategory: quality.failureCategory } : {},
2913
+ ...quality.latencyMs !== undefined ? { latencyMs: quality.latencyMs } : {},
2914
+ ...quality.tokens ? { tokens: quality.tokens } : {}
2915
+ });
2916
+ syncTrackerState(tracker.currentParentSessionId);
2917
+ }).catch((err) => appendLog("event", "质量回送失败", err));
1996
2918
  }
1997
2919
  } else if (e.type === "session.deleted") {
1998
2920
  tracker.updateByChildSessionId(sessionId, "errored");
@@ -2096,6 +3018,6 @@ var CoHubPlugin = async (input, options) => {
2096
3018
  var server = CoHubPlugin;
2097
3019
  var src_default = CoHubPlugin;
2098
3020
  export {
2099
- server,
2100
- src_default as default
3021
+ src_default as default,
3022
+ server
2101
3023
  };