@tea-agent/loop-agent 0.43.0-next.10 → 0.43.0-next.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/build-stamp.json +3 -3
- package/dist/commands/client-recovery.js +20 -55
- package/dist/executors/dag-pi-executor.js +27 -6
- package/dist/executors/shell-executor.js +3 -2
- package/dist/shared/pi-context-pressure/extension.js +26 -16
- package/dist/shared/pi-provider-recovery.js +182 -0
- package/dist/shared/pi-retry-settings.js +40 -0
- package/dist/worker/console/chat/browser-policy.js +6 -36
- package/dist/worker/console/chat/browser-routes.js +3 -11
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +2 -1
- package/dist/worker/console/chat/pi-runtime.js +11 -12
- package/dist/worker/console/chat/routes.js +1 -5
- package/dist/worker/console/chat/session-mode-view.js +5 -4
- package/dist/worker/console/chat/session-mode.js +11 -10
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-D_fNbWc_.js → abnfDiagram-N423BO3Z-BEg5712w.js} +1 -1
- package/dist/worker/console/static/assets/{arc-_HuqLW6m.js → arc-CkQvaBGZ.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-Djy2qY1m.js → architectureDiagram-T3A2C74G-BrdwFNJ7.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-bB-1TSyY.js → blockDiagram-VBNYF7ZC-M_LjP2kx.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-C5Hmghrz.js → c4Diagram-5PPSVZJV-Bwr5Dtjc.js} +1 -1
- package/dist/worker/console/static/assets/channel-CKmyKgY-.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-Duyl39oy.js → chunk-2GRJ4B5K-CDc44stW.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-x6mdnSyf.js → chunk-2Q5K7J3B-DZELwsTt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-WlBuFj7W.js → chunk-5RXB4S5H-D7YI6SWX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CTF429HX.js → chunk-5VM5RSS4-Bc46bEyj.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Dwd6r-SF.js → chunk-6Q2QTUOP-Bj4vc-Dp.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C-Br82D1.js → chunk-GF5L2VYU-DRi-0nDX.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CuGp0DeE.js → chunk-JWPE2WC7-Dr-wFY60.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CJhFX6Lh.js → chunk-KBJHAD2P-CqIHE_D7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DpGOa_jT.js → chunk-RYQCIY6F-C-Dl8wzk.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DXJaTynd.js → chunk-XXDRQBXY-CV4Mydz3.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D8Wc3jJP.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D8Wc3jJP.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CRBZWjE4.js → cose-bilkent-JH36ORCC-C3MamSMJ.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BdXcxtlb.js → cynefin-VYW2F7L2-zo_Jy2sf.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-07UKsOts.js → cynefinDiagram-MW4NZA55-BB2PN1AG.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-D38p9__S.js → dagre-VZM6K2ZE-BimYueoi.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-nSiBNNjH.js → diagram-7IWD3JNH-Di1ObJRS.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-BFOTot5D.js → diagram-B4RE2ZJO-C83l-IJx.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DypKaZ6k.js → diagram-LBJQPF4R-B3S3fOpm.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-B6yoOQeW.js → diagram-Q27KOJAE-urW_Zhe7.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BHYS6viX.js → diagram-UB23O5K3-nvIbCwF6.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-D2VD5Snj.js → ebnfDiagram-BXEA7PRR-DG6O3KpP.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-E98mWxN1.js → erDiagram-JOGREHBK-DpXdtdsj.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CT1zxhqP.js → flowDiagram-UKHOOZJN-DvXWHrND.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-6GcngNcy.js → ganttDiagram-PKOTCBZU-6F7SbvUS.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DCL6NvKe.js → gitGraphDiagram-DS77QQ5N-D1tZnFec.js} +1 -1
- package/dist/worker/console/static/assets/{index-24jp3aBh.js → index-CWarwRqc.js} +94 -94
- package/dist/worker/console/static/assets/{index-CY6y6I8M.css → index-SAANIK8g.css} +1 -1
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C0627e-9.js → infoDiagram-6WML65LV-HN4qwJ2M.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-C_u3P6Ik.js → ishikawaDiagram-WSZJBQD7-OJ4qwDRo.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DPDPv9HL.js → journeyDiagram-NVQOT4AX-CJZ6giQk.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-D3OHdWiO.js → kanban-definition-27J2QSJJ-co60m19S.js} +1 -1
- package/dist/worker/console/static/assets/{linear-DI88zD7n.js → linear-D1KeFqZY.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-p2l8IrWY.js → mermaid.core-i7dDGqov.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-B1bAX-oS.js → mindmap-definition-FAOFIHXS-1SkQxBh1.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DwOnWxdD.js → pegDiagram-VL7TDLO6-B10dhI-Y.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-xRkTXnJC.js → pieDiagram-7S7Q4E2Y-CiUKUo6j.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-_9UC5Zio.js → quadrantDiagram-CIZ2JOQS-C5vH4LXZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CgEUirWJ.js → railroadDiagram-AXF67PYL-5nlWOCEj.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-Czbpwiyz.js → requirementDiagram-LRYGKXZP-CILd6QS3.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-_j7_KiqY.js → sankeyDiagram-W5VNT64P-Dfp18ZO2.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BNJa6Hyi.js → sequenceDiagram-SI44F4Z6-B9pkswBi.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DedpKtOc.js → sizeCapture-X5ZJPWSS-C1-eSgx4.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BBvthr7c.js → stateDiagram-OKZ733FA-COJsOoAF.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Bpfb0q5s.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-Ds7umgEM.js → swimlanes-SLNWSIFB-Dv5NXJUM.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BEwGGabW.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKa-p1nF.js → timeline-definition-Z64GVDOM-DRdrZ7eR.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-LoUNR56o.js → vennDiagram-T6HMQDX7-0-2TlwCj.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bqbl6ym7.js → wardleyDiagram-T6FBY63Y-3-o3q92P.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-B8dUg4lf.js → xychartDiagram-ELKLHX3M-DSgOgdUi.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/composer-session-ledger.js +192 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +9 -1
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +78 -35
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +145 -70
- package/dist/worker/console/static-src/operator-chat/useComposer.js +182 -69
- package/dist/workflows/dag/frontend-closeout.js +1 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
- package/dist/workflows/dag/frontend-review-context.js +2 -1
- package/dist/workflows/dag/frontend-review-scopes.js +2 -2
- package/dist/workflows/dag/frontend-verification-trace.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +51 -159
- package/docs/architecture/runtime-boundaries.md +3 -3
- package/docs/templates/frontend-implementation-dag.json +9 -8
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +3 -3
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/references/contract-protocol.md +3 -1
- package/skills/frontend-design-review/SKILL.md +12 -7
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +12 -0
- package/skills/frontend-plan/references/decision-contract.md +25 -19
- package/skills/frontend-review/SKILL.md +12 -18
- package/skills/frontend-review/references/review-findings.md +24 -43
- package/dist/worker/console/static/assets/channel-C0A6UEIa.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DzguNppy.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DzguNppy.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BABVQnfM.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D0qsLaxR.js +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,18 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
- 修复前端 Plan 无法提交实现步骤的问题:finalize_plan 支持按 AC 绑定实施动作,缺漏在节点内返回修正,避免空计划反复进入设计审查。
|
|
6
|
+
|
|
7
|
+
- 修复前端最终审查反复猜错范围 ID:明确 checkpoint 参数和补交步骤,收尾正确识别 checkpoint 之后的审批结论。
|
|
8
|
+
|
|
9
|
+
- 前端实现流程简化为需求实现覆盖审查和 `npm run build`,不再强制单测或测试方案;行为测试可后补,构建结果不冒充行为验证。
|
|
10
|
+
|
|
5
11
|
### 改进
|
|
6
12
|
|
|
13
|
+
- Operator Chat 内置内部 provider 瞬态异常恢复:即使目标仓库未执行 `loop-agent init`,也能识别完整或被裁剪的 `UnknownError` / `TypeValidationError` / `LLMRequestError` / 502 / 中文「网络波动、模型处理超时」响应,将其规范化后交给 Pi 原生有界重试;Console 缺省使用 loop-agent 推荐重试参数,项目显式字段优先于用户全局字段,用户配置优先于推荐默认值。共享纯分类器同时生成 OpenCode 插件谓词,overflow、认证、权限、配额、取消和业务错误继续 fail-fast。
|
|
14
|
+
|
|
15
|
+
- Operator Chat 浏览器面板默认以完整权限运行(去掉 iframe 沙箱与「临时解锁」状态条),并放开本机回环地址限制——`localhost` / `127.0.0.1` / `::1` 等本地开发服务器可直接在面板内浏览;地址栏仍仅放行 http/https,`javascript:` / `data:` / `file:` 等危险协议继续拦截,拒绝嵌入站点(X-Frame-Options / frame-ancestors)的解释面板与出口保持不变。
|
|
16
|
+
|
|
7
17
|
- 前端 Plan 普通分片按本批完整需求材料打包,固定提示词、用户消息和工具定义不再挤占 24 KB 分片目标,也不再触发发送前的机械二分,减少小需求逐 AC 会话;实际模型容量检查与超限恢复继续生效。
|
|
8
18
|
|
|
9
19
|
- 前端 Plan 小任务合并规划不再受固定 48 KB 输入门槛限制,避免提示词、用户消息或工具定义变长导致额外分片;完整输入、其余任务规模条件及容量恢复机制保持不变。
|
|
10
20
|
- 节点侧栏修复执行分次下拉菜单被执行记录工具栏遮挡的问题,直接展示所选失败尝试的简要原因,并移除重复的左右切换箭头。
|
|
11
21
|
- Operator Chat 的 Pi Runtime 按职责拆出工具构造、运行态检视和 SDK 适配,并共用创建/恢复/分叉的会话准备流程;对外入口、错误类型和会话行为保持不变。
|
|
12
22
|
|
|
23
|
+
### 修复
|
|
24
|
+
|
|
25
|
+
- 修复 Operator Chat 的跨会话输入污染与快速重复提交:切换会话时立即撤销旧会话发送权,草稿、附件和 ↑ 输入历史按会话独立保存;发送、排队、撤回、编辑、插话、文件建议和图片读取的迟到结果只回写原会话。提交改用带快照的有界 attempt,相同内容的快速双 Enter/双击只产生一次请求,失败恢复或排队响应不再覆盖后来输入;流式更新和会话切换保持同一个 composer DOM,中文 IME 更稳定。同步修复 durable 用户消息确认后时间重排导致闪烁光标跑到用户消息上方的问题。
|
|
26
|
+
|
|
27
|
+
- 前端设计审查的写入门禁只统计已提交的审批终态,避免审查范围、发现记录导致合法审批被误判为缺失或歧义;要求修改时保留具体审查意见,缺失、重复及冲突终态仍阻断写入。
|
|
28
|
+
|
|
13
29
|
## [0.42.1] - 2026-09-09
|
|
14
30
|
|
|
15
31
|
### 新增
|
|
16
32
|
|
|
33
|
+
- Console Chat 的 `/goal` 持续目标能力对 Pi 模式开放(与工作流模式同语义):斜杠面板两模式均可见 `/goal`;Pi 会话注册 `get_goal` / `create_goal` / `update_goal` 工具并纳入激活集;Goal HTTP 接口(创建/暂停/恢复/清除)在 Pi 会话不再返回模式拒绝;GoalBar 与目标编辑面板在 Pi 会话渲染;压缩恢复、每轮 Goal 上下文与有界自主续跑(仅显式 `/goal` 才 arm,maxRounds 上限,reload/重启/出错/暂停即 disarm)在 Pi 会话可用。
|
|
34
|
+
|
|
17
35
|
- Operator Chat 浏览器面板(DSH 风格):Console 右侧辅助栏新增「浏览器」tab——地址栏(自动补 https、仅 http/https、禁止 javascript/data/file 等危险协议与本机回环地址)、后退/前进/刷新导航栈、系统浏览器跳转;页面运行在无 same-origin、无 top-navigation 的沙箱 iframe 中(访问的页面拿不到 Console 的存储与 API),沙箱状态条支持单页面临时解锁(红色警示 + 一键恢复);对拒绝被嵌入的站点(X-Frame-Options / frame-ancestors)给出明确解释面板与「在系统浏览器打开 / 仍要加载」出口,探测端点仅返回响应头、仅限 http(s) 非回环、8 秒硬超时。
|
|
18
36
|
- Operator Chat 源代码管理(DSH 风格):模型获得 `scm` 工具(status/diff/stage/unstage/discard/commit/log/branch/push/pull,固定参数 git 无 shell、限时限量输出);Console 右侧辅助栏新增「源码」面板(DSH GitView 布局——分支下拉切换、已暂存/更改分组与全部暂存/取消暂存、单行提交框 Ctrl+Enter、内联分页历史带 refs 徽标与相对时间、点击文件看 diff、右键菜单放弃更改/复制路径);模型与面板共用同一工作区级 ScmService。
|
|
19
37
|
- Operator Chat 在具备 Aimax 凭据资格时,向对话进程注入 `$AIMAX_AUTH_TOKEN`(明文或 `sm4:v1:` 解密后的 ticket),供 skills 读取;文件缺失、无法解析或解密失败时不注入该变量。
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.43.0-next.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-09-
|
|
3
|
+
"version": "0.43.0-next.12",
|
|
4
|
+
"gitSha": "59f2e162841fbf8171ebaa6963c6cec95eb98512",
|
|
5
|
+
"builtAt": "2026-09-10T14:30:24.298Z"
|
|
6
6
|
}
|
|
@@ -18,6 +18,7 @@ export const BACKOFF_MS = [2000, 4000, 8000, 16000, 30000];
|
|
|
18
18
|
export const MAX_SESSION_RETRIES = 5;
|
|
19
19
|
/** Hard cap for OpenCode overflow compact+resume recoveries (≤ 2). */
|
|
20
20
|
export const MAX_OVERFLOW_RECOVERIES = 1;
|
|
21
|
+
import { buildGeneratedTransientProviderClassifierSource, classifyTransientProviderError, } from "../shared/pi-provider-recovery.js";
|
|
21
22
|
import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
|
|
22
23
|
import { CONTEXT_OVERFLOW_PHRASES, buildGeneratedOverflowClassifierSource, isContextOverflow as classifySharedOverflow, } from "../shared/pi-context-pressure/overflow.js";
|
|
23
24
|
export { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY };
|
|
@@ -51,17 +52,6 @@ const PERMANENT_MESSAGE_PATTERNS = [
|
|
|
51
52
|
pattern: /\b(business validation|invalid task|schema validation)\b/i,
|
|
52
53
|
},
|
|
53
54
|
];
|
|
54
|
-
const TRANSIENT_MESSAGE_PATTERNS = [
|
|
55
|
-
/\bcode[:\s]*502\b/i,
|
|
56
|
-
/\b502\b/,
|
|
57
|
-
/\bLLMRequestError\b/i,
|
|
58
|
-
/\bnetwork fluctuation\b/i,
|
|
59
|
-
/\btimeout\b/i,
|
|
60
|
-
/\btransient provider failure\b/i,
|
|
61
|
-
/\bnon-standard (payload|response)\b/i,
|
|
62
|
-
/\bupstream returned\b/i,
|
|
63
|
-
/\bmodel processing timeout\b/i,
|
|
64
|
-
];
|
|
65
55
|
export function isClientRecoveryMode(value) {
|
|
66
56
|
return (typeof value === "string" &&
|
|
67
57
|
CLIENT_RECOVERY_MODES.includes(value));
|
|
@@ -172,30 +162,16 @@ export function isContextOverflow(error) {
|
|
|
172
162
|
return classifySharedOverflow(error);
|
|
173
163
|
}
|
|
174
164
|
function looksTransient(error) {
|
|
175
|
-
|
|
176
|
-
if (TRANSIENT_MESSAGE_PATTERNS.some((pattern) => pattern.test(text)))
|
|
177
|
-
return true;
|
|
178
|
-
if (error && typeof error === "object") {
|
|
179
|
-
const record = error;
|
|
180
|
-
if (record.code === 502 || record.status === 502)
|
|
181
|
-
return true;
|
|
182
|
-
if (record.data && typeof record.data === "object") {
|
|
183
|
-
const data = record.data;
|
|
184
|
-
if (data.code === 502 || data.status === 502)
|
|
185
|
-
return true;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return false;
|
|
165
|
+
return classifyTransientProviderError(error).retryable;
|
|
189
166
|
}
|
|
190
167
|
/**
|
|
191
168
|
* Decide whether an OpenCode error should be resumed by the project plugin.
|
|
192
|
-
*
|
|
169
|
+
* Compensates wrapped UnknownError / TypeValidation failures and known stripped
|
|
170
|
+
* internal-provider fluctuation messages that built-in APIError retry misses.
|
|
193
171
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* signal (502 / LLMRequestError / timeout / etc.). A pure schema mismatch
|
|
198
|
-
* without a transient signal is still rejected (resumable: false).
|
|
172
|
+
* The historical success reason remains `transient-unknown-error` for report
|
|
173
|
+
* compatibility. A pure schema mismatch without a transient signal is still
|
|
174
|
+
* rejected (resumable: false).
|
|
199
175
|
*/
|
|
200
176
|
export function classifyTransientUnknownError(error) {
|
|
201
177
|
const permanent = permanentReason(error);
|
|
@@ -208,6 +184,9 @@ export function classifyTransientUnknownError(error) {
|
|
|
208
184
|
}
|
|
209
185
|
const isUnknownShape = hasUnknownErrorShape(error);
|
|
210
186
|
const isTypeValidationShape = hasTypeValidationShape(error);
|
|
187
|
+
if (looksTransient(error)) {
|
|
188
|
+
return { resumable: true, reason: "transient-unknown-error" };
|
|
189
|
+
}
|
|
211
190
|
if (!isUnknownShape && !isTypeValidationShape) {
|
|
212
191
|
return {
|
|
213
192
|
resumable: false,
|
|
@@ -215,14 +194,11 @@ export function classifyTransientUnknownError(error) {
|
|
|
215
194
|
interaction: PERMANENT_ERROR_INTERACTION,
|
|
216
195
|
};
|
|
217
196
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
return { resumable: true, reason: "transient-unknown-error" };
|
|
197
|
+
return {
|
|
198
|
+
resumable: false,
|
|
199
|
+
reason: "unknown-non-transient",
|
|
200
|
+
interaction: PERMANENT_ERROR_INTERACTION,
|
|
201
|
+
};
|
|
226
202
|
}
|
|
227
203
|
export function nextBackoffMs(attempt) {
|
|
228
204
|
if (!Number.isInteger(attempt) ||
|
|
@@ -478,8 +454,9 @@ export function buildOpenCodeTransientRetryPluginSource() {
|
|
|
478
454
|
* loop-agent OpenCode transient recovery plugin
|
|
479
455
|
* Path: ${OPENCODE_TRANSIENT_RETRY_PLUGIN_PATH}
|
|
480
456
|
*
|
|
481
|
-
* Compensates
|
|
482
|
-
*
|
|
457
|
+
* Compensates wrapped UnknownError / TypeValidation failures and known
|
|
458
|
+
* stripped internal-provider fluctuation messages that OpenCode built-in
|
|
459
|
+
* APIError retry does not cover. Never runs concurrently with session.status === "retry".
|
|
483
460
|
* Permanent failures use interaction "${PERMANENT_ERROR_INTERACTION}".
|
|
484
461
|
* User aborts (MessageAbortedError / cancel words) are silently cleared.
|
|
485
462
|
* Best-effort file log: ${OPENCODE_TRANSIENT_RETRY_LOG_PATH}.
|
|
@@ -502,7 +479,7 @@ const LOG_PATH = (() => {
|
|
|
502
479
|
const PERMANENT_INTERACTION = ${JSON.stringify(PERMANENT_ERROR_INTERACTION)};
|
|
503
480
|
const BACKOFF_MS = ${JSON.stringify([...BACKOFF_MS])};
|
|
504
481
|
const MAX_RETRIES = ${MAX_SESSION_RETRIES};
|
|
505
|
-
${
|
|
482
|
+
${buildGeneratedTransientProviderClassifierSource()}
|
|
506
483
|
void logToFile("load ok");
|
|
507
484
|
|
|
508
485
|
const sessionState = new Map();
|
|
@@ -563,19 +540,7 @@ function isPermanent(error) {
|
|
|
563
540
|
|
|
564
541
|
function isTransientUnknown(error) {
|
|
565
542
|
if (isPermanent(error)) return false;
|
|
566
|
-
|
|
567
|
-
const hasUnknown = /UnknownError/i.test(text) || (error && /UnknownError/i.test(String(error.name || "")));
|
|
568
|
-
const hasTypeValidation =
|
|
569
|
-
/\\bTypeValidationError\\b/i.test(text) ||
|
|
570
|
-
/Type validation failed/i.test(text) ||
|
|
571
|
-
(error && /TypeValidationError/i.test(String(error.name || "")));
|
|
572
|
-
if (!hasUnknown && !hasTypeValidation) return false;
|
|
573
|
-
return (
|
|
574
|
-
/\\b(code[:\\s]*502|502|LLMRequestError|network fluctuation|timeout|transient provider failure|non-standard)\\b/i.test(
|
|
575
|
-
text,
|
|
576
|
-
) ||
|
|
577
|
-
(error && (error.code === 502 || error.status === 502))
|
|
578
|
-
);
|
|
543
|
+
return isKnownTransientProviderError(error);
|
|
579
544
|
}
|
|
580
545
|
|
|
581
546
|
function buildResumePrompt(sessionId, error, attempt) {
|
|
@@ -2286,9 +2286,13 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2286
2286
|
const finalizePlanTool = defineTool({
|
|
2287
2287
|
name: "finalize_plan",
|
|
2288
2288
|
label: "finalize_plan",
|
|
2289
|
-
description: "Commit the finalize_plan terminal. Requirements, verification targets, and evidence gaps (optional) were already committed incrementally through record_plan_requirement / record_plan_verification_target / record_plan_evidence_gap; finalize_plan assembles them
|
|
2290
|
-
promptSnippet: "Commit
|
|
2289
|
+
description: "Commit the finalize_plan terminal. Requirements, verification targets, and evidence gaps (optional) were already committed incrementally through record_plan_requirement / record_plan_verification_target / record_plan_evidence_gap; finalize_plan requires implementationSteps: [{requirementIds, action}] covering every frozen requirement (shared steps allowed), and assembles them with the ledger and optional remaining fields, publishes the canonical editable patch on a target-surface fact, and commits the terminal. A successful terminal commit occurs exactly once. If validation fails, correct only the reported facts and retry finalize.",
|
|
2290
|
+
promptSnippet: "Commit finalize_plan with implementationSteps: [{requirementIds, action}] covering every frozen requirement; optional residualRisks / realIntegrationGap.",
|
|
2291
2291
|
parameters: Type.Object({
|
|
2292
|
+
implementationSteps: Type.Optional(Type.Array(Type.Object({
|
|
2293
|
+
requirementIds: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
2294
|
+
action: Type.String({ minLength: 1 }),
|
|
2295
|
+
}, { additionalProperties: false }))),
|
|
2292
2296
|
residualRisks: optionalStringArray,
|
|
2293
2297
|
realIntegrationGap: optionalString,
|
|
2294
2298
|
}, { additionalProperties: false }),
|
|
@@ -2305,8 +2309,23 @@ export async function createFrontendPlanLedgerTools(input) {
|
|
|
2305
2309
|
if (missingData.length)
|
|
2306
2310
|
return planToolReceipt({ ok: false, kind: "finalize_plan", code: "PLAN_DATA_FLOW_INCOMPLETE", error: missingData.map(f => f.reason).join("; ") });
|
|
2307
2311
|
const fragment = assemblePlanPatchFromCommittedFacts(committed, contractInheritance) ?? {};
|
|
2312
|
+
const requiredIds = new Set([
|
|
2313
|
+
...(input.requirementIds ?? []),
|
|
2314
|
+
...contractInheritance.keys(),
|
|
2315
|
+
...(Array.isArray(fragment.requirements) ? fragment.requirements.flatMap((entry) => isRecordObject(entry) && typeof entry.id === "string" ? [entry.id] : []) : []),
|
|
2316
|
+
]);
|
|
2317
|
+
const steps = params?.implementationSteps ?? [];
|
|
2318
|
+
const coveredIds = new Set(steps.flatMap(step => step.requirementIds));
|
|
2319
|
+
const missingRequirementIds = [...requiredIds].filter(id => !coveredIds.has(id));
|
|
2320
|
+
const unknownRequirementIds = [...coveredIds].filter(id => !requiredIds.has(id));
|
|
2321
|
+
if (missingRequirementIds.length || unknownRequirementIds.length || steps.some(step => !step.action.trim())) {
|
|
2322
|
+
return planToolReceipt({ ok: false, kind: "finalize_plan", code: "PLAN_IMPLEMENTATION_STEPS_INCOMPLETE",
|
|
2323
|
+
error: `Submit finalize_plan with non-empty implementationSteps: [{requirementIds: [exact frozen IDs], action: "what to change and where"}]. Cover every requirement; shared steps may cover several IDs. Missing: ${missingRequirementIds.join(", ") || "none"}; unknown: ${unknownRequirementIds.join(", ") || "none"}. Actions must not be blank. Do not add a test plan.`,
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2308
2326
|
const patch = {
|
|
2309
2327
|
...fragment,
|
|
2328
|
+
...(steps.length ? { implementationSteps: steps.map(step => `[${[...new Set(step.requirementIds)].join(", ")}] ${step.action.trim()}`) } : {}),
|
|
2310
2329
|
...(params?.residualRisks
|
|
2311
2330
|
? { residualRisks: params.residualRisks }
|
|
2312
2331
|
: {}),
|
|
@@ -3802,7 +3821,7 @@ const FRONTEND_PLAN_SEGMENTS = [
|
|
|
3802
3821
|
toolNames: null,
|
|
3803
3822
|
instruction: [
|
|
3804
3823
|
"PLAN PHASE — finalize.",
|
|
3805
|
-
"All record_* tools are available only for a narrowly named correction if the finalize receipt reports missing or invalid facts. Otherwise call finalize_plan
|
|
3824
|
+
"All record_* tools are available only for a narrowly named correction if the finalize receipt reports missing or invalid facts. Otherwise call finalize_plan with implementationSteps: [{requirementIds, action}] covering every frozen requirement. Describe concrete changes within approved paths, including what constraints preserve; shared steps may cover related IDs. Do not add tests or a test plan.",
|
|
3806
3825
|
].join(" "),
|
|
3807
3826
|
},
|
|
3808
3827
|
];
|
|
@@ -4318,12 +4337,14 @@ export async function runFrontendReviewSegmentedSessions(input) {
|
|
|
4318
4337
|
index--;
|
|
4319
4338
|
continue;
|
|
4320
4339
|
}
|
|
4340
|
+
const completionInstruction = `${item.repairs ? "REPAIR: the prior session did not commit all required checkpoints/verdict. Do not repeat the review narrative. " : ""}Complete these exact runtime scope IDs using complete_review_scope: ${JSON.stringify(scopes.map(scope => scope.id))}. These are scope IDs, not node IDs. ${finalScope ? `After those checkpoints succeed, call exactly one terminal tool: ${terminalKinds.join(" or ")}. A prose conclusion is not a committed verdict.` : "More scopes remain; do not submit an overall verdict yet."}`;
|
|
4341
|
+
const userMessage = `${input.sessionOptions.userMessage}\n\n${completionInstruction}`;
|
|
4321
4342
|
if (++calls > 128)
|
|
4322
4343
|
return { ...last, ok: false, failureCategory: "frontend-input-exhausted", stderr: "FRONTEND_REVIEW_RECOVERY_EXHAUSTED: session quota reached" };
|
|
4323
|
-
last = await observeFrontendSession({ ...input.observation, phase: `${input.phase}/scope`, scopeIds: scopes.map(s => s.id), prompt, userMessage
|
|
4344
|
+
last = await observeFrontendSession({ ...input.observation, phase: `${input.phase}/scope`, scopeIds: scopes.map(s => s.id), prompt, userMessage, customTools,
|
|
4324
4345
|
artifactPath: input.observation?.artifactPath ? `${input.observation.artifactPath}-${calls}.json` : undefined,
|
|
4325
4346
|
committedCount: () => protocol.committedFacts().length, durableCommittedCount: () => protocol.committedFacts().length,
|
|
4326
|
-
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
4347
|
+
}, observer => input.piStepFn({ ...input.sessionOptions, prompt, userMessage, onAttemptObservation: observer, writerToolPolicy: { requireSdk: true, customTools } }));
|
|
4327
4348
|
await input.tools.flush();
|
|
4328
4349
|
await input.inventory.validate();
|
|
4329
4350
|
if (last.timedOut || /interrupt|termination-unconfirmed|budget_breach/.test(last.failureCategory))
|
|
@@ -4607,7 +4628,7 @@ export async function runFrontendPlanSegmentedSessions(input) {
|
|
|
4607
4628
|
const buildPhasePrompt = (segment, missing = [], scopeIds = allRequirementIds) => {
|
|
4608
4629
|
const compact = allRequirementIds.length > 0
|
|
4609
4630
|
? compactFrontendPlanPromptForRequirementSlice(input.basePrompt, scopeIds, {
|
|
4610
|
-
includeRequirementText: segment.id === "global-mock-data",
|
|
4631
|
+
includeRequirementText: segment.id === "global-mock-data" || segment.id === "finalize",
|
|
4611
4632
|
includeVerificationTargets: false,
|
|
4612
4633
|
includeDesignEvidence: segment.id === "global-dependency-deviation" || segment.id === "finalize",
|
|
4613
4634
|
includeChecklist: false,
|
|
@@ -2329,7 +2329,7 @@ async function collectReviewTerminalKinds(runDir, reviewNodeId) {
|
|
|
2329
2329
|
const kind = record.fact.kind;
|
|
2330
2330
|
return typeof kind === "string" ? kind : "";
|
|
2331
2331
|
})
|
|
2332
|
-
.filter(
|
|
2332
|
+
.filter((kind) => kind === "approve_review" || kind === "request_review_changes");
|
|
2333
2333
|
}
|
|
2334
2334
|
catch {
|
|
2335
2335
|
return [];
|
|
@@ -2348,7 +2348,7 @@ async function collectDesignTerminalKinds(runDir, designNodeId) {
|
|
|
2348
2348
|
const kind = record.fact.kind;
|
|
2349
2349
|
return typeof kind === "string" ? kind : "";
|
|
2350
2350
|
})
|
|
2351
|
-
.filter(
|
|
2351
|
+
.filter((kind) => kind === "approve_design" || kind === "request_design_changes");
|
|
2352
2352
|
}
|
|
2353
2353
|
catch {
|
|
2354
2354
|
return [];
|
|
@@ -2974,6 +2974,7 @@ async function executeFrontendCloseout(input, meta) {
|
|
|
2974
2974
|
const contract = frontendImplementationContractSchema.parse(JSON.parse(contractRaw));
|
|
2975
2975
|
const trace = context.verificationTrace;
|
|
2976
2976
|
const verificationTargets = (trace?.targets ?? []).map((target) => ({
|
|
2977
|
+
mode: contract.verificationTargets.find((entry) => entry.id === target.id)?.mode ?? "static",
|
|
2977
2978
|
status: target.status === "ok" ? "ok" : "failed",
|
|
2978
2979
|
}));
|
|
2979
2980
|
const mockEvidenceBound = contract.mockApi.strategy !== "not-needed" &&
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { shouldRewriteAssistantTransientProviderError } from "../pi-provider-recovery.js";
|
|
1
2
|
import { buildOverflowCheckpoint } from "./checkpoint.js";
|
|
2
3
|
import { shouldRewriteAssistantOverflow } from "./overflow.js";
|
|
3
4
|
import { clearRetrieveBypass, compressProviderPayload, createSiftRetrieveToolDefinition, SIFT_RETRIEVE_TOOL_NAME, } from "./sift-bridge.js";
|
|
@@ -11,35 +12,44 @@ export function createCompactionController() {
|
|
|
11
12
|
export function createContextPressureExtension(options) {
|
|
12
13
|
const l2 = options.policy.mode !== "off";
|
|
13
14
|
const l1 = Boolean(options.sift);
|
|
14
|
-
|
|
15
|
+
const transientRecovery = options.normalizeTransientProviderErrors === true;
|
|
16
|
+
if (!l2 && !l1 && !transientRecovery)
|
|
15
17
|
return undefined;
|
|
16
18
|
return {
|
|
17
19
|
name: options.name ?? "operator-chat-context-pressure",
|
|
18
20
|
hidden: true,
|
|
19
21
|
factory: (api) => {
|
|
20
|
-
if (l2) {
|
|
22
|
+
if (l2 || transientRecovery) {
|
|
21
23
|
api.on("message_end", (event) => {
|
|
22
24
|
const message = event.message;
|
|
23
25
|
if (!message || typeof message !== "object")
|
|
24
26
|
return undefined;
|
|
25
27
|
const rec = message;
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
+
if (l2) {
|
|
29
|
+
if (typeof rec.errorMessage === "string") {
|
|
30
|
+
options.controller.lastError = rec.errorMessage;
|
|
31
|
+
}
|
|
32
|
+
if (rec.role === "assistant") {
|
|
33
|
+
clearRetrieveBypassIfPresent(options.sift);
|
|
34
|
+
}
|
|
35
|
+
const rewritten = shouldRewriteAssistantOverflow(message);
|
|
36
|
+
if (rewritten) {
|
|
37
|
+
incrementCounter(options.controller.counters, "overflow");
|
|
38
|
+
return {
|
|
39
|
+
message: {
|
|
40
|
+
...rec,
|
|
41
|
+
errorMessage: rewritten,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
28
45
|
}
|
|
29
|
-
if (
|
|
30
|
-
|
|
46
|
+
if (transientRecovery) {
|
|
47
|
+
return shouldRewriteAssistantTransientProviderError(message);
|
|
31
48
|
}
|
|
32
|
-
|
|
33
|
-
if (!rewritten)
|
|
34
|
-
return undefined;
|
|
35
|
-
incrementCounter(options.controller.counters, "overflow");
|
|
36
|
-
return {
|
|
37
|
-
message: {
|
|
38
|
-
...rec,
|
|
39
|
-
errorMessage: rewritten,
|
|
40
|
-
},
|
|
41
|
-
};
|
|
49
|
+
return undefined;
|
|
42
50
|
});
|
|
51
|
+
}
|
|
52
|
+
if (l2) {
|
|
43
53
|
api.on("session_before_compact", (event) => {
|
|
44
54
|
const reason = event.reason;
|
|
45
55
|
if (reason === "overflow") {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { buildGeneratedOverflowClassifierSource, isContextOverflow, } from "./pi-context-pressure/overflow.js";
|
|
2
|
+
export const NORMALIZED_TRANSIENT_PROVIDER_PREFIX = "provider returned error:";
|
|
3
|
+
const ERROR_TEXT_LIMIT = 1_500;
|
|
4
|
+
const PERMANENT_ERROR_PATTERNS = [
|
|
5
|
+
/\b(?:401|403|429)\b|unauthori[sz]ed|forbidden|authentication|invalid api[-_ ]?key|认证错误|鉴权失败/i,
|
|
6
|
+
/\b(?:quota|billing|insufficient.?credit|out of budget)\b|余额不足|配额(?:不足|耗尽)/i,
|
|
7
|
+
/rate.?limit|too many requests|请求.{0,8}(?:频繁|过多)|频率.{0,8}限制|限流/i,
|
|
8
|
+
/aborted by user|user cancel|cancelled|canceled|用户取消/i,
|
|
9
|
+
/business validation|invalid task|schema validation|业务校验/i,
|
|
10
|
+
];
|
|
11
|
+
export const BUILT_IN_TRANSIENT_PROVIDER_RULES = Object.freeze([
|
|
12
|
+
Object.freeze({
|
|
13
|
+
id: "wrapped-transient-provider-error",
|
|
14
|
+
all: Object.freeze([
|
|
15
|
+
/\b(?:UnknownError|TypeValidationError)\b|Type validation failed/i,
|
|
16
|
+
]),
|
|
17
|
+
any: Object.freeze([
|
|
18
|
+
/\b502\b|\bcode[:\s]*502\b/i,
|
|
19
|
+
/\bLLMRequestError\b/i,
|
|
20
|
+
/network fluctuation|timeout|model processing timeout/i,
|
|
21
|
+
/网络波动|模型处理超时|当前请求超时/i,
|
|
22
|
+
/transient provider failure|non-standard (?:payload|response)|upstream returned/i,
|
|
23
|
+
]),
|
|
24
|
+
}),
|
|
25
|
+
Object.freeze({
|
|
26
|
+
id: "internal-llm-request-error",
|
|
27
|
+
all: Object.freeze([/\bLLMRequestError\b/i]),
|
|
28
|
+
any: Object.freeze([
|
|
29
|
+
/\b502\b|\bcode[:\s]*502\b/i,
|
|
30
|
+
/network fluctuation|timeout|model processing timeout/i,
|
|
31
|
+
/网络波动|模型处理超时|当前请求超时/i,
|
|
32
|
+
/non-standard (?:payload|response)|upstream returned/i,
|
|
33
|
+
]),
|
|
34
|
+
}),
|
|
35
|
+
Object.freeze({
|
|
36
|
+
id: "internal-provider-fluctuation",
|
|
37
|
+
any: Object.freeze([/网络波动|模型处理超时|当前请求超时/i]),
|
|
38
|
+
}),
|
|
39
|
+
]);
|
|
40
|
+
function stringifyErrorPart(value) {
|
|
41
|
+
if (typeof value === "string")
|
|
42
|
+
return value;
|
|
43
|
+
if (value == null)
|
|
44
|
+
return "";
|
|
45
|
+
try {
|
|
46
|
+
return JSON.stringify(value);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return String(value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function errorText(error) {
|
|
53
|
+
if (typeof error === "string")
|
|
54
|
+
return error;
|
|
55
|
+
if (error == null)
|
|
56
|
+
return "";
|
|
57
|
+
if (typeof error !== "object")
|
|
58
|
+
return String(error);
|
|
59
|
+
const record = error;
|
|
60
|
+
return [
|
|
61
|
+
stringifyErrorPart(record.name),
|
|
62
|
+
stringifyErrorPart(record.message),
|
|
63
|
+
stringifyErrorPart(record.status),
|
|
64
|
+
stringifyErrorPart(record.code),
|
|
65
|
+
stringifyErrorPart(record.data),
|
|
66
|
+
stringifyErrorPart(error),
|
|
67
|
+
]
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join(" ");
|
|
70
|
+
}
|
|
71
|
+
function patternMatches(pattern, text) {
|
|
72
|
+
pattern.lastIndex = 0;
|
|
73
|
+
const matched = pattern.test(text);
|
|
74
|
+
pattern.lastIndex = 0;
|
|
75
|
+
return matched;
|
|
76
|
+
}
|
|
77
|
+
function ruleMatches(rule, text) {
|
|
78
|
+
const all = rule.all ?? [];
|
|
79
|
+
const any = rule.any ?? [];
|
|
80
|
+
if (all.length === 0 && any.length === 0)
|
|
81
|
+
return false;
|
|
82
|
+
if (!all.every((pattern) => patternMatches(pattern, text)))
|
|
83
|
+
return false;
|
|
84
|
+
return any.length === 0 || any.some((pattern) => patternMatches(pattern, text));
|
|
85
|
+
}
|
|
86
|
+
function isPermanentProviderError(error, text) {
|
|
87
|
+
if (isContextOverflow(error))
|
|
88
|
+
return true;
|
|
89
|
+
return PERMANENT_ERROR_PATTERNS.some((pattern) => patternMatches(pattern, text));
|
|
90
|
+
}
|
|
91
|
+
export function classifyTransientProviderError(error, options = {}) {
|
|
92
|
+
const text = errorText(error);
|
|
93
|
+
if (!text || isPermanentProviderError(error, text)) {
|
|
94
|
+
return { retryable: false, reason: "permanent" };
|
|
95
|
+
}
|
|
96
|
+
const rules = [
|
|
97
|
+
...BUILT_IN_TRANSIENT_PROVIDER_RULES,
|
|
98
|
+
...(options.additionalRules ?? []),
|
|
99
|
+
];
|
|
100
|
+
const matched = rules.find((rule) => ruleMatches(rule, text));
|
|
101
|
+
return matched
|
|
102
|
+
? { retryable: true, ruleId: matched.id }
|
|
103
|
+
: { retryable: false, reason: "no-match" };
|
|
104
|
+
}
|
|
105
|
+
export function normalizeTransientProviderErrorMessage(current, options = {}) {
|
|
106
|
+
if (!current || current.startsWith(NORMALIZED_TRANSIENT_PROVIDER_PREFIX)) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
if (!classifyTransientProviderError(current, options).retryable)
|
|
110
|
+
return undefined;
|
|
111
|
+
const bounded = current.length <= ERROR_TEXT_LIMIT
|
|
112
|
+
? current
|
|
113
|
+
: `${current.slice(0, ERROR_TEXT_LIMIT - 1)}…`;
|
|
114
|
+
return `${NORMALIZED_TRANSIENT_PROVIDER_PREFIX} ${bounded}`;
|
|
115
|
+
}
|
|
116
|
+
export function shouldRewriteAssistantTransientProviderError(message, options = {}) {
|
|
117
|
+
if (!message || typeof message !== "object")
|
|
118
|
+
return undefined;
|
|
119
|
+
const record = message;
|
|
120
|
+
if (record.role !== "assistant")
|
|
121
|
+
return undefined;
|
|
122
|
+
if (record.stopReason !== undefined && record.stopReason !== "error") {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
const current = typeof record.errorMessage === "string" ? record.errorMessage : "";
|
|
126
|
+
const rewritten = normalizeTransientProviderErrorMessage(current, options);
|
|
127
|
+
return rewritten
|
|
128
|
+
? { message: { ...record, errorMessage: rewritten } }
|
|
129
|
+
: undefined;
|
|
130
|
+
}
|
|
131
|
+
function serializePatterns(patterns) {
|
|
132
|
+
return JSON.stringify(patterns.map((pattern) => ({ source: pattern.source, flags: pattern.flags })));
|
|
133
|
+
}
|
|
134
|
+
/** Generate the pure predicate embedded in the project-level OpenCode plugin. */
|
|
135
|
+
export function buildGeneratedTransientProviderClassifierSource() {
|
|
136
|
+
const rules = BUILT_IN_TRANSIENT_PROVIDER_RULES.map((rule) => ({
|
|
137
|
+
id: rule.id,
|
|
138
|
+
all: (rule.all ?? []).map((pattern) => ({
|
|
139
|
+
source: pattern.source,
|
|
140
|
+
flags: pattern.flags,
|
|
141
|
+
})),
|
|
142
|
+
any: (rule.any ?? []).map((pattern) => ({
|
|
143
|
+
source: pattern.source,
|
|
144
|
+
flags: pattern.flags,
|
|
145
|
+
})),
|
|
146
|
+
}));
|
|
147
|
+
return `${buildGeneratedOverflowClassifierSource()}
|
|
148
|
+
const TRANSIENT_PROVIDER_PERMANENT_PATTERNS = ${serializePatterns(PERMANENT_ERROR_PATTERNS)};
|
|
149
|
+
const TRANSIENT_PROVIDER_RULES = ${JSON.stringify(rules)};
|
|
150
|
+
function transientProviderStringify(value) {
|
|
151
|
+
if (typeof value === "string") return value;
|
|
152
|
+
if (value == null) return "";
|
|
153
|
+
try { return JSON.stringify(value); } catch { return String(value); }
|
|
154
|
+
}
|
|
155
|
+
function transientProviderText(error) {
|
|
156
|
+
if (typeof error === "string") return error;
|
|
157
|
+
if (error == null) return "";
|
|
158
|
+
if (typeof error !== "object") return String(error);
|
|
159
|
+
return [
|
|
160
|
+
transientProviderStringify(error.name),
|
|
161
|
+
transientProviderStringify(error.message),
|
|
162
|
+
transientProviderStringify(error.status),
|
|
163
|
+
transientProviderStringify(error.code),
|
|
164
|
+
transientProviderStringify(error.data),
|
|
165
|
+
transientProviderStringify(error),
|
|
166
|
+
].filter(Boolean).join(" ");
|
|
167
|
+
}
|
|
168
|
+
function transientProviderPatternMatches(pattern, text) {
|
|
169
|
+
return new RegExp(pattern.source, pattern.flags).test(text);
|
|
170
|
+
}
|
|
171
|
+
function transientProviderRuleMatches(rule, text) {
|
|
172
|
+
if (rule.all.length === 0 && rule.any.length === 0) return false;
|
|
173
|
+
if (!rule.all.every((pattern) => transientProviderPatternMatches(pattern, text))) return false;
|
|
174
|
+
return rule.any.length === 0 || rule.any.some((pattern) => transientProviderPatternMatches(pattern, text));
|
|
175
|
+
}
|
|
176
|
+
function isKnownTransientProviderError(error) {
|
|
177
|
+
const text = transientProviderText(error);
|
|
178
|
+
if (!text || isOverflowMessage(text)) return false;
|
|
179
|
+
if (TRANSIENT_PROVIDER_PERMANENT_PATTERNS.some((pattern) => transientProviderPatternMatches(pattern, text))) return false;
|
|
180
|
+
return TRANSIENT_PROVIDER_RULES.some((rule) => transientProviderRuleMatches(rule, text));
|
|
181
|
+
}`;
|
|
182
|
+
}
|
|
@@ -21,3 +21,43 @@ export const PI_RECOMMENDED_RETRY = {
|
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
export const PI_RECOMMENDED_COMPACTION = { enabled: true };
|
|
24
|
+
function validBoolean(value) {
|
|
25
|
+
return typeof value === "boolean";
|
|
26
|
+
}
|
|
27
|
+
function validNonNegativeNumber(value) {
|
|
28
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
29
|
+
}
|
|
30
|
+
function preferredOptionalNonNegativeNumber(...values) {
|
|
31
|
+
return values.find(validNonNegativeNumber);
|
|
32
|
+
}
|
|
33
|
+
function preferredValue(projectValue, globalValue, fallback, isValid) {
|
|
34
|
+
if (isValid(projectValue))
|
|
35
|
+
return projectValue;
|
|
36
|
+
if (isValid(globalValue))
|
|
37
|
+
return globalValue;
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Apply Console-owned retry defaults without writing settings files.
|
|
42
|
+
* Explicit project fields win over global fields, which win over defaults.
|
|
43
|
+
*/
|
|
44
|
+
export function applyProcessRetryOverride(manager) {
|
|
45
|
+
if (!manager?.applyOverrides)
|
|
46
|
+
return { applied: false };
|
|
47
|
+
const project = manager.getProjectSettings?.()?.retry;
|
|
48
|
+
const global = manager.getGlobalSettings?.()?.retry;
|
|
49
|
+
const currentProvider = manager.getProviderRetrySettings?.();
|
|
50
|
+
const timeoutMs = preferredOptionalNonNegativeNumber(project?.provider?.timeoutMs, global?.provider?.timeoutMs, currentProvider?.timeoutMs);
|
|
51
|
+
const retry = {
|
|
52
|
+
enabled: preferredValue(project?.enabled, global?.enabled, PI_RECOMMENDED_RETRY.enabled, validBoolean),
|
|
53
|
+
maxRetries: preferredValue(project?.maxRetries, global?.maxRetries, PI_RECOMMENDED_RETRY.maxRetries, validNonNegativeNumber),
|
|
54
|
+
baseDelayMs: preferredValue(project?.baseDelayMs, global?.baseDelayMs, PI_RECOMMENDED_RETRY.baseDelayMs, validNonNegativeNumber),
|
|
55
|
+
provider: {
|
|
56
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
57
|
+
maxRetries: preferredValue(project?.provider?.maxRetries, global?.provider?.maxRetries, PI_RECOMMENDED_RETRY.provider.maxRetries, validNonNegativeNumber),
|
|
58
|
+
maxRetryDelayMs: preferredValue(project?.provider?.maxRetryDelayMs, global?.provider?.maxRetryDelayMs, PI_RECOMMENDED_RETRY.provider.maxRetryDelayMs, validNonNegativeNumber),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
manager.applyOverrides({ retry });
|
|
62
|
+
return { applied: true, retry };
|
|
63
|
+
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure URL policy for the Operator Chat browser panel: normalize user input
|
|
3
|
-
* into an http(s) URL
|
|
4
|
-
* embed in the panel iframe. Dependency-free so it is unit-testable.
|
|
3
|
+
* into an http(s) URL. Dependency-free so it is unit-testable.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* services by user action. The Console's OWN origin is explicitly allowed —
|
|
11
|
-
* the sandbox still renders it in an opaque origin with no same-origin
|
|
12
|
-
* privileges, exactly like any other site.
|
|
5
|
+
* Only the scheme is gated: javascript:, data:, file: and friends must never
|
|
6
|
+
* reach the iframe. Local addresses (localhost / 127.0.0.1 / ::1 / LAN
|
|
7
|
+
* hosts) are deliberately allowed — the panel is a development console and
|
|
8
|
+
* its users routinely browse local dev servers.
|
|
13
9
|
*/
|
|
14
10
|
/**
|
|
15
11
|
* Extract the `frame-ancestors` source list of a Content-Security-Policy
|
|
@@ -47,16 +43,6 @@ export function embeddabilityOf(probe) {
|
|
|
47
43
|
}
|
|
48
44
|
return "embeddable";
|
|
49
45
|
}
|
|
50
|
-
/** A loopback hostname (localhost, IPv6 ::1, 127.0.0.0/8, 0.0.0.0). */
|
|
51
|
-
export function isLoopbackHostname(hostname) {
|
|
52
|
-
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
53
|
-
if (host === "localhost" || host === "::1" || host === "0.0.0.0")
|
|
54
|
-
return true;
|
|
55
|
-
const parts = host.split(".");
|
|
56
|
-
return (parts.length === 4 &&
|
|
57
|
-
parts[0] === "127" &&
|
|
58
|
-
parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255));
|
|
59
|
-
}
|
|
60
46
|
/**
|
|
61
47
|
* Schemes that must never reach the iframe even without `//` (javascript:,
|
|
62
48
|
* data:, file:, ...). Host:port lookalikes (example.com:8080) are NOT here —
|
|
@@ -88,12 +74,8 @@ const FORBIDDEN_SCHEMES = new Set([
|
|
|
88
74
|
/**
|
|
89
75
|
* Normalize one address-bar input against the navigation policy.
|
|
90
76
|
* @param input raw user text.
|
|
91
|
-
* @param selfOrigin the Console's own origin (window.location.origin). The
|
|
92
|
-
* Console itself may be browsed in the panel (the sandbox keeps it opaque),
|
|
93
|
-
* so it is let through BEFORE the loopback check — its host is normally
|
|
94
|
-
* loopback.
|
|
95
77
|
*/
|
|
96
|
-
export function normalizeBrowserUrl(input
|
|
78
|
+
export function normalizeBrowserUrl(input) {
|
|
97
79
|
const trimmed = input.trim();
|
|
98
80
|
if (trimmed === "")
|
|
99
81
|
return { kind: "invalid" };
|
|
@@ -127,17 +109,5 @@ export function normalizeBrowserUrl(input, selfOrigin) {
|
|
|
127
109
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
128
110
|
return { kind: "blocked", reason: "scheme" };
|
|
129
111
|
}
|
|
130
|
-
// The Console's own origin is allowed (browsed in an opaque origin like
|
|
131
|
-
// any other site); checked before the loopback gate because its host is
|
|
132
|
-
// normally loopback.
|
|
133
|
-
try {
|
|
134
|
-
if (url.origin === new URL(selfOrigin).origin)
|
|
135
|
-
return { kind: "ok", url: url.href };
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
// Unparsable selfOrigin (never in practice): fall through to the gate.
|
|
139
|
-
}
|
|
140
|
-
if (isLoopbackHostname(url.hostname))
|
|
141
|
-
return { kind: "blocked", reason: "loopback" };
|
|
142
112
|
return { kind: "ok", url: url.href };
|
|
143
113
|
}
|