@tea-agent/loop-agent 0.43.0-next.3 → 0.43.0-next.4

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 (87) hide show
  1. package/CHANGELOG.md +15 -14
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/worker/console/chat/assistant-content.js +23 -2
  4. package/dist/worker/console/chat/explore-tools.js +13 -0
  5. package/dist/worker/console/chat/pi-runtime.js +18 -0
  6. package/dist/worker/console/chat/routes.js +11 -0
  7. package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
  8. package/dist/worker/console/chat/terminal-routes.js +216 -0
  9. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  10. package/dist/worker/console/chat/terminal-tools.js +199 -0
  11. package/dist/worker/console/chat/tool-preview.js +11 -0
  12. package/dist/worker/console/chat/tools.js +1 -0
  13. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-hUmM_GRf.js → abnfDiagram-N423BO3Z-CCj36aaC.js} +1 -1
  14. package/dist/worker/console/static/assets/{arc-Bt-xuieW.js → arc-Cl3MIVFh.js} +1 -1
  15. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DTqkL4DJ.js → architectureDiagram-T3A2C74G-C_n4kMJS.js} +1 -1
  16. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Cy0MNnzV.js → blockDiagram-VBNYF7ZC-DlWYJ7cK.js} +1 -1
  17. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DDdlT0Pd.js → c4Diagram-5PPSVZJV-Ud5pS7vS.js} +1 -1
  18. package/dist/worker/console/static/assets/channel-CEMRpNm-.js +1 -0
  19. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-3JawwNZ6.js → chunk-2GRJ4B5K-CQPwOv_j.js} +1 -1
  20. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DoTp7Cri.js → chunk-2Q5K7J3B-CMzjxrWI.js} +1 -1
  21. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CcirhZaS.js → chunk-5RXB4S5H-pcnXnD6t.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-VrG21gnP.js → chunk-5VM5RSS4-CEB3AvU2.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DDOgIS70.js → chunk-6Q2QTUOP-DSKnbXbp.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CBjmgvJ_.js → chunk-GF5L2VYU-CS6-PjCZ.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CCQSht8M.js → chunk-JWPE2WC7-f6q8Tnzz.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BpGMkUUV.js → chunk-KBJHAD2P-DkB0QX3q.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DNDT41QL.js → chunk-RYQCIY6F-BdB4jAv6.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-euXnDFbn.js → chunk-XXDRQBXY-BVm1bnX6.js} +1 -1
  29. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C9lhaZkR.js +1 -0
  30. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C9lhaZkR.js +1 -0
  31. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BJ6NAeql.js → cose-bilkent-JH36ORCC-FV_P-IEp.js} +1 -1
  32. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-F5JKIvIq.js → cynefin-VYW2F7L2-BM7RtkA2.js} +1 -1
  33. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Cwmkl_Gi.js → cynefinDiagram-MW4NZA55-C1BZwTG5.js} +1 -1
  34. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BmCZ8815.js → dagre-VZM6K2ZE-C6McNKmY.js} +1 -1
  35. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-zWA3hUnX.js → diagram-7IWD3JNH-C0aWRUaE.js} +1 -1
  36. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BzA-888F.js → diagram-B4RE2ZJO-Bfk_SKE8.js} +1 -1
  37. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-tPNheoch.js → diagram-LBJQPF4R-prx1T4qf.js} +1 -1
  38. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Cdi7Fyqp.js → diagram-Q27KOJAE-DBvty0eU.js} +1 -1
  39. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DFK9VBf1.js → diagram-UB23O5K3-CF0em8e9.js} +1 -1
  40. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-xCUr1kpg.js → ebnfDiagram-BXEA7PRR-CF90b4Ws.js} +1 -1
  41. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BANQHY_A.js → erDiagram-JOGREHBK-hfunxzof.js} +1 -1
  42. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-VHJYEMkP.js → flowDiagram-UKHOOZJN-D-QSy4Nl.js} +1 -1
  43. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DiyZQ0C6.js → ganttDiagram-PKOTCBZU-DaBGNcvT.js} +1 -1
  44. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CZtIeBBa.js → gitGraphDiagram-DS77QQ5N-BChcubGL.js} +1 -1
  45. package/dist/worker/console/static/assets/index-CJkCY1XR.js +464 -0
  46. package/dist/worker/console/static/assets/index-DUBCbiqv.css +1 -0
  47. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BuSspC0X.js → infoDiagram-6WML65LV-BkC67fIa.js} +1 -1
  48. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DRJZyPFy.js → ishikawaDiagram-WSZJBQD7-ibhLczCD.js} +1 -1
  49. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DIfXIVuE.js → journeyDiagram-NVQOT4AX-DCRMitay.js} +1 -1
  50. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CT10_7Lt.js → kanban-definition-27J2QSJJ-DJ_hKaYI.js} +1 -1
  51. package/dist/worker/console/static/assets/{linear-ufemj74H.js → linear-CIlLsYsA.js} +1 -1
  52. package/dist/worker/console/static/assets/{mermaid.core-DEQe8-_m.js → mermaid.core-C-5IQMP_.js} +5 -5
  53. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BRLUrbrC.js → mindmap-definition-FAOFIHXS-TCzV5TQT.js} +1 -1
  54. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BgibKtiF.js → pegDiagram-VL7TDLO6-bfhd2BQH.js} +1 -1
  55. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-D3eyfdV9.js → pieDiagram-7S7Q4E2Y-BxFRAm5N.js} +1 -1
  56. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DXeTk5Px.js → quadrantDiagram-CIZ2JOQS-C-m12lxv.js} +1 -1
  57. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BgXdFEe7.js → railroadDiagram-AXF67PYL-CwmLiGgx.js} +1 -1
  58. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CseC2A7R.js → requirementDiagram-LRYGKXZP-oc0Q1jiD.js} +1 -1
  59. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-D80PFzfJ.js → sankeyDiagram-W5VNT64P-9Ght6Yur.js} +1 -1
  60. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BWzHW7LS.js → sequenceDiagram-SI44F4Z6-ndm3lyz9.js} +1 -1
  61. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-xWQYTLAn.js → sizeCapture-X5ZJPWSS-7sPlKVcw.js} +1 -1
  62. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CLxtpZeL.js → stateDiagram-OKZ733FA-Izl21-oS.js} +1 -1
  63. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CmElLFpP.js +1 -0
  64. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DwugtmWV.js → swimlanes-SLNWSIFB-CtO3D65o.js} +2 -2
  65. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DKRbGG8V.js +8 -0
  66. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DHEOwHIe.js → timeline-definition-Z64GVDOM-Cyg5cY7z.js} +1 -1
  67. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-pmbZKlBK.js → vennDiagram-T6HMQDX7-CwtE_TFK.js} +1 -1
  68. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CivHoM5q.js → wardleyDiagram-T6FBY63Y-Dtt9v8OO.js} +1 -1
  69. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BfHIE7o6.js → xychartDiagram-ELKLHX3M-B2Y3ryb6.js} +1 -1
  70. package/dist/worker/console/static/index.html +2 -2
  71. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
  72. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
  73. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  74. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  75. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  76. package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
  77. package/dist/worker/console/static-src/operator-chat/useChatThread.js +39 -4
  78. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  79. package/dist/worker/console/workspace-context.js +9 -1
  80. package/package.json +6 -3
  81. package/dist/worker/console/static/assets/channel-BpzRnZmU.js +0 -1
  82. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BhlMVqCc.js +0 -1
  83. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BhlMVqCc.js +0 -1
  84. package/dist/worker/console/static/assets/index-BlN6ytLr.css +0 -1
  85. package/dist/worker/console/static/assets/index-CA-jfWlE.js +0 -449
  86. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CiVoq4G-.js +0 -1
  87. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BkdgdpfR.js +0 -8
package/CHANGELOG.md CHANGED
@@ -2,25 +2,26 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### 新增
6
+
7
+ - Operator Chat 在具备 Aimax 凭据资格时,向对话进程注入 `$AIMAX_AUTH_TOKEN`(明文或 `sm4:v1:` 解密后的 ticket),供 skills 读取;文件缺失、无法解析或解密失败时不注入该变量。
8
+ - Operator Chat 终端管理(DSH 风格):模型获得 `terminal` 持久 PTY 工具(跨平台 shell 目录——Windows 提供 powershell/bash,macOS/Linux 提供 bash/zsh,pwsh 7 装了即用;cwd/环境变量/后台任务跨调用保留,完成标记协议 + 超时与截断保护,枚举与默认值按平台动态生成);Console 右侧辅助栏新增「终端」面板(xterm.js 渲染,实时查看/接管模型的 shell,按平台动态展示可创建的 shell,支持发送输入、Ctrl+C、终止并关闭);模型 shell 与用户面板共用同一会话注册表并开在工作区仓库根目录,命名区分来源。
9
+
5
10
  ### 改进
6
11
 
7
- - 前端 DAG 修正否定约束导致的小任务误升级;Plan 在分片前统一 compact 判定并保留完整执行组。支持冻结 coverage 并发上限(1–4,默认 4),限流后减少后续并发且不增加重试。自动恢复子进程与 DAG 共用长跑超时,避免继承通用 120 秒上限而中途终止。
8
- - 前端 DAG 保留完整需求并按作用域/执行组分批,工具提交改为先校验和耐久保存再回执;支持按组共享覆盖、逐端点/逐 finding 提交和缺项恢复。小任务走短流程,容量耗尽直接缩小剩余工作;模型窗口不再硬编码为 100K/16K。独立 AC、来源保真与 writer/verify 门禁保持有效。
9
- - 观测台阅读与口径修补:失败节点的产出页把「错误输出」提到正文之前,未登记文件关系时不再展示空文件卡;检查器 Tab 条放不下时两端渐隐提示并保证选中 Tab 可见,每页顶部说明卡可收起。异常 Task 的分类与建议跟进显示中文业务术语(未知 token 原样透传并可悬停看原始值),资源池「合计」改为口径明确的「已建 Task」并附悬停说明,数据更新文案改为「数据更新于 … · 仅提示」。DAG 详情面包屑使用可读运行标题(缺失回退运行 ID、窄屏截断),「记录保存位置」移入「诊断信息」折叠区,页面刷新按钮与标题同行靠右。
10
- - 观测台保留检查器 Tab 导航,日志分类收进下拉框,辅助操作改为带提示的图标;所有执行记录区域(检查器执行记录/产出、运行页最新输出)统一配备对话式「回到底部」悬浮按钮:内容可滚动且视口离开底部即出现,贴底或内容不足一屏时隐藏,点击回到底部;活跃运行同时恢复跟随并提示未读条数,终态运行同样可用。执行分次仅在相关页面固定显示,上翻暂停跟随,并记住分次阅读位置。补充日志搜索、错误过滤、图定位和完整脱敏文本下载。
11
- - 实际上下文改为中文材料目录优先、技术信息折叠;修复历史轮次混读当前上下文和旧请求覆盖新尝试的问题。保留仅看错误与原始协议事件,原始协议使用直接可达的图标开关,移除单项更多菜单和日志 HTML 导出;交接包明确固定来源并支持刷新。
12
- - 检查器隐藏无内容或明确不适用的入口:单次执行不显示尝试对比,Shell/静态节点不显示实际上下文,静态节点不显示执行记录;切换节点与新增尝试时自动更新,保留用户当前的比较选择。
13
- - 资源池筛选覆盖任务标题和全部任务区域,返回列表及历史分页保留阅读状态;重跑草稿和影响预览可恢复,结果不确定时原样重试已确认的请求,避免重复创建运行。
14
- - 执行记录默认跟随最新内容;上翻自动暂停并出现对话式「回到底部」按钮,点击回到底部;历史记录保留阅读位置,终态运行同样可用。
15
- - DAG 节点检查器仅对最新且仍运行的执行默认跟随;右下角显示阅读状态、新记录提示和“回到最新”,上翻即暂停。
12
+ - 优化 Operator Chat 长会话的流式渲染:完成回合、消息行、工具卡和用户输入锚点建立稳定的 memo 边界,流式文本更新不再重复渲染整段历史或重装锚点测量监听;缓存会在历史消息或工具实际更新时正确失效;无新增折叠回合时不再提交空状态及重复写入本地存储,工具参数与结果格式化按真实输入变化缓存。
13
+ - 对话页「过程折叠」对齐 DSH 细节:折叠后的内容仍保留在页面中,浏览器 Ctrl+F 可直接命中并自动展开;折叠行展开时不再多出底部空隙;箭头改为 16px、颜色更淡的固定图标(靠旋转而非换图标),并补齐系统「减少动态效果」偏好;汇总文案新增「N subagent」,内置 Explore 子代理不再计入普通工具调用。
16
14
 
17
15
  ### 修复
18
16
 
19
- - 前端 Plan 重试复用首次冻结的分片归属,避免重试提示词变长导致进度绑定不匹配或重复处理已完成需求;布局缺失、损坏或漂移时保留证据并阻断派发。风险路由仅忽略完整的范围排除声明,条件句和嵌套否定中的真实权限风险在生成期与运行期均保持升级。
20
- - 执行记录搜索直接高亮命中文字,标识当前跳转记录;未搜索或无匹配时禁用上下箭头,清空搜索恢复原文。
21
-
22
- - 恢复运行在导入节点缺少明确读取范围时阻断工作区漂移,避免复用受变更影响的旧结果;读取范围明确且不受影响时仍可按路径复用。
23
- - 前端验证将子目录执行的 Vitest 报告统一为仓库相对路径,并对齐实际目录别名,避免已通过测试被误判为未执行;被筛除或失败的目标仍阻断。
17
+ - 修复对话页存在运行活动时,收起状态的右侧辅助栏展开按钮覆盖「运行活动 N」及数字徽标的问题;中大尺寸布局现在保留 8px 操作间距,窄屏布局不变。
18
+ - 修复 Operator Chat 消息流的六处实际使用问题:
19
+ - **回复被吞**:断线/重连(含每 25 秒的 SSE 事件窗口轮转)后,保留事件里已经生成的正文会在事件流恢复时重新应用到对应回合,不再被静默丢弃;仅当该回合已有更新内容时才跳过,避免重复气泡。
20
+ - **用户输入消失或重复**:服务端确认后,提交回滚(排队抢占、被拒绝、连接失败)不再删掉已确认的消息;确认帧到达时按请求 ID 或持久消息 ID 原地合并乐观气泡与会话快照,retained replay 不再追加同一条用户消息。
21
+ - **回复重复**:上下文溢出自动压缩并重新生成后,被打断的旧回复会被取代,不再与重新生成的回复同时显示。
22
+ - **只有思考没有正文**:模型回复以工具调用或思考结尾时,正文会作为回答显示,不再被折叠进「过程」区。
23
+ - **空气泡误删**:只包含思考或工具内容、正文为空的回合不再被清理掉。
24
+ - **停止语义**:被停止中断的工具调用显示「已停止」,不再显示为「出错」并强制展开、计入「N 次出错」。
24
25
 
25
26
  ## [0.42.0] - 2026-09-07
26
27
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.43.0-next.3",
4
- "gitSha": "58353199a043d50d24063322a173d81b4b8f1d2f",
5
- "builtAt": "2026-09-08T04:08:10.997Z"
3
+ "version": "0.43.0-next.4",
4
+ "gitSha": "a40dde963afaa57ff95e0e49aa33e051b5580827",
5
+ "builtAt": "2026-09-08T23:28:40.065Z"
6
6
  }
@@ -115,9 +115,30 @@ export function splitFinalAssistantBlocks(blocks, options = {}) {
115
115
  if (lastProcessIndex === -1) {
116
116
  return { answerBlocks: displayable, processBlocks: [] };
117
117
  }
118
+ const trailing = displayable.slice(lastProcessIndex + 1);
119
+ if (trailing.length > 0) {
120
+ return {
121
+ answerBlocks: trailing,
122
+ processBlocks: displayable.slice(0, lastProcessIndex + 1),
123
+ };
124
+ }
125
+ // The stage ends on a non-text block (a tool call after the prose, or a final
126
+ // thinking block). Returning an empty answer here hid the whole reply inside
127
+ // the collapsed process subtree; keep the last prose block as the visible
128
+ // answer instead (2026-09-08).
129
+ let lastTextIndex = -1;
130
+ for (let i = displayable.length - 1; i >= 0; i--) {
131
+ if (isFinalAnswerBlock(displayable[i])) {
132
+ lastTextIndex = i;
133
+ break;
134
+ }
135
+ }
136
+ if (lastTextIndex === -1) {
137
+ return { answerBlocks: [], processBlocks: displayable };
138
+ }
118
139
  return {
119
- answerBlocks: displayable.slice(lastProcessIndex + 1),
120
- processBlocks: displayable.slice(0, lastProcessIndex + 1),
140
+ answerBlocks: [displayable[lastTextIndex]],
141
+ processBlocks: displayable.filter((_block, index) => index !== lastTextIndex),
121
142
  };
122
143
  }
123
144
  export function joinAssistantText(blocks) {
@@ -71,6 +71,10 @@ export const SECRET_SCRUB_PATTERNS = [
71
71
  name: "private-key-block",
72
72
  re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----/g,
73
73
  },
74
+ {
75
+ name: "aimax-auth-token",
76
+ re: /\bAIMAX_AUTH_TOKEN=["']?[^\s"']+/g,
77
+ },
74
78
  ];
75
79
  /** Throw a typed explore error (kept as a value object so callers can read .code). */
76
80
  export class ExploreDeniedError extends Error {
@@ -89,6 +93,10 @@ export class ExploreDeniedError extends Error {
89
93
  */
90
94
  export function scrubSecrets(input) {
91
95
  let out = input;
96
+ const liveToken = process.env.AIMAX_AUTH_TOKEN;
97
+ if (liveToken && liveToken.length >= 8 && out.includes(liveToken)) {
98
+ out = out.split(liveToken).join("[REDACTED:aimax-auth-token]");
99
+ }
92
100
  const redactions = [];
93
101
  for (const { name, re } of SECRET_SCRUB_PATTERNS) {
94
102
  re.lastIndex = 0;
@@ -98,6 +106,11 @@ export function scrubSecrets(input) {
98
106
  redactions.push({ name, count: matches.length });
99
107
  }
100
108
  }
109
+ if (liveToken && liveToken.length >= 8 && input.includes(liveToken)) {
110
+ const existing = redactions.find((entry) => entry.name === "aimax-auth-token");
111
+ if (!existing)
112
+ redactions.push({ name: "aimax-auth-token", count: 1 });
113
+ }
101
114
  return { scrubbed: out, redactions };
102
115
  }
103
116
  /** True if a repo-relative path matches a sensitive-file denylist pattern. */
@@ -43,6 +43,7 @@ import { resolveDefaultChatModel, resolveLowChatModel, } from "./model-resolver.
43
43
  import { filterActiveInterviewTools } from "../interview/tools.js";
44
44
  import { RUNTIME_CONTEXT_TEXT_MAX, redactRuntimeText, } from "./runtime-context.js";
45
45
  import { buildTodoContinuationContext, } from "./todos.js";
46
+ import { buildPersistentTerminalTools } from "./terminal-tools.js";
46
47
  import { buildGoalContinuationContext, } from "./goals.js";
47
48
  import { injectWizardLocalHeaders, } from "./sdd-data-alignment.js";
48
49
  /**
@@ -1054,6 +1055,9 @@ export class ConsolePiRuntime {
1054
1055
  },
1055
1056
  });
1056
1057
  }
1058
+ async syncAimaxAuthToken() {
1059
+ await this.options.sddDataAlignment?.auth?.getToken();
1060
+ }
1057
1061
  /** Absolute path for repo-scoped resource preferences (may be undefined in unit tests). */
1058
1062
  get resourcePreferencesPath() {
1059
1063
  return this.options.resourcePreferencesPath;
@@ -1341,6 +1345,7 @@ export class ConsolePiRuntime {
1341
1345
  * `initializing: true`; dependent routes wait on the same promise.
1342
1346
  */
1343
1347
  async createSession(init) {
1348
+ await this.syncAimaxAuthToken();
1344
1349
  const sessionId = `operator-chat-${randomBytes(12).toString("hex")}`;
1345
1350
  this.disposedSessions.delete(sessionId);
1346
1351
  const sessionManager = await this.bindings.createSessionManager({
@@ -1438,6 +1443,7 @@ export class ConsolePiRuntime {
1438
1443
  async materializeSession(sessionId, init) {
1439
1444
  if (this.disposedSessions.has(sessionId))
1440
1445
  return;
1446
+ await this.syncAimaxAuthToken();
1441
1447
  const agentDir = await safeGetAgentDir(this.bindings);
1442
1448
  // Capture before services parse models.json. A concurrent edit can therefore
1443
1449
  // cause at most one harmless extra refresh, never a missed next-turn refresh.
@@ -1547,6 +1553,16 @@ export class ConsolePiRuntime {
1547
1553
  const goalTools = this.options.goalRegistry
1548
1554
  ? await buildGoalTools({ sessionId, registry: this.options.goalRegistry })
1549
1555
  : [];
1556
+ const terminalTools = this.options.terminalRegistry
1557
+ ? await buildPersistentTerminalTools({
1558
+ ownerSessionId: sessionId,
1559
+ registry: this.options.terminalRegistry,
1560
+ defaultCwd: this.options.cwd,
1561
+ }).catch((error) => {
1562
+ process.stderr.write(`[console] chat terminal-tools build failed: ${error instanceof Error ? error.message : String(error)}\n`);
1563
+ return [];
1564
+ })
1565
+ : [];
1550
1566
  const agentTool = await buildAgentCustomTool({
1551
1567
  parentSessionId: sessionId,
1552
1568
  orchestrator: this.exploreOrchestrator,
@@ -1571,6 +1587,7 @@ export class ConsolePiRuntime {
1571
1587
  ...(questionTool ? [questionTool] : []),
1572
1588
  ...(todoTool ? [todoTool] : []),
1573
1589
  ...goalTools,
1590
+ ...terminalTools,
1574
1591
  ...(agentTool ? [agentTool] : []),
1575
1592
  ];
1576
1593
  }
@@ -2347,6 +2364,7 @@ export class ConsolePiRuntime {
2347
2364
  * than silently creating a fresh empty session and losing prior context.
2348
2365
  */
2349
2366
  async reopenSession(init) {
2367
+ await this.syncAimaxAuthToken();
2350
2368
  const sessionId = init.sessionId;
2351
2369
  // Wait for in-flight create materialization rather than racing reopen.
2352
2370
  const initState = this.sessionInit.get(sessionId);
@@ -44,6 +44,8 @@ import { buildGoalCompactionInstructions, buildGoalContinuationContext, } from "
44
44
  import { classifyGoalRoundAdmission, } from "./goal-round-driver.js";
45
45
  import { isTerminalOperationState } from "../../../infrastructure/console/operation-store.js";
46
46
  import { SddDataAlignmentError, } from "./sdd-data-alignment.js";
47
+ import { TerminalSessionRegistry } from "./terminal-sessions.js";
48
+ import { handleTerminalRequest } from "./terminal-routes.js";
47
49
  function projectBrowserChatSession(record) {
48
50
  const { telemetrySessionId, ...projected } = record;
49
51
  void telemetrySessionId;
@@ -4146,6 +4148,15 @@ export async function handleChatMessages(res, deps, sessionId, url) {
4146
4148
  export async function handleChatRequest(req, res, deps, pathname, workspaceScope) {
4147
4149
  if (!deps)
4148
4150
  return false;
4151
+ // Terminal management API (model + Console UI shared PTY registry).
4152
+ if (await handleTerminalRequest(req, res, {
4153
+ terminals: deps.terminals ?? new TerminalSessionRegistry(),
4154
+ gateMutation: (r) => gateMutation(r, deps),
4155
+ // Terminals open in the workspace repo root, not the Console process cwd.
4156
+ defaultCwd: deps.appData.repoRoot,
4157
+ }, pathname)) {
4158
+ return true;
4159
+ }
4149
4160
  if (!pathname.startsWith("/api/operator/v1/chat/") &&
4150
4161
  !pathname.startsWith("/api/operator/v1/pi/"))
4151
4162
  return false;
@@ -1,4 +1,4 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { createDecipheriv, randomBytes } from "node:crypto";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -10,8 +10,49 @@ export const SDD_SKILL_NAMES = [
10
10
  /** Backward-compatible name for callers that need a stable representative value. */
11
11
  export const SDD_DEFAULT_SKILL_NAME = "SDD-implementation-test-review";
12
12
  export const SDD_TELEMETRY_URL = "https://wizard-ai.paic.com.cn/code_pilot/api/skill-usage-log";
13
- export const SDD_TELEMETRY_SIGN = "52aad0c09e6b46818a11ee702d6fe0ba";
13
+ /** SM4 key hex; also the SDD telemetry `ws-sign` value. */
14
+ export const DEFAULT_AI_MA_PUB_SALT_KEY = "52aad0c09e6b46818a11ee702d6fe0ba";
15
+ export const SDD_TELEMETRY_SIGN = DEFAULT_AI_MA_PUB_SALT_KEY;
16
+ export const AIMAX_AUTH_TOKEN_ENV = "AIMAX_AUTH_TOKEN";
14
17
  export const WIZARD_LOCAL_DEFAULT_USER_AGENT = "Builder/OpenCode ai-sdk/provider-utils/4.0.21 runtime/bun/1.3.11";
18
+ const SM4_V1_PREFIX = "sm4:v1:";
19
+ /** Decode a plaintext ticket or `sm4:v1:` ciphertext. Unknown prefixes and crypto errors fail closed. */
20
+ export function decodeAimaxAuthToken(raw, keyHex = DEFAULT_AI_MA_PUB_SALT_KEY) {
21
+ const candidate = raw.trim();
22
+ if (!candidate)
23
+ return undefined;
24
+ if (!candidate.startsWith("sm4:"))
25
+ return candidate;
26
+ if (!candidate.startsWith(SM4_V1_PREFIX))
27
+ return undefined;
28
+ const hex = candidate.slice(SM4_V1_PREFIX.length);
29
+ if (!hex || hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex))
30
+ return undefined;
31
+ const key = Buffer.from(keyHex.trim(), "hex");
32
+ if (key.length !== 16)
33
+ return undefined;
34
+ try {
35
+ const encrypted = Buffer.from(hex, "hex");
36
+ if (encrypted.length === 0 || encrypted.length % 16 !== 0)
37
+ return undefined;
38
+ const decipher = createDecipheriv("sm4-ecb", key, null);
39
+ decipher.setAutoPadding(true);
40
+ const plain = Buffer.concat([decipher.update(encrypted), decipher.final()])
41
+ .toString("utf8")
42
+ .trim();
43
+ return plain || undefined;
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ /** Set or delete `AIMAX_AUTH_TOKEN`. Empty string is treated as absent. */
50
+ export function syncAimaxAuthTokenEnv(env, token) {
51
+ if (token)
52
+ env[AIMAX_AUTH_TOKEN_ENV] = token;
53
+ else
54
+ delete env[AIMAX_AUTH_TOKEN_ENV];
55
+ }
15
56
  function sameAuthFileVersion(left, right) {
16
57
  if (!left.exists || !right.exists)
17
58
  return left.exists === right.exists;
@@ -21,9 +62,22 @@ function sameAuthFileVersion(left, right) {
21
62
  /** Reads ~/.aima-go/auth.json with metadata-based parse caching and fail-closed semantics. */
22
63
  export class AimaxAuthReader {
23
64
  authFile;
65
+ env;
24
66
  cached;
25
- constructor(authFile = path.join(os.homedir(), ".aima-go", "auth.json")) {
67
+ constructor(authFile = path.join(os.homedir(), ".aima-go", "auth.json"), env = process.env) {
26
68
  this.authFile = authFile;
69
+ this.env = env;
70
+ }
71
+ resolveKeyHex() {
72
+ const injected = typeof this.env.AI_MA_PUB_SALT_KEY === "string" ? this.env.AI_MA_PUB_SALT_KEY.trim() : "";
73
+ if (injected)
74
+ return injected;
75
+ const host = process.env.AI_MA_PUB_SALT_KEY?.trim();
76
+ return host || DEFAULT_AI_MA_PUB_SALT_KEY;
77
+ }
78
+ finish(token) {
79
+ syncAimaxAuthTokenEnv(this.env, token);
80
+ return token;
27
81
  }
28
82
  async getToken() {
29
83
  let version;
@@ -34,22 +88,24 @@ export class AimaxAuthReader {
34
88
  }
35
89
  catch {
36
90
  this.cached = { version: { exists: false } };
37
- return undefined;
91
+ return this.finish(undefined);
92
+ }
93
+ if (this.cached && sameAuthFileVersion(this.cached.version, version)) {
94
+ return this.finish(this.cached.token);
38
95
  }
39
- if (this.cached && sameAuthFileVersion(this.cached.version, version))
40
- return this.cached.token;
41
96
  let token;
42
97
  try {
43
98
  const parsed = JSON.parse(await readFile(this.authFile, "utf8"));
44
99
  const candidate = parsed["aimax-auth-token"];
45
- if (typeof candidate === "string" && candidate.trim())
46
- token = candidate;
100
+ if (typeof candidate === "string" && candidate.trim()) {
101
+ token = decodeAimaxAuthToken(candidate, this.resolveKeyHex());
102
+ }
47
103
  }
48
104
  catch {
49
105
  // Malformed or unreadable files are intentionally ineligible.
50
106
  }
51
107
  this.cached = { version, ...(token ? { token } : {}) };
52
- return token;
108
+ return this.finish(token);
53
109
  }
54
110
  async isEligible() {
55
111
  return Boolean(await this.getToken());
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Operator Chat — terminal management HTTP API (Console UI side).
3
+ *
4
+ * Endpoints (all under /api/operator/v1/chat):
5
+ * GET /terminals list terminal sessions (all owners)
6
+ * POST /terminals spawn { shell, name?, cwd?, ownerSessionId? }
7
+ * GET /terminals/:id session info + buffered output
8
+ * POST /terminals/:id/input { data } write to the PTY
9
+ * POST /terminals/:id/resize { cols, rows }
10
+ * POST /terminals/:id/interrupt send Ctrl+C
11
+ * POST /terminals/:id/kill stop and await full exit
12
+ * GET /terminals/:id/stream SSE: buffered replay + live chunks
13
+ *
14
+ * Mutating routes require the Console boot token (same gate as other chat
15
+ * mutations). The stream is read-only and uses the same origin checks as the
16
+ * chat event stream.
17
+ */
18
+ import { TerminalSessionError, availableShellKinds, } from "./terminal-sessions.js";
19
+ function sendJson(res, status, body) {
20
+ const payload = JSON.stringify(body);
21
+ res.writeHead(status, {
22
+ "content-type": "application/json; charset=utf-8",
23
+ "cache-control": "no-store",
24
+ });
25
+ res.end(payload);
26
+ }
27
+ function mapTerminalError(res, error) {
28
+ if (error instanceof TerminalSessionError) {
29
+ const status = error.code === "TERMINAL_NOT_FOUND"
30
+ ? 404
31
+ : error.code === "TERMINAL_EXITED" || error.code === "TERMINAL_SEND_ACTIVE"
32
+ ? 409
33
+ : 500;
34
+ sendJson(res, status, {
35
+ ok: false,
36
+ error: { code: error.code, message: error.message },
37
+ });
38
+ return;
39
+ }
40
+ sendJson(res, 500, {
41
+ ok: false,
42
+ error: {
43
+ code: "TERMINAL_INTERNAL",
44
+ message: error instanceof Error ? error.message : String(error),
45
+ },
46
+ });
47
+ }
48
+ async function readJsonBody(req, maxBytes = 512 * 1024) {
49
+ const chunks = [];
50
+ let total = 0;
51
+ for await (const chunk of req) {
52
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
53
+ total += buf.length;
54
+ if (total > maxBytes)
55
+ throw new Error("request body too large");
56
+ chunks.push(buf);
57
+ }
58
+ if (chunks.length === 0)
59
+ return {};
60
+ const raw = Buffer.concat(chunks).toString("utf8");
61
+ if (!raw.trim())
62
+ return {};
63
+ return JSON.parse(raw);
64
+ }
65
+ /** Route dispatcher; returns true when the request was handled. */
66
+ export async function handleTerminalRequest(req, res, deps, pathname) {
67
+ if (!pathname.startsWith("/api/operator/v1/chat/terminals"))
68
+ return false;
69
+ const method = (req.method ?? "GET").toUpperCase();
70
+ const rest = pathname.slice("/api/operator/v1/chat/terminals".length);
71
+ const parts = rest.split("/").filter(Boolean);
72
+ try {
73
+ // GET /terminals — list (+ available shell kinds for this platform)
74
+ if (method === "GET" && parts.length === 0) {
75
+ sendJson(res, 200, {
76
+ ok: true,
77
+ data: {
78
+ terminals: deps.terminals.list(),
79
+ availableShells: availableShellKinds().kinds,
80
+ },
81
+ });
82
+ return true;
83
+ }
84
+ // POST /terminals — spawn
85
+ if (method === "POST" && parts.length === 0) {
86
+ const gate = deps.gateMutation(req);
87
+ if (!gate.ok) {
88
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
89
+ return true;
90
+ }
91
+ const body = await readJsonBody(req);
92
+ const kinds = availableShellKinds().kinds;
93
+ const shell = kinds.includes(body.shell)
94
+ ? body.shell
95
+ : undefined;
96
+ if (!shell) {
97
+ sendJson(res, 400, {
98
+ ok: false,
99
+ error: {
100
+ code: "INVALID_INPUT",
101
+ message: `shell must be one of: ${kinds.join(", ")} (available on this platform)`,
102
+ },
103
+ });
104
+ return true;
105
+ }
106
+ const info = deps.terminals.spawn({
107
+ ownerSessionId: typeof body.ownerSessionId === "string" && body.ownerSessionId
108
+ ? body.ownerSessionId
109
+ : "console-ui",
110
+ name: typeof body.name === "string" && body.name ? body.name : `${shell}-ui`,
111
+ shell: shell,
112
+ cwd: typeof body.cwd === "string" && body.cwd
113
+ ? body.cwd
114
+ : (deps.defaultCwd ?? undefined),
115
+ cols: typeof body.cols === "number" ? body.cols : undefined,
116
+ rows: typeof body.rows === "number" ? body.rows : undefined,
117
+ });
118
+ sendJson(res, 201, { ok: true, data: info });
119
+ return true;
120
+ }
121
+ const sessionId = parts[0];
122
+ if (!sessionId)
123
+ return false;
124
+ const sub = parts[1] ?? "";
125
+ // GET /terminals/:id — info + buffered output
126
+ if (method === "GET" && parts.length === 1) {
127
+ const info = deps.terminals.get(sessionId);
128
+ const output = deps.terminals.readAll(sessionId);
129
+ sendJson(res, 200, { ok: true, data: { ...info, output } });
130
+ return true;
131
+ }
132
+ // GET /terminals/:id/stream — SSE
133
+ if (method === "GET" && sub === "stream") {
134
+ // Validate existence first (404 before opening the stream).
135
+ const info = deps.terminals.get(sessionId);
136
+ void info;
137
+ res.writeHead(200, {
138
+ "content-type": "text/event-stream; charset=utf-8",
139
+ "cache-control": "no-cache, no-store",
140
+ connection: "keep-alive",
141
+ "x-accel-buffering": "no",
142
+ });
143
+ const writeFrame = (event, data) => {
144
+ if (res.writableEnded)
145
+ return;
146
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
147
+ };
148
+ const buffered = deps.terminals.readAll(sessionId);
149
+ writeFrame("snapshot", {
150
+ truncated: buffered.truncated,
151
+ totalOutputChars: buffered.totalOutputChars,
152
+ text: buffered.text,
153
+ info: deps.terminals.get(sessionId),
154
+ });
155
+ const unsubscribe = deps.terminals.subscribe(sessionId, (chunk) => {
156
+ writeFrame("data", { chunk });
157
+ });
158
+ const heartbeat = setInterval(() => {
159
+ if (!res.writableEnded)
160
+ res.write(": heartbeat\n\n");
161
+ }, 15_000);
162
+ res.once("close", () => {
163
+ clearInterval(heartbeat);
164
+ unsubscribe();
165
+ });
166
+ return true;
167
+ }
168
+ // Everything else mutates → gate first.
169
+ const gate = deps.gateMutation(req);
170
+ if (!gate.ok) {
171
+ sendJson(res, gate.status, { ok: false, error: { code: gate.code, message: gate.message } });
172
+ return true;
173
+ }
174
+ if (method === "POST" && sub === "input" && parts.length === 2) {
175
+ const body = await readJsonBody(req);
176
+ if (typeof body.data !== "string") {
177
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "data (string) is required" } });
178
+ return true;
179
+ }
180
+ deps.terminals.send(sessionId, body.data);
181
+ sendJson(res, 200, { ok: true });
182
+ return true;
183
+ }
184
+ if (method === "POST" && sub === "resize" && parts.length === 2) {
185
+ const body = await readJsonBody(req);
186
+ const cols = Number(body.cols);
187
+ const rows = Number(body.rows);
188
+ if (!Number.isInteger(cols) || !Number.isInteger(rows) || cols < 2 || rows < 2 || cols > 500 || rows > 200) {
189
+ sendJson(res, 400, { ok: false, error: { code: "INVALID_INPUT", message: "cols and rows (integers 2..500/200) are required" } });
190
+ return true;
191
+ }
192
+ deps.terminals.resize(sessionId, cols, rows);
193
+ sendJson(res, 200, { ok: true });
194
+ return true;
195
+ }
196
+ if (method === "POST" && sub === "interrupt" && parts.length === 2) {
197
+ deps.terminals.interrupt(sessionId);
198
+ sendJson(res, 200, { ok: true });
199
+ return true;
200
+ }
201
+ if (method === "POST" && sub === "kill" && parts.length === 2) {
202
+ await deps.terminals.kill(sessionId);
203
+ sendJson(res, 200, { ok: true });
204
+ return true;
205
+ }
206
+ sendJson(res, 404, { ok: false, error: { code: "NOT_FOUND", message: `unknown terminal route: ${method} ${pathname}` } });
207
+ return true;
208
+ }
209
+ catch (error) {
210
+ if (!res.headersSent)
211
+ mapTerminalError(res, error);
212
+ else
213
+ res.end();
214
+ return true;
215
+ }
216
+ }