@tea-agent/loop-agent 0.29.2 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +56 -24
  2. package/bin/agent-worker.js +0 -0
  3. package/dist/commands/client-recovery.js +2 -10
  4. package/dist/commands/init.js +1 -1
  5. package/dist/executors/dag-pi-executor.js +15 -4
  6. package/dist/executors/pi-executor.js +5 -3
  7. package/dist/executors/pi-playwright-cli-tool.js +9 -14
  8. package/dist/executors/pi-sdk-executor.js +16 -0
  9. package/dist/shared/operator/capabilities.js +9 -9
  10. package/dist/shared/pi-retry-settings.js +23 -0
  11. package/dist/worker/console/chat/pi-runtime.js +167 -41
  12. package/dist/worker/console/chat/routes.js +21 -2
  13. package/dist/worker/console/chat/runtime-context.js +50 -8
  14. package/dist/worker/console/chat/tool-preview.js +90 -0
  15. package/dist/worker/console/chat/turn-process.js +178 -0
  16. package/dist/worker/console/chat/usage.js +144 -16
  17. package/dist/worker/console/night-aux-ticker.js +141 -0
  18. package/dist/worker/console/operation-runner.js +21 -4
  19. package/dist/worker/console/operator-user-error.js +12 -0
  20. package/dist/worker/console/recovery-cta.js +6 -6
  21. package/dist/worker/console/routes.js +61 -0
  22. package/dist/worker/console/server.js +7 -0
  23. package/dist/worker/console/static/assets/index-D9gnJn_l.js +29 -0
  24. package/dist/worker/console/static/assets/index-rajoXwkM.css +1 -0
  25. package/dist/worker/console/static/index.html +2 -2
  26. package/dist/worker/console/static-src/night-prepare-result.js +118 -0
  27. package/dist/worker/scheduler/clock-install/win32-schtasks.js +70 -15
  28. package/dist/workflows/dag/backend-test-writer-completeness.js +398 -26
  29. package/dist/workflows/dag/dynamic-runtime/map.js +10 -4
  30. package/dist/workflows/dag/frontend-test-case-checklist.js +3 -3
  31. package/dist/workflows/dag/init-hybrid.js +247 -239
  32. package/dist/workflows/dag/node-execution.js +13 -2
  33. package/dist/workflows/dag/types.js +21 -7
  34. package/docs/templates/backend-test-dag.json +664 -460
  35. package/docs/templates/frontend-test-case-checklist.md +1 -1
  36. package/docs/templates/frontend-test-dag.generate-cases.prompt.md +1 -1
  37. package/docs/templates/frontend-test-dag.json +1 -1
  38. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  39. package/docs/templates/frontend-test-dag.review-cases.prompt.md +1 -1
  40. package/docs/templates/init-managed-agents.md +6 -3
  41. package/harness.json +2 -2
  42. package/package.json +1 -1
  43. package/skills/playwright-cli/SKILL.md +1 -1
  44. package/skills/playwright-cli-case-generator/SKILL.md +1 -1
  45. package/dist/worker/console/static/assets/index-Cwx-ZVEQ.js +0 -29
  46. package/dist/worker/console/static/assets/index-Yyn3ynVv.css +0 -1
package/CHANGELOG.md CHANGED
@@ -2,52 +2,84 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [0.29.2] - 2026-08-07
5
+ ## [0.30.0] - 2026-08-08
6
6
 
7
7
  ### 重点更新
8
8
 
9
- - 优化后端测试生成 writer 的续写重试机制,直接内嵌具体目标路径以恢复输出,不再依赖读取受限路径
10
- - 修复三处完整性门禁(Completeness Gate)误报,避免将正确的测试生成误判为残缺写集
9
+ - 后端测试生成引入 map-shard 拓扑,将大型单节点拆分为 plan + manifest-shell + map_agent,每个模块用例获得独立的 16K 输出预算,消除大文件多轮写入被截断或完整性误报的风险
10
+ - 修复 map_agent 单个子节点超出 token 限制导致后续所有子节点被错误级联跳过的问题,确保各模块测试用例独立生成互不干扰
11
+ - Completeness Gate 升级为返回结构化缺陷原因,帮助模型精准定位并修复格式问题,有效结束续写死循环
12
+ - 后端测试用例生成全面转向需求驱动覆盖,以产品需求为基准,API 契约作为场景补充,提升测试覆盖的精准度
13
+
14
+ ### 新增
15
+
16
+ - 新增 plan 节点专属完整性评估器,仅校验 README 覆盖范围和模块索引,不再错误要求下游 map 子节点产出的文件
17
+ - 新增 `scripts/regen-backend-test-dag.mjs` 脚本,用于从 runtime builder 重建打包镜像,保持 runtime 与 packaged template 字节对齐
18
+ - Completeness Gate 的 `moduleStructurallyComplete` 新增结构化原因输出,明确指出空文件、标题层级错误、ID 格式非法或缺失必选章节等具体缺陷
19
+ - 新增 `looksLikeValidPythonModule` 校验逻辑,用于准确识别共享辅助代码和工厂函数,兼容包含合法不平衡分隔符的 fixture 字符串
11
20
 
12
21
  ### 改进
13
22
 
14
- - 后端测试生成 writer(N2 MD / N5 pytest)在输出截断或残缺写集后,续写重试直接内嵌具体目标路径清单,不再依赖读取 `.harness/**` 路径
15
- - 即使遇到结果为空或输出失败的异常,也会评估完整性门禁,将可恢复的部分写入升格为不完整写集,避免重试空转
23
+ - N2/N3 提示词改为需求驱动覆盖:Coverage Scope 固定为 new-operation/full-contract,以产品需求为基准全覆盖,API 契约补充场景维度,并禁止使用导致用例降级的 `## 测试类` 分组
24
+ - dagDynamicExpansionChildTaskSchema 扩展 `writerOutcomePolicy` 和 `retryPolicy`,使 map_agent 子节点能正确声明实现产出并受 Completeness Gate 保护
25
+ - Completeness Gate 白名单泛化,通过识别子节点 ID 前缀准确匹配 map 分片子节点
26
+ - DAG 执行器模型矩阵统一路由至当前环境实际可用的 wizard-local 模型,避免因 API Key 缺失导致节点大面积失败
16
27
 
17
28
  ### 修复
18
29
 
19
- - 修复 README 正文提及的编号式文件名被误判为缺失模块的问题
20
- - 修复覆盖率矩阵(Coverage Matrix)表头识别被覆盖率范围(Coverage Scope)数据行遮蔽的问题
21
- - 修复合法多行 `def` pytest 脚本被误判为截断的问题(真正截断现仍由括号配平计数准确捕获)
30
+ - 修复 map_agent 中单个子节点超出 `maxTokensPerCase` 被误当作全局预算耗尽,导致后续兄弟子节点全部被跳过(SKIPPED)的问题
31
+ - 修复 plan 节点 Completeness Gate 错误要求下游 map 子节点产出文件齐全,导致计划节点始终判定为不完整并陷入重试死循环的问题
32
+ - 修复 manifest shell 命令因正则反引号和转义符在多层 shell 解析中被破坏,导致确定性 manifest 节点执行报错的问题,现采用 base64 编码彻底规避 bash 转义干扰
33
+ - 修复 pytest-plan 资源检查因严格括号配平启发式算法,误判包含合法不平衡分隔符的 fixture 字符串为不完整的问题
22
34
 
23
- ## [0.29.1] - 2026-08-07
35
+ ## [0.29.3] - 2026-08-07
24
36
 
25
37
  ### 重点更新
26
38
 
27
- - 前端测试默认执行流程大幅精简,收敛至约 11 个静态节点,合并了多项检查与报告步骤
28
- - 前端测试浏览器默认改为无头(headless)模式,减少环境干扰
29
- - 优化证据缺失处理逻辑,非路径逃逸的异常不再强制阻断报告生成
30
- - 修复文档说明被误判为可执行命令的问题,提升执行稳定性
39
+ - 夜间任务控制台升级为引导式工作台,支持点选预约、审阅写入集与任务看板,并保留人工审批门禁
40
+ - 失败 DAG 默认改为从失败节点安全续跑,避免因服务波动无理由新建任务
41
+ - Operator Chat 支持内联展示工具执行过程,并改进 Pi 用量统计的准确解析与状态栏展示
42
+ - 增强 Pi 网关限流容错,遇到 429 状态码时自动按 60 秒间隔退避重试
31
43
 
32
44
  ### 新增
33
45
 
34
- - 新增前端测试标准场景物化节点,支持执行后针对阻塞或缺失证据进行最多两轮有界重跑
35
- - 新增前端测试标准场景规范模板(frontend-test-standard-scenarios.v1.json)
36
- - 前端测试独占节点支持 tools-only 写保护策略,在保留工具级写沙箱的同时跳过 Git 基线的硬失败限制
46
+ - 控制台新增前台辅助夜间任务调度开关,在控制台开启时可按间隔触发调度(仅前台有效,过夜仍需系统定时器)
37
47
 
38
48
  ### 改进
39
49
 
40
- - 前端测试默认 DAG 结构精简至约 11 个静态节点:合并双重重跑、检查清单与清单文件、证据与结果、L5 与 HTML 报告
41
- - 前端测试主 HTML 报告不再依赖回顾(retrospect)节点,回顾功能改为可选开启
42
- - 权威清单文件(manifest.json)仅记录规范化后的测试用例,并在未配置重试时省略相关静态节点
43
- - 强化运营 HTML 报告的状态过滤功能,并优化失败与阻塞原因的展示
50
+ - 失败 DAG 恢复策略改为先诊断再从失败节点续跑,统一对齐系统提示、恢复入口与工具说明
51
+ - Operator Chat 将工具执行过程内联至消息时间线,支持折叠查看,切会话后可恢复工具记录
52
+ - Operator Chat 用量栏改进 Pi 用量解析,准确展示输入、输出、缓存读取与费用信息
53
+ - 注入上下文面板升级为更大可读面板,展示脱敏后的完整系统提示、技能正文与激活工具列表
44
54
 
45
55
  ### 修复
46
56
 
47
- - 修复前端测试检查清单将不可用或阻塞状态的说明文字误判为非法可执行命令的问题
48
- - 修复前端测试收尾阶段因证据缺失或格式错误导致强制失败的问题,现仅对路径逃逸强制报错,其余降级为建议性提示
49
- - 修复前端测试合并后清单文件物化异常的问题,确保仅写入规范化用例并恢复负向测试用例
50
- - 回退误改的全局默认 Pi 模型(DEFAULT_PI_MODEL),将其恢复为 glm-5.2,模型路由统一交由 harness 复杂度矩阵处理
57
+ - 修复 Windows 计划任务因 /TR 参数超过 261 字符限制导致创建失败的问题,改用短 .cmd 路径绕过
58
+ - 修复 package-lock 中硬编码的镜像源地址导致发布链路 npm ci 失败的问题,统一指向官方源
59
+ - 修复 playwright-cli open 传入不支持的 --headless/--headed 参数报错的问题
60
+ - 修复 Pi 网关返回 429 限流时重试过快导致任务失败的问题,现自动识别并按 60 秒间隔退避
61
+ - 修复标准 DAG 节点将任务相对路径误当作仓库根路径读取,导致找文件超时的问题
62
+ - 修复 Operator Chat 重开会话时因 JSONL 丢失而静默新建会话 ID 导致会话不匹配的问题
63
+ - 修复夜间任务异步执行结果丢失及 agent-worker 可执行文件权限的问题
64
+
65
+ ## [0.29.2] - 2026-08-07
66
+
67
+ ### 重点更新
68
+
69
+ - 优化后端测试生成 writer 的续写重试机制,直接内嵌具体目标路径以恢复输出,不再依赖读取受限路径
70
+ - 修复三处完整性门禁(Completeness Gate)误报,避免将正确的测试生成误判为残缺写集
71
+ - 修复标准 Happy Path / `contract-pi` 等节点 source prompt:只暴露仓库相对可读路径(`.harness/tasks/<taskId>/source/...`),并明确禁止把 task-relative 的 `source/需求.md` 当工具读路径
72
+
73
+ ### 改进
74
+
75
+ - 后端测试生成 writer(N2 MD / N5 pytest)在输出截断或残缺写集后,续写重试直接内嵌具体目标路径清单,不再依赖读取 `.harness/**` 路径
76
+ - 即使遇到结果为空或输出失败的异常,也会评估完整性门禁,将可恢复的部分写入升格为不完整写集,避免重试空转
77
+
78
+ ### 修复
79
+
80
+ - 修复 README 正文提及的编号式文件名被误判为缺失模块的问题
81
+ - 修复覆盖率矩阵(Coverage Matrix)表头识别被覆盖率范围(Coverage Scope)数据行遮蔽的问题
82
+ - 修复合法多行 `def` 的 pytest 脚本被误判为截断的问题(真正截断现仍由括号配平计数准确捕获)
51
83
 
52
84
  ## [0.29.0] - 2026-08-07
53
85
 
File without changes
@@ -18,16 +18,8 @@ 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
- export const PI_RECOMMENDED_RETRY = {
22
- enabled: true,
23
- maxRetries: 5,
24
- baseDelayMs: 3000,
25
- provider: {
26
- maxRetries: 0,
27
- maxRetryDelayMs: 60000,
28
- },
29
- };
30
- export const PI_RECOMMENDED_COMPACTION = { enabled: true };
21
+ import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
22
+ export { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY };
31
23
  /**
32
24
  * Shared overflow phrase sources used by isContextOverflow and generated plugins.
33
25
  * Keep English + Chinese intranet gateway messages in one place to avoid drift.
@@ -2217,7 +2217,7 @@ function buildTargetFeatureWorkflow(input) {
2217
2217
  '- `taskKind: "backend-test"` selects the dedicated backend test DAG. Its Pi nodes analyze requirements, generate and review backend cases, generate pytest, and retrospect on results; shell gate/execution nodes enforce the review verdict and run the target project\'s pytest. The backend test templates (`backend-test-dag.json` and the `backend-test-dag.*.prompt.md` files) ship inside the loop-agent package as static references and are projected to target projects under the governance `templates/` directory.',
2218
2218
  '- `taskKind: "knowledge-sync"` selects the Feature-scoped test-knowledge write-back DAG (collect → draft → validate → apply → pointer). Bind `featureId` in `task.json` (or hardConstraints / requirement text). It writes only under `features/<featureId>/…` after final verification evidence exists.',
2219
2219
  '- `taskKind: "knowledge-graph-bootstrap"` selects the business knowledge-graph bootstrap DAG (preflight → inventory → propose → validate → review → gate → promote → materialize). AI writes only `knowledge/bootstrap/staging/**`; promote is merge-new-only.',
2220
- '- `taskKind: "frontend-test"` selects the FE-test RAG DAG. It writes a traceable frontend RAG package and Markdown case manifest, then executes manifest cases serially with `playwright-cli` in isolated test environments and retains per-case evidence. It never generates pytest or Playwright source code. `frontendTest.maxCasesPerBatch` defaults to 20 (maximum 50); optional `maxTokensPerCase` and `maxTotalTokens` stop only later cases after a completed case\'s token usage is recorded, marking them `blocked: token-budget-exhausted`. Generated browser startup uses `playwright-cli open --browser=chrome --headless <base-url>`; the generic playwright-cli skill is unchanged.',
2220
+ '- `taskKind: "frontend-test"` selects the FE-test RAG DAG. It writes a traceable frontend RAG package and Markdown case manifest, then executes manifest cases serially with `playwright-cli` in isolated test environments and retains per-case evidence. It never generates pytest or Playwright source code. `frontendTest.maxCasesPerBatch` defaults to 20 (maximum 50); optional `maxTokensPerCase` and `maxTotalTokens` stop only later cases after a completed case\'s token usage is recorded, marking them `blocked: token-budget-exhausted`. Generated browser startup uses `playwright-cli open --browser=chrome <base-url>`; the generic playwright-cli skill is unchanged.',
2221
2221
  "- Only eligible read-only Pi nodes (planner, scout, reviewer, verifier, closeout with no write-capable tool profile) receive the conservative automatic retry policy. Supervisor, implementer, writer, docs-only, dynamic, shell, static, and decision-gate nodes are not retried automatically. Eligible nodes cannot write repository files; the controller only records immutable attempt evidence under `.harness/dag-runs/<state>/<run-id>/<node-id>/attempt-<n>.json`.",
2222
2222
  "",
2223
2223
  "Use the package-backed public knowledge CLI for graph operations. Do not require target projects to run package-only kb runtime scripts:",
@@ -9,7 +9,7 @@ import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/d
9
9
  import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
10
10
  import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
11
11
  import { isWriterEmptyDiffRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
12
- import { assessBackendTestMdWriterCompleteness, assessBackendTestPytestWriterCompleteness, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
12
+ import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
13
13
  import { redactSecrets, truncateUtf8Preview } from "../shared/preview.js";
14
14
  export const DAG_PI_READONLY_TOOLS = ["read", "grep", "find", "ls"];
15
15
  /** Bounded writer tools: no unrestricted bash; edit/write are policy-wrapped via SDK customTools. */
@@ -688,9 +688,20 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
688
688
  if (writeGuardOk &&
689
689
  isBackendTestCompletenessRetryCandidate(input.task)) {
690
690
  try {
691
- const progress = input.task.id === "generate-backend-pytest-pi"
692
- ? await assessBackendTestPytestWriterCompleteness(input.cwd)
693
- : await assessBackendTestMdWriterCompleteness(input.cwd);
691
+ const writerRole = backendTestWriterProgressRoleForTask(input.task.id);
692
+ const progress = isBackendTestShardChildTask(input.task)
693
+ ? await assessBackendTestShardChildCompleteness({
694
+ workspaceRoot: input.cwd,
695
+ role: writerRole === "pytest-generate" ? "pytest-generate" : "md-generate",
696
+ writeSet: input.task.writeSet ?? [],
697
+ })
698
+ : isBackendTestMdPlanTask(input.task)
699
+ ? await assessBackendTestMdPlanCompleteness(input.cwd)
700
+ : isBackendTestPytestPlanTask(input.task)
701
+ ? await assessBackendTestPytestPlanCompleteness(input.cwd)
702
+ : writerRole === "pytest-generate"
703
+ ? await assessBackendTestPytestWriterCompleteness(input.cwd)
704
+ : await assessBackendTestMdWriterCompleteness(input.cwd);
694
705
  await writeBackendTestWriterProgressArtifacts({
695
706
  runDir: meta.runDir,
696
707
  progress,
@@ -961,10 +961,12 @@ export function classifyPiFailure(input) {
961
961
  if (input.exitCode === 0 && input.assistantText.trim())
962
962
  return "success";
963
963
  const combined = `${input.stderr}\n${input.stdout}`.toLowerCase();
964
- if (/quota|usage limit|reached.*limit|5\s*小时|5小时/.test(combined))
965
- return "quota";
966
- if (/rate.?limit/.test(combined))
964
+ // Prefer explicit rate-limit / HTTP 429 before generic "limit" quota phrases.
965
+ // Chinese gateways often say "请求频率已达到限制" without English "rate limit".
966
+ if (/rate.?limit|too many requests|\b429\b|请求频率|频率.*限制|rpm|tpm/.test(combined))
967
967
  return "rate-limit";
968
+ if (/quota|usage limit|reached.*limit|5\s*小时|5小时|insufficient_quota|billing/.test(combined))
969
+ return "quota";
968
970
  if (/\bunauthorized\b|\bhttp\s*40[13]\b|invalid api key|authentication (?:failed|required|error)|auth(?:entication)? failed/.test(combined))
969
971
  return "auth";
970
972
  if (/unknown provider|unknown model|model.*unavailable|provider.*unavailable|\bunavailable\b|overloaded|capacity|temporarily unavailable/.test(combined))
@@ -197,21 +197,19 @@ function validateOpenArgs(args, baseUrl) {
197
197
  for (const arg of args) {
198
198
  assertNoControlMeta(arg);
199
199
  assertNoSessionFlag(arg);
200
- if (arg === "--browser=chrome" || arg === "--headless") {
200
+ if (arg === "--browser=chrome") {
201
201
  flags.add(arg);
202
202
  normalized.push(arg);
203
203
  continue;
204
204
  }
205
- // Accept legacy --headed but normalize to headless for frontend-test CI/dogfood.
206
- if (arg === "--headed") {
207
- flags.add("--headless");
208
- if (!normalized.includes("--headless"))
209
- normalized.push("--headless");
205
+ // playwright-cli defaults to headless and does not accept --headless/--headed.
206
+ // Strip legacy flags so model/docs remnants never reach the binary.
207
+ if (arg === "--headless" || arg === "--headed") {
210
208
  continue;
211
209
  }
212
210
  if (arg.startsWith("--browser=") ||
213
211
  arg === "--browser") {
214
- throw new PlaywrightCliPolicyError("open-browser-flags", "open must use --browser=chrome --headless only");
212
+ throw new PlaywrightCliPolicyError("open-browser-flags", "open must use --browser=chrome only (default headless; do not pass --headless/--headed)");
215
213
  }
216
214
  if (/^[a-z][a-z0-9+.-]*:/i.test(arg) || arg.startsWith("http")) {
217
215
  url = arg;
@@ -219,12 +217,9 @@ function validateOpenArgs(args, baseUrl) {
219
217
  }
220
218
  throw new PlaywrightCliPolicyError("open-args", `unsupported open argument: ${arg}`);
221
219
  }
222
- if (!flags.has("--browser=chrome") || !flags.has("--headless")) {
223
- // controller injects required flags when missing from model args
224
- if (!flags.has("--browser=chrome"))
225
- normalized.unshift("--browser=chrome");
226
- if (!flags.has("--headless"))
227
- normalized.push("--headless");
220
+ if (!flags.has("--browser=chrome")) {
221
+ // controller injects required browser flag when missing from model args
222
+ normalized.unshift("--browser=chrome");
228
223
  }
229
224
  if (!url) {
230
225
  throw new PlaywrightCliPolicyError("open-url-required", "open requires an absolute http(s) URL");
@@ -515,7 +510,7 @@ export function preparePlaywrightCliArgv(input, ctx) {
515
510
  catch {
516
511
  throw new PlaywrightCliPolicyError("goto-url-invalid", "goto requires a URL relative to the controller baseUrl");
517
512
  }
518
- const openLike = validateOpenArgs(["--browser=chrome", "--headless", target], ctx.baseUrl);
513
+ const openLike = validateOpenArgs(["--browser=chrome", target], ctx.baseUrl);
519
514
  return openLike[openLike.length - 1];
520
515
  });
521
516
  if (args.length !== 1) {
@@ -2,6 +2,7 @@ import { appendFile, mkdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractAssistantTextFromPiJson, } from "./pi-executor.js";
4
4
  import { serializeSessionEvent } from "./pi-event-serializer.js";
5
+ import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
5
6
  let sdkSessionFactoryOverride;
6
7
  let sdkImportOverrideForTests;
7
8
  let sdkModuleOverrideForTests;
@@ -154,6 +155,7 @@ async function createSdkSession(sdk, input, shared) {
154
155
  const createAgentSession = sdk.createAgentSession;
155
156
  const SessionManager = sdk.SessionManager;
156
157
  const DefaultResourceLoader = sdk.DefaultResourceLoader;
158
+ const SettingsManager = sdk.SettingsManager;
157
159
  const getAgentDir = sdk.getAgentDir;
158
160
  const ModelRuntime = sdk.ModelRuntime;
159
161
  const agentDir = shared?.agentDir ?? getAgentDir();
@@ -168,12 +170,25 @@ async function createSdkSession(sdk, input, shared) {
168
170
  const model = input.provider && input.model
169
171
  ? await resolveModel(modelRuntime, input.provider, input.model)
170
172
  : undefined;
173
+ // Inject rate-limit-tolerant auto-retry settings for the session lifecycle.
174
+ // Prefer SettingsManager.inMemory so harness runs do not depend on ~/.pi or
175
+ // project .pi/settings.json being present/trusted.
176
+ const settingsManager = typeof SettingsManager?.inMemory === "function"
177
+ ? SettingsManager.inMemory({
178
+ retry: {
179
+ ...PI_RECOMMENDED_RETRY,
180
+ provider: { ...PI_RECOMMENDED_RETRY.provider },
181
+ },
182
+ compaction: { ...PI_RECOMMENDED_COMPACTION },
183
+ }, { projectTrusted: true })
184
+ : undefined;
171
185
  const loader = new DefaultResourceLoader({
172
186
  cwd: input.cwd,
173
187
  agentDir,
174
188
  noContextFiles: true,
175
189
  noSkills: true,
176
190
  noExtensions: true,
191
+ ...(settingsManager ? { settingsManager } : {}),
177
192
  appendSystemPromptOverride: (base) => [
178
193
  ...base,
179
194
  input.appendSystemPrompt,
@@ -191,6 +206,7 @@ async function createSdkSession(sdk, input, shared) {
191
206
  resourceLoader: loader,
192
207
  tools: input.toolNames,
193
208
  modelRuntime,
209
+ ...(settingsManager ? { settingsManager } : {}),
194
210
  ...(model ? { model } : {}),
195
211
  ...(input.thinking ? { thinkingLevel: input.thinking } : {}),
196
212
  ...(Array.isArray(input.customTools) && input.customTools.length > 0
@@ -776,7 +776,7 @@ export function buildOperatorCapabilitiesDocument() {
776
776
  resultSchemaVersion: 1,
777
777
  envelopeSchemaVersion: 1,
778
778
  requiredErrorCodes: ["NOT_FOUND", "INVALID_INPUT"],
779
- description: "DAG run handoff report by run id.",
779
+ description: "DAG run handoff report by run id. On failure, read primaryFailure.nodeId + primaryRecovery first; prefer dagRerunPlan from that node before standaloneTaskRerun or a new task.",
780
780
  inputParams: [
781
781
  {
782
782
  name: "runId",
@@ -919,19 +919,19 @@ export function buildOperatorCapabilitiesDocument() {
919
919
  resultSchemaVersion: 1,
920
920
  envelopeSchemaVersion: 1,
921
921
  requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
922
- description: "Read-only subgraph rerun plan for confirmation (no mutation).",
922
+ description: "Read-only R1 subgraph plan (no mutation). Call before dagRerun. Prefer primaryFailure.nodeId from dagReport. If eligible=false (writer/decision/fingerprint), do not force execute — fall back to standaloneTaskRerun or same-task advance. Returns planHash required by execute.",
923
923
  inputParams: [
924
924
  {
925
925
  name: "runId",
926
926
  type: "string",
927
927
  required: true,
928
- description: "dag run id",
928
+ description: "completed failed|partial_failed parent dag run id",
929
929
  },
930
930
  {
931
931
  name: "fromNode",
932
932
  type: "string",
933
933
  required: true,
934
- description: "node id to rerun from",
934
+ description: "node id to rerun from (prefer failed/attention node from dagReport primaryFailure.nodeId)",
935
935
  },
936
936
  ],
937
937
  modelCallable: "always",
@@ -949,31 +949,31 @@ export function buildOperatorCapabilitiesDocument() {
949
949
  "BINDING_DRIFT",
950
950
  "INVALID_INPUT",
951
951
  ],
952
- description: "Execute continuation run from effective node with confirmed plan hash.",
952
+ description: "Execute R1 continuation from effective node. Requires planHash from a fresh dagRerunPlan (eligible=true). Prefer this over new task / standaloneTaskRerun for provider flake and safe downstream failures. Human Gate required.",
953
953
  inputParams: [
954
954
  {
955
955
  name: "runId",
956
956
  type: "string",
957
957
  required: true,
958
- description: "dag run id",
958
+ description: "parent dag run id (same as dagRerunPlan)",
959
959
  },
960
960
  {
961
961
  name: "fromNode",
962
962
  type: "string",
963
963
  required: true,
964
- description: "node id to rerun from",
964
+ description: "node id matching the planned dagRerunPlan",
965
965
  },
966
966
  {
967
967
  name: "planHash",
968
968
  type: "string",
969
969
  required: true,
970
- description: "rerun plan hash",
970
+ description: "sha256 plan hash from dagRerunPlan (must match exactly)",
971
971
  },
972
972
  {
973
973
  name: "reason",
974
974
  type: "string",
975
975
  required: true,
976
- description: "rerun reason",
976
+ description: "why continue from this node (e.g. provider flake; resume from review-pi)",
977
977
  },
978
978
  ],
979
979
  modelCallable: "prepare-only",
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Recommended Pi auto-retry settings for gateway rate limits.
3
+ *
4
+ * Work-hours gateways often enforce rolling windows (e.g. 100 req / 10 min).
5
+ * Pi's built-in default (2s / 4s / 8s) exhausts before the window recovers;
6
+ * a 60s base delay yields 1m → 2m → 4m … and tolerates transient 429s.
7
+ *
8
+ * Used by:
9
+ * - project / user `.pi` settings merge (`client-recovery`)
10
+ * - loop-agent Pi SDK executor (injected SettingsManager, independent of ~/.pi)
11
+ */
12
+ export const PI_RECOMMENDED_RETRY = {
13
+ enabled: true,
14
+ maxRetries: 5,
15
+ /** First retry wait; subsequent attempts use exponential backoff. */
16
+ baseDelayMs: 60_000,
17
+ provider: {
18
+ /** Disable nested SDK provider retries; outer auto-retry owns the budget. */
19
+ maxRetries: 0,
20
+ maxRetryDelayMs: 60_000,
21
+ },
22
+ };
23
+ export const PI_RECOMMENDED_COMPACTION = { enabled: true };