@tekmidian/pai 0.39.0 → 0.40.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 (126) hide show
  1. package/README.md +41 -0
  2. package/dist/{auto-route-BlWOWT4z.mjs → auto-route-BwzmlW3W.mjs} +2 -2
  3. package/dist/{auto-route-BlWOWT4z.mjs.map → auto-route-BwzmlW3W.mjs.map} +1 -1
  4. package/dist/{chain-CTtHligI.mjs → chain-DhVHVnmT.mjs} +263 -791
  5. package/dist/chain-DhVHVnmT.mjs.map +1 -0
  6. package/dist/cli/index.mjs +10 -8
  7. package/dist/cli/index.mjs.map +1 -1
  8. package/dist/cli/program.mjs +10 -8
  9. package/dist/{clusters-wlK0w41E.mjs → clusters-DRHhfli9.mjs} +1 -1
  10. package/dist/{clusters-wlK0w41E.mjs.map → clusters-DRHhfli9.mjs.map} +1 -1
  11. package/dist/{config-B64vFg14.mjs → config-D1G9IFpn.mjs} +50 -3
  12. package/dist/config-D1G9IFpn.mjs.map +1 -0
  13. package/dist/{config-C_ErGddD.mjs → config-DD52BPB-.mjs} +1 -1
  14. package/dist/config-Dl8lT4Lu.mjs +527 -0
  15. package/dist/config-Dl8lT4Lu.mjs.map +1 -0
  16. package/dist/{context-handover-cache-BpUojjsi.mjs → context-handover-cache-C6_rs7JT.mjs} +112 -24
  17. package/dist/context-handover-cache-C6_rs7JT.mjs.map +1 -0
  18. package/dist/daemon/index.mjs +12 -10
  19. package/dist/daemon/index.mjs.map +1 -1
  20. package/dist/daemon-BSL9sXxm.mjs +22 -0
  21. package/dist/{daemon-CTkN_Vn8.mjs → daemon-CpdVUnl_.mjs} +26 -26
  22. package/dist/{daemon-CTkN_Vn8.mjs.map → daemon-CpdVUnl_.mjs.map} +1 -1
  23. package/dist/daemon-mcp/index.mjs +127 -6
  24. package/dist/daemon-mcp/index.mjs.map +1 -1
  25. package/dist/detector-BLVOCiJ_.mjs +5 -0
  26. package/dist/{detector-Bwk_4Pk4.mjs → detector-smpCm7ku.mjs} +1 -1
  27. package/dist/{detector-Bwk_4Pk4.mjs.map → detector-smpCm7ku.mjs.map} +1 -1
  28. package/dist/{factory-A9x-T2Kg.mjs → factory-Bc5EyN54.mjs} +7 -7
  29. package/dist/{factory-A9x-T2Kg.mjs.map → factory-Bc5EyN54.mjs.map} +1 -1
  30. package/dist/factory-C7ua_3ik.mjs +3 -0
  31. package/dist/{providers-FYkZjn_C.mjs → fallback-CdXv-Np0.mjs} +411 -51
  32. package/dist/fallback-CdXv-Np0.mjs.map +1 -0
  33. package/dist/hooks/context-compression-hook.mjs +136 -22
  34. package/dist/hooks/context-compression-hook.mjs.map +4 -4
  35. package/dist/hooks/post-compact-inject.mjs +98 -7
  36. package/dist/hooks/post-compact-inject.mjs.map +4 -4
  37. package/dist/hooks/route-agents-to-worker.mjs +75 -6
  38. package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
  39. package/dist/hooks/worker-proxy.mjs +75 -6
  40. package/dist/hooks/worker-proxy.mjs.map +2 -2
  41. package/dist/hooks/worker-status-line.mjs +149 -90
  42. package/dist/hooks/worker-status-line.mjs.map +4 -4
  43. package/dist/{indexer-backend-TG64CCQC.mjs → indexer-backend-rohVVKrA.mjs} +1 -1
  44. package/dist/{indexer-backend-TG64CCQC.mjs.map → indexer-backend-rohVVKrA.mjs.map} +1 -1
  45. package/dist/{ipc-client-BmypMNYk.mjs → ipc-client-C7AVpsyv.mjs} +2 -2
  46. package/dist/{ipc-client-BmypMNYk.mjs.map → ipc-client-C7AVpsyv.mjs.map} +1 -1
  47. package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
  48. package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
  49. package/dist/{latent-ideas-B7wq75Pt.mjs → latent-ideas-iNp5s55R.mjs} +2 -2
  50. package/dist/{latent-ideas-B7wq75Pt.mjs.map → latent-ideas-iNp5s55R.mjs.map} +1 -1
  51. package/dist/{link-boost-HkG7JWZR.mjs → link-boost-NNzADHX_.mjs} +1 -1
  52. package/dist/{link-boost-HkG7JWZR.mjs.map → link-boost-NNzADHX_.mjs.map} +1 -1
  53. package/dist/{main-resolver-DlaLOFBA.mjs → main-resolver-CM1IHbuu.mjs} +78 -75
  54. package/dist/main-resolver-CM1IHbuu.mjs.map +1 -0
  55. package/dist/main-resolver-E5WlilhM.mjs +6 -0
  56. package/dist/{neighborhood-lThN-MaQ.mjs → neighborhood-AGL7oXQ2.mjs} +1 -1
  57. package/dist/{neighborhood-lThN-MaQ.mjs.map → neighborhood-AGL7oXQ2.mjs.map} +1 -1
  58. package/dist/{note-context-b6k0mAKi.mjs → note-context-BHwYpSFf.mjs} +1 -1
  59. package/dist/{note-context-b6k0mAKi.mjs.map → note-context-BHwYpSFf.mjs.map} +1 -1
  60. package/dist/{planner-BDI7bE9B.mjs → planner-Cm3g6fWH.mjs} +5 -3
  61. package/dist/{planner-BDI7bE9B.mjs.map → planner-Cm3g6fWH.mjs.map} +1 -1
  62. package/dist/{postgres-mW1n7Vi1.mjs → postgres-Dwxe290H.mjs} +1 -1
  63. package/dist/{postgres-mW1n7Vi1.mjs.map → postgres-Dwxe290H.mjs.map} +1 -1
  64. package/dist/{program-DXBwJV7h.mjs → program-AR0qlYRg.mjs} +334 -78
  65. package/dist/program-AR0qlYRg.mjs.map +1 -0
  66. package/dist/query-feedback-CGcEiGtY.mjs +3 -0
  67. package/dist/{query-feedback-BV4CcxqS.mjs → query-feedback-DZfjI34w.mjs} +1 -1
  68. package/dist/{query-feedback-BV4CcxqS.mjs.map → query-feedback-DZfjI34w.mjs.map} +1 -1
  69. package/dist/router-CAtyVQud.mjs +3 -0
  70. package/dist/{router-DcHKnEPa.mjs → router-DHracTUC.mjs} +1 -1
  71. package/dist/{router-DcHKnEPa.mjs.map → router-DHracTUC.mjs.map} +1 -1
  72. package/dist/server-BOuAOj9b.mjs +386 -0
  73. package/dist/server-BOuAOj9b.mjs.map +1 -0
  74. package/dist/skills/Worker/SKILL.md +19 -1
  75. package/dist/{sources-kLnQsNrW.mjs → sources-ju6AA7lL.mjs} +1 -1
  76. package/dist/{sources-kLnQsNrW.mjs.map → sources-ju6AA7lL.mjs.map} +1 -1
  77. package/dist/{sqlite-BenGr3UP.mjs → sqlite-kFZeWCJ2.mjs} +1 -1
  78. package/dist/{sqlite-BenGr3UP.mjs.map → sqlite-kFZeWCJ2.mjs.map} +1 -1
  79. package/dist/{state-Ca9F_sZc.mjs → state-DH_mp5Xe.mjs} +1 -1
  80. package/dist/{state-CAeyOdfq.mjs → state-ahFAbD8b.mjs} +1 -1
  81. package/dist/{state-CAeyOdfq.mjs.map → state-ahFAbD8b.mjs.map} +1 -1
  82. package/dist/{themes-BI4GMRP9.mjs → themes-DqhNUUQk.mjs} +1 -1
  83. package/dist/{themes-BI4GMRP9.mjs.map → themes-DqhNUUQk.mjs.map} +1 -1
  84. package/dist/{tools-DGcB3o_S.mjs → tools-CMlHa9lZ.mjs} +14 -14
  85. package/dist/{tools-DGcB3o_S.mjs.map → tools-CMlHa9lZ.mjs.map} +1 -1
  86. package/dist/{tools-Bp7hj6OW.mjs → tools-COna9vZR.mjs} +1 -1
  87. package/dist/{trace-bobARFEX.mjs → trace-DTP6k4B_.mjs} +1 -1
  88. package/dist/{trace-bobARFEX.mjs.map → trace-DTP6k4B_.mjs.map} +1 -1
  89. package/dist/{vault-indexer-C3OfXTNF.mjs → vault-indexer-YMH22mLU.mjs} +1 -1
  90. package/dist/{vault-indexer-C3OfXTNF.mjs.map → vault-indexer-YMH22mLU.mjs.map} +1 -1
  91. package/dist/{work-queue-worker-R7UGIag_.mjs → work-queue-worker-B3nl8yyn.mjs} +23 -26
  92. package/dist/work-queue-worker-B3nl8yyn.mjs.map +1 -0
  93. package/dist/work-queue-worker-BM1GcAAq.mjs +14 -0
  94. package/dist/{zettelkasten-m5QPtb-L.mjs → zettelkasten-DDvKRm2V.mjs} +3 -3
  95. package/dist/{zettelkasten-m5QPtb-L.mjs.map → zettelkasten-DDvKRm2V.mjs.map} +1 -1
  96. package/docs/commands/README.md +5 -2
  97. package/docs/commands/worker.md +66 -5
  98. package/docs/images/provider-abstraction-live.png +0 -0
  99. package/docs/images/workers.png +0 -0
  100. package/docs/launcher-independence.md +70 -0
  101. package/docs/provider-abstraction.md +87 -0
  102. package/docs/provider-independence-details.md +330 -0
  103. package/docs/provider-independence.md +75 -0
  104. package/docs/worker.md +72 -11
  105. package/package.json +1 -1
  106. package/scripts/build-hooks.mjs +6 -1
  107. package/scripts/build-skill-stubs.mjs +17 -4
  108. package/src/hooks/ts/lib/context-fill.test.ts +158 -4
  109. package/src/hooks/ts/lib/context-fill.ts +102 -35
  110. package/statusline-command.sh +120 -21
  111. package/dist/chain-CTtHligI.mjs.map +0 -1
  112. package/dist/config-B64vFg14.mjs.map +0 -1
  113. package/dist/context-handover-cache-BpUojjsi.mjs.map +0 -1
  114. package/dist/daemon-CmHHmKpg.mjs +0 -20
  115. package/dist/detector-DO730Zq0.mjs +0 -5
  116. package/dist/factory-BD-np0Vd.mjs +0 -3
  117. package/dist/main-resolver-D6IImXvF.mjs +0 -4
  118. package/dist/main-resolver-DlaLOFBA.mjs.map +0 -1
  119. package/dist/program-DXBwJV7h.mjs.map +0 -1
  120. package/dist/providers-FYkZjn_C.mjs.map +0 -1
  121. package/dist/query-feedback-DSVyHtrG.mjs +0 -3
  122. package/dist/router-Bk77E7hj.mjs +0 -3
  123. package/dist/runtime-paths-rni52zHX.mjs +0 -50
  124. package/dist/runtime-paths-rni52zHX.mjs.map +0 -1
  125. package/dist/work-queue-worker-DW8lz-Oo.mjs +0 -11
  126. package/dist/work-queue-worker-R7UGIag_.mjs.map +0 -1
@@ -1,448 +1,12 @@
1
- import { n as CONFIG_FILE } from "./config-B64vFg14.mjs";
2
- import { appendFileSync, closeSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
1
+ import { a as WorkersConfigError, h as resolveModelCapability, l as isModelCapability, m as readWorkersSection, o as assertProviderRunnable, p as providerKeyPath, s as classModelCapability } from "./config-Dl8lT4Lu.mjs";
2
+ import { _ as workersLogDir, d as setCooldown, f as eventsPath, g as statusPath, h as panesDir, i as buildRunEnv, m as noMcpConfigPath, n as ensureProxyRunning, o as isQuotaFailure, p as ledgerPath, s as nextAutoProvider, t as DEFAULT_PROXY_PORT, u as resolveTarget } from "./server-BOuAOj9b.mjs";
3
+ import { appendFileSync, closeSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, renameSync, rmSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
3
4
  import { homedir, tmpdir } from "node:os";
4
5
  import { dirname, join, relative } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
6
  import { connect, createServer } from "node:net";
7
7
  import { execFileSync, spawn } from "node:child_process";
8
8
  import { createInterface } from "node:readline";
9
9
 
10
- //#region src/config/json-store.ts
11
- /**
12
- * json-store.ts — read/write JSON config files without destroying them
13
- *
14
- * The failure this exists to prevent:
15
- *
16
- * try { return JSON.parse(read(path)); } catch { return {}; }
17
- * ... later ...
18
- * write(path, JSON.stringify(ourData));
19
- *
20
- * An unreadable file becomes an empty object, and the next write makes that
21
- * permanent. Silently, exit code 0. This shape appeared three times in this
22
- * repo against three different files, and twice in AIBroker.
23
- *
24
- * The distinction that matters is between *missing* and *unreadable*:
25
- *
26
- * missing — legitimate first run. Start fresh; writing is safe.
27
- * unreadable — the file exists and we could not parse it. Those bytes are
28
- * the only copy of something. Never overwrite them.
29
- *
30
- * Collapsing the second into the first is the bug.
31
- *
32
- * NOT everything deserves this guard. For a transient buffer — an undelivered
33
- * message queue, a cache — starting fresh IS the correct recovery, and
34
- * refusing to write would disable the feature permanently. Use `writeJsonAtomic`
35
- * alone there: it still prevents a crash from truncating a good file, without
36
- * blocking recovery. Reserve `readJsonStrict` for data a user cannot rebuild.
37
- */
38
- /**
39
- * Read a JSON file, distinguishing "absent" from "damaged".
40
- *
41
- * @param path file to read
42
- * @param label how to name it to the user, e.g. "~/.claude.json"
43
- * @throws if the file exists but cannot be read or parsed
44
- */
45
- function readJsonStrict(path, label = path) {
46
- if (!existsSync(path)) return {};
47
- let raw;
48
- try {
49
- raw = readFileSync(path, "utf8");
50
- } catch (e) {
51
- throw new Error(`Could not read ${label}: ${e instanceof Error ? e.message : String(e)}\nRefusing to continue — writing now would replace its contents with ours alone.`);
52
- }
53
- try {
54
- return JSON.parse(raw);
55
- } catch (e) {
56
- throw new Error(`${label} exists but is not valid JSON: ${e instanceof Error ? e.message : String(e)}\nRefusing to continue — overwriting it would destroy whatever it holds.\nRepair the file, or move it aside and re-run this command.`);
57
- }
58
- }
59
- /**
60
- * Write JSON without risking the existing file.
61
- *
62
- * Keeps a `.bak-pai` copy of the previous contents, then writes to a temp file
63
- * and renames. Rename is atomic within a filesystem, so a crash mid-write
64
- * leaves the original intact rather than truncated — which is how these files
65
- * become corrupt in the first place.
66
- */
67
- function writeJsonAtomic(path, data, opts = {}) {
68
- const { backup = true, label = path } = opts;
69
- const serialized = JSON.stringify(data, null, 2) + "\n";
70
- const dir = dirname(path);
71
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
72
- if (backup && existsSync(path)) try {
73
- copyFileSync(path, `${path}.bak-pai`);
74
- } catch (e) {
75
- throw new Error(`Could not back up ${label}: ${e instanceof Error ? e.message : String(e)}\nRefusing to write without a backup.`);
76
- }
77
- const tmp = `${path}.tmp-pai-${process.pid}`;
78
- try {
79
- writeFileSync(tmp, serialized, "utf8");
80
- renameSync(tmp, path);
81
- } catch (e) {
82
- try {
83
- if (existsSync(tmp)) unlinkSync(tmp);
84
- } catch {}
85
- throw new Error(`Failed to write ${label}: ${e instanceof Error ? e.message : String(e)}\nThe original is unchanged.`);
86
- }
87
- }
88
-
89
- //#endregion
90
- //#region src/workers/config.ts
91
- /**
92
- * config.ts — the `workers` section of ~/.config/pai/config.json
93
- *
94
- * Everything that knows about worker providers reads this module: the CLI
95
- * (`pai worker …`), the MCP tools (worker_*), and the Agent-routing hook.
96
- * Keep it free of commander/MCP imports so all three layers stay thin over it.
97
- *
98
- * Keys are never stored here — only paths to 0600 files. A provider without a
99
- * keyFile is a local server and gets the placeholder token "local".
100
- *
101
- * `protocol: "openai"` providers run through the PAI proxy (src/workers/proxy):
102
- * `upstreamUrl` is their Chat Completions base, and `run` points
103
- * ANTHROPIC_BASE_URL at the local proxy with the provider name in the path.
104
- * `engine: "codex"` providers run through the Codex CLI instead of Claude
105
- * Code.
106
- */
107
- const DEFAULT_COST_TIER = 3;
108
- /** Tags a provider may carry; classes filter auto-routing on them. */
109
- const PROVIDER_TAGS = [
110
- "code",
111
- "vision",
112
- "image-gen",
113
- "long-context",
114
- "fast",
115
- "reasoning"
116
- ];
117
- /** The standard task classes; `workers.classes` maps each to a target. */
118
- const WORKER_CLASSES = [
119
- "draft",
120
- "plan",
121
- "implement",
122
- "review",
123
- "research",
124
- "spotcheck",
125
- "simple",
126
- "complex",
127
- "image"
128
- ];
129
- const DEFAULT_LOG_DIR = "~/.claude/logs/workers";
130
- const DEFAULT_PANE = {
131
- enabled: true,
132
- fontSize: 13,
133
- autoExitSecs: 60
134
- };
135
- const DEFAULT_ROUTING = {
136
- order: [],
137
- cooldownMinutes: 30,
138
- retryOnQuota: true
139
- };
140
- const DEFAULT_TREE = {
141
- maxDepth: 2,
142
- maxChildren: 4
143
- };
144
- /**
145
- * The default MCP sets every config starts from. `desktop` names the clickr
146
- * server so `--mcp desktop` hands a worker the machine controls (read-only
147
- * tools always; actuating ones after the operator hands the controls over,
148
- * see `pai worker controls`). A user's mcpSets section is merged over this,
149
- * so `desktop: []` removes the set deliberately.
150
- */
151
- const DEFAULT_MCP_SETS = { desktop: ["clickr"] };
152
- function defaultWorkersConfig() {
153
- return {
154
- enabled: false,
155
- active: null,
156
- providers: {},
157
- classes: {},
158
- mcpSets: { ...DEFAULT_MCP_SETS },
159
- pane: { ...DEFAULT_PANE },
160
- logDir: DEFAULT_LOG_DIR,
161
- routing: {
162
- ...DEFAULT_ROUTING,
163
- order: []
164
- },
165
- tree: { ...DEFAULT_TREE }
166
- };
167
- }
168
- var WorkersConfigError = class extends Error {};
169
- function bad(path, why) {
170
- throw new WorkersConfigError(`workers${path}: ${why}`);
171
- }
172
- function str(v) {
173
- return typeof v === "string" ? v : "";
174
- }
175
- function parseProvider(name, raw) {
176
- if (typeof raw !== "object" || raw === null) bad(`.providers.${name}`, "must be an object");
177
- const p = raw;
178
- const protocol = p.protocol === void 0 ? "anthropic" : str(p.protocol);
179
- if (protocol !== "anthropic" && protocol !== "openai") bad(`.providers.${name}.protocol`, `"${str(p.protocol)}" is neither "anthropic" nor "openai"`);
180
- const engine = p.engine === void 0 ? "claude" : str(p.engine);
181
- if (engine !== "claude" && engine !== "codex") bad(`.providers.${name}.engine`, `"${str(p.engine)}" is neither "claude" nor "codex"`);
182
- const baseUrl = str(p.baseUrl);
183
- if (!baseUrl && protocol !== "openai") bad(`.providers.${name}.baseUrl`, "is required");
184
- const keyFile = p.keyFile === void 0 || p.keyFile === null || str(p.keyFile) === "" ? null : str(p.keyFile);
185
- const modelsRaw = p.models === void 0 ? {} : p.models;
186
- if (typeof modelsRaw !== "object" || modelsRaw === null) bad(`.providers.${name}.models`, "must be an object");
187
- const m = modelsRaw;
188
- const defaultModel = str(m.default);
189
- if (!defaultModel) bad(`.providers.${name}.models.default`, "is required");
190
- const fast = m.fast === void 0 ? void 0 : str(m.fast);
191
- const env = {};
192
- if (p.env !== void 0) {
193
- if (typeof p.env !== "object" || p.env === null || Array.isArray(p.env)) bad(`.providers.${name}.env`, "must be an object of string values");
194
- for (const [k, v] of Object.entries(p.env)) {
195
- if (typeof v !== "string") bad(`.providers.${name}.env.${k}`, "must be a string");
196
- env[k] = v;
197
- }
198
- }
199
- const quotaSkipAt = p.quotaSkipAt === void 0 ? void 0 : p.quotaSkipAt;
200
- if (quotaSkipAt !== void 0) {
201
- if (typeof quotaSkipAt !== "number" || quotaSkipAt < 0 || quotaSkipAt > 100) bad(`.providers.${name}.quotaSkipAt`, "must be a number between 0 and 100");
202
- }
203
- const contextWindow = p.contextWindow === void 0 ? void 0 : p.contextWindow;
204
- if (contextWindow !== void 0) {
205
- if (typeof contextWindow !== "number" || contextWindow <= 0) bad(`.providers.${name}.contextWindow`, "must be a positive number of tokens");
206
- }
207
- const upstreamUrl = str(p.upstreamUrl);
208
- if (protocol === "openai" && !upstreamUrl) bad(`.providers.${name}.upstreamUrl`, `is required for protocol "openai" (the Chat Completions base, e.g. "https://api.openai.com/v1")`);
209
- const costTier = p.costTier === void 0 ? void 0 : p.costTier;
210
- if (costTier !== void 0) {
211
- if (typeof costTier !== "number" || !Number.isInteger(costTier) || costTier < 1 || costTier > 5) bad(`.providers.${name}.costTier`, "must be an integer 1 (cheapest) … 5 (most expensive)");
212
- }
213
- let tags;
214
- if (p.tags !== void 0) {
215
- if (!Array.isArray(p.tags) || p.tags.some((x) => typeof x !== "string")) bad(`.providers.${name}.tags`, `must be an array of tags from: ${PROVIDER_TAGS.join(", ")}`);
216
- for (const t of p.tags) if (!PROVIDER_TAGS.includes(t)) bad(`.providers.${name}.tags`, `"${t}" is not a tag (from: ${PROVIDER_TAGS.join(", ")})`);
217
- tags = p.tags;
218
- }
219
- return {
220
- enabled: p.enabled === void 0 ? true : p.enabled === true,
221
- protocol,
222
- baseUrl,
223
- keyFile,
224
- models: fast ? {
225
- default: defaultModel,
226
- fast
227
- } : { default: defaultModel },
228
- env,
229
- ...str(p.note) ? { note: str(p.note) } : {},
230
- ...upstreamUrl ? { upstreamUrl } : {},
231
- ...engine !== "claude" ? { engine } : {},
232
- ...str(p.quotaProbe) ? { quotaProbe: str(p.quotaProbe) } : {},
233
- ...quotaSkipAt !== void 0 ? { quotaSkipAt } : {},
234
- ...contextWindow !== void 0 ? { contextWindow } : {},
235
- ...costTier !== void 0 ? { costTier } : {},
236
- ...tags ? { tags } : {}
237
- };
238
- }
239
- /**
240
- * Parse and validate a raw `workers` value. Missing section → defaults.
241
- * Unknown-but-typed garbage → WorkersConfigError naming the offending field.
242
- */
243
- function parseWorkersConfig(raw) {
244
- const d = defaultWorkersConfig();
245
- if (raw === void 0 || raw === null) return d;
246
- if (typeof raw !== "object" || Array.isArray(raw)) bad("", "section must be an object");
247
- const w = raw;
248
- const providers = {};
249
- if (w.providers !== void 0) {
250
- if (typeof w.providers !== "object" || w.providers === null || Array.isArray(w.providers)) bad(".providers", "must be an object keyed by provider name");
251
- for (const [name, p] of Object.entries(w.providers)) providers[name] = parseProvider(name, p);
252
- }
253
- const classes = {};
254
- const classesRaw = w.classes !== void 0 ? w.classes : w.roles;
255
- if (classesRaw !== void 0) {
256
- if (typeof classesRaw !== "object" || classesRaw === null || Array.isArray(classesRaw)) bad(w.classes !== void 0 ? ".classes" : ".roles", "must be an object of class → provider[/alias] or {provider, mcp, maxCostTier, requireTags, order}");
257
- for (const [cls, target] of Object.entries(classesRaw)) if (typeof target === "object" && target !== null && !Array.isArray(target)) {
258
- const o = target;
259
- const provider = str(o.provider);
260
- if (o.provider !== void 0 && (!provider || provider.includes(" "))) bad(`.classes.${cls}.provider`, `invalid provider "${provider}"`);
261
- let mcp;
262
- if (o.mcp !== void 0) {
263
- if (!Array.isArray(o.mcp) || o.mcp.some((x) => typeof x !== "string")) bad(`.classes.${cls}.mcp`, "must be an array of MCP server or set names");
264
- mcp = o.mcp;
265
- }
266
- let maxCostTier;
267
- if (o.maxCostTier !== void 0) {
268
- if (typeof o.maxCostTier !== "number" || !Number.isInteger(o.maxCostTier) || o.maxCostTier < 1 || o.maxCostTier > 5) bad(`.classes.${cls}.maxCostTier`, "must be an integer 1 … 5");
269
- maxCostTier = o.maxCostTier;
270
- }
271
- let requireTags;
272
- if (o.requireTags !== void 0) {
273
- if (!Array.isArray(o.requireTags) || o.requireTags.some((x) => typeof x !== "string")) bad(`.classes.${cls}.requireTags`, `must be an array of tags from: ${PROVIDER_TAGS.join(", ")}`);
274
- for (const t of o.requireTags) if (!PROVIDER_TAGS.includes(t)) bad(`.classes.${cls}.requireTags`, `"${t}" is not a tag (from: ${PROVIDER_TAGS.join(", ")})`);
275
- requireTags = o.requireTags;
276
- }
277
- let order;
278
- if (o.order !== void 0) {
279
- if (!Array.isArray(o.order) || o.order.some((x) => typeof x !== "string")) bad(`.classes.${cls}.order`, "must be an array of provider names");
280
- order = o.order;
281
- }
282
- const obj = {
283
- ...provider ? { provider } : {},
284
- ...mcp ? { mcp } : {},
285
- ...maxCostTier !== void 0 ? { maxCostTier } : {},
286
- ...requireTags ? { requireTags } : {},
287
- ...order ? { order } : {}
288
- };
289
- classes[cls] = Object.keys(obj).length ? obj : {};
290
- } else {
291
- const t = str(target);
292
- if (!t || t.includes(" ")) bad(`.classes.${cls}`, `invalid target "${t}"`);
293
- classes[cls] = t;
294
- }
295
- }
296
- const mcpSets = { ...DEFAULT_MCP_SETS };
297
- if (w.mcpSets !== void 0) {
298
- if (typeof w.mcpSets !== "object" || w.mcpSets === null || Array.isArray(w.mcpSets)) bad(".mcpSets", "must be an object of set name → [server names]");
299
- for (const [setName, servers] of Object.entries(w.mcpSets)) {
300
- if (!Array.isArray(servers) || servers.some((x) => typeof x !== "string")) bad(`.mcpSets.${setName}`, "must be an array of MCP server names");
301
- mcpSets[setName] = servers;
302
- }
303
- }
304
- let pane = { ...DEFAULT_PANE };
305
- if (w.pane !== void 0) {
306
- if (typeof w.pane !== "object" || w.pane === null) bad(".pane", "must be an object");
307
- const pc = w.pane;
308
- if (pc.enabled !== void 0 && typeof pc.enabled !== "boolean") bad(".pane.enabled", "must be boolean");
309
- if (pc.fontSize !== void 0 && (typeof pc.fontSize !== "number" || pc.fontSize <= 0)) bad(".pane.fontSize", "must be a positive number of points");
310
- if (pc.autoExitSecs !== void 0 && typeof pc.autoExitSecs !== "number") bad(".pane.autoExitSecs", "must be a number");
311
- pane = {
312
- enabled: pc.enabled === void 0 ? DEFAULT_PANE.enabled : pc.enabled === true,
313
- fontSize: pc.fontSize === void 0 ? DEFAULT_PANE.fontSize : pc.fontSize,
314
- autoExitSecs: pc.autoExitSecs === void 0 ? DEFAULT_PANE.autoExitSecs : pc.autoExitSecs
315
- };
316
- }
317
- let routing = {
318
- ...DEFAULT_ROUTING,
319
- order: []
320
- };
321
- if (w.routing !== void 0) {
322
- if (typeof w.routing !== "object" || w.routing === null) bad(".routing", "must be an object");
323
- const r = w.routing;
324
- if (r.order !== void 0) {
325
- if (!Array.isArray(r.order) || r.order.some((x) => typeof x !== "string")) bad(".routing.order", "must be an array of provider names");
326
- routing.order = r.order;
327
- }
328
- if (r.cooldownMinutes !== void 0 && typeof r.cooldownMinutes !== "number") bad(".routing.cooldownMinutes", "must be a number");
329
- if (r.retryOnQuota !== void 0 && typeof r.retryOnQuota !== "boolean") bad(".routing.retryOnQuota", "must be boolean");
330
- routing = {
331
- order: routing.order,
332
- cooldownMinutes: r.cooldownMinutes === void 0 ? DEFAULT_ROUTING.cooldownMinutes : r.cooldownMinutes,
333
- retryOnQuota: r.retryOnQuota === void 0 ? DEFAULT_ROUTING.retryOnQuota : r.retryOnQuota === true
334
- };
335
- }
336
- let tree = { ...DEFAULT_TREE };
337
- if (w.tree !== void 0) {
338
- if (typeof w.tree !== "object" || w.tree === null || Array.isArray(w.tree)) bad(".tree", "must be an object");
339
- const t = w.tree;
340
- if (t.maxDepth !== void 0) {
341
- if (typeof t.maxDepth !== "number" || !Number.isInteger(t.maxDepth) || t.maxDepth < 0) bad(".tree.maxDepth", "must be a non-negative integer");
342
- }
343
- if (t.maxChildren !== void 0) {
344
- if (typeof t.maxChildren !== "number" || !Number.isInteger(t.maxChildren) || t.maxChildren < 1) bad(".tree.maxChildren", "must be a positive integer");
345
- }
346
- tree = {
347
- maxDepth: t.maxDepth === void 0 ? DEFAULT_TREE.maxDepth : t.maxDepth,
348
- maxChildren: t.maxChildren === void 0 ? DEFAULT_TREE.maxChildren : t.maxChildren
349
- };
350
- }
351
- const active = w.active === void 0 || w.active === null ? null : str(w.active);
352
- if (active !== null && active !== "auto" && !(active in providers)) {}
353
- return {
354
- enabled: w.enabled === void 0 ? d.enabled : w.enabled === true,
355
- active,
356
- providers,
357
- classes,
358
- mcpSets,
359
- pane,
360
- logDir: str(w.logDir) || d.logDir,
361
- routing,
362
- tree
363
- };
364
- }
365
- /**
366
- * Read the whole config file and return (raw, workers) — the raw record so
367
- * callers can rewrite it preserving every other section, the parsed+validated
368
- * workers section. Unreadable config throws (readJsonStrict), missing is fine.
369
- */
370
- function readWorkersSection() {
371
- const raw = readJsonStrict(CONFIG_FILE, "~/.config/pai/config.json");
372
- return {
373
- raw,
374
- workers: parseWorkersConfig(raw.workers)
375
- };
376
- }
377
- /** Write the workers section back into the config file, atomically. */
378
- function writeWorkersSection(raw, workers) {
379
- raw.workers = workers;
380
- writeJsonAtomic(CONFIG_FILE, raw, { label: "~/.config/pai/config.json" });
381
- }
382
- /** Expand a leading ~ (config values are written with `~` to stay portable). */
383
- function expandHome(p) {
384
- if (p === "~" || p.startsWith("~/")) return join(homedir(), p.slice(1));
385
- return p;
386
- }
387
- function assertProviderRunnable(name, p) {
388
- if (p.protocol === "openai" && !p.upstreamUrl) throw new WorkersConfigError(`provider "${name}" uses protocol "openai" but has no upstreamUrl — set its Chat Completions base (e.g. "https://api.openai.com/v1") so the PAI proxy knows where to translate to.`);
389
- }
390
- /** Where a provider's key file lives, or null. Shared by run + test + add. */
391
- function providerKeyPath(p) {
392
- return p.keyFile ? expandHome(p.keyFile) : null;
393
- }
394
- const DEFAULT_CONTEXT_WINDOW = 2e5;
395
- /** Cost tier of a provider for class filtering (unset = 3, the middle). */
396
- function providerCostTier(p) {
397
- return p.costTier ?? DEFAULT_COST_TIER;
398
- }
399
- /** Context window used by the meter when the init event carries none. */
400
- function providerContextWindow(p) {
401
- return p.contextWindow ?? DEFAULT_CONTEXT_WINDOW;
402
- }
403
- /** Directory under which inline keys (MCP `key` field) are stored, 0600. */
404
- function keysDir() {
405
- return join(homedir(), ".config", "pai", "keys");
406
- }
407
-
408
- //#endregion
409
- //#region src/workers/paths.ts
410
- /**
411
- * paths.ts — where worker run artefacts live.
412
- *
413
- * Everything a run writes (event mirror, status file, pane registry, routing
414
- * state, the empty MCP config) sits under one logDir so the whole tree is
415
- * disposable and configurable: `workers.logDir`, default ~/.claude/logs/workers.
416
- */
417
- /** Absolute logDir for the given config. */
418
- function workersLogDir(config) {
419
- return expandHome(config.logDir);
420
- }
421
- function statusPath(logDir, id) {
422
- return join(logDir, `${id}.status`);
423
- }
424
- function eventsPath(logDir, id) {
425
- return join(logDir, `${id}.jsonl`);
426
- }
427
- function ledgerPath(logDir) {
428
- return join(logDir, "ledger.log");
429
- }
430
- function routingStatePath(logDir) {
431
- return join(logDir, "routing-state.json");
432
- }
433
- function panesDir(logDir) {
434
- return join(logDir, "panes");
435
- }
436
- /**
437
- * The strict empty MCP config handed to headless workers. Written into the
438
- * logDir on demand (never into the user's vendor config directory — this is
439
- * PAI state, not vendor state).
440
- */
441
- function noMcpConfigPath(logDir) {
442
- return join(logDir, "no-mcp.json");
443
- }
444
-
445
- //#endregion
446
10
  //#region src/workers/args.ts
447
11
  function parseRunnerArgs(argv) {
448
12
  let prompt = null;
@@ -603,11 +167,27 @@ function ledgerSummary(path, scope, lastN = 15, now = /* @__PURE__ */ new Date()
603
167
  * runner wrote, plus `provider` (routing is multi-provider now) and `session`
604
168
  * (the AIBroker identity of the launching session, see scope.ts).
605
169
  */
170
+ /** Label a worker gets when launched with neither --label nor a prompt. */
171
+ const UNLABELED = "unlabeled";
606
172
  /** Context-meter percentage 0–100, null when the numbers are missing. */
607
173
  function contextPercent(s) {
608
174
  if (!s.contextTokens || !s.contextWindow) return null;
609
175
  return Math.round(s.contextTokens / s.contextWindow * 100);
610
176
  }
177
+ /** Compact token count: 84k, 200k, 900. */
178
+ function fmtContextK(n) {
179
+ return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
180
+ }
181
+ /**
182
+ * The one context-meter label, shared by every renderer so identical
183
+ * numbers always render identically: `ctx 84k/200k (42%)` — used-style
184
+ * percent. Empty when the numbers are missing (callers drop the part).
185
+ */
186
+ function contextLabel(s) {
187
+ const pct = contextPercent(s);
188
+ if (pct === null) return "";
189
+ return `ctx ${fmtContextK(s.contextTokens ?? 0)}/${fmtContextK(s.contextWindow ?? 0)} (${pct}%)`;
190
+ }
611
191
  /** Timestamp format shared by status files and the ledger. */
612
192
  function nowStamp(d = /* @__PURE__ */ new Date()) {
613
193
  const p = (n) => String(n).padStart(2, "0");
@@ -700,6 +280,12 @@ function describeTool(name, inp) {
700
280
  * matches (AIBroker absent, unnamed session, non-iTerm terminal).
701
281
  *
702
282
  * If neither is available, viewers fall back to "all workers".
283
+ *
284
+ * A third case: workers spawned from a Claude Code Bash tool (the
285
+ * orchestrator pattern) have neither — Claude Code exports no terminal or
286
+ * session identity to its Bash children. Those record `spawnerSession`, the
287
+ * orchestrator's claude session id bridged through the status line's
288
+ * session map (see below), so their tab still claims them.
703
289
  */
704
290
  const AIBROKER_REGISTRY = join(homedir(), ".aibroker", "session-names.json");
705
291
  /**
@@ -769,213 +355,32 @@ function scopeKey(term) {
769
355
  function sessionTag(worker) {
770
356
  return worker.session?.name ? `[${worker.session.name}]` : "";
771
357
  }
772
-
773
- //#endregion
774
- //#region src/workers/routing.ts
775
- /**
776
- * routing.ts — provider selection: flag > class > active (possibly "auto").
777
- *
778
- * Auto-routing walks `workers.routing.order` — or the class's own `order` —
779
- * and takes the first provider that is enabled, out of cooldown, (when it
780
- * defines a quotaProbe) under its quotaSkipAt threshold, and — when the class
781
- * constrains it — within `maxCostTier` and carrying all `requireTags`. A run
782
- * that dies of a quota/rate error puts its provider in cooldown for
783
- * cooldownMinutes; when that happens before the first tool call and
784
- * retryOnQuota is set, the runner restarts the same task on the next provider
785
- * (ledger: WORKER-REROUTE).
786
- *
787
- * An explicit --provider or a class mapping that pins a provider always
788
- * bypasses all of this.
789
- */
790
- const QUOTA_SKIP_DEFAULT = 95;
791
- const PROBE_TIMEOUT_MS = 1e4;
792
- function readRoutingState(logDir) {
793
- const path = routingStatePath(logDir);
794
- if (!existsSync(path)) return { cooldowns: {} };
795
- try {
796
- return { cooldowns: JSON.parse(readFileSync(path, "utf8")).cooldowns ?? {} };
797
- } catch {
798
- return { cooldowns: {} };
799
- }
800
- }
801
- function writeRoutingState(logDir, state) {
802
- const path = routingStatePath(logDir);
803
- const dir = dirname(path);
804
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
805
- const tmp = `${path}.tmp`;
806
- writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n", "utf8");
807
- renameSync(tmp, path);
808
- }
809
- function cooldownRemaining(state, provider, now = /* @__PURE__ */ new Date()) {
810
- const end = state.cooldowns[provider];
811
- if (!end) return 0;
812
- const ms = Date.parse(end) - now.getTime();
813
- return ms > 0 ? ms : 0;
814
- }
815
- function setCooldown(logDir, provider, minutes, now = /* @__PURE__ */ new Date()) {
816
- const state = readRoutingState(logDir);
817
- state.cooldowns[provider] = new Date(now.getTime() + minutes * 6e4).toISOString();
818
- writeRoutingState(logDir, state);
819
- }
820
- function clearCooldown(logDir, provider) {
821
- const state = readRoutingState(logDir);
822
- if (!(provider in state.cooldowns)) return;
823
- delete state.cooldowns[provider];
824
- writeRoutingState(logDir, state);
358
+ /** How old a map entry may be for a spawn to adopt it (status lines refresh constantly while a session lives). */
359
+ const SPAWNER_SESSION_TTL_MS = 10 * 6e4;
360
+ /** Map entries not refreshed within this window are pruned on write. */
361
+ const SESSION_MAP_PRUNE_MS = 60 * 6e4;
362
+ function sessionMapPath(logDir) {
363
+ return join(logDir, "claude-session-map.json");
825
364
  }
826
365
  /**
827
- * Run a provider's quotaProbe and return its percentage (0–100), or null when
828
- * there is no probe or it printed nothing usable. A probe must never break
829
- * routing: failures read as "unknown", not as "full".
366
+ * The claude session a new run was spawned by, when it can be known: a run
367
+ * inside another worker inherits its spawner (chain stages keep the
368
+ * orchestrator's tab that way); otherwise a fresh map entry for `cwd`.
830
369
  */
831
- function probeQuota(provider) {
832
- if (!provider.quotaProbe) return null;
370
+ function resolveSpawnerSession(logDir, cwd, env = process.env, now = Date.now()) {
371
+ const parentId = env.PAI_WORKER_ID;
372
+ if (parentId) {
373
+ const inherited = loadStatus(logDir, parentId)?.spawnerSession;
374
+ if (inherited) return inherited;
375
+ }
376
+ const path = sessionMapPath(logDir);
377
+ if (!existsSync(path) || !cwd) return null;
833
378
  try {
834
- const m = execFileSync("/bin/sh", ["-c", provider.quotaProbe], {
835
- timeout: PROBE_TIMEOUT_MS,
836
- encoding: "utf8",
837
- stdio: [
838
- "ignore",
839
- "pipe",
840
- "ignore"
841
- ]
842
- }).match(/(\d+(?:\.\d+)?)/);
843
- if (!m) return null;
844
- return Math.min(100, Math.max(0, Math.round(parseFloat(m[1]))));
845
- } catch {
846
- return null;
847
- }
848
- }
849
- function quotaSkipThreshold(provider) {
850
- return provider.quotaSkipAt ?? QUOTA_SKIP_DEFAULT;
851
- }
852
- /** Does this provider exceed its quota threshold right now? */
853
- function quotaExceeded(provider) {
854
- const used = probeQuota(provider);
855
- return used !== null && used >= quotaSkipThreshold(provider);
856
- }
857
- var NoProviderError = class extends WorkersConfigError {};
858
- function mustExist(config, name) {
859
- const p = config.providers[name];
860
- if (!p) throw new NoProviderError(`no worker provider named "${name}". Configured: ${Object.keys(config.providers).join(", ") || "(none)"}.\nAdd one with: pai worker providers add <name> --base-url <url> --key-file <path> --model <model>`);
861
- return p;
862
- }
863
- function mustBeRunnable(name, p) {
864
- if (!p.enabled) throw new NoProviderError(`provider "${name}" is disabled. Enable it with: pai worker providers enable ${name}`);
865
- return p;
866
- }
867
- /** Why one provider of a routing order did not qualify (message fragment). */
868
- function exclusionReason(config, state, name, cls) {
869
- const p = config.providers[name];
870
- if (!p) return "not configured";
871
- if (!p.enabled) return "disabled";
872
- if (cooldownRemaining(state, name) > 0) return "cooldown";
873
- if (quotaExceeded(p)) return "quota";
874
- const tier = providerCostTier(p);
875
- if (cls?.maxCostTier !== void 0 && tier > cls.maxCostTier) return `cost tier ${tier} > max ${cls.maxCostTier}`;
876
- if (cls?.requireTags?.length) {
877
- const have = p.tags ?? [];
878
- const missing = cls.requireTags.filter((t) => !have.includes(t));
879
- if (missing.length) return `missing tags: ${missing.join(", ")}`;
880
- }
881
- return null;
882
- }
883
- /**
884
- * Resolve which provider (and model alias) a run uses.
885
- *
886
- * @param flagProvider --provider value, highest precedence
887
- * @param className --class value (the old --role), looked up in workers.classes
888
- */
889
- function resolveTarget(config, logDir, opts = {}) {
890
- if (opts.flagProvider) return {
891
- providerName: opts.flagProvider,
892
- provider: mustBeRunnable(opts.flagProvider, mustExist(config, opts.flagProvider)),
893
- modelAlias: null,
894
- classMcp: null,
895
- via: "flag"
896
- };
897
- const clsTarget = opts.className ? config.classes[opts.className] : void 0;
898
- const cls = typeof clsTarget === "object" && clsTarget !== null ? clsTarget : void 0;
899
- if (opts.className && clsTarget !== void 0 && typeof clsTarget !== "object") {
900
- const [name, alias] = clsTarget.split("/");
901
- return {
902
- providerName: name,
903
- provider: mustBeRunnable(name, mustExist(config, name)),
904
- modelAlias: alias ?? null,
905
- classMcp: null,
906
- via: "class"
907
- };
908
- }
909
- if (opts.className && cls?.provider) {
910
- const provider = mustBeRunnable(cls.provider, mustExist(config, cls.provider));
911
- return {
912
- providerName: cls.provider,
913
- provider,
914
- modelAlias: null,
915
- classMcp: cls.mcp ?? null,
916
- via: "class"
917
- };
918
- }
919
- if (opts.className && clsTarget === void 0) {
920
- if (!WORKER_CLASSES.includes(opts.className)) throw new NoProviderError(`no class named "${opts.className}". Standard classes: ${WORKER_CLASSES.join(", ")}.Defined: ${Object.keys(config.classes).join(", ") || "(none)"}.\nSet one with: pai worker classes set ${opts.className}=<provider[/alias]>`);
921
- }
922
- if (config.active !== "auto") {
923
- const name = config.active;
924
- if (!name) throw new NoProviderError("no active worker provider. Add one with: pai worker providers add <name> --base-url <url> --key-file <path> --model <model>\n(or point one that exists at it: pai worker providers use <name>)");
925
- return {
926
- providerName: name,
927
- provider: mustBeRunnable(name, mustExist(config, name)),
928
- modelAlias: null,
929
- classMcp: null,
930
- via: "active"
931
- };
932
- }
933
- const state = readRoutingState(logDir);
934
- const order = cls?.order ?? config.routing.order;
935
- const excluded = [];
936
- for (const name of order) {
937
- const p = config.providers[name];
938
- const why = exclusionReason(config, state, name, cls);
939
- if (why) {
940
- excluded.push(`${name}: ${why}`);
941
- continue;
942
- }
943
- return {
944
- providerName: name,
945
- provider: p,
946
- modelAlias: null,
947
- classMcp: cls?.mcp ?? null,
948
- via: "auto"
949
- };
950
- }
951
- throw new NoProviderError(`auto-routing found no usable provider${opts.className ? ` for class "${opts.className}"` : ""} (order: [${order.join(", ")}]).${excluded.length ? `\nExcluded: ${excluded.join("; ")}.` : ""}\nClear a cooldown with: pai worker providers enable <name>; widen the class with: pai worker classes set ${opts.className ?? "<class>"}=<target>`);
952
- }
953
- /**
954
- * Next provider after `from` in auto order, applying the same filters.
955
- * Used by rerouting; null when the order is exhausted.
956
- */
957
- function nextAutoProvider(config, logDir, from, now = /* @__PURE__ */ new Date()) {
958
- const state = readRoutingState(logDir);
959
- const order = config.routing.order;
960
- const start = order.indexOf(from);
961
- for (let i = start + 1; i < order.length; i++) {
962
- const name = order[i];
963
- const p = config.providers[name];
964
- if (!p || !p.enabled) continue;
965
- const end = state.cooldowns[name];
966
- if (end && Date.parse(end) > now.getTime()) continue;
967
- if (quotaExceeded(p)) continue;
968
- return name;
969
- }
379
+ const entry = JSON.parse(readFileSync(path, "utf8"))[cwd];
380
+ if (entry && entry.session && now - entry.ts < SPAWNER_SESSION_TTL_MS) return entry.session;
381
+ } catch {}
970
382
  return null;
971
383
  }
972
- /**
973
- * Was this failure a quota/rate failure? Detected from the result text the
974
- * endpoint produced (HTTP status is not visible in the stream events).
975
- */
976
- function isQuotaFailure(resultText) {
977
- return /usage limit reached|rate limit|quota/i.test(resultText);
978
- }
979
384
 
980
385
  //#endregion
981
386
  //#region src/workers/pane.ts
@@ -987,7 +392,10 @@ function isQuotaFailure(resultText) {
987
392
  * one splits the lowest live worker pane horizontally, so panes stack top to
988
393
  * bottom. The split never sizes the new session — that would grow the whole
989
394
  * window; instead the window's bounds are read before the split and restored
990
- * right after, so the panes share the space the window already had. Each
395
+ * right after, so the panes share the space the window already had. A split
396
+ * would also make the new session the tab's active one — the scripts re-select
397
+ * the launching session right after, so a pane opening never steals the
398
+ * keystrokes the operator is typing. Each
991
399
  * pane runs `pai worker follow <id> --auto-exit <n>` under the `pai-worker`
992
400
  * dynamic profile (Close Sessions On End), so panes disappear by themselves.
993
401
  *
@@ -1066,6 +474,13 @@ const WORKER_SPLIT_SCRIPT = `on run(argv)
1066
474
  tell newS
1067
475
  write text followCmd
1068
476
  end tell
477
+ -- a split makes the new session the tab's active one:
478
+ -- re-select the launching session so keystrokes keep
479
+ -- going where the operator was typing, not into the
480
+ -- pane's chat prompt
481
+ try
482
+ select s
483
+ end try
1069
484
  try
1070
485
  set bounds of w to winBounds
1071
486
  end try
@@ -1149,6 +564,11 @@ const SPLIT_SCRIPT = `on run(argv)
1149
564
  tell newS
1150
565
  write text followCmd
1151
566
  end tell
567
+ -- keep the tab's active session where it was (see
568
+ -- WORKER_SPLIT_SCRIPT)
569
+ try
570
+ select s
571
+ end try
1152
572
  return "opened"
1153
573
  end if
1154
574
  end repeat
@@ -1476,9 +896,11 @@ function parentFromEnv(env = process.env) {
1476
896
  * Depth of `id` in the worker forest: 0 for a top-level worker, 1 + the
1477
897
  * parent's depth for a sub-worker. Parents without a status file (chain ids,
1478
898
  * unknown ids) count as roots; a cycle reads as its own depth and is cut off
1479
- * after the statuses it walked.
899
+ * after the statuses it walked. `chatIsRoot` stops one level short of the
900
+ * chat-pane tracker (origin "chat"): the status line treats the pane as the
901
+ * bar itself, so the workers it spawned are its top level.
1480
902
  */
1481
- function workerDepth(statuses, id) {
903
+ function workerDepth(statuses, id, opts) {
1482
904
  const byId = new Map(statuses.map((s) => [s.id, s]));
1483
905
  let depth = 0;
1484
906
  let cur = byId.get(id);
@@ -1487,7 +909,7 @@ function workerDepth(statuses, id) {
1487
909
  seen.add(cur.parent);
1488
910
  const next = byId.get(cur.parent);
1489
911
  if (!next) break;
1490
- depth += 1;
912
+ if (!(opts?.chatIsRoot && next.origin === "chat")) depth += 1;
1491
913
  cur = next;
1492
914
  }
1493
915
  return depth;
@@ -1722,7 +1144,7 @@ async function handoffFromInside(logDir, env, payload, deps = {}) {
1722
1144
  * commits its own work on that branch — the no-commit rule applies to the
1723
1145
  * main branch only — and the parent (or the operator) merges the result back:
1724
1146
  *
1725
- * pai worker merge <id> git merge --no-ff worker/<id> + remove worktree
1147
+ * pai worker merge <id> git merge --no-ff worker/<id> + remove worktree + delete branch
1726
1148
  * pai worker discard <id> remove worktree and branch, keep nothing
1727
1149
  *
1728
1150
  * `ps` marks a worker with an unmerged branch `⎇`. Draft and review run in
@@ -1873,26 +1295,160 @@ function removeWorktree(cwd, dir, force) {
1873
1295
  } catch {}
1874
1296
  }
1875
1297
  /**
1876
- * `pai worker merge <id>`: merge the worker's branch into the original
1877
- * checkout with --no-ff (the merge commit names the worker), then remove the
1878
- * worktree. The branch stays for the record; the status gains `merged: true`.
1298
+ * Tracked changes (staged or not) plus untracked files everything a
1299
+ * `git add -A` in `wtDir` would commit. The same two calls the old patch
1300
+ * carry used; name-only, not porcelain: a worktree-only change renders as
1301
+ * " M path" and the shared git() helper trims that leading space away.
1302
+ */
1303
+ function uncommittedPaths(wtDir) {
1304
+ const tracked = git(wtDir, [
1305
+ "diff",
1306
+ "--name-only",
1307
+ "HEAD"
1308
+ ]).split("\n").filter(Boolean);
1309
+ const untracked = git(wtDir, [
1310
+ "ls-files",
1311
+ "--others",
1312
+ "--exclude-standard"
1313
+ ]).split("\n").filter(Boolean);
1314
+ return [...tracked, ...untracked];
1315
+ }
1316
+ /**
1317
+ * Commit a worktree's uncommitted changes to its branch so the merge carries
1318
+ * them. `git merge` only moves committed work, so a worker that stopped
1319
+ * without committing would lose its edits to `worktree remove` — exactly
1320
+ * what happened live on 2026-09-17. Returns the salvaged paths, [] when the
1321
+ * worktree is clean. A failed commit throws with the worktree untouched:
1322
+ * its edits are still on disk, so nothing is lost.
1323
+ */
1324
+ function salvageUncommitted(wtDir, label) {
1325
+ const paths = uncommittedPaths(wtDir);
1326
+ if (!paths.length) return [];
1327
+ try {
1328
+ git(wtDir, ["add", "-A"]);
1329
+ git(wtDir, [
1330
+ "commit",
1331
+ "-m",
1332
+ `salvaged: ${label}`
1333
+ ]);
1334
+ } catch (e) {
1335
+ throw new Error(`cannot salvage the uncommitted changes in ${wtDir} — ${e.message}; nothing was lost: commit them there by hand, then re-run merge`);
1336
+ }
1337
+ return paths;
1338
+ }
1339
+ /**
1340
+ * The dirty paths of a checkout, parsed from `git status --porcelain -z`:
1341
+ * NUL-separated (a path with a newline in it cannot corrupt the parse),
1342
+ * rename entries contribute both sides, and any quoting is stripped.
1343
+ */
1344
+ function dirtyPaths(cwd) {
1345
+ const fields = execFileSync("git", [
1346
+ "-C",
1347
+ cwd,
1348
+ "status",
1349
+ "--porcelain",
1350
+ "-z"
1351
+ ], {
1352
+ encoding: "utf8",
1353
+ timeout: 3e4,
1354
+ stdio: [
1355
+ "ignore",
1356
+ "pipe",
1357
+ "pipe"
1358
+ ]
1359
+ }).split("\0");
1360
+ const strip = (p) => p.startsWith("\"") && p.endsWith("\"") ? p.slice(1, -1) : p;
1361
+ const out = [];
1362
+ for (let i = 0; i < fields.length; i++) {
1363
+ const f = fields[i];
1364
+ if (!f || f.length < 4 || f.charAt(2) !== " ") continue;
1365
+ out.push(strip(f.slice(3)));
1366
+ const xy = f.slice(0, 2);
1367
+ if (xy.includes("R") || xy.includes("C")) {
1368
+ const orig = fields[i + 1];
1369
+ if (orig && orig.charAt(2) !== " ") {
1370
+ out.push(strip(orig));
1371
+ i += 1;
1372
+ }
1373
+ }
1374
+ }
1375
+ return out;
1376
+ }
1377
+ /**
1378
+ * Refuse the merge when the original checkout is dirty in paths the branch
1379
+ * touches: the merge would overwrite those edits or fail on them, either way
1380
+ * leaving a half-state. No merge is made, the worktree stays.
1381
+ */
1382
+ function assertNoDirtyOverlap(cwd, incoming, id, branch) {
1383
+ const dirty = new Set(dirtyPaths(cwd));
1384
+ const overlap = [...new Set(incoming)].filter((p) => dirty.has(p)).sort();
1385
+ if (!overlap.length) return;
1386
+ throw new Error(`worker ${id}: the checkout ${cwd} has uncommitted changes in paths ${branch} touches: ${overlap.join(", ")}. Commit or stash them in the checkout, then re-run: pai worker merge ${id}. No merge was made; the worktree was kept.`);
1387
+ }
1388
+ /** Never remove a worktree that still holds uncommitted changes. */
1389
+ function assertWorktreeClean(wtDir, id) {
1390
+ const leftover = uncommittedPaths(wtDir);
1391
+ if (leftover.length) throw new Error(`worker ${id}: the worktree ${wtDir} still holds uncommitted changes (${leftover.join(", ")}) — it was NOT removed; commit or copy them by hand, then re-run merge`);
1392
+ }
1393
+ /**
1394
+ * `pai worker merge <id>`: salvage whatever the worker left uncommitted onto
1395
+ * its branch, refuse when the original checkout is dirty in paths the branch
1396
+ * touches, merge with --no-ff (the merge commit names the worker), then
1397
+ * remove the worktree and delete the branch; the status gains `merged: true`.
1398
+ * A branch with nothing to merge is refused loudly — after salvage that
1399
+ * genuinely means it holds nothing, and reporting success there would
1400
+ * destroy the worktree for no gain.
1879
1401
  */
1880
1402
  function mergeWorker(logDir, id) {
1881
1403
  const st = mustHaveBranch(logDir, id);
1882
1404
  if (st.merged) return `worker ${id}: branch ${st.branch} already merged`;
1883
- git(st.cwd, [
1884
- "merge",
1885
- "--no-ff",
1886
- st.branch,
1887
- "-m",
1888
- `merge worker ${id} (${st.label})`
1405
+ const salvaged = existsSync(st.worktreeDir) ? salvageUncommitted(st.worktreeDir, st.label || UNLABELED) : [];
1406
+ if ((parseInt(git(st.cwd, [
1407
+ "rev-list",
1408
+ "--count",
1409
+ `HEAD..${st.branch}`
1410
+ ]), 10) || 0) <= 0) throw new Error(`worker ${id}: branch ${st.branch} has no commits to merge. The worktree ${st.worktreeDir} was NOT removed — uncommitted work there would be destroyed. Commit it yourself, or drop everything with: pai worker discard ${id}`);
1411
+ const mergeBase = git(st.cwd, [
1412
+ "merge-base",
1413
+ "HEAD",
1414
+ st.branch
1889
1415
  ]);
1416
+ const incomingPaths = git(st.cwd, [
1417
+ "diff",
1418
+ "--name-only",
1419
+ mergeBase,
1420
+ st.branch
1421
+ ]).split("\n").filter(Boolean);
1422
+ assertNoDirtyOverlap(st.cwd, incomingPaths, id, st.branch);
1423
+ try {
1424
+ git(st.cwd, [
1425
+ "merge",
1426
+ "--no-ff",
1427
+ st.branch,
1428
+ "-m",
1429
+ `merge worker ${id} (${st.label})`
1430
+ ]);
1431
+ } catch (e) {
1432
+ throw new Error(`worker ${id}: git refused the merge of ${st.branch} — ${e.message}. The worktree ${st.worktreeDir} was kept: resolve the conflict, then re-run merge`);
1433
+ }
1434
+ if (existsSync(st.worktreeDir)) assertWorktreeClean(st.worktreeDir, id);
1890
1435
  removeWorktree(st.cwd, st.worktreeDir, false);
1436
+ let branchGone = true;
1437
+ try {
1438
+ git(st.cwd, [
1439
+ "branch",
1440
+ "-d",
1441
+ st.branch
1442
+ ]);
1443
+ } catch {
1444
+ branchGone = false;
1445
+ }
1891
1446
  saveStatus(logDir, {
1892
1447
  ...st,
1893
1448
  merged: true
1894
1449
  });
1895
- return `merged ${st.branch} into ${st.cwd} (worktree removed)`;
1450
+ const base = `merged ${st.branch} into ${st.cwd} (worktree removed${branchGone ? ", branch deleted" : "; branch kept: git refused -d"})`;
1451
+ return salvaged.length ? `${base}; salvaged ${salvaged.length} uncommitted change(s): ${salvaged.join(", ")}` : base;
1896
1452
  }
1897
1453
  /** `pai worker discard <id>`: drop worktree and branch, keep nothing. */
1898
1454
  function discardWorker(logDir, id) {
@@ -1936,7 +1492,8 @@ function worktreeSystemPrompt(id, branch, dir) {
1936
1492
  ` ${dir} on branch ${branch} (worker id ${id}).`,
1937
1493
  "Commit your work on that branch as you go (git add / git commit) — committing here is expected;",
1938
1494
  "the no-commit rule applies to the main branch only, and this is not it.",
1939
- "Do not merge, rebase or push; the operator merges your branch back with `pai worker merge`."
1495
+ "Do not merge, rebase or push; the operator merges your branch back with `pai worker merge`.",
1496
+ "Use ONLY relative paths inside the worktree, never absolute worktree paths — absolute paths break after merge and leak machine layout."
1940
1497
  ].join("\n");
1941
1498
  }
1942
1499
 
@@ -1961,6 +1518,8 @@ const WORKER_CONTRACT_PROMPT = [
1961
1518
  "changed: files you touched (path + one-line summary). commands: the commands that verify the work.",
1962
1519
  "checks: each with ok true/false and the evidence in detail. open: what you could not finish, if anything.",
1963
1520
  "notes: one line, the headline a reviewer reads first.",
1521
+ "Never write shell heredocs — write the script to a file with the Write tool, then run it",
1522
+ "(heredocs fail silently or get swallowed).",
1964
1523
  "",
1965
1524
  `Operator messages: a user turn starting with ${OPERATOR_MARK} was typed by the operator while you`,
1966
1525
  "run (the prompt of a resumed run is the operator's too). Answer it FIRST, in one or two plain lines —",
@@ -2123,101 +1682,6 @@ function describeMcp(config, claudeJson = CLAUDE_JSON) {
2123
1682
  return lines;
2124
1683
  }
2125
1684
 
2126
- //#endregion
2127
- //#region src/workers/proxy/server.ts
2128
- /**
2129
- * server.ts — the PAI worker proxy: Anthropic Messages API on the front,
2130
- * OpenAI Chat Completions on the back, loopback only.
2131
- *
2132
- * One proxy serves every OpenAI-protocol provider: Claude Code points
2133
- * ANTHROPIC_BASE_URL at `http://127.0.0.1:8797/<provider>` and the provider
2134
- * name in the path selects the upstream (`upstreamUrl` + `keyFile` from the
2135
- * workers config, re-read per request so config edits apply without a
2136
- * restart). `run` starts the proxy on demand (detached, pid file under the
2137
- * logDir) and `pai worker proxy stop` stops it again.
2138
- *
2139
- * All translation lives in translate.ts; this file is only HTTP plumbing.
2140
- */
2141
- const DEFAULT_PROXY_PORT = 8797;
2142
- const HOST = "127.0.0.1";
2143
- function proxyPidPath(logDir) {
2144
- return join(logDir, "proxy.pid");
2145
- }
2146
- /** True when something already accepts connections on the port. */
2147
- function proxyListening(port = DEFAULT_PROXY_PORT, timeoutMs = 400) {
2148
- return new Promise((resolve) => {
2149
- const sock = connect({
2150
- port,
2151
- host: HOST
2152
- });
2153
- const done = (ok) => {
2154
- sock.removeAllListeners();
2155
- sock.destroy();
2156
- resolve(ok);
2157
- };
2158
- sock.setTimeout(timeoutMs, () => done(false));
2159
- sock.once("connect", () => done(true));
2160
- sock.once("error", () => done(false));
2161
- });
2162
- }
2163
- /**
2164
- * Where the standalone proxy artefact lives, found from this module's own
2165
- * location (dist/cli bundle → ../hooks/, dev checkout → <repo>/dist/hooks).
2166
- */
2167
- function standaloneProxyPath() {
2168
- let dir = dirname(fileURLToPath(import.meta.url));
2169
- for (let i = 0; i < 6; i++) {
2170
- for (const cand of [
2171
- join(dir, "worker-proxy.mjs"),
2172
- join(dir, "hooks", "worker-proxy.mjs"),
2173
- join(dir, "dist", "hooks", "worker-proxy.mjs")
2174
- ]) if (existsSync(cand)) return cand;
2175
- const parent = dirname(dir);
2176
- if (parent === dir) break;
2177
- dir = parent;
2178
- }
2179
- return null;
2180
- }
2181
- /**
2182
- * Ensure a proxy is listening on the port; start the detached standalone when
2183
- * nothing answers. Returns the base URL (`http://127.0.0.1:<port>`).
2184
- */
2185
- async function ensureProxyRunning(port, logDir) {
2186
- if (await proxyListening(port)) return `http://${HOST}:${port}`;
2187
- const script = standaloneProxyPath();
2188
- if (!script) throw new Error(`the PAI worker proxy is not running and its build was not found — run \`bun run build\` (or start one with: pai worker proxy --port ${port})`);
2189
- if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
2190
- const child = spawn(process.execPath, [
2191
- script,
2192
- "--port",
2193
- String(port)
2194
- ], {
2195
- detached: true,
2196
- stdio: "ignore"
2197
- });
2198
- child.unref();
2199
- writeFileSync(proxyPidPath(logDir), `${child.pid}\n`, "utf8");
2200
- for (let i = 0; i < 40; i++) {
2201
- if (await proxyListening(port)) return `http://${HOST}:${port}`;
2202
- await new Promise((r) => setTimeout(r, 250));
2203
- }
2204
- throw new Error(`the PAI worker proxy did not come up on port ${port} (see ${proxyPidPath(logDir)})`);
2205
- }
2206
- /** `pai worker proxy stop`: SIGTERM the pid from the pid file. */
2207
- function stopProxy(logDir) {
2208
- const path = proxyPidPath(logDir);
2209
- if (!existsSync(path)) return "no proxy pid file — nothing to stop";
2210
- const pid = parseInt(readFileSync(path, "utf8").trim(), 10);
2211
- unlinkSync(path);
2212
- if (!Number.isFinite(pid)) return "stale proxy pid file removed";
2213
- try {
2214
- process.kill(pid, "SIGTERM");
2215
- } catch {
2216
- return `proxy pid ${pid} was not running (pid file removed)`;
2217
- }
2218
- return `proxy pid ${pid} stopped`;
2219
- }
2220
-
2221
1685
  //#endregion
2222
1686
  //#region src/workers/codex.ts
2223
1687
  /**
@@ -2413,34 +1877,6 @@ function codexInstalled() {
2413
1877
  * Auto-routed runs that die of a quota error before the first tool call are
2414
1878
  * restarted on the next provider in routing order (WORKER-REROUTE ledger line).
2415
1879
  */
2416
- /** Environment for a run through `provider`. Caller's env minus the Anthropic key. */
2417
- function buildRunEnv(provider, headless, proxyUrl) {
2418
- const env = { ...process.env };
2419
- delete env.ANTHROPIC_API_KEY;
2420
- let token = "local";
2421
- if (proxyUrl) env.ANTHROPIC_BASE_URL = proxyUrl;
2422
- else {
2423
- const keyPath = providerKeyPath(provider);
2424
- if (keyPath) {
2425
- try {
2426
- token = readFileSync(keyPath, "utf8").trim();
2427
- } catch {
2428
- throw new Error(`key file not readable: ${keyPath}`);
2429
- }
2430
- if (!token) throw new Error(`key file is empty: ${keyPath}`);
2431
- }
2432
- env.ANTHROPIC_BASE_URL = provider.baseUrl;
2433
- }
2434
- env.ANTHROPIC_AUTH_TOKEN = token;
2435
- env.ANTHROPIC_DEFAULT_HAIKU_MODEL = provider.models.fast ?? provider.models.default;
2436
- env.ANTHROPIC_DEFAULT_SONNET_MODEL = provider.models.default;
2437
- env.ANTHROPIC_DEFAULT_OPUS_MODEL = provider.models.default;
2438
- for (const [k, v] of Object.entries(provider.env)) env[k] = v;
2439
- env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
2440
- if (headless) env.PAI_WORKER = "1";
2441
- else env.ENABLE_TOOL_SEARCH = "true";
2442
- return env;
2443
- }
2444
1880
  /** The strict empty MCP config for headless workers, written on demand. */
2445
1881
  function ensureNoMcpConfig(logDir) {
2446
1882
  const path = noMcpConfigPath(logDir);
@@ -2484,10 +1920,35 @@ function usageContextTokens(u) {
2484
1920
  const t = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.output_tokens ?? 0);
2485
1921
  return t > 0 ? t : null;
2486
1922
  }
1923
+ /**
1924
+ * Context never shrinks mid-segment, so the status keeps the high-water
1925
+ * mark of the usage it has seen: a smaller later reading (short reply,
1926
+ * sidechain answer) must not drag the meter down. Compaction is the one
1927
+ * legitimate drop — see `resetContextTokensOnCompact`.
1928
+ */
1929
+ function bumpContextTokens(status, tokens) {
1930
+ if (tokens === null || tokens <= 0) return;
1931
+ status.contextTokens = Math.max(status.contextTokens ?? 0, tokens);
1932
+ }
1933
+ /**
1934
+ * A compact boundary (`system`/`compact_boundary`, the shape Claude Code
1935
+ * writes with `compactMetadata.preTokens`; `compact` kept as the older
1936
+ * spelling) legitimately restarts the context at a lower size: the floor
1937
+ * drops to the event's own usage — usually none — so the next usage
1938
+ * reading re-seeds the meter at the fresh, smaller context.
1939
+ */
1940
+ function resetContextTokensOnCompact(status, e) {
1941
+ status.contextTokens = usageContextTokens(e.usage) ?? null;
1942
+ }
1943
+ /** A compact boundary in a worker's stream, in either event spelling. */
1944
+ function isCompactBoundary(e) {
1945
+ return e.type === "system" && (e.subtype === "compact_boundary" || e.subtype === "compact");
1946
+ }
2487
1947
  /** Context window announced by the init event, when the endpoint sends one. */
2488
1948
  function initContextWindow(e) {
2489
1949
  if (typeof e.context_window === "number" && e.context_window > 0) return e.context_window;
2490
1950
  if (e.model_info && typeof e.model_info.context_window === "number" && e.model_info.context_window > 0) return e.model_info.context_window;
1951
+ if (/\[1m\]$/.test((e.model ?? "").trim())) return 1e6;
2491
1952
  return null;
2492
1953
  }
2493
1954
  /**
@@ -2500,7 +1961,7 @@ async function runWorker(opts) {
2500
1961
  const logDir = workersLogDir(config);
2501
1962
  mkdirSync(logDir, { recursive: true });
2502
1963
  if (opts.className === "plan" && !opts._planner) {
2503
- const { runPlanner } = await import("./planner-BDI7bE9B.mjs");
1964
+ const { runPlanner } = await import("./planner-Cm3g6fWH.mjs");
2504
1965
  return runPlanner(opts);
2505
1966
  }
2506
1967
  const parent = launchParent(opts.parent);
@@ -2511,8 +1972,10 @@ async function runWorker(opts) {
2511
1972
  });
2512
1973
  assertProviderRunnable(target.providerName, target.provider);
2513
1974
  const parsed = parseRunnerArgs(opts.claudeArgs);
2514
- const label = opts.label ?? shortText(parsed.prompt ?? "(no prompt)", 70);
2515
- const model = opts.modelFlag ?? (target.modelAlias === "fast" ? target.provider.models.fast ?? target.provider.models.default : target.provider.models.default);
1975
+ const label = opts.label ?? shortText(parsed.prompt ?? UNLABELED, 70);
1976
+ const alias = target.modelAlias;
1977
+ const capability = alias && isModelCapability(alias) ? alias : classModelCapability(opts.className);
1978
+ const model = opts.modelFlag ?? resolveModelCapability(target.provider, capability);
2516
1979
  try {
2517
1980
  if (target.provider.engine === "codex") return await executeCodexRun({
2518
1981
  config,
@@ -2567,6 +2030,7 @@ async function executeRun(a) {
2567
2030
  const cwd = a.cwd ?? process.cwd();
2568
2031
  const term = process.env.ITERM_SESSION_ID ?? "";
2569
2032
  const session = resolveSession(term);
2033
+ const spawnerSession = resolveSpawnerSession(logDir, cwd);
2570
2034
  let worktree = null;
2571
2035
  if (headless && worktreeWanted(a.worktreeFlag, {
2572
2036
  cwd,
@@ -2599,8 +2063,9 @@ async function executeRun(a) {
2599
2063
  last: headless ? "starting" : "interactive",
2600
2064
  rc: null,
2601
2065
  secs: null,
2066
+ origin: headless ? "spawn" : "chat",
2602
2067
  ...session ? { session } : {},
2603
- contextWindow: providerContextWindow(target.provider),
2068
+ ...spawnerSession ? { spawnerSession } : {},
2604
2069
  ...a.parent ? {
2605
2070
  parent: a.parent,
2606
2071
  stage: a.stage
@@ -2749,10 +2214,12 @@ async function executeRun(a) {
2749
2214
  const cw = initContextWindow(e);
2750
2215
  if (cw) status.contextWindow = cw;
2751
2216
  saveStatus(logDir, status);
2217
+ } else if (isCompactBoundary(e)) {
2218
+ resetContextTokensOnCompact(status, e);
2219
+ saveStatus(logDir, status);
2752
2220
  } else if (e.type === "assistant") {
2753
2221
  status.turns += 1;
2754
- const tokens = usageContextTokens(e.message?.usage);
2755
- if (tokens) status.contextTokens = tokens;
2222
+ bumpContextTokens(status, usageContextTokens(e.message?.usage));
2756
2223
  for (const block of e.message?.content ?? []) if (block.type === "tool_use") {
2757
2224
  status.tools += 1;
2758
2225
  status.last = describeTool(block.name ?? "?", block.input);
@@ -2760,7 +2227,8 @@ async function executeRun(a) {
2760
2227
  saveStatus(logDir, status);
2761
2228
  } else if (e.type === "result") {
2762
2229
  const tokens = usageContextTokens(e.usage);
2763
- if (tokens) status.contextTokens = tokens;
2230
+ if (e.is_compact) status.contextTokens = tokens ?? status.contextTokens;
2231
+ else bumpContextTokens(status, tokens);
2764
2232
  const report = parseWorkerReport(e.result ?? "");
2765
2233
  if (report?.notes) status.last = shortText(report.notes, 90);
2766
2234
  else if (e.result) status.last = shortText(e.result, 90);
@@ -2861,6 +2329,7 @@ async function executeCodexRun(a) {
2861
2329
  const cwd = a.cwd ?? process.cwd();
2862
2330
  const term = process.env.ITERM_SESSION_ID ?? "";
2863
2331
  const session = resolveSession(term);
2332
+ const spawnerSession = resolveSpawnerSession(logDir, cwd);
2864
2333
  let worktree = null;
2865
2334
  if (worktreeWanted(a.worktreeFlag, {
2866
2335
  cwd,
@@ -2894,8 +2363,10 @@ async function executeCodexRun(a) {
2894
2363
  last: "starting",
2895
2364
  rc: null,
2896
2365
  secs: null,
2366
+ origin: "spawn",
2897
2367
  ...session ? { session } : {},
2898
- contextWindow: providerContextWindow(target.provider),
2368
+ ...spawnerSession ? { spawnerSession } : {},
2369
+ ...target.provider.contextWindow ? { contextWindow: target.provider.contextWindow } : {},
2899
2370
  ...a.parent ? {
2900
2371
  parent: a.parent,
2901
2372
  stage: a.stage
@@ -2948,7 +2419,8 @@ async function executeCodexRun(a) {
2948
2419
  type: "system",
2949
2420
  subtype: "init",
2950
2421
  model,
2951
- cwd
2422
+ cwd,
2423
+ ...target.provider.contextWindow ? { context_window: target.provider.contextWindow } : {}
2952
2424
  });
2953
2425
  let killed = false;
2954
2426
  process.once("SIGTERM", onCodexSignal("SIGTERM"));
@@ -2976,7 +2448,7 @@ async function executeCodexRun(a) {
2976
2448
  status.turns = fold.turns;
2977
2449
  status.tools = fold.tools;
2978
2450
  if (fold.last) status.last = shortText(fold.last, 90);
2979
- if (fold.contextTokens) status.contextTokens = fold.contextTokens;
2451
+ bumpContextTokens(status, fold.contextTokens);
2980
2452
  saveStatus(logDir, status);
2981
2453
  for (const ev of fold.events.splice(0)) writeEvent(ev);
2982
2454
  });
@@ -3321,5 +2793,5 @@ async function runChain(opts, deps = {}) {
3321
2793
  }
3322
2794
 
3323
2795
  //#endregion
3324
- export { contextPercent as A, workersLogDir as B, quotaSkipThreshold as C, workerInScope as D, sessionTag as E, ledgerSummary as F, keysDir as G, PROVIDER_TAGS as H, parseRunnerArgs as I, readWorkersSection as J, parseWorkersConfig as K, shortText as L, loadStatuses as M, newWorkerId as N, ageOf as O, appendLedger as P, eventsPath as R, probeQuota as S, resolveSession as T, WorkersConfigError as U, DEFAULT_LOG_DIR as V, expandHome as W, readJsonStrict as X, writeWorkersSection as Y, writeJsonAtomic as Z, workerDepth as _, testProvider as a, openPaneForWorker as b, stopProxy as c, renderReport as d, discardWorker as f, sayToWorker as g, readInbox as h, runWorker as i, loadStatus as j, alive as k, describeMcp as l, handoffFromInside as m, swapPromptArg as n, DEFAULT_PROXY_PORT as o, mergeWorker as p, providerCostTier as q, printResult as r, ensureProxyRunning as s, runChain as t, parseWorkerReport as u, checkPaneForWorker as v, currentTabKey as w, clearCooldown as x, openFollowPane as y, ledgerPath as z };
3325
- //# sourceMappingURL=chain-CTtHligI.mjs.map
2796
+ export { appendLedger as A, ageOf as C, loadStatus as D, contextPercent as E, parseRunnerArgs as M, shortText as N, loadStatuses as O, UNLABELED as S, contextLabel as T, openPaneForWorker as _, testProvider as a, sessionTag as b, renderReport as c, handoffFromInside as d, readInbox as f, openFollowPane as g, checkPaneForWorker as h, runWorker as i, ledgerSummary as j, newWorkerId as k, discardWorker as l, workerDepth as m, swapPromptArg as n, describeMcp as o, sayToWorker as p, printResult as r, parseWorkerReport as s, runChain as t, mergeWorker as u, currentTabKey as v, alive as w, workerInScope as x, resolveSession as y };
2797
+ //# sourceMappingURL=chain-DhVHVnmT.mjs.map