open-tui-orchestrator 0.9.16 → 0.9.18

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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.18 — core 1.16.5
4
+
5
+ ### Fixed
6
+
7
+ - **The per-unit memory sampler now actually runs (and the learning loop is live).** Two Windows-specific killers, both found by exact reproduction: (1) spawning the sampler PowerShell with `detached: true` made it die silently before executing a single line (no write, no error, no exit) — the spawn is now a plain child; (2) the tree walk used an unfiltered `Get-CimInstance Win32_Process` snapshot, which stalls under load, and crashed on a null pid in the tree — it now walks children with per-parent filtered queries plus null guards. The sampler also writes a `.start` marker and an `.error` file (and the runner tees its stderr), so a silent failure can never happen again unnoticed. Verified end-to-end: a real orchestration recorded six 68MB peaks for its agent into `unit-memory.json`, so estimates now come from measurements (p90 × safety) instead of the conservative default. The 0.9.17 mechanism itself is unchanged and was already verified; safety behaviour (falling back to the default when no samples exist) stays.
8
+
9
+ ## 0.9.17 — core 1.16.4
10
+
11
+ ### Changed
12
+
13
+ - **Concurrency is now memory-fit, not a formula (bottom-layer mechanism; zero per-agent special cases).** The old ceiling — `floor(freeMB / 1000) × 2` — assumed every unit costs the same, which is false across hosts (measured spread ≈3–4×), so it under-used memory for light hosts and over-admitted for heavy ones (and could merge blocks the machine could easily have run in parallel). New flow: once the TODO list is split (dependencies/conflicts honoured), the engine estimates each unit's footprint from a generic per-host learning table (p90 of measured peaks × `ORCH_UNIT_MEM_SAFETY=1.3`; below `ORCH_UNIT_MEM_MIN_SAMPLES=3` samples it falls back to the generic default `ORCH_UNIT_MEM_DEFAULT_MB=640`), then merges the most mergeable pair — dependency pair → conflict pair → largest pair — until the all-parallel estimate clears the floor `ORCH_MIN_FREE_MEM_MB=256`, or only one block is left. A block costs `1 + ORCH_UNIT_MEM_BLOCK_GROWTH × (tasks−1)` (default 0.15, clamped below 1 so every merge strictly lowers the estimate — the convergence guarantee). When even a single unit cannot clear the floor the run proceeds anyway with a loud `[mem-fit] WARNING` (work is never blocked by memory).
14
+ - **A single fitted block means no parallelism ⇒ no spawn.** The request is handed back inline (`reason: single-block`) per the tool's boundary: orchestrate parallel units, or don't spawn at all. (Forced invocations still spawn.) The old `capacity-below-two-windows` gate is retired; `ORCH_MAX_WINDOWS` remains a ceiling.
15
+ - **Per-unit peaks are measured generically and fed back.** The runner attaches one lightweight sampler (`unit-memory-sampler.ps1`) to every unit's process tree — it only needs the unit's root pid, so it is host-agnostic — and records the peak into the machine-level store `<tmp>/orchestrator/unit-memory.json` (override `ORCH_UNIT_MEM_STORE`). Sampling and recording fail open and never affect orchestration; records are deleted after being read, which de-duplicates them naturally. Run logs gain a `[mem-fit]` line, every unit logs `[mem] unit <key> peak=<n>MB`, and `--plan` dry runs report the fitted blocks plus a `memFit` object.
16
+ - **Auxiliary (interactive) window admission** now uses the same single-unit fit instead of the retired formula, so no dual track remains.
17
+
18
+ ### Tests
19
+
20
+ - `test/unit-memory.test.mjs` — 13 red-capable criteria: config defaults/clamps (incl. the empty-string trap), store fail-open + round-trip, estimate (default vs learned p90×safety), merge order (dependency → conflict → largest), floor respect, extreme-collapse honesty (`fits=false`), monotonicity under `g<1`, idempotence, and host-genericity (two hosts, one request, different fits).
21
+
3
22
  ## 0.9.16 — core 1.16.3
4
23
 
5
24
  ### Fixed
package/README-zh.md CHANGED
@@ -177,6 +177,6 @@ node cli.mjs --agent claude --doctor
177
177
  node cli.mjs --agent openclaw --doctor
178
178
  ~~~
179
179
 
180
- 当前通用核心为 1.16.3,离线回归为 322/322 通过;覆盖无窗 headless 派发默认(`--mode window` 保留可见弹窗;launcher/身份/租约/恢复契约同构,隐藏载具不抢焦点)、单元载体分级(TUI 类 agent 走 ConPTY 伪终端载体——真实控制台、无窗口、不抢焦点,身份/租约/恢复契约与窗口模式同构;print 类维持无控制台载体)、执行看板(块与任务状态实时 `[board]` 行、run `events.jsonl`、只读 `--watch <runId>` 流式视图含 `--jsonl`、`--status` 活动 run 摘要)、任务不丢失、多块并行容量调度、依赖波次、同项目任务按资源键精细化拆分、全局容量租约(排队块等待指数退避+抖动;`ORCH_WAIT_BACKOFF_MS`/`ORCH_WAIT_BACKOFF_MAX_MS` 可调;协调器互斥等待亦可调可重试:`ORCH_MUTEX_WAIT_MS`/`ORCH_MUTEX_RETRIES`,锁票按龄回收 `ORCH_MUTEX_TICKET_TTL_MS`)、Windows Terminal 启动(同步拉起,弹窗可靠且不抢发起窗焦点)、失焦启动回归、进程身份、断点恢复、恢复账本加固(launcherFiles 工作区包容、验收契约哈希绑定、注册表锁老化回收)、守护进程驱动的控制器丢失自动恢复(test/guardian-live-result.json)与手动 --resume-run 收编存活窗口恢复(test/recovery-live-result.json)、发起方自动识别(环境标记优先、进程链兜底:kimi/codex/claude/pi/opencode/openclaw/hermes)、`orch` 强制编排入口、print-class 数据驱动画像适配管线(表面探测→画像→解析自检→活验守门→重封→自动提交,失败一律回滚;画像 agent 按 `modelDetection` 跟随发起窗 model/effort;hermes 画像已落地——hermes 发起即自动挡:识别其身份并以自身 CLI 执行(`--no-restore-cwd` 防旧会话 CWD 写偏))、调用方自助通道(调用方自带计划文件在覆盖/依赖/回退保证不变的前提下跑通且全程零模型调用、调用方自带单元命令让任意 CLI 走同一启动器/租约/恢复/验收契约、只读 `--tasks` 账本助手、单元命令的 resume 身份一致性、未知 agent 的两条出路诊断)、档位去向留痕(summary 记录 model/effort 与三态来源:explicit/宿主会话/host-default;机器可读入口 stdout 保持纯数据)、机器可调用失败契约(公开入口失败一律只回一行 `[orchestrator] FATAL: <原因>`、无 Node 栈帧(`ORCH_DEBUG=1` 才打);未知 run 不甩 `ENOENT`、不挂起——`--verify-run`/`--resume-run` 立刻回 `Run not found: <id>`,`--watch` 先给有界宽限 `ORCH_WATCH_GRACE_MS`(默认 60s,覆盖 `--run-windows` 仍在规划、run 目录尚未建立的竞态)再拒,看板中途丢 `summary.json` 也立刻报错而不静默停摆)、维护锁、当前核心安全审计及修复(docs/security-audit-2026-09-09.md)、弱网配置、SDK 诊断、宿主 CLI 探测超时重试与逐候选拒绝诊断、OpenClaw 版本门诊断(tui 接口探针先行:未验证版本以探针实测结果 + 最后验证版本 fail-closed——版本常量只记「最后验证于」,不是准入门槛)、恢复账本校验覆盖盘符根工作区和干净发行副本。测试环境要求:离线套件假定发起 agent 的 CLI(默认 `codex`)可在 PATH 解析(Windows 下前置 codex vendor bin,即回归环境约定);缺失时依赖 agent 解析的测试文件按设计快速失败(fail-closed),其中两个运行期依赖 CLI 的用例已标注 `{skip}`。真实模型额度、不同 Windows 用户安装、长时间断网恢复和每个未来 Agent 的现场兼容性也仍需单独验收。
180
+ 当前通用核心为 1.16.5,离线回归为 336/336 通过;覆盖无窗 headless 派发默认(`--mode window` 保留可见弹窗;launcher/身份/租约/恢复契约同构,隐藏载具不抢焦点)、单元载体分级(TUI 类 agent 走 ConPTY 伪终端载体——真实控制台、无窗口、不抢焦点,身份/租约/恢复契约与窗口模式同构;print 类维持无控制台载体)、执行看板(块与任务状态实时 `[board]` 行、run `events.jsonl`、只读 `--watch <runId>` 流式视图含 `--jsonl`、`--status` 活动 run 摘要)、任务不丢失、内存拟合动态并发(按宿主的单元内存画像学习;“合并到放得下”并保 256MB 底线)、依赖波次、同项目任务按资源键精细化拆分、全局容量租约(排队块等待指数退避+抖动;`ORCH_WAIT_BACKOFF_MS`/`ORCH_WAIT_BACKOFF_MAX_MS` 可调;协调器互斥等待亦可调可重试:`ORCH_MUTEX_WAIT_MS`/`ORCH_MUTEX_RETRIES`,锁票按龄回收 `ORCH_MUTEX_TICKET_TTL_MS`)、Windows Terminal 启动(同步拉起,弹窗可靠且不抢发起窗焦点)、失焦启动回归、进程身份、断点恢复、恢复账本加固(launcherFiles 工作区包容、验收契约哈希绑定、注册表锁老化回收)、守护进程驱动的控制器丢失自动恢复(test/guardian-live-result.json)与手动 --resume-run 收编存活窗口恢复(test/recovery-live-result.json)、发起方自动识别(环境标记优先、进程链兜底:kimi/codex/claude/pi/opencode/openclaw/hermes)、`orch` 强制编排入口、print-class 数据驱动画像适配管线(表面探测→画像→解析自检→活验守门→重封→自动提交,失败一律回滚;画像 agent 按 `modelDetection` 跟随发起窗 model/effort;hermes 画像已落地——hermes 发起即自动挡:识别其身份并以自身 CLI 执行(`--no-restore-cwd` 防旧会话 CWD 写偏))、调用方自助通道(调用方自带计划文件在覆盖/依赖/回退保证不变的前提下跑通且全程零模型调用、调用方自带单元命令让任意 CLI 走同一启动器/租约/恢复/验收契约、只读 `--tasks` 账本助手、单元命令的 resume 身份一致性、未知 agent 的两条出路诊断)、档位去向留痕(summary 记录 model/effort 与三态来源:explicit/宿主会话/host-default;机器可读入口 stdout 保持纯数据)、机器可调用失败契约(公开入口失败一律只回一行 `[orchestrator] FATAL: <原因>`、无 Node 栈帧(`ORCH_DEBUG=1` 才打);未知 run 不甩 `ENOENT`、不挂起——`--verify-run`/`--resume-run` 立刻回 `Run not found: <id>`,`--watch` 先给有界宽限 `ORCH_WATCH_GRACE_MS`(默认 60s,覆盖 `--run-windows` 仍在规划、run 目录尚未建立的竞态)再拒,看板中途丢 `summary.json` 也立刻报错而不静默停摆)、维护锁、当前核心安全审计及修复(docs/security-audit-2026-09-09.md)、弱网配置、SDK 诊断、宿主 CLI 探测超时重试与逐候选拒绝诊断、OpenClaw 版本门诊断(tui 接口探针先行:未验证版本以探针实测结果 + 最后验证版本 fail-closed——版本常量只记「最后验证于」,不是准入门槛)、恢复账本校验覆盖盘符根工作区和干净发行副本。测试环境要求:离线套件假定发起 agent 的 CLI(默认 `codex`)可在 PATH 解析(Windows 下前置 codex vendor bin,即回归环境约定);缺失时依赖 agent 解析的测试文件按设计快速失败(fail-closed),其中两个运行期依赖 CLI 的用例已标注 `{skip}`。真实模型额度、不同 Windows 用户安装、长时间断网恢复和每个未来 Agent 的现场兼容性也仍需单独验收。
181
181
 
182
182
  未知宿主、无可执行 CLI 或显式路径无效时,命令会直接失败并给出原因;不会自动安装、猜测替代路径或换用其它 Agent。
package/README.md CHANGED
@@ -177,6 +177,6 @@ node cli.mjs --agent claude --doctor
177
177
  node cli.mjs --agent openclaw --doctor
178
178
  ```
179
179
 
180
- The current generic core is 1.16.3, with 322/322 offline tests passing. Coverage includes headless spawn mode as the default (--mode window keeps the visible Windows Terminal popups; the launcher/identity/lease/recovery contract is shared, and the hidden carrier cannot steal focus; TUI-class agents run on a ConPTY pseudo-terminal carrier — a real console that stays fully invisible, while print-class CLIs keep the no-console carrier), a live run board (per-transition [board] stdout lines, run events.jsonl, read-only --watch <runId> streaming with --jsonl, and --status active-run rows), lossless task accounting, the memory-derived block-capacity ceiling, dependency waves, fine-grained same-project splitting by resource keys, global capacity leases with exponential + jittered wait backoff for queued blocks (ORCH_WAIT_BACKOFF_MS / ORCH_WAIT_BACKOFF_MAX_MS) and a tunable, retried coordinator-mutex wait (ORCH_MUTEX_WAIT_MS / ORCH_MUTEX_RETRIES) with age-based ticket reclamation (ORCH_MUTEX_TICKET_TTL_MS), Windows Terminal launch (synchronous and reliable, without stealing the initiating window's focus), unfocused-start regressions, process identity, checkpoint recovery, recovery-ledger hardening (launcher-files workspace containment, acceptance-contract hash binding, registry-lock stale reclaim), guardian-driven automatic resume after controller loss (test/guardian-live-result.json) plus manual --resume-run adoption of surviving windows (test/recovery-live-result.json), initiating-agent detection (env markers first, then the process chain: kimi/codex/claude/pi/opencode/openclaw/hermes), the `orch` force-execute entry, print-class data-driven profile adaptation (surface probe → profile → resolution self-check → live verification → reseal → auto-commit, rolled back on failure; profile `modelDetection` lets profiled agents follow the initiating window's model/effort; an adapter-less agent with a landed profile (hermes) is a first-class auto host — the initiating agent is detected and drives its own CLI as the executor with `--no-restore-cwd` guarding against stale-session CWD writes), the caller-driven channel (a caller-supplied plan file kept under the unchanged coverage/dependency/fallback guarantees with zero model calls in the run, caller-supplied unit commands driving any CLI in the same launcher/lease/recovery/acceptance contract, the read-only --tasks ledger helper, resume identity rules for unit commands, and the two-path diagnostic for unknown agents), model/effort traceability (summary records model/effort plus the source: explicit / host session / host-default; machine-readable stdout stays pure data), the machine-caller failure contract (every public entry fails closed with one `[orchestrator] FATAL: <reason>` line and no Node stack unless `ORCH_DEBUG=1`; an unknown run id never dumps `ENOENT` and never hangs — `--verify-run`/`--resume-run` refuse immediately with `Run not found: <id>`, and `--watch` honours a bounded `ORCH_WATCH_GRACE_MS` grace (default 60 s, covering the window in which `--run-windows` is still planning) before refusing, reporting a summary that disappears mid-watch instead of watching forever), the maintenance lock, a current-core security audit with fixes landed (docs/security-audit-2026-09-09.md), weak-network configuration, SDK diagnostics, host CLI probe timeout retry with per-candidate rejection diagnostics, OpenClaw version-gate diagnostics (the runtime tui interface probe runs first; unverified versions fail closed carrying the probe result and the last-verified version — the constant records "last verified", it is not an admission gate), recovery-ledger containment that covers drive-root workspaces, and clean package publication. Test-env requirement: the offline suite assumes the initiating agent CLI (default `codex`) is resolvable on PATH — on Windows prepend the codex vendor bin (the documented regression convention). Without it, agent-resolution-dependent test files fail fast by design (fail-closed); the two runtime-CLI-dependent cases are annotated `{skip}`. Real model quotas, installation under a different Windows user, prolonged offline recovery and field compatibility for future Agents also require separate acceptance.
180
+ The current generic core is 1.16.5, with 336/336 offline tests passing. Coverage includes headless spawn mode as the default (--mode window keeps the visible Windows Terminal popups; the launcher/identity/lease/recovery contract is shared, and the hidden carrier cannot steal focus; TUI-class agents run on a ConPTY pseudo-terminal carrier — a real console that stays fully invisible, while print-class CLIs keep the no-console carrier), a live run board (per-transition [board] stdout lines, run events.jsonl, read-only --watch <runId> streaming with --jsonl, and --status active-run rows), lossless task accounting, memory-fit dynamic concurrency (per-host learned unit-memory profiles; merge-to-fit under a free-memory floor), dependency waves, fine-grained same-project splitting by resource keys, global capacity leases with exponential + jittered wait backoff for queued blocks (ORCH_WAIT_BACKOFF_MS / ORCH_WAIT_BACKOFF_MAX_MS) and a tunable, retried coordinator-mutex wait (ORCH_MUTEX_WAIT_MS / ORCH_MUTEX_RETRIES) with age-based ticket reclamation (ORCH_MUTEX_TICKET_TTL_MS), Windows Terminal launch (synchronous and reliable, without stealing the initiating window's focus), unfocused-start regressions, process identity, checkpoint recovery, recovery-ledger hardening (launcher-files workspace containment, acceptance-contract hash binding, registry-lock stale reclaim), guardian-driven automatic resume after controller loss (test/guardian-live-result.json) plus manual --resume-run adoption of surviving windows (test/recovery-live-result.json), initiating-agent detection (env markers first, then the process chain: kimi/codex/claude/pi/opencode/openclaw/hermes), the `orch` force-execute entry, print-class data-driven profile adaptation (surface probe → profile → resolution self-check → live verification → reseal → auto-commit, rolled back on failure; profile `modelDetection` lets profiled agents follow the initiating window's model/effort; an adapter-less agent with a landed profile (hermes) is a first-class auto host — the initiating agent is detected and drives its own CLI as the executor with `--no-restore-cwd` guarding against stale-session CWD writes), the caller-driven channel (a caller-supplied plan file kept under the unchanged coverage/dependency/fallback guarantees with zero model calls in the run, caller-supplied unit commands driving any CLI in the same launcher/lease/recovery/acceptance contract, the read-only --tasks ledger helper, resume identity rules for unit commands, and the two-path diagnostic for unknown agents), model/effort traceability (summary records model/effort plus the source: explicit / host session / host-default; machine-readable stdout stays pure data), the machine-caller failure contract (every public entry fails closed with one `[orchestrator] FATAL: <reason>` line and no Node stack unless `ORCH_DEBUG=1`; an unknown run id never dumps `ENOENT` and never hangs — `--verify-run`/`--resume-run` refuse immediately with `Run not found: <id>`, and `--watch` honours a bounded `ORCH_WATCH_GRACE_MS` grace (default 60 s, covering the window in which `--run-windows` is still planning) before refusing, reporting a summary that disappears mid-watch instead of watching forever), the maintenance lock, a current-core security audit with fixes landed (docs/security-audit-2026-09-09.md), weak-network configuration, SDK diagnostics, host CLI probe timeout retry with per-candidate rejection diagnostics, OpenClaw version-gate diagnostics (the runtime tui interface probe runs first; unverified versions fail closed carrying the probe result and the last-verified version — the constant records "last verified", it is not an admission gate), recovery-ledger containment that covers drive-root workspaces, and clean package publication. Test-env requirement: the offline suite assumes the initiating agent CLI (default `codex`) is resolvable on PATH — on Windows prepend the codex vendor bin (the documented regression convention). Without it, agent-resolution-dependent test files fail fast by design (fail-closed); the two runtime-CLI-dependent cases are annotated `{skip}`. Real model quotas, installation under a different Windows user, prolonged offline recovery and field compatibility for future Agents also require separate acceptance.
181
181
 
182
182
  An unknown host, missing CLI or invalid explicit path fails with a diagnostic. The tool does not install software, guess a replacement path or switch to another Agent.
@@ -12,7 +12,7 @@
12
12
 
13
13
  2026-09-17 现场修复轮(core 1.15.2 / 0.9.12):**单元停滞看门狗 + 残窗收口**。① 停滞判据=进度(检查点/结果/启动器 out·pty 日志/宿主会话 mtime)停止超过 `ORCH_UNIT_STALL_MS`(默认 600000ms)→ 判 `stalled` → 走正常收口(杀净后代、launcher 自查 exit 0;绝不强杀 launcher 本体)→ 计入该块 attempts 预算自动重跑;`ORCH_UNIT_TIMEOUT_MS`(默认 2700000ms)限制单元寿命;预算耗尽的 reason 现在带上次失败文本。实机演示(`D:/temp/orch-stall-live/`,sleeper 单元 + ORCH_UNIT_STALL_MS=8000):两窗弹出→8s 判停滞→自动关→自动重开 a2→再次判停滞→`finalized success=false`(rc=1),无残留进程、无残留窗口;判据 `test/unit-stall.test.mjs`(修复前会一直挂到运行上限)。② 残窗收口=只按**本 run 自己**录下的窗口句柄(`win-*.window.json`)且「窗口属 Windows Terminal + 宿主无任何子进程(尸体窗)」才发 WM_CLOSE;纯函数判据 `leftoverWindowHandles()`(`test/leftover-windows.test.mjs`)+ 运行前自动收口(`sweepLeftoverRunWindows()` 在 runner 之前);实逮并清掉本轮矩阵/停电演练留下的 8 个尸体窗(可见窗口 12→4,余下全为不可见宿主窗)。③ 旁记:force-kill launcher 的思路被现场否决(会在 WT 留下「[已退出进程,代码为 1]」死窗格)——正确收口是杀后代让 launcher 自己 exit 0。
14
14
 
15
- 当前源码封印记录通用核心 1.16.345 个核心文件和 68 个运行时文件。最新离线回归为 322/322 通过;当前已覆盖任务不丢失、多块并行容量调度、依赖波次、单块/纯串行计划不触发普通开窗、同项目任务按资源键精细化拆分并行(资源不明则整组串行兜底)、全局容量租约(排队等待指数退避+抖动:首等 600ms、×1.7、封顶 5s、抖动 ±20%;`ORCH_WAIT_BACKOFF_MS`/`ORCH_WAIT_BACKOFF_MAX_MS` 可调)、无窗 headless 派发默认(`--mode window` 保留可见弹窗;launcher/身份/租约/恢复契约同构,隐藏载具不抢焦点;2026-09-10 在 pi/claude/opencode/kimi 四宿主实跑通过:双块收据齐、零弹窗)、单元载体分级(TUI 类 agent 的 ConPTY 伪终端载体:真实控制台、无窗口、不抢焦点,身份/租约/恢复契约与窗口模式同构;print 类维持无控制台载体;2026-09-12 在 codex(pty)与 pi(stdio)双载体实跑通过)、执行看板(块与任务状态实时 `[board]` 行、run `events.jsonl` 事件流、只读 `--watch <runId>` 流式视图、`--status` 活动 run 摘要)、Windows Terminal 启动(同步拉起,弹窗可靠且不抢发起窗焦点)、进程身份、断点身份、损坏账本拒绝、账本 launcherFiles 工作区包容校验、验收契约创建时哈希锚定(resume/verify 篡改即拒)、注册表锁老化回收、轮询截止时间、SDK 缺失诊断、CLI 只读 `--version` 版本痕迹(不改状态、不解析 agent CLI)、强制编排入口 `orch`(跳过全部 inline 回退——单块/纯串行/只读定性/自改都会派发单元,维护锁仍是唯一门)、调用方自助通道(`--plan-file` 自带计划文件走既有覆盖/依赖/回退硬校验且全程零模型调用,`--unit-cmd`/`--unit-cmd-file` 自带单元命令绕过宿主识别与探测、任意 CLI 走同一启动器/租约/恢复/验收契约,只读 `--tasks` 账本助手,单元命令的 resume 身份一致性,未知 agent 的两条出路诊断)、print-class 数据驱动画像适配管线(`node scripts/adapt.mjs` 与 `ORCH_AUTO_ADAPT=1` 自动模式:表面探测→画像→解析自检→活验守门→重封→自动提交,失败一律回滚;2026-09-11 aider 0.86.2 实弹完成表面/画像/自检并在未登录时正确 fail-closed;画像自动发现 model/effort 探测规格(`modelDetection`,只读 env/配置文件精确键名),发起窗 model/effort 对画像 agent 同样跟随;hermes 画像正式落地:`--agent hermes` 解析为 profile 宿主、hermes 发起即自动挡——适配探活实测 12.5s 取回令牌)、档位去向留痕(summary 的 model/effort 与三态来源 explicit/宿主会话/host-default;机器可读入口 stdout 保持纯数据)、机器可调用失败契约(公开入口失败只回一行 `[orchestrator] FATAL: <原因>`、无栈帧(`ORCH_DEBUG=1` 才打);未知 run 不甩 `ENOENT` 也不挂起:`--verify-run`/`--resume-run` 立刻 fail-closed,`--watch` 有界宽限 `ORCH_WATCH_GRACE_MS`(默认 60s)后 fail-closed,看板中途丢 `summary.json` 立刻报错)、宿主 CLI 探测超时重试与逐候选拒绝诊断(scripts/core/host-probe.mjs)、OpenClaw 版本门诊断(接口探针先行:版本常量只记「最后验证于」——未验证版本必须以 `tui` 接口探针实测结果 + node-pty 状态 + 最后验证版本 fail-closed;2026-09-14 本机 2026.9.3 实测:探针 OK、node-pty present;扩版仍需逐版 config/transcript/TUI 验收 + 用户侧 gateway 凭据)、恢复账本 launcherFiles 包容校验覆盖盘符根工作区、发起方自动识别(环境标记优先、进程链就近命中:kimi/codex/claude/pi/opencode/openclaw/hermes)、kimi、claude 与 opencode 适配器的启动器回环、控制器异常死亡后的守护进程自动接管恢复(test/guardian-live-result.json)与手动 --resume-run 收编存活窗口恢复(test/recovery-live-result.json)、维护锁(自身维护期间动作入口只回固定话术并放行,docs/maintenance-lock.md)和干净发布副本。2026-09-09 已完成当前核心安全复扫(双路 LLM 源码审计,docs/security-audit-2026-09-09.md):无 Critical/High,4 个 Medium 与 1 个 Low 已修复并纳入回归,其余 Low 登记为已知问题;该报告、对应修复提交与本次封印共同构成当前核心的新安全基线,替换 2026-09-05 绑定旧哈希的历史基线。长期环境验收仍单独列为发行门槛。覆盖率摘要保存在 `coverage/coverage-summary.json`,它是验证产物,不会随发行副本发布。
15
+ 当前源码封印记录通用核心 1.16.547 个核心文件和 70 个运行时文件。最新离线回归为 336/336 通过;当前已覆盖任务不丢失、多块并行容量调度、依赖波次、单块/纯串行计划不触发普通开窗、同项目任务按资源键精细化拆分并行(资源不明则整组串行兜底)、全局容量租约(排队等待指数退避+抖动:首等 600ms、×1.7、封顶 5s、抖动 ±20%;`ORCH_WAIT_BACKOFF_MS`/`ORCH_WAIT_BACKOFF_MAX_MS` 可调)、无窗 headless 派发默认(`--mode window` 保留可见弹窗;launcher/身份/租约/恢复契约同构,隐藏载具不抢焦点;2026-09-10 在 pi/claude/opencode/kimi 四宿主实跑通过:双块收据齐、零弹窗)、单元载体分级(TUI 类 agent 的 ConPTY 伪终端载体:真实控制台、无窗口、不抢焦点,身份/租约/恢复契约与窗口模式同构;print 类维持无控制台载体;2026-09-12 在 codex(pty)与 pi(stdio)双载体实跑通过)、执行看板(块与任务状态实时 `[board]` 行、run `events.jsonl` 事件流、只读 `--watch <runId>` 流式视图、`--status` 活动 run 摘要)、Windows Terminal 启动(同步拉起,弹窗可靠且不抢发起窗焦点)、进程身份、断点身份、损坏账本拒绝、账本 launcherFiles 工作区包容校验、验收契约创建时哈希锚定(resume/verify 篡改即拒)、注册表锁老化回收、轮询截止时间、SDK 缺失诊断、CLI 只读 `--version` 版本痕迹(不改状态、不解析 agent CLI)、强制编排入口 `orch`(跳过全部 inline 回退——单块/纯串行/只读定性/自改都会派发单元,维护锁仍是唯一门)、调用方自助通道(`--plan-file` 自带计划文件走既有覆盖/依赖/回退硬校验且全程零模型调用,`--unit-cmd`/`--unit-cmd-file` 自带单元命令绕过宿主识别与探测、任意 CLI 走同一启动器/租约/恢复/验收契约,只读 `--tasks` 账本助手,单元命令的 resume 身份一致性,未知 agent 的两条出路诊断)、print-class 数据驱动画像适配管线(`node scripts/adapt.mjs` 与 `ORCH_AUTO_ADAPT=1` 自动模式:表面探测→画像→解析自检→活验守门→重封→自动提交,失败一律回滚;2026-09-11 aider 0.86.2 实弹完成表面/画像/自检并在未登录时正确 fail-closed;画像自动发现 model/effort 探测规格(`modelDetection`,只读 env/配置文件精确键名),发起窗 model/effort 对画像 agent 同样跟随;hermes 画像正式落地:`--agent hermes` 解析为 profile 宿主、hermes 发起即自动挡——适配探活实测 12.5s 取回令牌)、档位去向留痕(summary 的 model/effort 与三态来源 explicit/宿主会话/host-default;机器可读入口 stdout 保持纯数据)、机器可调用失败契约(公开入口失败只回一行 `[orchestrator] FATAL: <原因>`、无栈帧(`ORCH_DEBUG=1` 才打);未知 run 不甩 `ENOENT` 也不挂起:`--verify-run`/`--resume-run` 立刻 fail-closed,`--watch` 有界宽限 `ORCH_WATCH_GRACE_MS`(默认 60s)后 fail-closed,看板中途丢 `summary.json` 立刻报错)、宿主 CLI 探测超时重试与逐候选拒绝诊断(scripts/core/host-probe.mjs)、OpenClaw 版本门诊断(接口探针先行:版本常量只记「最后验证于」——未验证版本必须以 `tui` 接口探针实测结果 + node-pty 状态 + 最后验证版本 fail-closed;2026-09-14 本机 2026.9.3 实测:探针 OK、node-pty present;扩版仍需逐版 config/transcript/TUI 验收 + 用户侧 gateway 凭据)、恢复账本 launcherFiles 包容校验覆盖盘符根工作区、发起方自动识别(环境标记优先、进程链就近命中:kimi/codex/claude/pi/opencode/openclaw/hermes)、kimi、claude 与 opencode 适配器的启动器回环、控制器异常死亡后的守护进程自动接管恢复(test/guardian-live-result.json)与手动 --resume-run 收编存活窗口恢复(test/recovery-live-result.json)、维护锁(自身维护期间动作入口只回固定话术并放行,docs/maintenance-lock.md)和干净发布副本。2026-09-09 已完成当前核心安全复扫(双路 LLM 源码审计,docs/security-audit-2026-09-09.md):无 Critical/High,4 个 Medium 与 1 个 Low 已修复并纳入回归,其余 Low 登记为已知问题;该报告、对应修复提交与本次封印共同构成当前核心的新安全基线,替换 2026-09-05 绑定旧哈希的历史基线。长期环境验收仍单独列为发行门槛。覆盖率摘要保存在 `coverage/coverage-summary.json`,它是验证产物,不会随发行副本发布。
16
16
 
17
17
  Codex、pi 和固定版本 OpenClaw 适配器各自负责宿主参数、会话输出、完成判定和关闭流程。Windows Terminal 是默认可见承载;PowerShell 只作为生命周期脚本和进程枚举辅助,不承担宿主适配。没有经过对应适配器验证的 CLI 会直接失败,不自动安装或回退到其它 Agent。
18
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-tui-orchestrator",
3
- "version": "0.9.16",
3
+ "version": "0.9.18",
4
4
  "private": false,
5
5
  "description": "Split a multi-part agent request into a verified TODO ledger, then run the parallel blocks in hidden or visible TUI windows of the agent CLI you already use (Windows).",
6
6
  "type": "module",
package/release.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": 2,
3
- "coreVersion": "1.16.3",
3
+ "coreVersion": "1.16.5",
4
4
  "files": {
5
5
  "adapt-lib.mjs": "4902e049b9b63c1d8eab285720a490b2925bb1611aacc060aa5db3289107d61d",
6
6
  "agent-auth-prompt.txt": "18fa9c01972cd99a2fe2b1b74a440b34921a051ba6c0d54daf2f5bf1096457b0",
@@ -9,7 +9,7 @@
9
9
  "checkpoint.mjs": "246de59db9a7aac48b12eea1554ff281eaf9dd897e620ecf73d954f39b3cf94a",
10
10
  "claude-host.mjs": "8ab7661e2b56eab07709b2a16c7f6e04dab8708edfbe672300254109c2323aaa",
11
11
  "claude-runtime.mjs": "06e7aba4a5c62a2bf0f12e09089db778c265a0aa21b31a53dbac7245586abe20",
12
- "contracts.mjs": "7db0defc9ab034e0ad9adffbee47d7123cd19d791f6922e2f3142b31e0e4b68c",
12
+ "contracts.mjs": "cdf1a8c233496c9e3ad6daf2862bceb04612dc6f7d8ba76ae6a1572a7a49b43c",
13
13
  "failure-report.mjs": "0eae69efeabef1bde13855ae37f6cfdc18f55de58ad8a680cf35df6d67dc764b",
14
14
  "host-cli.mjs": "6ad40cb2a0d216d064359d1d3e3fb38cd66218b518acdde7cd644b41e8450a0c",
15
15
  "host-model.mjs": "8dd0fae5745f6a5a223d97d1d985ce3d2bd1c111fb8693b1efb5b72874f5b7d3",
@@ -30,7 +30,7 @@
30
30
  "openclaw-window.mjs": "869f8b5cde5a68d7d7d689b70cd250cd1a4e7158c714bfda023f23d0386a8552",
31
31
  "opencode-host.mjs": "6fe7ea46fa86530da3fb17005b16821d1d3d66d2139077718a9250026b854695",
32
32
  "opencode-runtime.mjs": "e641e5cd6eacf38f581c46153ebcef1f00f21eb4576dddecc8a967c10b29ddf8",
33
- "orchestrate-sdk.mjs": "5e9e50ca9f790e7af5a475304346e0a49eab9b6f7cc795d80b9032aecdb1591e",
33
+ "orchestrate-sdk.mjs": "a4320f8ad6decb96fa12aa53f56d9d4a32df2b2b8822bcff8e4daa920e69490c",
34
34
  "pi-host.mjs": "85b5d278d381f79dd76891e07542f64957d6d651ee33b0ac12de7731ff7e1b49",
35
35
  "pi-runtime.mjs": "8a50996febfea9ff0b84941f9e1693869f61b7295cd4bac308e481c2d554b7f4",
36
36
  "pi-shutdown.mjs": "c7d206b5d9f7560071ccec318a3d38faa27383d8531997b19ddfe572feb8f9f9",
@@ -42,15 +42,17 @@
42
42
  "recovery.mjs": "faa98340e970651ad1ddca9c9dda60345e3e0a4b8e163f78b4674ed231b0b2a4",
43
43
  "run-board.mjs": "ee98dac04d2babe68ff3c0a88f40547a380188ed8de7135cf54c9a0f469ae9e3",
44
44
  "run-guardian.mjs": "e1b2b17663c0a9a8a408e8a25484b87adcf7144f4d08f81c03d736a704feda6b",
45
- "runner.mjs": "6038ccd5d7d8a9f3f1fab345d61cbd7adc6a483da3e277a2db0eb6ac7e12a1e9",
45
+ "runner.mjs": "59357979209e7fb44a18f446c7666eb8b503b6c4f96664f2d5bc00c7039a57f4",
46
46
  "runtime-context.mjs": "39ee58751764034504a6e3c5a6cb53ded725abee5a5ccd7675ade7d690bdb58b",
47
47
  "unit-carrier.mjs": "462b4512910a458815a1d5f7c1bce2e0e43e53e0511f5be3e13f118c879aeb48",
48
48
  "unit-command.mjs": "93791c8e1d5ce7ccd87a9eb2a78438961faa723cb445535cadfef1254f1168ff",
49
+ "unit-memory-sampler.ps1": "9852cbe22d6876ec14a685064be0678fac497431cff7100aac3007fb0985a27f",
50
+ "unit-memory.mjs": "d7e066ce6d19740237a6375809535cd3d9e59a0250abe1995fbfbd1bb3ccf35f",
49
51
  "unit-runtime.mjs": "21a7a287e59c068c6d277281d4fb092852cfe21f4bb2df62d6695106f4f20665"
50
52
  },
51
53
  "runtimeFiles": {
52
54
  "cli.mjs": "8b6817604286769142d0620ed8da96d433596e84362cee66594e099c88f1a1c4",
53
- "package.json": "db4fa479639c9ce2c46172526f50a9234e5921da3832356ed0637c13b203775b",
55
+ "package.json": "33ad87fd2a5ce24098fadfa195f7e511e0fbc244231f9566508e1a2bf378218c",
54
56
  "repair.mjs": "5a456b42560bfa21fd1e66ccdff7416f88e82fe711db5c04279717e73275a076",
55
57
  "scripts/recover-stale.mjs": "c6dfecdfd7621b77999b710127aa1f5e3b444032bc0e2f91ec95f94c07c59f97",
56
58
  "scripts/adapt.mjs": "20209c74212d334ea5aa1dfe35816a1b7a4b7f0415eb27c5a1975925dbbd10c5",
@@ -63,7 +65,7 @@
63
65
  "scripts/core/checkpoint.mjs": "246de59db9a7aac48b12eea1554ff281eaf9dd897e620ecf73d954f39b3cf94a",
64
66
  "scripts/core/claude-host.mjs": "8ab7661e2b56eab07709b2a16c7f6e04dab8708edfbe672300254109c2323aaa",
65
67
  "scripts/core/claude-runtime.mjs": "06e7aba4a5c62a2bf0f12e09089db778c265a0aa21b31a53dbac7245586abe20",
66
- "scripts/core/contracts.mjs": "7db0defc9ab034e0ad9adffbee47d7123cd19d791f6922e2f3142b31e0e4b68c",
68
+ "scripts/core/contracts.mjs": "cdf1a8c233496c9e3ad6daf2862bceb04612dc6f7d8ba76ae6a1572a7a49b43c",
67
69
  "scripts/core/failure-report.mjs": "0eae69efeabef1bde13855ae37f6cfdc18f55de58ad8a680cf35df6d67dc764b",
68
70
  "scripts/core/host-cli.mjs": "6ad40cb2a0d216d064359d1d3e3fb38cd66218b518acdde7cd644b41e8450a0c",
69
71
  "scripts/core/host-model.mjs": "8dd0fae5745f6a5a223d97d1d985ce3d2bd1c111fb8693b1efb5b72874f5b7d3",
@@ -84,7 +86,7 @@
84
86
  "scripts/core/openclaw-window.mjs": "869f8b5cde5a68d7d7d689b70cd250cd1a4e7158c714bfda023f23d0386a8552",
85
87
  "scripts/core/opencode-host.mjs": "6fe7ea46fa86530da3fb17005b16821d1d3d66d2139077718a9250026b854695",
86
88
  "scripts/core/opencode-runtime.mjs": "e641e5cd6eacf38f581c46153ebcef1f00f21eb4576dddecc8a967c10b29ddf8",
87
- "scripts/core/orchestrate-sdk.mjs": "5e9e50ca9f790e7af5a475304346e0a49eab9b6f7cc795d80b9032aecdb1591e",
89
+ "scripts/core/orchestrate-sdk.mjs": "a4320f8ad6decb96fa12aa53f56d9d4a32df2b2b8822bcff8e4daa920e69490c",
88
90
  "scripts/core/pi-host.mjs": "85b5d278d381f79dd76891e07542f64957d6d651ee33b0ac12de7731ff7e1b49",
89
91
  "scripts/core/pi-runtime.mjs": "8a50996febfea9ff0b84941f9e1693869f61b7295cd4bac308e481c2d554b7f4",
90
92
  "scripts/core/pi-shutdown.mjs": "c7d206b5d9f7560071ccec318a3d38faa27383d8531997b19ddfe572feb8f9f9",
@@ -96,10 +98,12 @@
96
98
  "scripts/core/recovery.mjs": "faa98340e970651ad1ddca9c9dda60345e3e0a4b8e163f78b4674ed231b0b2a4",
97
99
  "scripts/core/run-board.mjs": "ee98dac04d2babe68ff3c0a88f40547a380188ed8de7135cf54c9a0f469ae9e3",
98
100
  "scripts/core/run-guardian.mjs": "e1b2b17663c0a9a8a408e8a25484b87adcf7144f4d08f81c03d736a704feda6b",
99
- "scripts/core/runner.mjs": "6038ccd5d7d8a9f3f1fab345d61cbd7adc6a483da3e277a2db0eb6ac7e12a1e9",
101
+ "scripts/core/runner.mjs": "59357979209e7fb44a18f446c7666eb8b503b6c4f96664f2d5bc00c7039a57f4",
100
102
  "scripts/core/runtime-context.mjs": "39ee58751764034504a6e3c5a6cb53ded725abee5a5ccd7675ade7d690bdb58b",
101
103
  "scripts/core/unit-carrier.mjs": "462b4512910a458815a1d5f7c1bce2e0e43e53e0511f5be3e13f118c879aeb48",
102
104
  "scripts/core/unit-command.mjs": "93791c8e1d5ce7ccd87a9eb2a78438961faa723cb445535cadfef1254f1168ff",
105
+ "scripts/core/unit-memory-sampler.ps1": "9852cbe22d6876ec14a685064be0678fac497431cff7100aac3007fb0985a27f",
106
+ "scripts/core/unit-memory.mjs": "d7e066ce6d19740237a6375809535cd3d9e59a0250abe1995fbfbd1bb3ccf35f",
103
107
  "scripts/core/unit-runtime.mjs": "21a7a287e59c068c6d277281d4fb092852cfe21f4bb2df62d6695106f4f20665",
104
108
  "scripts/gate.mjs": "f578fb41d112dd1d44fe5b46e451d4753ed7251ea150e8f44acb138340d604ae",
105
109
  "scripts/host-cli.mjs": "994448626766f53883a29d17a5af547dcb67f6d2b9e7e5b6b6ff60f28582978c",
@@ -4,7 +4,7 @@ import crypto from 'node:crypto';
4
4
  import {spawnSync} from 'node:child_process';
5
5
  import os from 'node:os';
6
6
 
7
- export const CORE_VERSION = '1.16.3';
7
+ export const CORE_VERSION = '1.16.5';
8
8
  export const SUCCESS = new Set(['verified']);
9
9
  export const TERMINAL = new Set(['verified','failed','blocked','cancelled','awaiting-verification','preflight-failed','popup-failed','timeout','needs-reconciliation']);
10
10
 
@@ -61,12 +61,13 @@
61
61
  */
62
62
  import {nativeArgumentLines} from "./native-argv.mjs";
63
63
  import {CORE_VERSION, levelWaves, keysOverlap, summaryComplete,requestTasks,bindClassifiedTasks,ledgerPlan,computeMaxWindows,powershellExe,systemToolExe} from "./contracts.mjs";
64
+ import {memConfig, loadUnitMemory, saveUnitMemory, recordUnitPeak, unitEstimateMb, admitCount, fitBlocks, describeFit, unitMemoryStorePath} from "./unit-memory.mjs";
64
65
  import {formatFailureLines} from "./failure-report.mjs";
65
66
  import {effectiveUnitCarrier, ptyAvailability} from "./unit-carrier.mjs";
66
67
  // 锚定 SystemRoot 的系统工具:裸名会先按进程 cwd(=任意工作区)解析,防二进制投毒。
67
68
  const PS_EXE=powershellExe(),WHERE_EXE=systemToolExe("where.exe");
68
69
  import {runWindows,verifyRun} from "./runner.mjs";
69
- import {launcherIdentity,processSnapshot,sameProcess,coordinatorDir,reserve,bind} from "./leases.mjs";
70
+ import {launcherIdentity,processSnapshot,sameProcess,coordinatorDir,reserve,release,bind} from "./leases.mjs";
70
71
  import {runPiPlanner,writePiLauncher,waitPiResult} from './pi-runtime.mjs';
71
72
  import {runOpenClawPlanner,writeOpenClawLauncher} from './openclaw-runtime.mjs';
72
73
  import {runOpenCodePlanner,writeOpenCodeLauncher,waitOpenCodeResult} from './opencode-runtime.mjs';
@@ -492,8 +493,42 @@ export function inlineEnvJson(extra = {}) {
492
493
  totalMB: Math.round(os.totalmem() / 1024 ** 2),
493
494
  }, extra);
494
495
  }
495
- async function decomposeSafely(request, maxWindows = computeMaxWindows(availableMemoryMb())) {
496
- const finalize = plan => capPlanBlocks(normalizePlan(plan), maxWindows);
496
+ // —— 通用内存拟合(0.9.17):底层机制、宿主无关;学习表按宿主存数据,代码里没有任何宿主分支。
497
+ let LAST_MEM_FIT = null;
498
+ export function unitMemoryStoreFile() {
499
+ return unitMemoryStorePath(process.env);
500
+ }
501
+ function fitPlanViaMemory(plan) {
502
+ const cfg = memConfig();
503
+ const store = loadUnitMemory(unitMemoryStoreFile());
504
+ const unitMb = unitEstimateMb(store, AGENT.agent, cfg);
505
+ const res = fitBlocks(plan, availableMemoryMb(), unitMb, cfg);
506
+ LAST_MEM_FIT = {unitMb: res.unitMb, growth: res.blockGrowth, estMb: res.estMb, freeMb: Math.round(res.freeMb), floorMb: res.floorMb, fits: res.fits, merges: res.merges};
507
+ console.log(describeFit(res));
508
+ if (!res.fits) {
509
+ console.warn('[mem-fit] WARNING: even one unit cannot clear the floor (est ' + Math.round(res.estMb) + 'MB, free ' + Math.round(res.freeMb) + 'MB, floor ' + res.floorMb + 'MB); proceeding anyway (work is never blocked) — closing heavy apps would help.');
510
+ }
511
+ return {plan: res.blocks, memFit: LAST_MEM_FIT};
512
+ }
513
+ // 辅助窗(交互式看窗)准入上限:与旧公式同形(上限),但用学习画像反推——留不出底线就不放行(0 ⇒ 排队等待)。
514
+ function auxAdmissionLimit(auxCap) {
515
+ const cfg = memConfig();
516
+ const store = loadUnitMemory(unitMemoryStoreFile());
517
+ const unitMb = unitEstimateMb(store, AGENT.agent, cfg);
518
+ const n = admitCount(availableMemoryMb(), unitMb, cfg);
519
+ if (n <= 0) return 0;
520
+ return auxCap ? Math.min(n, auxCap) : n;
521
+ }
522
+ function readSavedBlockCount() {
523
+ try {
524
+ const p = path.join(WORKSPACE, 'temp', 'orchestrator', 'runs', String(process.env.ORCH_RUN_ID || ''), 'summary.json');
525
+ const n = JSON.parse(fs.readFileSync(p, 'utf8'))?.blocks?.length;
526
+ if (Number.isInteger(n) && n > 0) return n;
527
+ } catch {}
528
+ return Math.max(1, Number(process.env.ORCH_MAX_WINDOWS || 0) || 4);
529
+ }
530
+ async function decomposeSafely(request) {
531
+ const finalize = plan => fitPlanViaMemory(normalizePlan(plan)).plan; // 0.9.17:内存拟合取代表驱动固定容量压缩
497
532
  const hints = resolveHints(); // hints 参与缓存键(moduleToProj/foldRules/tail),防跨项目映射撞缓存
498
533
  // 0) 缓存优先:相同请求+相同 hints 的拆块结果永不自动失效(仅 --clear-cache 或删除缓存文件才重置),
499
534
  // 命中直接复用 -> 越用越快(与下方 loadDecomposeCache 的“永不自动失效”实现保持一致)。
@@ -1052,7 +1087,7 @@ async function warmupWindow(pidf, token) {
1052
1087
  }
1053
1088
  async function spawnWindowWithRetry(lp, title, pidf, maxRetry = 3, token, reservedByRunner = false) {
1054
1089
  const auxiliaryId="aux-"+path.basename(lp); let coordinator;
1055
- if(!reservedByRunner){coordinator=coordinatorDir(WORKSPACE);const auxCap=Math.max(0,Number(process.env.ORCH_MAX_WINDOWS||0)||0);const claim=reserve(coordinator,{id:auxiliaryId,keys:["aux:"+token]},auxCap?Math.min(computeMaxWindows(availableMemoryMb()),auxCap):computeMaxWindows(availableMemoryMb()));if(!claim.ok)throw new Error("Window admission waiting: "+claim.reason);}
1090
+ if(!reservedByRunner){coordinator=coordinatorDir(WORKSPACE);const auxCap=Math.max(0,Number(process.env.ORCH_MAX_WINDOWS||0)||0);const auxLimit=auxAdmissionLimit(auxCap);let claim=reserve(coordinator,{id:auxiliaryId,keys:["aux:"+token]},auxLimit);if(!claim.ok){try{release(coordinator,auxiliaryId);}catch{}claim=reserve(coordinator,{id:auxiliaryId,keys:["aux:"+token]},auxLimit);}if(!claim.ok)throw new Error("Window admission waiting: "+claim.reason);}
1056
1091
  // 恢复调用方传入的重试上限(默认 3):sync 拉起后每次尝试 ~2.5s,重试成本可控,兜底 WT 偶发不弹窗。
1057
1092
  for (let attempt = 1; attempt <= maxRetry; attempt++) {
1058
1093
  spawnVisibleWindow(lp, title, pidf);
@@ -1097,7 +1132,7 @@ function spawnHeadlessUnit(lp, title) {
1097
1132
  }
1098
1133
  async function spawnHeadlessWithRetry(lp, title, pidf, maxRetry = 3, token, reservedByRunner = false) {
1099
1134
  const auxiliaryId="aux-"+path.basename(lp); let coordinator;
1100
- if(!reservedByRunner){coordinator=coordinatorDir(WORKSPACE);const auxCap=Math.max(0,Number(process.env.ORCH_MAX_WINDOWS||0)||0);const claim=reserve(coordinator,{id:auxiliaryId,keys:["aux:"+token]},auxCap?Math.min(computeMaxWindows(availableMemoryMb()),auxCap):computeMaxWindows(availableMemoryMb()));if(!claim.ok)throw new Error("Window admission waiting: "+claim.reason);}
1135
+ if(!reservedByRunner){coordinator=coordinatorDir(WORKSPACE);const auxCap=Math.max(0,Number(process.env.ORCH_MAX_WINDOWS||0)||0);const auxLimit=auxAdmissionLimit(auxCap);let claim=reserve(coordinator,{id:auxiliaryId,keys:["aux:"+token]},auxLimit);if(!claim.ok){try{release(coordinator,auxiliaryId);}catch{}claim=reserve(coordinator,{id:auxiliaryId,keys:["aux:"+token]},auxLimit);}if(!claim.ok)throw new Error("Window admission waiting: "+claim.reason);}
1101
1136
  // Hidden spawn is deterministic (no OS window manager in the loop), so a
1102
1137
  // single attempt usually confirms; keep the retry budget for cold-start
1103
1138
  // hiccups and parity with the window path.
@@ -2389,9 +2424,8 @@ export async function main() {
2389
2424
  checkDiskSpace({ warnOnly: true });
2390
2425
  if (PLAN_FILE) {
2391
2426
  // 计划文件模式:纯确定性,零 LLM(不读缓存、不写缓存、不调模型)。
2392
- const capacity = computeMaxWindows(availableMemoryMb());
2393
- const effective = ledgerEffectivePlan(request, planFromFile(request, loadPlanFile(PLAN_FILE), capacity), capacity);
2394
- console.log(JSON.stringify({ blocks: effective }, null, 2));
2427
+ const fitted = fitPlanViaMemory(ledgerEffectivePlan(request, planFromFile(request, loadPlanFile(PLAN_FILE), Number.MAX_SAFE_INTEGER), Number.MAX_SAFE_INTEGER)).plan;
2428
+ console.log(JSON.stringify({ blocks: fitted, memFit: LAST_MEM_FIT }, null, 2));
2395
2429
  return;
2396
2430
  }
2397
2431
  requireHost();
@@ -2586,24 +2620,22 @@ export async function main() {
2586
2620
  if(!FORCE_EXECUTE&&isOrchestratorSelfMod(request)){console.log(JSON.stringify({mode:'inline',reason:'Modify the orchestrator in the initiating conversation before opening windows'}));return;}
2587
2621
  // 计划文件模式:调用方已显式给出计划并要求编排——跳过只读定性(零模型调用);自改文本门与容量门仍在。
2588
2622
  if(!resume&&!FORCE_EXECUTE&&!PLAN_FILE&&await classifyExecutionMode(request)==='inline'){console.log(JSON.stringify({mode:'inline',reason:'Read-only analysis'}));return;}
2589
- const capacity=computeMaxWindows(availableMemoryMb());
2590
- if(!resume&&capacity<2&&!FORCE_EXECUTE){
2591
- console.log(JSON.stringify({mode:'inline',reason:'capacity-below-two-windows',capacity,env:inlineEnvJson({capacity}),request,tasks:requestTasks(request),next:'在发起对话直接执行原始请求;不要再次调用 --run-windows'}));
2592
- return;
2593
- }
2623
+ // 0.9.17:固定容量公式退役。容量由计划成型后的内存拟合决定(fitPlanViaMemory):
2624
+ // 拟合后 1 块 = 无并行 = 交给下方 windowExecutionPolicy 的 single-block/serial 内联交回调用方。
2594
2625
  if(!checkDiskSpace()) {process.exitCode=1;return;}
2595
2626
  // 载体就绪门:pty 载体的引擎必须在启动任何单元前可解析(缺失立即报错,不让单元静默秒退)。
2596
2627
  if(SPAWN_MODE==='headless'){try{assertUnitCarrierReady();}catch(error){console.error(error.message);process.exitCode=1;return;}}
2597
2628
  let preflightPlan;
2598
2629
  if(!resume){
2599
- preflightPlan=PLAN_FILE?ledgerEffectivePlan(request,planFromFile(request,loadPlanFile(PLAN_FILE),capacity),capacity):await decomposeSafely(request,capacity);
2630
+ preflightPlan=PLAN_FILE?fitPlanViaMemory(ledgerEffectivePlan(request,planFromFile(request,loadPlanFile(PLAN_FILE),Number.MAX_SAFE_INTEGER),Number.MAX_SAFE_INTEGER)).plan:await decomposeSafely(request);
2600
2631
  const policy=windowExecutionPolicy(preflightPlan);
2601
2632
  if(policy.mode==='inline'&&!FORCE_EXECUTE){
2602
- console.log(JSON.stringify({mode:'inline',reason:policy.reason,capacity,env:inlineEnvJson({capacity}),request,tasks:requestTasks(request),blocks:preflightPlan.map(b=>({title:b.title,type:b.type,dependsOn:b.dependsOn||[],tasks:b.tasks||[]})),next:'在发起对话直接执行原始请求;不要再次调用 --run-windows'}));
2633
+ console.log(JSON.stringify({mode:'inline',reason:policy.reason,capacity:preflightPlan.length,memFit:LAST_MEM_FIT,env:inlineEnvJson({capacity:preflightPlan.length}),request,tasks:requestTasks(request),blocks:preflightPlan.map(b=>({title:b.title,type:b.type,dependsOn:b.dependsOn||[],tasks:b.tasks||[]})),next:'在发起对话直接执行原始请求;不要再次调用 --run-windows'}));
2603
2634
  return;
2604
2635
  }
2605
2636
  if(policy.mode==='inline')console.log('[orchestrator] force-execute: inline gate bypassed ('+policy.reason+'); spawning unit(s) anyway');
2606
2637
  }
2638
+ const capacity=resume?readSavedBlockCount():preflightPlan.length;
2607
2639
  armEnforcement();
2608
2640
  // 残窗收口:resume 前先收掉上次被杀留下的尸体窗(判据见 sweepLeftoverRunWindows),
2609
2641
  // 免得用户对着一排「[已退出进程…]」的窗手动关。
@@ -3,11 +3,13 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import crypto from 'node:crypto';
5
5
  import {fileURLToPath} from 'node:url';
6
- import {CORE_VERSION,ledgerPlan,levelWaves,verifyChecks,summaryComplete,TERMINAL,canonicalKey,requestTasks,acceptanceKeyDiff,bootId} from './contracts.mjs';
6
+ import {CORE_VERSION,ledgerPlan,levelWaves,verifyChecks,summaryComplete,TERMINAL,canonicalKey,requestTasks,acceptanceKeyDiff,bootId,powershellExe} from './contracts.mjs';
7
7
  import {coordinatorDir,reserve,bind,release,prepare,atomicJson,launcherIdentity,processSnapshot,sameProcess,claimRun,releaseRun,waitBackoffMs} from './leases.mjs';
8
8
  import {scheduleReady,recoveryTasks,taskPolicy,validateRecoverySummary} from './recovery.mjs';
9
9
  import {readCheckpoints} from './checkpoint.mjs';
10
10
  import {boardSignature,boardJson,boardSummaryLine} from './run-board.mjs';
11
+ import {memConfig, loadUnitMemory, saveUnitMemory, recordUnitPeak, unitMemoryStorePath} from './unit-memory.mjs';
12
+ import {spawn} from 'node:child_process';
11
13
  // 队列退避参数(env 可调,测试提速用):首等 base、指数 ×1.7、封顶 max(抖动上界 ×1.2)。
12
14
  const WAIT_BACKOFF={base:Math.max(50,Number(process.env.ORCH_WAIT_BACKOFF_MS||600)||600),max:0};
13
15
  WAIT_BACKOFF.max=Math.max(WAIT_BACKOFF.base,Number(process.env.ORCH_WAIT_BACKOFF_MAX_MS||5000)||5000);
@@ -19,6 +21,50 @@ const pause=ms=>new Promise(r=>setTimeout(r,ms));
19
21
  // 就判停滞 → 关窗/强制收口单元树 → 计入 attempts 预算自动重跑;预算耗尽则诚实以 stalled 收口。
20
22
  const UNIT_STALL_MS=Math.max(1000,Number(process.env.ORCH_UNIT_STALL_MS||600000)||600000);
21
23
  const UNIT_TIMEOUT_MS=Math.max(5000,Number(process.env.ORCH_UNIT_TIMEOUT_MS||2700000)||2700000);
24
+
25
+ // —— 0.9.17 通用内存采样接线(宿主无关):只拿单元根 pid;采样/回写任何失败都不影响编排(fail-open)。
26
+ function unitPeakFile(la, key, token) {
27
+ return la ? path.join(path.dirname(String(la.pidf)), 'unit-peak-' + key + '-' + String(token || 'x').slice(-8) + '.json') : '';
28
+ }
29
+ function startUnitSampler(la, key, token, rootPid) {
30
+ try {
31
+ if (!la || !Number.isInteger(rootPid) || rootPid <= 0) {
32
+ console.error('[mem] sampler skipped (rootPid=' + String(rootPid) + ', la=' + (la ? 'yes' : 'no') + ')');
33
+ return false;
34
+ }
35
+ const sampler = path.join(path.dirname(fileURLToPath(import.meta.url)), 'unit-memory-sampler.ps1');
36
+ // 2026-09-18 实测定论:detached:true 会让 PS 子进程在 Windows 上静默死掉(一行不执行、不报错、不退出)
37
+ // —— 去 detached,做普通子进程(引擎寿命 ≥ 单元寿命,采样器随单元结束自然收尾)。
38
+ const child = spawn(powershellExe(), ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', sampler, '-RootPid', String(rootPid), '-OutFile', unitPeakFile(la, key, token), '-IntervalMs', '6000'], {windowsHide: true, stdio: ['ignore', 'ignore', 'pipe']});
39
+ // 采样器的 stderr 落盘(2026-09-18:静默失败排查用;引擎先退出也尽量写)。
40
+ try {
41
+ const errLog = unitPeakFile(la, key, token) + '.stderr.txt';
42
+ child.stderr.on('data', (buf) => { try { fs.appendFileSync(errLog, buf); } catch {} });
43
+ } catch {}
44
+ child.unref();
45
+ console.log('[mem] sampler started pid=' + String(child.pid) + ' root=' + rootPid + ' out=' + unitPeakFile(la, key, token));
46
+ return true;
47
+ } catch (error) {
48
+ console.error('[mem] sampler failed to start: ' + String(error && error.message));
49
+ return false;
50
+ }
51
+ }
52
+ async function recordUnitPeakIfReady(la, key, token, agent, waitForSample) {
53
+ try {
54
+ if (!la) return;
55
+ const file = unitPeakFile(la, key, token);
56
+ for (let i = 0; waitForSample && i < 10 && !fs.existsSync(file); i += 1) await pause(1000);
57
+ if (!fs.existsSync(file)) { if (waitForSample) console.error('[mem] no peak sample for ' + key + ' (sampler did not finish; markers: ' + file + '.start / .error)'); return; }
58
+ const peak = Number(JSON.parse(fs.readFileSync(file, 'utf8'))?.peakMB);
59
+ fs.rmSync(file, {force: true}); // 读过即销:天然去重,旧样本不会重复入账
60
+ if (!Number.isFinite(peak) || peak <= 0) return;
61
+ const storeFile = unitMemoryStorePath(process.env);
62
+ const store = loadUnitMemory(storeFile);
63
+ recordUnitPeak(store, agent, peak);
64
+ saveUnitMemory(storeFile, store);
65
+ console.log('[mem] unit ' + key + ' peak=' + peak + 'MB (agent=' + String(agent || 'unknown') + ')');
66
+ } catch {}
67
+ }
22
68
  // 偶发瞬时故障(2026-09-18 用户令):单元“启动即死、无终态检查点”这类瞬时故障必须自动检测 + 自动
23
69
  // 重跑,绝不把瞬时故障当结论甩给调用方。判据 = ①该块待定任务的检查点全部停在 started(无终态)
24
70
  // ②最近一次尝试的存活时长 ≤ 窗口(真跑过很久的失败不算瞬时)③验收未被 retrySafe:false 明确否决。
@@ -231,7 +277,7 @@ export async function runWindows(request,hooks,options={}) {
231
277
  async function execute(r) {
232
278
  const b=sum.blocks.find(b=>b.key===r.key),tasks=b.taskIds.map(id=>sum.tasks.find(t=>t.id===id));
233
279
  const leaseId=crypto.createHash('sha256').update(runKey).digest('hex').slice(0,16)+'-'+r.key;
234
- let attemptsThisController=0,spawnConfirmed=false,transientRetries=0,transientGrant=false,unproductiveRetries=0;
280
+ let attemptsThisController=0,spawnConfirmed=false,transientRetries=0,transientGrant=false,unproductiveRetries=0,peakSampled=false;
235
281
  const limit=Math.max(1,Math.min(3,Number(options.maxAttempts||2)));
236
282
  while(Date.now()<deadline) {
237
283
  let e=sum.results[r.key],la=e.launcherFiles,identity;spawnConfirmed=false;
@@ -339,6 +385,8 @@ export async function runWindows(request,hooks,options={}) {
339
385
  try {
340
386
  if(identity||fs.existsSync(la.rf)) {
341
387
  e.status='running';persist();
388
+ // 0.9.17:单元起来了 ⇒ 挂通用采样器(拿根 pid 收进程树峰值;成败都不影响编排)。
389
+ peakSampled=startUnitSampler(la, r.key, active.token, Number(identity?.ProcessId||hooks.readPid(la.pidf)))||peakSampled;
342
390
  const session=await hooks.session('【本块唯一标识】'+active.token,Math.min(90000,Math.max(0,deadline-Date.now())),la.rf);
343
391
  const unitBudget=Math.max(0,Math.min(deadline,Date.now()+UNIT_TIMEOUT_MS)-Date.now());
344
392
  const waited=session?await waitWithStallWatchdog(hooks,la,session,active,unitBudget):{result:{done:fs.existsSync(la.rf),exitCode:hooks.exitCode(la.rf)}};
@@ -351,6 +399,7 @@ export async function runWindows(request,hooks,options={}) {
351
399
  if(identity)await hooks.close(identity.ProcessId,identity);
352
400
  if(identity&&!(await waitForProcessExit(identity))){e.status='failed';e.reason='Launcher still alive; lease retained';persist();return;}
353
401
  await coordinate(()=>release(coordinator,e.leaseId||leaseId),Date.now()+30000);
402
+ await recordUnitPeakIfReady(la, r.key, active.token, options.agent, peakSampled);
354
403
  hooks.unregister(e.leaseId||leaseId);if(la)hooks.cleanup(la);
355
404
  e.endedAt=Date.now();active.endedAt=e.endedAt;active.exitCode=e.exitCode;
356
405
  collectVerification(sum,checks,workspace);persist();
@@ -0,0 +1,63 @@
1
+ # unit-memory-sampler.ps1 -- generic per-unit peak-memory sampler (0.9.17). ASCII only.
2
+ # One sampler per unit; snapshots the unit's process tree once, then polls a light-weight
3
+ # WorkingSet sum and records the peak. Host-agnostic: it only needs the unit's root pid.
4
+ # Usage: powershell -NoProfile -ExecutionPolicy Bypass -File unit-memory-sampler.ps1 -RootPid <pid> -OutFile <json> [-IntervalMs 12000] [-RescanEvery 5] [-MaxMs 3600000]
5
+ param(
6
+ [Parameter(Mandatory=$true)][int]$RootPid,
7
+ [Parameter(Mandatory=$true)][string]$OutFile,
8
+ [int]$IntervalMs = 12000,
9
+ [int]$RescanEvery = 5,
10
+ [int]$MaxMs = 3600000
11
+ )
12
+ $ErrorActionPreference = 'SilentlyContinue'
13
+ Set-Content -Path ($OutFile + '.start') -Value ((Get-Date).ToString('s')) -Encoding ascii
14
+ try {
15
+
16
+ function Get-TreeIds([int]$root) {
17
+ # 2026-09-18:全表 Get-CimInstance 在忙机器上会卡死(实测采样器刚起就被拖住 ✗)⇒ 改成按父进程过滤的递归查询。
18
+ $ids = @($root)
19
+ $frontier = @($root)
20
+ while ($frontier.Count -gt 0) {
21
+ $next = @()
22
+ foreach ($parent in $frontier) {
23
+ $next += @(Get-CimInstance Win32_Process -Filter ("ParentProcessId=" + $parent) -ErrorAction SilentlyContinue | Select-Object -ExpandProperty ProcessId | Where-Object { $_ })
24
+ }
25
+ $next = @($next | Where-Object { $_ -and ($ids -notcontains $_) })
26
+ if ($next.Count -eq 0) { break }
27
+ $ids += $next
28
+ $frontier = $next
29
+ }
30
+ return $ids
31
+ }
32
+
33
+ $start = Get-Date
34
+ $tree = @(Get-TreeIds -root $RootPid)
35
+ $peak = [int64]0
36
+ $loops = 0
37
+ while ((((Get-Date) - $start).TotalMilliseconds) -lt $MaxMs) {
38
+ $alive = $false
39
+ $sum = [int64]0
40
+ foreach ($procId in @($tree | Where-Object { $_ })) {
41
+ if (-not $procId) { continue }
42
+ $procObj = Get-Process -Id ([int]$procId) -ErrorAction SilentlyContinue
43
+ if ($procObj) {
44
+ $alive = $true
45
+ $sum += [int64]$procObj.WorkingSet64
46
+ }
47
+ }
48
+ if ($sum -gt $peak) { $peak = $sum }
49
+ if (-not $alive) { break }
50
+ $loops += 1
51
+ if (($RescanEvery -gt 0) -and (($loops % $RescanEvery) -eq 0)) {
52
+ $tree = @($tree + (Get-TreeIds -root $RootPid)) | Select-Object -Unique
53
+ }
54
+ Start-Sleep -Milliseconds $IntervalMs
55
+ }
56
+ $peakMb = [int][math]::Round($peak / 1MB, 0)
57
+ $stamp = (Get-Date).ToString('s')
58
+ $json = '{"schema":1,"rootPid":' + $RootPid + ',"peakMB":' + $peakMb + ',"sampledAt":"' + $stamp + '","loops":' + $loops + '}'
59
+ Set-Content -Path $OutFile -Value $json -Encoding ascii
60
+ } catch {
61
+ Set-Content -Path ($OutFile + '.error') -Value ($_ | Out-String) -Encoding ascii
62
+ exit 1
63
+ }
@@ -0,0 +1,195 @@
1
+ // unit-memory.mjs —— 通用「单元内存画像 + 合并适配」:底层机制,不针对任何宿主做专项。
2
+ // 设计规则(硬约束):
3
+ // 1) 只有一套机制;每个宿主(含未来接入的)都只是学习表里的一行数据;
4
+ // 2) 本文件内不得出现任何 per-agent 分支(if agent === 'xxx' 之类);
5
+ // 3) 默认值 / 安全系数 / 底限全部通用且可用环境变量配置。
6
+ // 判据:test/unit-memory.test.mjs(红能力:每个断言都能被反例击红)。
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ export const MEM_STORE_SCHEMA = 1;
11
+
12
+ export const MEM_DEFAULTS = Object.freeze({
13
+ floorMb: 256, // 系统可用内存硬底:全并发执行也不得让可用内存低于它(ORCH_MIN_FREE_MEM_MB)
14
+ defaultUnitMb: 640, // 未知宿主的保守单单元峰值(ORCH_UNIT_MEM_DEFAULT_MB)
15
+ safety: 1.3, // 学习值安全系数:estimate = p90 × safety(ORCH_UNIT_MEM_SAFETY)
16
+ minSamples: 3, // 学习生效所需的最少样本数(ORCH_UNIT_MEM_MIN_SAMPLES)
17
+ maxSamples: 50, // 每宿主保留的最近样本数
18
+ blockGrowth: 0.15, // 块内每多一个任务的成本增长(ORCH_UNIT_MEM_BLOCK_GROWTH,必须 <1 才能保证合并必降账)
19
+ });
20
+
21
+ export function memConfig(env = process.env) {
22
+ const num = (key, fallback) => {
23
+ const v = Number(String(env[key] ?? '').trim());
24
+ return Number.isFinite(v) && v > 0 ? v : fallback;
25
+ };
26
+ const rawText = String(env.ORCH_UNIT_MEM_BLOCK_GROWTH ?? '').trim();
27
+ const growthRaw = rawText === '' ? NaN : Number(rawText); // 空串≠0:未设置必须回落到默认值
28
+ const growth = Number.isFinite(growthRaw) && growthRaw >= 0 ? Math.min(growthRaw, 0.9) : MEM_DEFAULTS.blockGrowth;
29
+ return {
30
+ floorMb: num('ORCH_MIN_FREE_MEM_MB', MEM_DEFAULTS.floorMb),
31
+ defaultUnitMb: num('ORCH_UNIT_MEM_DEFAULT_MB', MEM_DEFAULTS.defaultUnitMb),
32
+ safety: num('ORCH_UNIT_MEM_SAFETY', MEM_DEFAULTS.safety),
33
+ minSamples: Math.max(1, Math.floor(num('ORCH_UNIT_MEM_MIN_SAMPLES', MEM_DEFAULTS.minSamples))),
34
+ blockGrowth: growth,
35
+ };
36
+ }
37
+
38
+ export function emptyStore() {
39
+ return {schema: MEM_STORE_SCHEMA, agents: {}};
40
+ }
41
+
42
+ // 学习表位置:机器级(跨工作区共享)。env ORCH_UNIT_MEM_STORE 可覆盖(测试隔离用)。
43
+ export function unitMemoryStorePath(env = process.env) {
44
+ const override = String(env.ORCH_UNIT_MEM_STORE || '').trim();
45
+ if (override) return override;
46
+ return path.join(osTmp(), 'orchestrator', 'unit-memory.json');
47
+ }
48
+ function osTmp() {
49
+ return process.env.TEMP || process.env.TMP || '/tmp';
50
+ }
51
+
52
+ // 读学习表;任何异常(文件缺失 / JSON 坏 / 结构不对)都回退为空表(fail-open 到默认值)。
53
+ export function loadUnitMemory(file) {
54
+ try {
55
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
56
+ if (!raw || typeof raw !== 'object' || !raw.agents || typeof raw.agents !== 'object') return emptyStore();
57
+ const agents = {};
58
+ for (const [key, row] of Object.entries(raw.agents)) {
59
+ const peaks = Array.isArray(row?.peaks)
60
+ ? row.peaks.filter((n) => Number.isFinite(n) && n > 0).map((n) => Math.round(n)).slice(-MEM_DEFAULTS.maxSamples)
61
+ : [];
62
+ agents[String(key)] = {peaks};
63
+ }
64
+ return {schema: MEM_STORE_SCHEMA, agents};
65
+ } catch {
66
+ return emptyStore();
67
+ }
68
+ }
69
+
70
+ // 原子写(tmp → rename),失败不抛(学习数据不值得让运行失败)。
71
+ export function saveUnitMemory(file, store) {
72
+ try {
73
+ fs.mkdirSync(path.dirname(file), {recursive: true});
74
+ const tmp = `${file}.tmp`;
75
+ fs.writeFileSync(tmp, JSON.stringify(store, null, 2));
76
+ fs.renameSync(tmp, file);
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ export function percentile(nums, p) {
84
+ const list = (nums || []).filter((n) => Number.isFinite(n)).sort((a, b) => a - b);
85
+ if (!list.length) return null;
86
+ const idx = Math.min(list.length - 1, Math.max(0, Math.ceil(p * list.length) - 1));
87
+ return list[idx];
88
+ }
89
+
90
+ export function recordUnitPeak(store, agent, peakMb) {
91
+ const key = String(agent || 'unknown');
92
+ if (!Number.isFinite(peakMb) || peakMb <= 0) return store;
93
+ const row = store.agents[key] || {peaks: []};
94
+ row.peaks = [...row.peaks, Math.round(peakMb)].slice(-MEM_DEFAULTS.maxSamples);
95
+ store.agents[key] = row;
96
+ return store;
97
+ }
98
+
99
+ // 单单元内存估计:样本够 → p90 × 安全系数;否则 → 通用默认值。宿主无关。
100
+ export function unitEstimateMb(store, agent, cfg = memConfig()) {
101
+ const peaks = store?.agents?.[String(agent || 'unknown')]?.peaks || [];
102
+ if (peaks.length >= cfg.minSamples) {
103
+ const p90 = percentile(peaks, 0.9);
104
+ if (Number.isFinite(p90)) return Math.max(1, Math.round(p90 * cfg.safety));
105
+ }
106
+ return Math.round(cfg.defaultUnitMb);
107
+ }
108
+
109
+ // 现在最多能放几个单元:floor((free − 底线) / 单元估计)。这是旧公式 floor(free/1000)×2 的诚实泛化——
110
+ // 同样只做准入上限,但用的是按宿主学习的真实单元大小(宿主无关,无分支)。
111
+ export function admitCount(freeMb, unitMb, cfg = memConfig()) {
112
+ const free = Number(freeMb);
113
+ const unit = Math.max(1, Number(unitMb) || cfg.defaultUnitMb);
114
+ if (!Number.isFinite(free)) return 1;
115
+ return Math.max(0, Math.floor((free - cfg.floorMb) / unit));
116
+ }
117
+
118
+ // 块的估算成本(单元数):1 + g×(块内任务数−1)。g<1 是收敛保证:每合并一次,总账必降。
119
+ function blockCost(block, growth) {
120
+ const tasks = (block?.tasks || []).length;
121
+ return 1 + growth * Math.max(0, tasks - 1);
122
+ }
123
+
124
+ function hintSet(block) {
125
+ const out = new Set();
126
+ for (const task of block?.tasks || []) for (const h of task?.conflict_hints || []) out.add(String(h));
127
+ return out;
128
+ }
129
+
130
+ // 选合并对:① 依赖对(本就该串行)→ ② 冲突提示有交集的相邻对 → ③ 任务数最多的一对(同数则取靠前者)。
131
+ function pickPair(blocks) {
132
+ const idxOf = new Map(blocks.map((b, i) => [b, i]));
133
+ for (let j = 0; j < blocks.length; j += 1) {
134
+ for (const dep of blocks[j].dependsOn || []) {
135
+ const i = blocks.findIndex((b) => b.title === dep || (b.tasks || []).some((t) => t.id === dep));
136
+ if (i >= 0 && i !== j) return [Math.min(i, j), Math.max(i, j), 'dependency'];
137
+ }
138
+ }
139
+ for (let i = 0; i < blocks.length - 1; i += 1) {
140
+ const a = hintSet(blocks[i]);
141
+ if (!a.size) continue;
142
+ const b = hintSet(blocks[i + 1]);
143
+ for (const h of b) if (a.has(h)) return [i, i + 1, 'conflict-hint'];
144
+ }
145
+ let best = null;
146
+ for (let i = 0; i < blocks.length; i += 1) {
147
+ for (let j = i + 1; j < blocks.length; j += 1) {
148
+ const weight = (blocks[i].tasks || []).length + (blocks[j].tasks || []).length;
149
+ if (!best || weight > best[2]) best = [i, j, weight];
150
+ }
151
+ }
152
+ if (best) return [best[0], best[1], 'largest'];
153
+ return [0, 1, 'fallback'];
154
+ }
155
+
156
+ // 把两个块并成一个「串行块」:tasks 按原顺序拼接;dependsOn 合并、去掉指向被并块自身的边,
157
+ // 其余块里指向 a/b 的边改指合并后的块(保证依赖图不碎)。
158
+ function mergeBlocks(blocks, i, j, seq) {
159
+ const [a, b] = [blocks[i], blocks[j]];
160
+ const mergedTasks = [...(a.tasks || []), ...(b.tasks || [])];
161
+ const own = new Set([a.title, b.title]);
162
+ const deps = [...new Set([...(a.dependsOn || []), ...(b.dependsOn || [])])].filter((d) => !own.has(d));
163
+ const merged = {title: `fit-merged-${seq}`, type: 'linked', dependsOn: deps, tasks: mergedTasks};
164
+ const rest = blocks.filter((_, k) => k !== i && k !== j).map((bl) => ({
165
+ ...bl,
166
+ dependsOn: (bl.dependsOn || []).map((d) => (own.has(d) ? merged.title : d)),
167
+ }));
168
+ const at = Math.min(i, j);
169
+ rest.splice(at, 0, merged);
170
+ return rest;
171
+ }
172
+
173
+ // 合并适配(纯函数):F − Σest ≥ floor 不满足 ⇒ 依次合并,直到满足或只剩一块。
174
+ // 返回 {blocks, unitMb, estMb, freeMb, floorMb, blockGrowth, merges, fits};merges 记录每一步(可审计)。
175
+ export function fitBlocks(blocks, freeMb, unitMb, cfg = memConfig()) {
176
+ let cur = (blocks || []).map((b) => ({...b}));
177
+ const merges = [];
178
+ const estMb = (bs) => bs.reduce((sum, b) => sum + blockCost(b, cfg.blockGrowth) * unitMb, 0);
179
+ const fits = (bs) => Number.isFinite(freeMb) ? freeMb - estMb(bs) >= cfg.floorMb : true;
180
+ let seq = 1;
181
+ while (cur.length > 1 && !fits(cur)) {
182
+ const [i, j, why] = pickPair(cur);
183
+ const titles = [cur[i].title, cur[j].title];
184
+ cur = mergeBlocks(cur, i, j, seq);
185
+ merges.push({step: seq, why, merged: titles});
186
+ seq += 1;
187
+ }
188
+ return {blocks: cur, unitMb, estMb: estMb(cur), freeMb, floorMb: cfg.floorMb, blockGrowth: cfg.blockGrowth, merges, fits: fits(cur)};
189
+ }
190
+
191
+ // 供日志用的一行描述。
192
+ export function describeFit(result) {
193
+ const m = result.merges.length ? result.merges.map((x) => `${x.step}:${x.why}(${x.merged.join('+')})`).join(' ') : 'none';
194
+ return `[mem-fit] units=${result.blocks.length} unitMb=${result.unitMb} growth=${result.blockGrowth} estMb=${result.estMb} freeMb=${result.freeMb} floorMb=${result.floorMb} fits=${result.fits} merges=${m}`;
195
+ }