@tekmidian/pai 0.39.1 → 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-CqlgL7uH.mjs → chain-DhVHVnmT.mjs} +263 -821
- 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 +76 -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/{fallback-DqYhiACU.mjs → fallback-CdXv-Np0.mjs} +139 -49
- 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 +36 -2
- package/dist/hooks/route-agents-to-worker.mjs.map +2 -2
- package/dist/hooks/worker-proxy.mjs +36 -2
- package/dist/hooks/worker-proxy.mjs.map +2 -2
- package/dist/hooks/worker-status-line.mjs +110 -86
- 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-DGV8VFib.mjs → planner-Cm3g6fWH.mjs} +5 -3
- package/dist/{planner-DGV8VFib.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-B-QJXCRb.mjs → program-AR0qlYRg.mjs} +309 -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 +6 -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 +4 -2
- package/docs/commands/worker.md +48 -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 +29 -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-CqlgL7uH.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/fallback-DqYhiACU.mjs.map +0 -1
- package/dist/main-resolver-D6IImXvF.mjs +0 -4
- package/dist/main-resolver-DlaLOFBA.mjs.map +0 -1
- package/dist/program-B-QJXCRb.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
|
@@ -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(
|
package/statusline-command.sh
CHANGED
|
@@ -35,8 +35,18 @@ DA_COLOR="${DA_COLOR:-purple}" # Color for the assistant name
|
|
|
35
35
|
# Extract data from JSON input
|
|
36
36
|
current_dir=$(echo "$input" | jq -r '.workspace.current_dir')
|
|
37
37
|
model_name=$(echo "$input" | jq -r '.model.display_name')
|
|
38
|
+
model_id=$(echo "$input" | jq -r '.model.id // .model.display_name // empty')
|
|
38
39
|
cc_version=$(echo "$input" | jq -r '.version // "unknown"')
|
|
39
40
|
|
|
41
|
+
# Provider detection: strip any [...] suffix (e.g. the [1m] context marker)
|
|
42
|
+
# so "glm-5.3[1m]" is recognized as a glm model. glm sessions read their plan
|
|
43
|
+
# usage from Z.ai, everything else keeps the Anthropic OAuth path.
|
|
44
|
+
model_base="${model_id%%\[*}"
|
|
45
|
+
is_glm=0
|
|
46
|
+
case "$model_base" in
|
|
47
|
+
[Gg][Ll][Mm]*) is_glm=1 ;;
|
|
48
|
+
esac
|
|
49
|
+
|
|
40
50
|
# Get directory name
|
|
41
51
|
dir_name=$(basename "$current_dir")
|
|
42
52
|
|
|
@@ -112,7 +122,14 @@ fi
|
|
|
112
122
|
|
|
113
123
|
# Extract context window usage from Claude Code's JSON input (no JSONL parsing needed)
|
|
114
124
|
context_pct=$(echo "$input" | jq -r '.context_window.used_percentage // 0' 2>/dev/null)
|
|
115
|
-
|
|
125
|
+
# When the payload omits the window size, derive it from the model id instead
|
|
126
|
+
# of assuming 200k for every model: [1m] models carry a 1M window, everything
|
|
127
|
+
# else keeps the claude default. This value is persisted below for hooks.
|
|
128
|
+
default_context_size=200000
|
|
129
|
+
case "$model_id" in
|
|
130
|
+
*"[1m]") default_context_size=1000000 ;;
|
|
131
|
+
esac
|
|
132
|
+
context_size=$(echo "$input" | jq -r --argjson d "$default_context_size" '.context_window.context_window_size // $d' 2>/dev/null)
|
|
116
133
|
context_used_k=$(( (context_pct * context_size / 100) / 1000 ))
|
|
117
134
|
context_max_k=$((context_size / 1000))
|
|
118
135
|
|
|
@@ -125,7 +142,7 @@ if [ -n "$statusline_session_id" ]; then
|
|
|
125
142
|
context_state_file="${TMPDIR:-/tmp}/pai-context-${statusline_session_id}.json"
|
|
126
143
|
jq -n \
|
|
127
144
|
--argjson used_percentage "${context_pct:-0}" \
|
|
128
|
-
--argjson context_window_size "${context_size
|
|
145
|
+
--argjson context_window_size "${context_size:-$default_context_size}" \
|
|
129
146
|
--arg session_id "$statusline_session_id" \
|
|
130
147
|
--argjson timestamp "$(date +%s000)" \
|
|
131
148
|
'{used_percentage: $used_percentage, context_window_size: $context_window_size, session_id: $session_id, timestamp: $timestamp}' \
|
|
@@ -347,11 +364,22 @@ if [ -n "$mcp_line2" ]; then
|
|
|
347
364
|
fi
|
|
348
365
|
|
|
349
366
|
|
|
350
|
-
#
|
|
367
|
+
# Usage suffix: provider-aware. glm sessions read the Z.ai plan quota
|
|
368
|
+
# (5-hour + weekly credit windows), everything else keeps the Anthropic
|
|
369
|
+
# OAuth path (5-hour + 1d pace + 7-day). Both cache for usage_cache_ttl.
|
|
351
370
|
usage_cache="/tmp/claude/statusline-usage-cache.json"
|
|
371
|
+
zai_cache="/tmp/claude/statusline-zai-cache.json"
|
|
352
372
|
usage_cache_ttl=60 # seconds
|
|
353
373
|
usage_suffix=""
|
|
354
374
|
|
|
375
|
+
# Color based on utilization: green < 50%, yellow 50-75%, red > 75%
|
|
376
|
+
_usage_color() {
|
|
377
|
+
local pct=$1
|
|
378
|
+
if [ "$pct" -gt 75 ] 2>/dev/null; then echo "$BRIGHT_RED"
|
|
379
|
+
elif [ "$pct" -gt 50 ] 2>/dev/null; then echo "$BRIGHT_YELLOW"
|
|
380
|
+
else echo "$BRIGHT_GREEN"; fi
|
|
381
|
+
}
|
|
382
|
+
|
|
355
383
|
_fetch_usage() {
|
|
356
384
|
# Try to get OAuth token from macOS Keychain
|
|
357
385
|
local token=""
|
|
@@ -367,18 +395,21 @@ _fetch_usage() {
|
|
|
367
395
|
[ -n "$response" ] && echo "$response" > "$usage_cache"
|
|
368
396
|
}
|
|
369
397
|
|
|
370
|
-
# Use cache if fresh, otherwise fetch in background
|
|
371
|
-
if [
|
|
372
|
-
|
|
373
|
-
|
|
398
|
+
# Use cache if fresh, otherwise fetch in background (Anthropic plan only)
|
|
399
|
+
if [ "$is_glm" -eq 0 ]; then
|
|
400
|
+
if [ -f "$usage_cache" ]; then
|
|
401
|
+
cache_age=$(( $(date +%s) - $(stat -f %m "$usage_cache" 2>/dev/null || echo 0) ))
|
|
402
|
+
if [ "$cache_age" -gt "$usage_cache_ttl" ]; then
|
|
403
|
+
_fetch_usage &
|
|
404
|
+
fi
|
|
405
|
+
else
|
|
374
406
|
_fetch_usage &
|
|
375
407
|
fi
|
|
376
|
-
else
|
|
377
|
-
_fetch_usage &
|
|
378
408
|
fi
|
|
379
409
|
|
|
380
|
-
# Read cached usage data
|
|
381
|
-
|
|
410
|
+
# Read cached usage data — skipped on glm sessions so Anthropic-plan numbers
|
|
411
|
+
# (usage line AND the advisor-mode budget file) never leak into a glm statusline
|
|
412
|
+
if [ "$is_glm" -eq 0 ] && [ -f "$usage_cache" ]; then
|
|
382
413
|
five_hour=$(jq -r '.five_hour.utilization // 0' "$usage_cache" 2>/dev/null)
|
|
383
414
|
seven_day=$(jq -r '.seven_day.utilization // 0' "$usage_cache" 2>/dev/null)
|
|
384
415
|
five_reset=$(jq -r '.five_hour.resets_at // empty' "$usage_cache" 2>/dev/null)
|
|
@@ -406,14 +437,6 @@ if [ -f "$usage_cache" ]; then
|
|
|
406
437
|
seven_reset_fmt=$([ "$seven_reset_epoch" -gt 0 ] 2>/dev/null && date -r "$seven_reset_epoch" "+%a %H:%M" 2>/dev/null || echo "")
|
|
407
438
|
fi
|
|
408
439
|
|
|
409
|
-
# Color based on utilization: green < 50%, yellow 50-75%, red > 75%
|
|
410
|
-
_usage_color() {
|
|
411
|
-
local pct=$1
|
|
412
|
-
if [ "$pct" -gt 75 ] 2>/dev/null; then echo "$BRIGHT_RED"
|
|
413
|
-
elif [ "$pct" -gt 50 ] 2>/dev/null; then echo "$BRIGHT_YELLOW"
|
|
414
|
-
else echo "$BRIGHT_GREEN"; fi
|
|
415
|
-
}
|
|
416
|
-
|
|
417
440
|
five_color=$(_usage_color "$five_hour_int")
|
|
418
441
|
seven_color=$(_usage_color "$seven_day_int")
|
|
419
442
|
|
|
@@ -504,6 +527,80 @@ if [ -f "$usage_cache" ]; then
|
|
|
504
527
|
usage_suffix="${usage_suffix} ${SEPARATOR_COLOR}│${RESET} ${seven_color}${seven_label}${RESET}"
|
|
505
528
|
fi
|
|
506
529
|
|
|
530
|
+
# Z.ai plan quota (glm sessions). The monitor endpoint exposes two credit
|
|
531
|
+
# windows: limits[] number==5 is the 5-hour window, number==1 the weekly one
|
|
532
|
+
# (nextResetTime in epoch ms) — no 1d window exists on this plan, so none is
|
|
533
|
+
# rendered. PAI_ZAI_QUOTA_URL overrides the endpoint (kill switch / testing).
|
|
534
|
+
_fetch_zai_usage() {
|
|
535
|
+
local key="${ZAI_API_KEY:-}"
|
|
536
|
+
if [ -z "$key" ] && [ -f "$HOME/.config/zai/api_key" ]; then
|
|
537
|
+
key=$(tr -d '[:space:]' < "$HOME/.config/zai/api_key")
|
|
538
|
+
fi
|
|
539
|
+
[ -n "$key" ] || return
|
|
540
|
+
mkdir -p /tmp/claude
|
|
541
|
+
local response
|
|
542
|
+
response=$(curl -sf --max-time 3 \
|
|
543
|
+
-H "Authorization: Bearer $key" \
|
|
544
|
+
-H "Accept: application/json" \
|
|
545
|
+
"${PAI_ZAI_QUOTA_URL:-https://api.z.ai/api/monitor/usage/quota/limit}" 2>/dev/null)
|
|
546
|
+
# A bad key returns HTTP 200 with empty limits — only cache responses
|
|
547
|
+
# that actually carry quota windows, so a broken fetch is never cached.
|
|
548
|
+
if [ -n "$response" ] && echo "$response" | jq -e '.data.limits | length > 0' >/dev/null 2>&1; then
|
|
549
|
+
echo "$response" > "$zai_cache"
|
|
550
|
+
fi
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if [ "$is_glm" -eq 1 ]; then
|
|
554
|
+
# Use cache if fresh, otherwise fetch in background
|
|
555
|
+
if [ -f "$zai_cache" ]; then
|
|
556
|
+
cache_age=$(( $(date +%s) - $(stat -f %m "$zai_cache" 2>/dev/null || echo 0) ))
|
|
557
|
+
if [ "$cache_age" -gt "$usage_cache_ttl" ]; then
|
|
558
|
+
_fetch_zai_usage &
|
|
559
|
+
fi
|
|
560
|
+
else
|
|
561
|
+
_fetch_zai_usage &
|
|
562
|
+
fi
|
|
563
|
+
|
|
564
|
+
zai_five_pct=""
|
|
565
|
+
zai_seven_pct=""
|
|
566
|
+
zai_five_reset_ms=""
|
|
567
|
+
zai_seven_reset_ms=""
|
|
568
|
+
if [ -f "$zai_cache" ]; then
|
|
569
|
+
zai_five_pct=$(jq -r '.data.limits[]? | select(.number == 5) | .percentage' "$zai_cache" 2>/dev/null | head -1)
|
|
570
|
+
zai_seven_pct=$(jq -r '.data.limits[]? | select(.number == 1) | .percentage' "$zai_cache" 2>/dev/null | head -1)
|
|
571
|
+
zai_five_reset_ms=$(jq -r '.data.limits[]? | select(.number == 5) | .nextResetTime' "$zai_cache" 2>/dev/null | head -1)
|
|
572
|
+
zai_seven_reset_ms=$(jq -r '.data.limits[]? | select(.number == 1) | .nextResetTime' "$zai_cache" 2>/dev/null | head -1)
|
|
573
|
+
fi
|
|
574
|
+
|
|
575
|
+
if [ -n "$zai_five_pct" ] && [ "$zai_five_pct" != "null" ]; then
|
|
576
|
+
zai_five_int=$(printf "%.0f" "$zai_five_pct" 2>/dev/null || echo "?")
|
|
577
|
+
zai_five_reset_fmt=""
|
|
578
|
+
if [ -n "$zai_five_reset_ms" ] && [ "$zai_five_reset_ms" != "null" ]; then
|
|
579
|
+
# nextResetTime is epoch milliseconds; date -r wants seconds
|
|
580
|
+
zai_five_epoch=$(( zai_five_reset_ms / 1000 ))
|
|
581
|
+
[ "$zai_five_epoch" -gt 0 ] 2>/dev/null && zai_five_reset_fmt=$(date -r "$zai_five_epoch" "+%H:%M" 2>/dev/null || echo "")
|
|
582
|
+
fi
|
|
583
|
+
zai_five_label="zai 5h: ${zai_five_int}%%"
|
|
584
|
+
[ -n "$zai_five_reset_fmt" ] && zai_five_label="${zai_five_label} → ${zai_five_reset_fmt}"
|
|
585
|
+
usage_suffix=" ${SEPARATOR_COLOR}│${RESET} $(_usage_color "$zai_five_int")${zai_five_label}${RESET}"
|
|
586
|
+
if [ -n "$zai_seven_pct" ] && [ "$zai_seven_pct" != "null" ]; then
|
|
587
|
+
zai_seven_int=$(printf "%.0f" "$zai_seven_pct" 2>/dev/null || echo "?")
|
|
588
|
+
zai_seven_reset_fmt=""
|
|
589
|
+
if [ -n "$zai_seven_reset_ms" ] && [ "$zai_seven_reset_ms" != "null" ]; then
|
|
590
|
+
zai_seven_epoch=$(( zai_seven_reset_ms / 1000 ))
|
|
591
|
+
[ "$zai_seven_epoch" -gt 0 ] 2>/dev/null && zai_seven_reset_fmt=$(date -r "$zai_seven_epoch" "+%a %H:%M" 2>/dev/null || echo "")
|
|
592
|
+
fi
|
|
593
|
+
zai_seven_label="7d: ${zai_seven_int}%%"
|
|
594
|
+
[ -n "$zai_seven_reset_fmt" ] && zai_seven_label="${zai_seven_label} → ${zai_seven_reset_fmt}"
|
|
595
|
+
usage_suffix="${usage_suffix} ${SEPARATOR_COLOR}│${RESET} $(_usage_color "$zai_seven_int")${zai_seven_label}${RESET}"
|
|
596
|
+
fi
|
|
597
|
+
else
|
|
598
|
+
# No key, timeout, non-200 or empty limits — show the marker instead
|
|
599
|
+
# of silently falling back to Anthropic-plan numbers.
|
|
600
|
+
usage_suffix=" ${SEPARATOR_COLOR}│${RESET} ${LINE3_ACCENT}zai 5h: ?${RESET}"
|
|
601
|
+
fi
|
|
602
|
+
fi
|
|
603
|
+
|
|
507
604
|
# LINE 3 - Context meter + usage limits
|
|
508
605
|
# Auto-compact remaining: how much context left until compaction triggers
|
|
509
606
|
ac_threshold="${CLAUDE_AUTOCOMPACT_PCT_OVERRIDE:-80}"
|
|
@@ -517,7 +614,7 @@ elif [ "$ac_remaining" -le 15 ] 2>/dev/null; then
|
|
|
517
614
|
else
|
|
518
615
|
ac_color="$BRIGHT_GREEN"
|
|
519
616
|
fi
|
|
520
|
-
ac_suffix=" ${ac_color}(${ac_remaining}%%)${RESET}"
|
|
617
|
+
ac_suffix=" ${ac_color}(${ac_remaining}%% left)${RESET}"
|
|
521
618
|
|
|
522
619
|
if [ "$context_pct" -gt 0 ] 2>/dev/null; then
|
|
523
620
|
# Color based on usage: green < 50%, yellow 50-75%, red > 75%
|
|
@@ -545,7 +642,9 @@ elif command -v pai >/dev/null 2>&1; then
|
|
|
545
642
|
worker_status_cmd="pai worker status-line"
|
|
546
643
|
fi
|
|
547
644
|
if [ -n "$worker_status_cmd" ]; then
|
|
548
|
-
|
|
645
|
+
# third arg: this session's claude id — lets the worker line claim workers
|
|
646
|
+
# spawned from this session's Bash tool, which carry no terminal identity
|
|
647
|
+
worker_line="$("$worker_status_cmd" "${ITERM_SESSION_ID:-}" "${PWD:-}" "${statusline_session_id:-}" 2>/dev/null)"
|
|
549
648
|
if [ -n "$worker_line" ]; then
|
|
550
649
|
printf "${LINE3_PRIMARY}🐝 ${RESET}${LINE3_ACCENT}%s${RESET}\n" "$worker_line"
|
|
551
650
|
fi
|