@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.
- package/README.md +41 -0
- package/dist/{auto-route-BlWOWT4z.mjs → auto-route-BwzmlW3W.mjs} +2 -2
- package/dist/{auto-route-BlWOWT4z.mjs.map → auto-route-BwzmlW3W.mjs.map} +1 -1
- package/dist/{chain-CTtHligI.mjs → chain-DhVHVnmT.mjs} +263 -791
- package/dist/chain-DhVHVnmT.mjs.map +1 -0
- package/dist/cli/index.mjs +10 -8
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/program.mjs +10 -8
- package/dist/{clusters-wlK0w41E.mjs → clusters-DRHhfli9.mjs} +1 -1
- package/dist/{clusters-wlK0w41E.mjs.map → clusters-DRHhfli9.mjs.map} +1 -1
- package/dist/{config-B64vFg14.mjs → config-D1G9IFpn.mjs} +50 -3
- package/dist/config-D1G9IFpn.mjs.map +1 -0
- package/dist/{config-C_ErGddD.mjs → config-DD52BPB-.mjs} +1 -1
- package/dist/config-Dl8lT4Lu.mjs +527 -0
- package/dist/config-Dl8lT4Lu.mjs.map +1 -0
- package/dist/{context-handover-cache-BpUojjsi.mjs → context-handover-cache-C6_rs7JT.mjs} +112 -24
- package/dist/context-handover-cache-C6_rs7JT.mjs.map +1 -0
- package/dist/daemon/index.mjs +12 -10
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BSL9sXxm.mjs +22 -0
- package/dist/{daemon-CTkN_Vn8.mjs → daemon-CpdVUnl_.mjs} +26 -26
- package/dist/{daemon-CTkN_Vn8.mjs.map → daemon-CpdVUnl_.mjs.map} +1 -1
- package/dist/daemon-mcp/index.mjs +127 -6
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/detector-BLVOCiJ_.mjs +5 -0
- package/dist/{detector-Bwk_4Pk4.mjs → detector-smpCm7ku.mjs} +1 -1
- package/dist/{detector-Bwk_4Pk4.mjs.map → detector-smpCm7ku.mjs.map} +1 -1
- package/dist/{factory-A9x-T2Kg.mjs → factory-Bc5EyN54.mjs} +7 -7
- package/dist/{factory-A9x-T2Kg.mjs.map → factory-Bc5EyN54.mjs.map} +1 -1
- package/dist/factory-C7ua_3ik.mjs +3 -0
- package/dist/{providers-FYkZjn_C.mjs → fallback-CdXv-Np0.mjs} +411 -51
- package/dist/fallback-CdXv-Np0.mjs.map +1 -0
- package/dist/hooks/context-compression-hook.mjs +136 -22
- package/dist/hooks/context-compression-hook.mjs.map +4 -4
- package/dist/hooks/post-compact-inject.mjs +98 -7
- package/dist/hooks/post-compact-inject.mjs.map +4 -4
- package/dist/hooks/route-agents-to-worker.mjs +75 -6
- package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
- package/dist/hooks/worker-proxy.mjs +75 -6
- package/dist/hooks/worker-proxy.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +149 -90
- package/dist/hooks/worker-status-line.mjs.map +4 -4
- package/dist/{indexer-backend-TG64CCQC.mjs → indexer-backend-rohVVKrA.mjs} +1 -1
- package/dist/{indexer-backend-TG64CCQC.mjs.map → indexer-backend-rohVVKrA.mjs.map} +1 -1
- package/dist/{ipc-client-BmypMNYk.mjs → ipc-client-C7AVpsyv.mjs} +2 -2
- package/dist/{ipc-client-BmypMNYk.mjs.map → ipc-client-C7AVpsyv.mjs.map} +1 -1
- package/dist/{kg-entity-r8duqhi9.mjs → kg-entity-DbOMPdF9.mjs} +1 -1
- package/dist/{kg-entity-r8duqhi9.mjs.map → kg-entity-DbOMPdF9.mjs.map} +1 -1
- package/dist/{latent-ideas-B7wq75Pt.mjs → latent-ideas-iNp5s55R.mjs} +2 -2
- package/dist/{latent-ideas-B7wq75Pt.mjs.map → latent-ideas-iNp5s55R.mjs.map} +1 -1
- package/dist/{link-boost-HkG7JWZR.mjs → link-boost-NNzADHX_.mjs} +1 -1
- package/dist/{link-boost-HkG7JWZR.mjs.map → link-boost-NNzADHX_.mjs.map} +1 -1
- package/dist/{main-resolver-DlaLOFBA.mjs → main-resolver-CM1IHbuu.mjs} +78 -75
- package/dist/main-resolver-CM1IHbuu.mjs.map +1 -0
- package/dist/main-resolver-E5WlilhM.mjs +6 -0
- package/dist/{neighborhood-lThN-MaQ.mjs → neighborhood-AGL7oXQ2.mjs} +1 -1
- package/dist/{neighborhood-lThN-MaQ.mjs.map → neighborhood-AGL7oXQ2.mjs.map} +1 -1
- package/dist/{note-context-b6k0mAKi.mjs → note-context-BHwYpSFf.mjs} +1 -1
- package/dist/{note-context-b6k0mAKi.mjs.map → note-context-BHwYpSFf.mjs.map} +1 -1
- package/dist/{planner-BDI7bE9B.mjs → planner-Cm3g6fWH.mjs} +5 -3
- package/dist/{planner-BDI7bE9B.mjs.map → planner-Cm3g6fWH.mjs.map} +1 -1
- package/dist/{postgres-mW1n7Vi1.mjs → postgres-Dwxe290H.mjs} +1 -1
- package/dist/{postgres-mW1n7Vi1.mjs.map → postgres-Dwxe290H.mjs.map} +1 -1
- package/dist/{program-DXBwJV7h.mjs → program-AR0qlYRg.mjs} +334 -78
- package/dist/program-AR0qlYRg.mjs.map +1 -0
- package/dist/query-feedback-CGcEiGtY.mjs +3 -0
- package/dist/{query-feedback-BV4CcxqS.mjs → query-feedback-DZfjI34w.mjs} +1 -1
- package/dist/{query-feedback-BV4CcxqS.mjs.map → query-feedback-DZfjI34w.mjs.map} +1 -1
- package/dist/router-CAtyVQud.mjs +3 -0
- package/dist/{router-DcHKnEPa.mjs → router-DHracTUC.mjs} +1 -1
- package/dist/{router-DcHKnEPa.mjs.map → router-DHracTUC.mjs.map} +1 -1
- package/dist/server-BOuAOj9b.mjs +386 -0
- package/dist/server-BOuAOj9b.mjs.map +1 -0
- package/dist/skills/Worker/SKILL.md +19 -1
- package/dist/{sources-kLnQsNrW.mjs → sources-ju6AA7lL.mjs} +1 -1
- package/dist/{sources-kLnQsNrW.mjs.map → sources-ju6AA7lL.mjs.map} +1 -1
- package/dist/{sqlite-BenGr3UP.mjs → sqlite-kFZeWCJ2.mjs} +1 -1
- package/dist/{sqlite-BenGr3UP.mjs.map → sqlite-kFZeWCJ2.mjs.map} +1 -1
- package/dist/{state-Ca9F_sZc.mjs → state-DH_mp5Xe.mjs} +1 -1
- package/dist/{state-CAeyOdfq.mjs → state-ahFAbD8b.mjs} +1 -1
- package/dist/{state-CAeyOdfq.mjs.map → state-ahFAbD8b.mjs.map} +1 -1
- package/dist/{themes-BI4GMRP9.mjs → themes-DqhNUUQk.mjs} +1 -1
- package/dist/{themes-BI4GMRP9.mjs.map → themes-DqhNUUQk.mjs.map} +1 -1
- package/dist/{tools-DGcB3o_S.mjs → tools-CMlHa9lZ.mjs} +14 -14
- package/dist/{tools-DGcB3o_S.mjs.map → tools-CMlHa9lZ.mjs.map} +1 -1
- package/dist/{tools-Bp7hj6OW.mjs → tools-COna9vZR.mjs} +1 -1
- package/dist/{trace-bobARFEX.mjs → trace-DTP6k4B_.mjs} +1 -1
- package/dist/{trace-bobARFEX.mjs.map → trace-DTP6k4B_.mjs.map} +1 -1
- package/dist/{vault-indexer-C3OfXTNF.mjs → vault-indexer-YMH22mLU.mjs} +1 -1
- package/dist/{vault-indexer-C3OfXTNF.mjs.map → vault-indexer-YMH22mLU.mjs.map} +1 -1
- package/dist/{work-queue-worker-R7UGIag_.mjs → work-queue-worker-B3nl8yyn.mjs} +23 -26
- package/dist/work-queue-worker-B3nl8yyn.mjs.map +1 -0
- package/dist/work-queue-worker-BM1GcAAq.mjs +14 -0
- package/dist/{zettelkasten-m5QPtb-L.mjs → zettelkasten-DDvKRm2V.mjs} +3 -3
- package/dist/{zettelkasten-m5QPtb-L.mjs.map → zettelkasten-DDvKRm2V.mjs.map} +1 -1
- package/docs/commands/README.md +5 -2
- package/docs/commands/worker.md +66 -5
- package/docs/images/provider-abstraction-live.png +0 -0
- package/docs/images/workers.png +0 -0
- package/docs/launcher-independence.md +70 -0
- package/docs/provider-abstraction.md +87 -0
- package/docs/provider-independence-details.md +330 -0
- package/docs/provider-independence.md +75 -0
- package/docs/worker.md +72 -11
- package/package.json +1 -1
- package/scripts/build-hooks.mjs +6 -1
- package/scripts/build-skill-stubs.mjs +17 -4
- package/src/hooks/ts/lib/context-fill.test.ts +158 -4
- package/src/hooks/ts/lib/context-fill.ts +102 -35
- package/statusline-command.sh +120 -21
- package/dist/chain-CTtHligI.mjs.map +0 -1
- package/dist/config-B64vFg14.mjs.map +0 -1
- package/dist/context-handover-cache-BpUojjsi.mjs.map +0 -1
- package/dist/daemon-CmHHmKpg.mjs +0 -20
- package/dist/detector-DO730Zq0.mjs +0 -5
- package/dist/factory-BD-np0Vd.mjs +0 -3
- package/dist/main-resolver-D6IImXvF.mjs +0 -4
- package/dist/main-resolver-DlaLOFBA.mjs.map +0 -1
- package/dist/program-DXBwJV7h.mjs.map +0 -1
- package/dist/providers-FYkZjn_C.mjs.map +0 -1
- package/dist/query-feedback-DSVyHtrG.mjs +0 -3
- package/dist/router-Bk77E7hj.mjs +0 -3
- package/dist/runtime-paths-rni52zHX.mjs +0 -50
- package/dist/runtime-paths-rni52zHX.mjs.map +0 -1
- package/dist/work-queue-worker-DW8lz-Oo.mjs +0 -11
- package/dist/work-queue-worker-R7UGIag_.mjs.map +0 -1
package/docs/worker.md
CHANGED
|
@@ -265,6 +265,49 @@ A quota failure before the first tool call is re-run on the next provider and
|
|
|
265
265
|
logged as `WORKER-REROUTE`. `pai worker providers enable <name>` clears a
|
|
266
266
|
cooldown by hand.
|
|
267
267
|
|
|
268
|
+
## When the Anthropic plan runs out
|
|
269
|
+
|
|
270
|
+
`pai worker fallback on [provider]` switches the whole machine: every NEW
|
|
271
|
+
Claude Code process — interactive sessions, task-bus sessions, the daemon's
|
|
272
|
+
headless summarizer — runs on that worker provider instead of the Anthropic
|
|
273
|
+
login, until `pai worker fallback off`. Use it when the plan budget is gone
|
|
274
|
+
but work must continue.
|
|
275
|
+
|
|
276
|
+
What `on` does:
|
|
277
|
+
|
|
278
|
+
- Writes the provider into the `env` block of `~/.claude/settings.json`:
|
|
279
|
+
`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, the three
|
|
280
|
+
`ANTHROPIC_DEFAULT_*_MODEL` pins (fast model as haiku, default as sonnet
|
|
281
|
+
and opus), the provider's own env (`API_TIMEOUT_MS` …), plus
|
|
282
|
+
`ENABLE_TOOL_SEARCH=true` and
|
|
283
|
+
`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`.
|
|
284
|
+
- Pins the top-level `model` key to the provider's default model.
|
|
285
|
+
- Saves the replaced values under `workers.fallback.saved` in
|
|
286
|
+
`~/.config/pai/config.json` and writes a `FALLBACK-ACTIVE.md` note into the
|
|
287
|
+
workers log dir telling running sessions what to do.
|
|
288
|
+
|
|
289
|
+
Notes:
|
|
290
|
+
|
|
291
|
+
- **The token sits in settings.json.** `on` reads it from the provider's key
|
|
292
|
+
file at switch time; `off` removes it again. That is the price of switching
|
|
293
|
+
every process without touching each one's environment.
|
|
294
|
+
- `off` restores settings.json exactly — only the keys `on` touched change.
|
|
295
|
+
Both files are written atomically; `CLAUDE_SETTINGS_PATH` points `on`/`off`
|
|
296
|
+
at a copy for dry runs.
|
|
297
|
+
- **Running sessions are not switched.** A Claude Code process keeps the
|
|
298
|
+
provider it started with until restarted. `pai worker fallback status`
|
|
299
|
+
lists running sessions (AIBroker registry when populated, else `ps`) and
|
|
300
|
+
the note path; its one-line instruction for each of them: restart the
|
|
301
|
+
session in its project directory, keep the same AIBroker name.
|
|
302
|
+
- The Agent hook is unchanged: subagents keep routing to `pai worker run`
|
|
303
|
+
workers, which set their own per-provider env.
|
|
304
|
+
- `on` is idempotent (re-applying heals an interrupted switch), and switching
|
|
305
|
+
providers while on restores the first switch's savings before saving fresh.
|
|
306
|
+
- MCP: `worker_fallback` (action `on`/`off`/`status`, optional `provider`);
|
|
307
|
+
chat phrases "switch everything to glm", "fallback on", "back to
|
|
308
|
+
anthropic", "fallback off", "is fallback on" are mapped in the Worker
|
|
309
|
+
skill.
|
|
310
|
+
|
|
268
311
|
## What a worker is
|
|
269
312
|
|
|
270
313
|
- One `claude -p … --output-format stream-json --verbose` process per call,
|
|
@@ -340,8 +383,8 @@ behaviour — no prompt row, no ticker, stdin still the operator channel.
|
|
|
340
383
|
Any worker may start its own workers: the runner exports `PAI_WORKER_ID` in
|
|
341
384
|
every worker's environment, and a `pai worker run` launched from inside one
|
|
342
385
|
records `parent` in its status — so the forest is visible in `ps` (children
|
|
343
|
-
indented under their parent, `├`/`└` connectors)
|
|
344
|
-
|
|
386
|
+
indented under their parent, `├`/`└` connectors) and each child gets its own
|
|
387
|
+
follow pane. Handoffs travel **up
|
|
345
388
|
only**, from a child to its parent:
|
|
346
389
|
|
|
347
390
|
```
|
|
@@ -352,8 +395,8 @@ pai worker handoff '{"kind":"proposal","text":"run this on a cheap provider","da
|
|
|
352
395
|
`result` is sent automatically when a child finishes). The handoff is appended
|
|
353
396
|
to `<logDir>/<parent>.inbox.jsonl` (durable, ordered) and, when the parent is
|
|
354
397
|
running, also delivered as an operator message `[handoff from <child id>]`. The
|
|
355
|
-
parent sees it in its pane (`◆ from <id> · kind: text`, magenta), `ps`
|
|
356
|
-
|
|
398
|
+
parent sees it in its pane (`◆ from <id> · kind: text`, magenta), `ps` shows
|
|
399
|
+
`◆N` for an inbox with N handoffs, and `replay`/`follow`
|
|
357
400
|
merge them into the transcript by timestamp. There is no sideways channel:
|
|
358
401
|
siblings never see each other, everything goes up.
|
|
359
402
|
|
|
@@ -425,9 +468,9 @@ drive the desktop until it is handed over.
|
|
|
425
468
|
|
|
426
469
|
Status files carry `contextTokens` (input + cache read + cache creation +
|
|
427
470
|
output of the last assistant turn) and `contextWindow` (from the init event,
|
|
428
|
-
else the provider's `contextWindow`, else 200 000). The `ps` table
|
|
429
|
-
|
|
430
|
-
|
|
471
|
+
else the provider's `contextWindow`, else 200 000). The `ps` table shows
|
|
472
|
+
`ctx 84k/200k (42%)` once it passes 60 % — yellow past 70 %, red past
|
|
473
|
+
85 % — and the pane's liveness line always shows it.
|
|
431
474
|
|
|
432
475
|
### MCP for workers
|
|
433
476
|
|
|
@@ -516,7 +559,25 @@ the CLI calls.
|
|
|
516
559
|
|
|
517
560
|
## Status line
|
|
518
561
|
|
|
519
|
-
Line 4 of the statusline
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
562
|
+
Line 4 of the statusline is the worker bar: the provider and running-worker
|
|
563
|
+
count, one row per running spawned worker, and today's ✓/✗ tally. It prefers
|
|
564
|
+
the standalone `~/.claude/worker-status-line.mjs` (plain node, built by
|
|
565
|
+
`bun run build`) and falls back to `pai worker status-line`.
|
|
566
|
+
|
|
567
|
+
Example: `glm ▶2 | fix black buttons · 6m · grep … | spotcheck login · 1m · Edit: button.ts ✓75 ✗4 today`
|
|
568
|
+
|
|
569
|
+
Segment by segment:
|
|
570
|
+
|
|
571
|
+
- provider tag (`glm`) — the chat pane's provider; `workers` when there is no tracked chat pane and the running workers are mixed
|
|
572
|
+
- `▶2` — this terminal's running spawned workers: `state=running` **and** a live pid **and** not the chat pane; nothing at zero, so an idle terminal shows just `glm`
|
|
573
|
+
- row `name · age · step` — the worker's `--label` (else the first 70 chars of the prompt, `unlabeled` when the run had neither), time since it started, and the verb of what it is doing; `#id` (the short form `pai worker ps` shows) joins the name only when two running workers share a label
|
|
574
|
+
- step (`grep …`) — the current tool trimmed to its verb: no `Bash:` prefix, no flags, no quotes, ≤24 chars; file tools keep their `Edit: button.ts` shape
|
|
575
|
+
- `✓75 ✗4 today` — today's finished workers: done vs failed
|
|
576
|
+
|
|
577
|
+
The chat pane itself contributes only its provider — its age, state and
|
|
578
|
+
inbox live in `pai worker ps`, never in the bar. Rows are flat and
|
|
579
|
+
oldest-first; the worker forest (`↳` depth) and `◆N` inbox marks are `ps`
|
|
580
|
+
territory too. A status file from before the `origin` flag (a running entry
|
|
581
|
+
with no origin, the unlabeled placeholder and zero turns) is treated as the
|
|
582
|
+
chat pane so it does not render as a phantom worker — that shim goes away
|
|
583
|
+
once every pane runs code that writes `origin`.
|
package/package.json
CHANGED
package/scripts/build-hooks.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from "fs";
|
|
30
30
|
import { join, resolve, basename } from "path";
|
|
31
31
|
import { homedir, platform } from "os";
|
|
32
|
+
import { isInsideWorkerWorktree } from "./lib/sync-guard.mjs";
|
|
32
33
|
|
|
33
34
|
const HOOKS_SRC = "src/hooks/ts";
|
|
34
35
|
const HOOKS_OUT = "dist/hooks";
|
|
@@ -143,7 +144,11 @@ console.log(
|
|
|
143
144
|
// --sync: Symlink (or copy on Windows) all deployable files to ~/.claude/
|
|
144
145
|
// ---------------------------------------------------------------------------
|
|
145
146
|
|
|
146
|
-
if (doSync) {
|
|
147
|
+
if (doSync && isInsideWorkerWorktree(process.cwd())) {
|
|
148
|
+
// Never repoint the live ~/.claude symlinks at a worktree that `pai worker
|
|
149
|
+
// merge` will delete — see scripts/lib/sync-guard.mjs.
|
|
150
|
+
console.log("✔ Hook symlinks skipped: build runs inside a worker worktree");
|
|
151
|
+
} else if (doSync) {
|
|
147
152
|
const useSymlinks = platform() !== "win32";
|
|
148
153
|
const claudeDir = join(homedir(), ".claude");
|
|
149
154
|
const hooksTarget = join(claudeDir, "Hooks");
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from "fs";
|
|
33
33
|
import { join, resolve } from "path";
|
|
34
34
|
import { homedir, platform } from "os";
|
|
35
|
+
import { isInsideWorkerWorktree } from "./lib/sync-guard.mjs";
|
|
35
36
|
|
|
36
37
|
const PROMPTS_DIR = "src/daemon-mcp/prompts";
|
|
37
38
|
const CUSTOM_DIR = join(PROMPTS_DIR, "custom");
|
|
@@ -107,13 +108,21 @@ function syncSymlinks(generatedNames) {
|
|
|
107
108
|
const source = resolve(join(STUBS_OUT, name));
|
|
108
109
|
const target = join(skillsDir, name);
|
|
109
110
|
|
|
111
|
+
// lstat once: dangling links must count as symlinks (existsSync misses them)
|
|
112
|
+
let entry;
|
|
113
|
+
try {
|
|
114
|
+
entry = lstatSync(target);
|
|
115
|
+
} catch {
|
|
116
|
+
// absent — fresh install
|
|
117
|
+
}
|
|
118
|
+
|
|
110
119
|
// Never overwrite non-symlink directories (user's own skills)
|
|
111
|
-
if (
|
|
120
|
+
if (entry && !entry.isSymbolicLink()) {
|
|
112
121
|
continue;
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
// Check existing symlink
|
|
116
|
-
if (
|
|
124
|
+
// Check existing symlink (may be dangling)
|
|
125
|
+
if (entry) {
|
|
117
126
|
if (resolve(readlinkSync(target)) === source) {
|
|
118
127
|
current++;
|
|
119
128
|
continue;
|
|
@@ -197,6 +206,10 @@ for (const [fileName, dir] of [
|
|
|
197
206
|
const customLabel = customNames.length > 0 ? ` (${builtinNames.length} built-in + ${customNames.length} custom)` : "";
|
|
198
207
|
console.log(`✔ ${generated} skill stubs generated in ${STUBS_OUT}/${customLabel}`);
|
|
199
208
|
|
|
200
|
-
if (doSync) {
|
|
209
|
+
if (doSync && isInsideWorkerWorktree(process.cwd())) {
|
|
210
|
+
// Never repoint the live ~/.claude/skills symlinks at a worktree that
|
|
211
|
+
// `pai worker merge` will delete — see scripts/lib/sync-guard.mjs.
|
|
212
|
+
console.log("✔ Skill symlinks skipped: build runs inside a worker worktree");
|
|
213
|
+
} else if (doSync) {
|
|
201
214
|
syncSymlinks(generatedDirNames);
|
|
202
215
|
}
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
resolveAutocompactPct,
|
|
15
15
|
measureCompactionTrigger,
|
|
16
16
|
modelFamily,
|
|
17
|
+
isForeignModelFamily,
|
|
18
|
+
nativeModelFamilies,
|
|
17
19
|
selectedCompactionSamples,
|
|
18
20
|
transcriptModelFamily,
|
|
19
21
|
crossedThresholds,
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
DEFAULT_CONTEXT_WINDOW,
|
|
22
24
|
type ContextFillReading,
|
|
23
25
|
} from "./context-fill.js";
|
|
26
|
+
import { contextWindowFromModelId } from "../../../utils/model-window.js";
|
|
24
27
|
|
|
25
28
|
let root: string;
|
|
26
29
|
|
|
@@ -705,9 +708,12 @@ describe("measureCompactionTrigger — foreign-model transcripts are ignored", (
|
|
|
705
708
|
});
|
|
706
709
|
|
|
707
710
|
describe("modelFamily / transcriptModelFamily", () => {
|
|
708
|
-
it("
|
|
711
|
+
it("keys families from the id itself, not from an Anthropic allowlist", () => {
|
|
709
712
|
expect(modelFamily("claude-opus-5")).toBe("claude");
|
|
710
|
-
expect(modelFamily("other-model-1")).toBe("
|
|
713
|
+
expect(modelFamily("other-model-1")).toBe("other-model-1");
|
|
714
|
+
// the variant suffix is stripped, so one family has one key
|
|
715
|
+
expect(modelFamily("glm-5.3[1m]")).toBe("glm-5.3");
|
|
716
|
+
expect(modelFamily("glm-5.3")).toBe("glm-5.3");
|
|
711
717
|
expect(modelFamily("<synthetic>")).toBe("unknown");
|
|
712
718
|
expect(modelFamily(null)).toBe("unknown");
|
|
713
719
|
expect(modelFamily("")).toBe("unknown");
|
|
@@ -722,11 +728,159 @@ describe("modelFamily / transcriptModelFamily", () => {
|
|
|
722
728
|
[assistantLine("other-model-1"), compactBoundaryLine(1, "2026-09-17T09:00:00.000Z"), assistantLine("claude-x-1")].join("\n") + "\n"
|
|
723
729
|
);
|
|
724
730
|
expect(transcriptModelFamily(path)).toBe("claude");
|
|
725
|
-
writeFileSync(path, [assistantLine("claude-x-1"), assistantLine("
|
|
726
|
-
expect(transcriptModelFamily(path)).toBe("
|
|
731
|
+
writeFileSync(path, [assistantLine("claude-x-1"), assistantLine("glm-5.3[1m]"), assistantLine("<synthetic>")].join("\n") + "\n");
|
|
732
|
+
expect(transcriptModelFamily(path)).toBe("glm-5.3");
|
|
727
733
|
expect(transcriptModelFamily(join(dir, "missing.jsonl"))).toBe("unknown");
|
|
728
734
|
} finally {
|
|
729
735
|
rmSync(dir, { recursive: true, force: true });
|
|
730
736
|
}
|
|
731
737
|
});
|
|
732
738
|
});
|
|
739
|
+
|
|
740
|
+
// ---------------------------------------------------------------------------
|
|
741
|
+
// Foreignness is a comparison, not a prefix: a family is foreign exactly when
|
|
742
|
+
// it is neither Claude nor one the worker registry configures. Keying it on
|
|
743
|
+
// the claude- prefix made a non-Anthropic PRIMARY model read as foreign and
|
|
744
|
+
// lose its own compaction/handover history.
|
|
745
|
+
// ---------------------------------------------------------------------------
|
|
746
|
+
|
|
747
|
+
describe("isForeignModelFamily / nativeModelFamilies", () => {
|
|
748
|
+
it("claude and unknown are never foreign; anything else depends on the registry", () => {
|
|
749
|
+
expect(isForeignModelFamily("claude")).toBe(false);
|
|
750
|
+
expect(isForeignModelFamily("unknown")).toBe(false);
|
|
751
|
+
expect(isForeignModelFamily("glm-5.3", new Set())).toBe(true);
|
|
752
|
+
expect(isForeignModelFamily("glm-5.3", new Set(["glm-5.3"]))).toBe(false);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it("collects the families of every configured provider model", () => {
|
|
756
|
+
const dir = mkdtempSync(join(tmpdir(), "pai-native-families-"));
|
|
757
|
+
const configPath = join(dir, "config.json");
|
|
758
|
+
writeFileSync(
|
|
759
|
+
configPath,
|
|
760
|
+
JSON.stringify({
|
|
761
|
+
workers: {
|
|
762
|
+
enabled: true,
|
|
763
|
+
active: "glm",
|
|
764
|
+
providers: {
|
|
765
|
+
glm: {
|
|
766
|
+
baseUrl: "https://api.example.com/api/anthropic",
|
|
767
|
+
keyFile: null,
|
|
768
|
+
models: { default: "example-5.3[1m]", fast: "example-5.3-flash" },
|
|
769
|
+
env: {},
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
})
|
|
774
|
+
);
|
|
775
|
+
try {
|
|
776
|
+
const families = nativeModelFamilies(configPath);
|
|
777
|
+
expect(families.has("example-5.3")).toBe(true); // variant suffix stripped
|
|
778
|
+
expect(families.has("example-5.3-flash")).toBe(true);
|
|
779
|
+
expect(isForeignModelFamily("example-5.3", families)).toBe(false);
|
|
780
|
+
expect(isForeignModelFamily("other-model-1", families)).toBe(true);
|
|
781
|
+
} finally {
|
|
782
|
+
rmSync(dir, { recursive: true, force: true });
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
it("an unreadable registry means nothing is native (the historical rule)", () => {
|
|
787
|
+
expect(nativeModelFamilies("/nonexistent/pai-config.json").size).toBe(0);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// ---------------------------------------------------------------------------
|
|
792
|
+
// A provider-model primary session keeps its compaction history: the samples
|
|
793
|
+
// are glm, the registry configures glm, so nothing is discarded — before and
|
|
794
|
+
// after a simulated compaction alike.
|
|
795
|
+
// ---------------------------------------------------------------------------
|
|
796
|
+
|
|
797
|
+
describe("measureCompactionTrigger — provider-model transcripts are native", () => {
|
|
798
|
+
it("keeps glm compaction history across a simulated compaction, still drops foreign models", () => {
|
|
799
|
+
const dir = mkdtempSync(join(tmpdir(), "pai-native-trigger-"));
|
|
800
|
+
const configPath = join(dir, "config.json");
|
|
801
|
+
writeFileSync(
|
|
802
|
+
configPath,
|
|
803
|
+
JSON.stringify({
|
|
804
|
+
workers: {
|
|
805
|
+
enabled: true,
|
|
806
|
+
active: "glm",
|
|
807
|
+
providers: {
|
|
808
|
+
glm: {
|
|
809
|
+
baseUrl: "https://api.example.com/api/anthropic",
|
|
810
|
+
keyFile: null,
|
|
811
|
+
models: { default: "glm-5.3[1m]", fast: "glm-5.3-flash" },
|
|
812
|
+
env: {},
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
})
|
|
817
|
+
);
|
|
818
|
+
const projectsDir = join(dir, "projects");
|
|
819
|
+
const cwd = "/fake/project/glm";
|
|
820
|
+
const projectDir = join(projectsDir, encodeForFixture(cwd));
|
|
821
|
+
mkdirSync(projectDir, { recursive: true });
|
|
822
|
+
writeFileSync(
|
|
823
|
+
join(projectDir, "live.jsonl"),
|
|
824
|
+
[
|
|
825
|
+
assistantLine("glm-5.3[1m]"),
|
|
826
|
+
compactBoundaryLine(990_000, "2026-09-16T09:00:00.000Z", "u1"), // before
|
|
827
|
+
assistantLine("glm-5.3[1m]"),
|
|
828
|
+
compactBoundaryLine(784_000, "2026-09-17T09:00:00.000Z", "u2"), // after
|
|
829
|
+
].join("\n") + "\n"
|
|
830
|
+
);
|
|
831
|
+
// a foreign-model worker transcript must still not shape the trigger
|
|
832
|
+
writeFileSync(
|
|
833
|
+
join(projectDir, "worker.jsonl"),
|
|
834
|
+
[assistantLine("other-model-1"), compactBoundaryLine(151_000, "2026-09-17T09:30:00.000Z", "u3")].join("\n") + "\n"
|
|
835
|
+
);
|
|
836
|
+
try {
|
|
837
|
+
expect(selectedCompactionSamples(cwd, projectsDir, configPath).map((s) => s.preTokens))
|
|
838
|
+
.toEqual([784_000, 990_000]);
|
|
839
|
+
expect(measureCompactionTrigger(cwd, projectsDir, configPath)).toBe(784_000);
|
|
840
|
+
} finally {
|
|
841
|
+
rmSync(dir, { recursive: true, force: true });
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// ---------------------------------------------------------------------------
|
|
847
|
+
// Window size from the model id — a session whose model declares its window
|
|
848
|
+
// ("[1m]") is measured against that, not against the assumed 200k default.
|
|
849
|
+
// ---------------------------------------------------------------------------
|
|
850
|
+
|
|
851
|
+
describe("contextWindowFromModelId", () => {
|
|
852
|
+
it("reads the bracketed variant suffix, and only that", () => {
|
|
853
|
+
expect(contextWindowFromModelId("glm-5.3[1m]")).toBe(1_000_000);
|
|
854
|
+
expect(contextWindowFromModelId("glm-5.3")).toBeNull();
|
|
855
|
+
expect(contextWindowFromModelId("")).toBeNull();
|
|
856
|
+
expect(contextWindowFromModelId(null)).toBeNull();
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
describe("contextFillFromTranscript — window from the transcript's own model", () => {
|
|
861
|
+
const glmTurn = (tokens: number) => ({
|
|
862
|
+
type: "assistant",
|
|
863
|
+
message: { role: "assistant", model: "glm-5.3[1m]", usage: { input_tokens: tokens, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 } },
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it("derives a 1M window from a glm-5.3[1m] transcript when the caller has none", () => {
|
|
867
|
+
const p = join(root, "glm.jsonl");
|
|
868
|
+
writeJsonl(p, [glmTurn(500_000)]);
|
|
869
|
+
const reading = contextFillFromTranscript(p);
|
|
870
|
+
expect(reading.windowSize).toBe(1_000_000);
|
|
871
|
+
expect(reading.fraction).toBeCloseTo(0.5);
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
it("an explicit caller window still wins, and a bare id falls back to the default", () => {
|
|
875
|
+
const p = join(root, "glm.jsonl");
|
|
876
|
+
writeJsonl(p, [glmTurn(500_000)]);
|
|
877
|
+
expect(contextFillFromTranscript(p, 200_000).windowSize).toBe(200_000);
|
|
878
|
+
|
|
879
|
+
const bare = join(root, "bare.jsonl");
|
|
880
|
+
writeJsonl(bare, [{
|
|
881
|
+
type: "assistant",
|
|
882
|
+
message: { role: "assistant", model: "glm-5.3", usage: { input_tokens: 500, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 } },
|
|
883
|
+
}]);
|
|
884
|
+
expect(contextFillFromTranscript(bare).windowSize).toBe(DEFAULT_CONTEXT_WINDOW);
|
|
885
|
+
});
|
|
886
|
+
});
|
|
@@ -32,10 +32,14 @@
|
|
|
32
32
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
33
33
|
import { homedir, tmpdir } from "node:os";
|
|
34
34
|
import { join } from "node:path";
|
|
35
|
+
import {
|
|
36
|
+
DEFAULT_CONTEXT_WINDOW,
|
|
37
|
+
contextWindowFromModelId,
|
|
38
|
+
stripModelVariant,
|
|
39
|
+
} from "../../../utils/model-window.js";
|
|
40
|
+
import { readWorkersSection } from "../../../workers/config.js";
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
* default statusline-command.sh already falls back to. */
|
|
38
|
-
export const DEFAULT_CONTEXT_WINDOW = 200_000;
|
|
42
|
+
export { DEFAULT_CONTEXT_WINDOW };
|
|
39
43
|
|
|
40
44
|
/** How many trailing lines of the transcript to scan for a usage entry.
|
|
41
45
|
* A single turn is rarely more than a handful of JSONL lines (assistant
|
|
@@ -147,44 +151,60 @@ function usageTotal(usage: UsageEntry): number {
|
|
|
147
151
|
* `message.usage` entry, read from the end backwards so a trailing line with
|
|
148
152
|
* no usage field (a plain text turn, a tool_result) doesn't hide one just
|
|
149
153
|
* before it.
|
|
154
|
+
*
|
|
155
|
+
* The window size, when the caller has none, is derived from the transcript's
|
|
156
|
+
* own last assistant model id ("glm-5.3[1m]" → 1,000,000) before falling back
|
|
157
|
+
* to the assumed default — a session whose model declares its window should
|
|
158
|
+
* not be measured against a guess.
|
|
150
159
|
*/
|
|
151
160
|
export function contextFillFromTranscript(
|
|
152
161
|
transcriptPath: string,
|
|
153
|
-
windowSize
|
|
162
|
+
windowSize?: number
|
|
154
163
|
): ContextFillReading {
|
|
155
|
-
if (!transcriptPath || !existsSync(transcriptPath))
|
|
164
|
+
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
165
|
+
return unknownReading(windowSize ?? DEFAULT_CONTEXT_WINDOW);
|
|
166
|
+
}
|
|
156
167
|
|
|
157
168
|
let raw: string;
|
|
158
169
|
try {
|
|
159
170
|
raw = readFileSync(transcriptPath, "utf-8");
|
|
160
171
|
} catch {
|
|
161
|
-
return unknownReading(windowSize);
|
|
172
|
+
return unknownReading(windowSize ?? DEFAULT_CONTEXT_WINDOW);
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
const lines = raw.trim().split("\n").filter((l) => l.trim());
|
|
165
176
|
const tail = lines.slice(-TRANSCRIPT_TAIL_LINES);
|
|
166
177
|
|
|
178
|
+
// one pass from the end: the first usage entry is the fill reading, and the
|
|
179
|
+
// last assistant model seen (before or at that point) declares the window
|
|
180
|
+
let lastModel: string | null = null;
|
|
167
181
|
for (let i = tail.length - 1; i >= 0; i--) {
|
|
168
|
-
let entry: { message?: { usage?: UsageEntry } };
|
|
182
|
+
let entry: { type?: string; message?: { usage?: UsageEntry; model?: unknown } };
|
|
169
183
|
try {
|
|
170
184
|
entry = JSON.parse(tail[i]);
|
|
171
185
|
} catch {
|
|
172
186
|
continue;
|
|
173
187
|
}
|
|
188
|
+
if (entry?.type === "assistant" && lastModel === null) {
|
|
189
|
+
const model = entry.message?.model;
|
|
190
|
+
if (typeof model === "string" && model !== "") lastModel = model;
|
|
191
|
+
}
|
|
174
192
|
const usage = entry?.message?.usage;
|
|
175
193
|
if (usage && typeof usage === "object") {
|
|
194
|
+
const window = windowSize ?? contextWindowFromModelId(lastModel) ?? DEFAULT_CONTEXT_WINDOW;
|
|
176
195
|
const usedTokens = usageTotal(usage);
|
|
177
196
|
return {
|
|
178
197
|
status: "ok",
|
|
179
198
|
usedTokens,
|
|
180
|
-
windowSize,
|
|
181
|
-
fraction: usedTokens /
|
|
199
|
+
windowSize: window,
|
|
200
|
+
fraction: usedTokens / window,
|
|
182
201
|
source: "transcript",
|
|
183
202
|
};
|
|
184
203
|
}
|
|
185
204
|
}
|
|
186
205
|
|
|
187
|
-
|
|
206
|
+
const window = windowSize ?? contextWindowFromModelId(lastModel) ?? DEFAULT_CONTEXT_WINDOW;
|
|
207
|
+
return unknownReading(window);
|
|
188
208
|
}
|
|
189
209
|
|
|
190
210
|
// ---------------------------------------------------------------------------
|
|
@@ -195,18 +215,18 @@ export function getContextFill(
|
|
|
195
215
|
input: { sessionId?: string; transcriptPath?: string; windowSize?: number },
|
|
196
216
|
now = Date.now()
|
|
197
217
|
): ContextFillReading {
|
|
198
|
-
const windowSize = input.windowSize ?? DEFAULT_CONTEXT_WINDOW;
|
|
199
|
-
|
|
200
218
|
if (input.sessionId) {
|
|
201
219
|
const fromStatusline = readStatuslineFill(input.sessionId, now);
|
|
202
220
|
if (fromStatusline) return fromStatusline;
|
|
203
221
|
}
|
|
204
222
|
|
|
205
223
|
if (input.transcriptPath) {
|
|
206
|
-
|
|
224
|
+
// windowSize passes through only when the caller knows it; otherwise the
|
|
225
|
+
// transcript's own model id is asked first (see contextFillFromTranscript)
|
|
226
|
+
return contextFillFromTranscript(input.transcriptPath, input.windowSize);
|
|
207
227
|
}
|
|
208
228
|
|
|
209
|
-
return unknownReading(windowSize);
|
|
229
|
+
return unknownReading(input.windowSize ?? DEFAULT_CONTEXT_WINDOW);
|
|
210
230
|
}
|
|
211
231
|
|
|
212
232
|
// ---------------------------------------------------------------------------
|
|
@@ -369,23 +389,61 @@ function listProjectTranscripts(cwd: string, projectsDir: string): string[] {
|
|
|
369
389
|
const SYNTHETIC_MODEL = "<synthetic>";
|
|
370
390
|
|
|
371
391
|
/**
|
|
372
|
-
* Which family of model wrote a transcript
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
392
|
+
* Which family of model wrote a transcript: a stable key derived from the id
|
|
393
|
+
* itself, never from an Anthropic allowlist. "claude" is the platform's own;
|
|
394
|
+
* any other id reduces to its base form before any bracketed variant suffix
|
|
395
|
+
* ("glm-5.3[1m]" → "glm-5.3"), so the same model family always produces the
|
|
396
|
+
* same key; "unknown" is no usable model field at all. Whether a family is
|
|
397
|
+
* foreign is decided by comparison (see isForeignModelFamily), not by the
|
|
398
|
+
* key — keying "foreign" here is what made non-Anthropic primary models lose
|
|
399
|
+
* their own compaction history.
|
|
376
400
|
*/
|
|
377
|
-
export type TranscriptModelFamily = "claude" | "
|
|
401
|
+
export type TranscriptModelFamily = "claude" | "unknown" | (string & {});
|
|
378
402
|
|
|
379
403
|
export function modelFamily(model: string | null | undefined): TranscriptModelFamily {
|
|
380
404
|
if (typeof model !== "string" || model === "" || model === SYNTHETIC_MODEL) return "unknown";
|
|
381
|
-
|
|
405
|
+
const base = stripModelVariant(model);
|
|
406
|
+
return base.startsWith("claude-") ? "claude" : base;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Is a model id foreign to this machine's sessions? A model is native when it
|
|
411
|
+
* is a Claude model or one the worker registry configures (any provider's
|
|
412
|
+
* default or fast alias — the models this stack actually runs on). Anything
|
|
413
|
+
* else, with a readable model id, is foreign. Doubt (no id, unreadable
|
|
414
|
+
* registry) reads as native: the callers that skip work on "foreign" must not
|
|
415
|
+
* skip it on doubt.
|
|
416
|
+
*/
|
|
417
|
+
export function isForeignModelFamily(
|
|
418
|
+
family: TranscriptModelFamily,
|
|
419
|
+
nativeFamilies?: Set<string>
|
|
420
|
+
): boolean {
|
|
421
|
+
if (family === "unknown" || family === "claude") return false;
|
|
422
|
+
return !(nativeFamilies ?? nativeModelFamilies()).has(family);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Families of every model id the worker registry configures. Never throws:
|
|
426
|
+
* an unreadable or missing registry means "nothing configured", which leaves
|
|
427
|
+
* every non-Claude family foreign — the historical behaviour. */
|
|
428
|
+
export function nativeModelFamilies(configPath?: string): Set<string> {
|
|
429
|
+
const families = new Set<string>();
|
|
430
|
+
try {
|
|
431
|
+
for (const p of Object.values(readWorkersSection(configPath).workers.providers)) {
|
|
432
|
+
families.add(modelFamily(p.models.default));
|
|
433
|
+
if (p.models.fast) families.add(modelFamily(p.models.fast));
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
return families;
|
|
437
|
+
}
|
|
438
|
+
return families;
|
|
382
439
|
}
|
|
383
440
|
|
|
384
441
|
/**
|
|
385
442
|
* The family of the LAST assistant model in a transcript — scanned from the
|
|
386
443
|
* end so a long transcript costs one read and a few lines of parsing.
|
|
387
|
-
* Unreadable or model-less transcripts are "unknown", never
|
|
388
|
-
* the callers that skip work on
|
|
444
|
+
* Unreadable or model-less transcripts are "unknown", never foreign:
|
|
445
|
+
* the callers that skip work on foreign models must not skip it on doubt.
|
|
446
|
+
* Pair with isForeignModelFamily to judge the key.
|
|
389
447
|
*/
|
|
390
448
|
export function transcriptModelFamily(path: string): TranscriptModelFamily {
|
|
391
449
|
let raw: string;
|
|
@@ -417,7 +475,11 @@ export function transcriptModelFamily(path: string): TranscriptModelFamily {
|
|
|
417
475
|
* the event's uuid (falling back to a timestamp+preTokens key for the rare
|
|
418
476
|
* line with no uuid) so an event mirrored into `sessions/` is counted once.
|
|
419
477
|
*/
|
|
420
|
-
function readCompactBoundarySamples(
|
|
478
|
+
function readCompactBoundarySamples(
|
|
479
|
+
cwd: string,
|
|
480
|
+
projectsDir: string,
|
|
481
|
+
nativeFamilies: Set<string>
|
|
482
|
+
): CompactBoundarySample[] {
|
|
421
483
|
const byKey = new Map<string, CompactBoundarySample>();
|
|
422
484
|
|
|
423
485
|
for (const path of listProjectTranscripts(cwd, projectsDir)) {
|
|
@@ -430,12 +492,14 @@ function readCompactBoundarySamples(cwd: string, projectsDir: string): CompactBo
|
|
|
430
492
|
|
|
431
493
|
// The model governing each sample: the most recent assistant
|
|
432
494
|
// `message.model` seen in this file before the compact_boundary. A
|
|
433
|
-
// transcript written by a
|
|
434
|
-
// provider, with a different context window) compacts
|
|
435
|
-
// size and must not shape THIS project's trigger — two
|
|
436
|
-
// compacting at ~151k pulled a real project's trigger from
|
|
437
|
-
// ~151k.
|
|
438
|
-
//
|
|
495
|
+
// transcript written by a model foreign to this machine (a headless
|
|
496
|
+
// worker on another provider, with a different context window) compacts
|
|
497
|
+
// at a different size and must not shape THIS project's trigger — two
|
|
498
|
+
// such workers compacting at ~151k pulled a real project's trigger from
|
|
499
|
+
// ~784k to ~151k. Native is Claude plus every model the registry
|
|
500
|
+
// configures, so a non-Anthropic PRIMARY model keeps its own history.
|
|
501
|
+
// Samples with no model seen yet are kept: older transcripts may lack
|
|
502
|
+
// the field.
|
|
439
503
|
let lastModel: string | null = null;
|
|
440
504
|
let foreignDiscards = 0;
|
|
441
505
|
|
|
@@ -464,7 +528,7 @@ function readCompactBoundarySamples(cwd: string, projectsDir: string): CompactBo
|
|
|
464
528
|
if (typeof preTokens !== "number" || !Number.isFinite(preTokens)) continue;
|
|
465
529
|
const timestampMs = entry.timestamp ? Date.parse(entry.timestamp) : NaN;
|
|
466
530
|
if (!Number.isFinite(timestampMs)) continue;
|
|
467
|
-
if (modelFamily(lastModel)
|
|
531
|
+
if (isForeignModelFamily(modelFamily(lastModel), nativeFamilies)) {
|
|
468
532
|
foreignDiscards++;
|
|
469
533
|
continue;
|
|
470
534
|
}
|
|
@@ -478,7 +542,7 @@ function readCompactBoundarySamples(cwd: string, projectsDir: string): CompactBo
|
|
|
478
542
|
if (foreignDiscards > 0) {
|
|
479
543
|
console.error(
|
|
480
544
|
`[context-fill] ignored ${foreignDiscards} compaction sample(s) from a ` +
|
|
481
|
-
`
|
|
545
|
+
`foreign-model transcript (model=${lastModel}): ${path}`
|
|
482
546
|
);
|
|
483
547
|
}
|
|
484
548
|
}
|
|
@@ -495,10 +559,12 @@ function readCompactBoundarySamples(cwd: string, projectsDir: string): CompactBo
|
|
|
495
559
|
*/
|
|
496
560
|
export function selectedCompactionSamples(
|
|
497
561
|
cwd: string,
|
|
498
|
-
projectsDir: string = CLAUDE_PROJECTS_DIR
|
|
562
|
+
projectsDir: string = CLAUDE_PROJECTS_DIR,
|
|
563
|
+
configPath?: string
|
|
499
564
|
): CompactBoundarySample[] {
|
|
500
565
|
if (!cwd) return [];
|
|
501
|
-
return readCompactBoundarySamples(cwd, projectsDir
|
|
566
|
+
return readCompactBoundarySamples(cwd, projectsDir, nativeModelFamilies(configPath))
|
|
567
|
+
.slice(0, MEASURED_TRIGGER_SAMPLE_SIZE);
|
|
502
568
|
}
|
|
503
569
|
|
|
504
570
|
/**
|
|
@@ -514,9 +580,10 @@ export function selectedCompactionSamples(
|
|
|
514
580
|
*/
|
|
515
581
|
export function measureCompactionTrigger(
|
|
516
582
|
cwd: string,
|
|
517
|
-
projectsDir: string = CLAUDE_PROJECTS_DIR
|
|
583
|
+
projectsDir: string = CLAUDE_PROJECTS_DIR,
|
|
584
|
+
configPath?: string
|
|
518
585
|
): number | null {
|
|
519
|
-
const samples = selectedCompactionSamples(cwd, projectsDir);
|
|
586
|
+
const samples = selectedCompactionSamples(cwd, projectsDir, configPath);
|
|
520
587
|
if (samples.length === 0) return null;
|
|
521
588
|
const trigger = Math.min(...samples.map((s) => s.preTokens));
|
|
522
589
|
console.error(
|