@cortexkit/opencode-magic-context 0.15.0 → 0.15.2

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 (37) hide show
  1. package/README.md +12 -6
  2. package/dist/agents/magic-context-prompt.d.ts.map +1 -1
  3. package/dist/cli/diagnostics.d.ts.map +1 -1
  4. package/dist/cli/doctor.d.ts.map +1 -1
  5. package/dist/cli.js +71 -82
  6. package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
  7. package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
  8. package/dist/features/magic-context/migrations.d.ts.map +1 -1
  9. package/dist/features/magic-context/storage-db.d.ts +20 -0
  10. package/dist/features/magic-context/storage-db.d.ts.map +1 -1
  11. package/dist/features/magic-context/tool-definition-tokens.d.ts +45 -0
  12. package/dist/features/magic-context/tool-definition-tokens.d.ts.map +1 -0
  13. package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
  14. package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
  15. package/dist/hooks/magic-context/tag-content-primitives.d.ts.map +1 -1
  16. package/dist/hooks/magic-context/transform.d.ts.map +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3459 -3328
  19. package/dist/plugin/rpc-handlers.d.ts.map +1 -1
  20. package/dist/shared/bounded-session-map.d.ts +45 -0
  21. package/dist/shared/bounded-session-map.d.ts.map +1 -0
  22. package/dist/shared/data-path.d.ts +12 -0
  23. package/dist/shared/data-path.d.ts.map +1 -1
  24. package/dist/shared/models-dev-cache.d.ts.map +1 -1
  25. package/dist/shared/rpc-types.d.ts +15 -5
  26. package/dist/shared/rpc-types.d.ts.map +1 -1
  27. package/dist/tui/data/context-db.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/shared/bounded-session-map.test.ts +97 -0
  30. package/src/shared/bounded-session-map.ts +84 -0
  31. package/src/shared/data-path.test.ts +69 -0
  32. package/src/shared/data-path.ts +18 -0
  33. package/src/shared/models-dev-cache.ts +5 -10
  34. package/src/shared/rpc-types.ts +15 -5
  35. package/src/tui/data/context-db.ts +1 -0
  36. package/src/tui/index.tsx +13 -4
  37. package/src/tui/slots/sidebar-content.tsx +33 -11
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAKzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAIlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAsclE;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,qBAAqB,EAChC,IAAI,EAAE;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;CACtC,GACF,IAAI,CAqGN"}
1
+ {"version":3,"file":"rpc-handlers.d.ts","sourceRoot":"","sources":["../../src/plugin/rpc-handlers.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AAMzE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAIlF,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AA0elE;;GAEG;AACH,wBAAgB,mBAAmB,CAC/B,SAAS,EAAE,qBAAqB,EAChC,IAAI,EAAE;IACF,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;CACtC,GACF,IAAI,CA4GN"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Bounded LRU map keyed by session id.
3
+ *
4
+ * Rationale: magic-context maintains several module-scope Maps that track
5
+ * per-session state (prepared injection cache, per-message token cache, etc.).
6
+ * These are cleared on the `session.deleted` event, but sessions that are
7
+ * never explicitly deleted — because OpenCode crashed, the user force-quit,
8
+ * the session was archived rather than deleted, or the session simply outlived
9
+ * the plugin process's interest in it — leak entries for the lifetime of the
10
+ * plugin process.
11
+ *
12
+ * In long-running OpenCode instances with thousands of sessions over time,
13
+ * an unbounded `Map<sessionId, LargeObject>` can retain tens of megabytes
14
+ * indefinitely. A session-scoped LRU with a generous cap (e.g. 100) covers
15
+ * any realistic working-set of active sessions a user actually cares about,
16
+ * while evicting cold session ids that will either never return or be
17
+ * rebuilt from durable SQLite state on their next transform pass.
18
+ *
19
+ * Implementation notes:
20
+ * - Built on `Map` which preserves insertion order. On every `set`/`get`
21
+ * touch we delete+reinsert to move the key to the tail (most-recent).
22
+ * - Eviction drops the oldest entry (first in iteration order).
23
+ * - The cached value type is generic — callers decide what per-session state
24
+ * to store. For injection/token state, all three properties of the cached
25
+ * object are safe to throw away: they are either recomputable from the
26
+ * messages array on the next pass, or reloadable from SQLite.
27
+ */
28
+ export declare class BoundedSessionMap<V> {
29
+ private readonly maxEntries;
30
+ private readonly store;
31
+ constructor(maxEntries: number);
32
+ get(sessionId: string): V | undefined;
33
+ /**
34
+ * Peek without touching recency — useful for `has`-style checks that
35
+ * should not rearrange LRU order. Use sparingly; `get` is the normal
36
+ * access path.
37
+ */
38
+ peek(sessionId: string): V | undefined;
39
+ has(sessionId: string): boolean;
40
+ set(sessionId: string, value: V): void;
41
+ delete(sessionId: string): boolean;
42
+ clear(): void;
43
+ get size(): number;
44
+ }
45
+ //# sourceMappingURL=bounded-session-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounded-session-map.d.ts","sourceRoot":"","sources":["../../src/shared/bounded-session-map.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,iBAAiB,CAAC,CAAC;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;gBAElC,UAAU,EAAE,MAAM;IAO9B,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IASrC;;;;OAIG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAItC,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAI/B,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IAYtC,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIlC,KAAK,IAAI,IAAI;IAIb,IAAI,IAAI,IAAI,MAAM,CAEjB;CACJ"}
@@ -1,3 +1,15 @@
1
1
  export declare function getDataDir(): string;
2
2
  export declare function getOpenCodeStorageDir(): string;
3
+ /**
4
+ * Resolve OpenCode's cache base directory.
5
+ *
6
+ * OpenCode uses the `xdg-basedir` package, which — on every platform, including
7
+ * Windows — falls back to `<homedir>/.cache` when `XDG_CACHE_HOME` is unset.
8
+ * A previous Windows-specific branch that resolved to `%LOCALAPPDATA%` did not
9
+ * match OpenCode's own resolution and caused `doctor --force` to target a
10
+ * non-existent directory, leaving the real cache at `C:\Users\<user>\.cache`
11
+ * untouched.
12
+ */
13
+ export declare function getCacheDir(): string;
14
+ export declare function getOpenCodeCacheDir(): string;
3
15
  //# sourceMappingURL=data-path.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"data-path.d.ts","sourceRoot":"","sources":["../../src/shared/data-path.ts"],"names":[],"mappings":"AAGA,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C"}
1
+ {"version":3,"file":"data-path.d.ts","sourceRoot":"","sources":["../../src/shared/data-path.ts"],"names":[],"mappings":"AAGA,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
@@ -1 +1 @@
1
- {"version":3,"file":"models-dev-cache.d.ts","sourceRoot":"","sources":["../../src/shared/models-dev-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAQH,UAAU,kBAAkB;IACxB,MAAM,EAAE;QACJ,SAAS,EAAE,MAAM,OAAO,CAAC;YAAE,IAAI,CAAC,EAAE;gBAAE,SAAS,CAAC,EAAE,OAAO,CAAA;aAAE,CAAA;SAAE,CAAC,CAAC;KAChE,CAAC;CACL;AA2ND;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDzF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAchG;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAkBpB;AAED,4CAA4C;AAC5C,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C;AAED,oDAAoD;AACpD,wBAAgB,sBAAsB,IAAI;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACrB,CAOA"}
1
+ {"version":3,"file":"models-dev-cache.d.ts","sourceRoot":"","sources":["../../src/shared/models-dev-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AASH,UAAU,kBAAkB;IACxB,MAAM,EAAE;QACJ,SAAS,EAAE,MAAM,OAAO,CAAC;YAAE,IAAI,CAAC,EAAE;gBAAE,SAAS,CAAC,EAAE,OAAO,CAAA;aAAE,CAAA;SAAE,CAAC,CAAC;KAChE,CAAC;CACL;AAqND;;;;;;;;;;GAUG;AACH,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDzF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAchG;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAkBpB;AAED,4CAA4C;AAC5C,wBAAgB,mBAAmB,IAAI,IAAI,CAK1C;AAED,oDAAoD;AACpD,wBAAgB,sBAAsB,IAAI;IACtC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACrB,CAOA"}
@@ -35,13 +35,23 @@ export interface SidebarSnapshot {
35
35
  */
36
36
  toolCallTokens: number;
37
37
  /**
38
- * Token estimate of tool schemas OpenCode sends alongside in the request
39
- * `tools` parameter (bash, edit, grep, MCP servers, ctx_* tools, etc.).
40
- * Computed as inputTokens systemPromptTokens messagesBlock −
41
- * toolCallTokens and clamped to 0. Display layer shows this as
42
- * "Tool Definitions". Includes tokenizer imprecision residual.
38
+ * Measured token cost of tool schemas (description + JSON-schema
39
+ * parameters) OpenCode sends in the request `tools` parameter. Populated
40
+ * by the `tool.definition` plugin hook, keyed by
41
+ * `{providerID, modelID, agentName}`. Zero until the first turn after
42
+ * plugin startup measures the current agent's tool set. Display layer
43
+ * shows this as "Tool Definitions".
43
44
  */
44
45
  toolDefinitionTokens: number;
46
+ /**
47
+ * Residual catch-all: provider-side wrapping not captured elsewhere —
48
+ * the JSON envelope around the `tools` array, tool-choice fields,
49
+ * provider-specific cache-control markers, tokenizer imprecision, etc.
50
+ * Computed as `inputTokens − systemPromptTokens − messagesBlock −
51
+ * toolCallTokens − toolDefinitionTokens` and clamped to ≥ 0. Display
52
+ * layer shows this as "Overhead".
53
+ */
54
+ overheadTokens: number;
45
55
  }
46
56
  export interface StatusDetail extends SidebarSnapshot {
47
57
  tagCounter: number;
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-types.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;;;;OAMG;IACH,oBAAoB,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,YAAa,SAAQ,eAAe;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,oBAAoB,EAAE,YAAY,GAAG,QAAQ,CAAC;IAC9C;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,uBAAuB,EAAE,MAAM,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,sBAAsB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB"}
1
+ {"version":3,"file":"rpc-types.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,oBAAoB,EAAE,MAAM,CAAC;IAC7B;;;;;;;OAOG;IACH,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAa,SAAQ,eAAe;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,oBAAoB,EAAE,YAAY,GAAG,QAAQ,CAAC;IAC9C;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,uBAAuB,EAAE,MAAM,CAAC;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,sBAAsB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB"}
@@ -1 +1 @@
1
- {"version":3,"file":"context-db.d.ts","sourceRoot":"","sources":["../../../src/tui/data/context-db.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA0B,eAAe,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEpG,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC;AAS9C,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAGrD;AAED,+BAA+B;AAC/B,wBAAgB,QAAQ,IAAI,IAAI,CAG/B;AA2BD,sDAAsD;AACtD,wBAAsB,mBAAmB,CACrC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAClB,OAAO,CAAC,eAAe,CAAC,CAc1B;AAED,wDAAwD;AACxD,wBAAsB,gBAAgB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CA4CvB;AAED,qCAAqC;AACrC,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ5E;AAED,6CAA6C;AAC7C,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQvE;AAED,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,yDAAyD;AACzD,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAchE"}
1
+ {"version":3,"file":"context-db.d.ts","sourceRoot":"","sources":["../../../src/tui/data/context-db.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAA0B,eAAe,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEpG,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,CAAC;AAS9C,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAGrD;AAED,+BAA+B;AAC/B,wBAAgB,QAAQ,IAAI,IAAI,CAG/B;AA4BD,sDAAsD;AACtD,wBAAsB,mBAAmB,CACrC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAClB,OAAO,CAAC,eAAe,CAAC,CAc1B;AAED,wDAAwD;AACxD,wBAAsB,gBAAgB,CAClC,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CA4CvB;AAED,qCAAqC;AACrC,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ5E;AAED,6CAA6C;AAC7C,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQvE;AAED,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,yDAAyD;AACzD,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,UAAU,EAAE,CAAC,CAchE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-magic-context",
3
- "version": "0.15.0",
3
+ "version": "0.15.2",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Magic Context — cross-session memory and context management",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { BoundedSessionMap } from "./bounded-session-map";
3
+
4
+ describe("BoundedSessionMap", () => {
5
+ it("rejects non-positive caps", () => {
6
+ expect(() => new BoundedSessionMap(0)).toThrow();
7
+ expect(() => new BoundedSessionMap(-5)).toThrow();
8
+ expect(() => new BoundedSessionMap(Number.NaN)).toThrow();
9
+ });
10
+
11
+ it("stores and retrieves values", () => {
12
+ const map = new BoundedSessionMap<number>(3);
13
+ map.set("a", 1);
14
+ map.set("b", 2);
15
+ expect(map.get("a")).toBe(1);
16
+ expect(map.get("b")).toBe(2);
17
+ expect(map.get("missing")).toBeUndefined();
18
+ expect(map.size).toBe(2);
19
+ });
20
+
21
+ it("evicts the oldest entry when cap is exceeded", () => {
22
+ const map = new BoundedSessionMap<string>(3);
23
+ map.set("a", "alpha");
24
+ map.set("b", "bravo");
25
+ map.set("c", "charlie");
26
+ map.set("d", "delta"); // evicts "a"
27
+ expect(map.has("a")).toBe(false);
28
+ expect(map.has("b")).toBe(true);
29
+ expect(map.has("c")).toBe(true);
30
+ expect(map.has("d")).toBe(true);
31
+ expect(map.size).toBe(3);
32
+ });
33
+
34
+ it("treats get() as a touch for LRU ordering", () => {
35
+ const map = new BoundedSessionMap<string>(3);
36
+ map.set("a", "alpha");
37
+ map.set("b", "bravo");
38
+ map.set("c", "charlie");
39
+ // Touch "a" — now "b" is the oldest.
40
+ expect(map.get("a")).toBe("alpha");
41
+ map.set("d", "delta");
42
+ expect(map.has("b")).toBe(false);
43
+ expect(map.has("a")).toBe(true);
44
+ expect(map.has("c")).toBe(true);
45
+ expect(map.has("d")).toBe(true);
46
+ });
47
+
48
+ it("peek() does NOT touch recency", () => {
49
+ const map = new BoundedSessionMap<number>(3);
50
+ map.set("a", 1);
51
+ map.set("b", 2);
52
+ map.set("c", 3);
53
+ expect(map.peek("a")).toBe(1);
54
+ // Adding a fourth entry should still evict "a" since peek didn't touch it.
55
+ map.set("d", 4);
56
+ expect(map.has("a")).toBe(false);
57
+ });
58
+
59
+ it("set() on existing key refreshes recency without growing size", () => {
60
+ const map = new BoundedSessionMap<number>(3);
61
+ map.set("a", 1);
62
+ map.set("b", 2);
63
+ map.set("c", 3);
64
+ map.set("a", 100); // refresh "a" to most-recent with new value
65
+ expect(map.size).toBe(3);
66
+ expect(map.get("a")).toBe(100);
67
+ map.set("d", 4); // evicts "b" (now oldest)
68
+ expect(map.has("b")).toBe(false);
69
+ expect(map.has("a")).toBe(true);
70
+ });
71
+
72
+ it("delete() removes entries and returns true when present", () => {
73
+ const map = new BoundedSessionMap<number>(3);
74
+ map.set("a", 1);
75
+ expect(map.delete("a")).toBe(true);
76
+ expect(map.delete("a")).toBe(false);
77
+ expect(map.size).toBe(0);
78
+ });
79
+
80
+ it("clear() drops all entries", () => {
81
+ const map = new BoundedSessionMap<number>(3);
82
+ map.set("a", 1);
83
+ map.set("b", 2);
84
+ map.clear();
85
+ expect(map.size).toBe(0);
86
+ expect(map.get("a")).toBeUndefined();
87
+ });
88
+
89
+ it("tolerates cap=1 edge case (every set evicts previous)", () => {
90
+ const map = new BoundedSessionMap<number>(1);
91
+ map.set("a", 1);
92
+ map.set("b", 2);
93
+ expect(map.has("a")).toBe(false);
94
+ expect(map.get("b")).toBe(2);
95
+ expect(map.size).toBe(1);
96
+ });
97
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Bounded LRU map keyed by session id.
3
+ *
4
+ * Rationale: magic-context maintains several module-scope Maps that track
5
+ * per-session state (prepared injection cache, per-message token cache, etc.).
6
+ * These are cleared on the `session.deleted` event, but sessions that are
7
+ * never explicitly deleted — because OpenCode crashed, the user force-quit,
8
+ * the session was archived rather than deleted, or the session simply outlived
9
+ * the plugin process's interest in it — leak entries for the lifetime of the
10
+ * plugin process.
11
+ *
12
+ * In long-running OpenCode instances with thousands of sessions over time,
13
+ * an unbounded `Map<sessionId, LargeObject>` can retain tens of megabytes
14
+ * indefinitely. A session-scoped LRU with a generous cap (e.g. 100) covers
15
+ * any realistic working-set of active sessions a user actually cares about,
16
+ * while evicting cold session ids that will either never return or be
17
+ * rebuilt from durable SQLite state on their next transform pass.
18
+ *
19
+ * Implementation notes:
20
+ * - Built on `Map` which preserves insertion order. On every `set`/`get`
21
+ * touch we delete+reinsert to move the key to the tail (most-recent).
22
+ * - Eviction drops the oldest entry (first in iteration order).
23
+ * - The cached value type is generic — callers decide what per-session state
24
+ * to store. For injection/token state, all three properties of the cached
25
+ * object are safe to throw away: they are either recomputable from the
26
+ * messages array on the next pass, or reloadable from SQLite.
27
+ */
28
+ export class BoundedSessionMap<V> {
29
+ private readonly maxEntries: number;
30
+ private readonly store = new Map<string, V>();
31
+
32
+ constructor(maxEntries: number) {
33
+ if (!Number.isFinite(maxEntries) || maxEntries < 1) {
34
+ throw new Error(`BoundedSessionMap: maxEntries must be >= 1, got ${maxEntries}`);
35
+ }
36
+ this.maxEntries = maxEntries;
37
+ }
38
+
39
+ get(sessionId: string): V | undefined {
40
+ const value = this.store.get(sessionId);
41
+ if (value === undefined) return undefined;
42
+ // Touch: move to most-recent position.
43
+ this.store.delete(sessionId);
44
+ this.store.set(sessionId, value);
45
+ return value;
46
+ }
47
+
48
+ /**
49
+ * Peek without touching recency — useful for `has`-style checks that
50
+ * should not rearrange LRU order. Use sparingly; `get` is the normal
51
+ * access path.
52
+ */
53
+ peek(sessionId: string): V | undefined {
54
+ return this.store.get(sessionId);
55
+ }
56
+
57
+ has(sessionId: string): boolean {
58
+ return this.store.has(sessionId);
59
+ }
60
+
61
+ set(sessionId: string, value: V): void {
62
+ if (this.store.has(sessionId)) {
63
+ // Refresh recency.
64
+ this.store.delete(sessionId);
65
+ } else if (this.store.size >= this.maxEntries) {
66
+ // Evict oldest entry. Map iteration is insertion-ordered.
67
+ const oldest = this.store.keys().next().value;
68
+ if (oldest !== undefined) this.store.delete(oldest);
69
+ }
70
+ this.store.set(sessionId, value);
71
+ }
72
+
73
+ delete(sessionId: string): boolean {
74
+ return this.store.delete(sessionId);
75
+ }
76
+
77
+ clear(): void {
78
+ this.store.clear();
79
+ }
80
+
81
+ get size(): number {
82
+ return this.store.size;
83
+ }
84
+ }
@@ -0,0 +1,69 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { getCacheDir, getDataDir, getOpenCodeCacheDir, getOpenCodeStorageDir } from "./data-path";
5
+
6
+ const savedEnv = {
7
+ XDG_CACHE_HOME: process.env.XDG_CACHE_HOME,
8
+ XDG_DATA_HOME: process.env.XDG_DATA_HOME,
9
+ LOCALAPPDATA: process.env.LOCALAPPDATA,
10
+ };
11
+
12
+ describe("data-path", () => {
13
+ beforeEach(() => {
14
+ process.env.XDG_CACHE_HOME = undefined;
15
+ process.env.XDG_DATA_HOME = undefined;
16
+ process.env.LOCALAPPDATA = undefined;
17
+ // Bun's env handling: explicit delete for unset
18
+ delete process.env.XDG_CACHE_HOME;
19
+ delete process.env.XDG_DATA_HOME;
20
+ delete process.env.LOCALAPPDATA;
21
+ });
22
+
23
+ afterEach(() => {
24
+ if (savedEnv.XDG_CACHE_HOME !== undefined)
25
+ process.env.XDG_CACHE_HOME = savedEnv.XDG_CACHE_HOME;
26
+ if (savedEnv.XDG_DATA_HOME !== undefined)
27
+ process.env.XDG_DATA_HOME = savedEnv.XDG_DATA_HOME;
28
+ if (savedEnv.LOCALAPPDATA !== undefined) process.env.LOCALAPPDATA = savedEnv.LOCALAPPDATA;
29
+ });
30
+
31
+ test("getCacheDir falls back to <homedir>/.cache when XDG_CACHE_HOME is unset (all platforms)", () => {
32
+ // Matches OpenCode's xdg-basedir behavior on every platform, including
33
+ // Windows. A previous bug mapped Windows to %LOCALAPPDATA% and caused
34
+ // doctor --force to target a non-existent cache directory.
35
+ expect(getCacheDir()).toBe(path.join(os.homedir(), ".cache"));
36
+ });
37
+
38
+ test("getCacheDir honors XDG_CACHE_HOME when set", () => {
39
+ process.env.XDG_CACHE_HOME = "/tmp/custom-cache";
40
+ expect(getCacheDir()).toBe("/tmp/custom-cache");
41
+ });
42
+
43
+ test("getCacheDir ignores LOCALAPPDATA on Windows (must match OpenCode's xdg-basedir)", () => {
44
+ // Even with LOCALAPPDATA set, cache must go to ~/.cache to match
45
+ // OpenCode's own resolution. Otherwise doctor --force clears the
46
+ // wrong directory on Windows.
47
+ process.env.LOCALAPPDATA = "C:\\Users\\Test\\AppData\\Local";
48
+ expect(getCacheDir()).toBe(path.join(os.homedir(), ".cache"));
49
+ });
50
+
51
+ test("getOpenCodeCacheDir appends 'opencode' to the cache base", () => {
52
+ expect(getOpenCodeCacheDir()).toBe(path.join(os.homedir(), ".cache", "opencode"));
53
+ });
54
+
55
+ test("getOpenCodeCacheDir with XDG_CACHE_HOME set", () => {
56
+ process.env.XDG_CACHE_HOME = "/tmp/custom-cache";
57
+ expect(getOpenCodeCacheDir()).toBe(path.join("/tmp/custom-cache", "opencode"));
58
+ });
59
+
60
+ test("getDataDir falls back to <homedir>/.local/share when XDG_DATA_HOME is unset", () => {
61
+ expect(getDataDir()).toBe(path.join(os.homedir(), ".local", "share"));
62
+ });
63
+
64
+ test("getOpenCodeStorageDir composes correctly", () => {
65
+ expect(getOpenCodeStorageDir()).toBe(
66
+ path.join(os.homedir(), ".local", "share", "opencode", "storage"),
67
+ );
68
+ });
69
+ });
@@ -8,3 +8,21 @@ export function getDataDir(): string {
8
8
  export function getOpenCodeStorageDir(): string {
9
9
  return path.join(getDataDir(), "opencode", "storage");
10
10
  }
11
+
12
+ /**
13
+ * Resolve OpenCode's cache base directory.
14
+ *
15
+ * OpenCode uses the `xdg-basedir` package, which — on every platform, including
16
+ * Windows — falls back to `<homedir>/.cache` when `XDG_CACHE_HOME` is unset.
17
+ * A previous Windows-specific branch that resolved to `%LOCALAPPDATA%` did not
18
+ * match OpenCode's own resolution and caused `doctor --force` to target a
19
+ * non-existent directory, leaving the real cache at `C:\Users\<user>\.cache`
20
+ * untouched.
21
+ */
22
+ export function getCacheDir(): string {
23
+ return process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache");
24
+ }
25
+
26
+ export function getOpenCodeCacheDir(): string {
27
+ return path.join(getCacheDir(), "opencode");
28
+ }
@@ -24,6 +24,7 @@ import { createHash } from "node:crypto";
24
24
  import { existsSync, readFileSync } from "node:fs";
25
25
  import { homedir, platform } from "node:os";
26
26
  import { join } from "node:path";
27
+ import { getCacheDir } from "./data-path";
27
28
  import { sessionLog } from "./logger";
28
29
 
29
30
  interface OpencodeClientLike {
@@ -59,16 +60,10 @@ function getModelsJsonPath(): string {
59
60
  const explicit = process.env.OPENCODE_MODELS_PATH?.trim();
60
61
  if (explicit) return explicit;
61
62
 
62
- const xdgCache = process.env.XDG_CACHE_HOME;
63
- const os = platform();
64
- let cacheBase: string;
65
- if (xdgCache) {
66
- cacheBase = xdgCache;
67
- } else if (os === "win32") {
68
- cacheBase = process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
69
- } else {
70
- cacheBase = join(homedir(), ".cache");
71
- }
63
+ // OpenCode uses `xdg-basedir`, which falls back to `<homedir>/.cache` on
64
+ // every platform (including Windows) when XDG_CACHE_HOME is unset. See
65
+ // shared/data-path.ts#getCacheDir for the shared helper.
66
+ const cacheBase = getCacheDir();
72
67
 
73
68
  // 2. Custom models source → hashed filename (matches OpenCode).
74
69
  // source === "https://models.dev" ? "models.json" : `models-${Hash.fast(source)}.json`
@@ -36,13 +36,23 @@ export interface SidebarSnapshot {
36
36
  */
37
37
  toolCallTokens: number;
38
38
  /**
39
- * Token estimate of tool schemas OpenCode sends alongside in the request
40
- * `tools` parameter (bash, edit, grep, MCP servers, ctx_* tools, etc.).
41
- * Computed as inputTokens systemPromptTokens messagesBlock −
42
- * toolCallTokens and clamped to 0. Display layer shows this as
43
- * "Tool Definitions". Includes tokenizer imprecision residual.
39
+ * Measured token cost of tool schemas (description + JSON-schema
40
+ * parameters) OpenCode sends in the request `tools` parameter. Populated
41
+ * by the `tool.definition` plugin hook, keyed by
42
+ * `{providerID, modelID, agentName}`. Zero until the first turn after
43
+ * plugin startup measures the current agent's tool set. Display layer
44
+ * shows this as "Tool Definitions".
44
45
  */
45
46
  toolDefinitionTokens: number;
47
+ /**
48
+ * Residual catch-all: provider-side wrapping not captured elsewhere —
49
+ * the JSON envelope around the `tools` array, tool-choice fields,
50
+ * provider-specific cache-control markers, tokenizer imprecision, etc.
51
+ * Computed as `inputTokens − systemPromptTokens − messagesBlock −
52
+ * toolCallTokens − toolDefinitionTokens` and clamped to ≥ 0. Display
53
+ * layer shows this as "Overhead".
54
+ */
55
+ overheadTokens: number;
46
56
  }
47
57
 
48
58
  export interface StatusDetail extends SidebarSnapshot {
@@ -51,6 +51,7 @@ const EMPTY_SNAPSHOT: SidebarSnapshot = {
51
51
  conversationTokens: 0,
52
52
  toolCallTokens: 0,
53
53
  toolDefinitionTokens: 0,
54
+ overheadTokens: 0,
54
55
  };
55
56
 
56
57
  /** Fetch sidebar snapshot from the server via RPC. */
package/src/tui/index.tsx CHANGED
@@ -207,12 +207,19 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
207
207
 
208
208
  const elapsed = () => (s().lastResponseTime > 0 ? Date.now() - s().lastResponseTime : 0)
209
209
 
210
- // Token breakdown segments — same colors as sidebar
210
+ // Token breakdown segments — same colors as sidebar. Kept in sync with
211
+ // slots/sidebar-content.tsx so the status dialog and sidebar read identically.
211
212
  const COLORS = {
213
+ // Cool / structured — injected by the plugin into message[0]
212
214
  system: "#c084fc",
213
215
  compartments: "#60a5fa",
214
216
  facts: "#fbbf24",
215
217
  memories: "#34d399",
218
+ // Warm / user-facing — chat and tool traffic
219
+ conversation: "#f87171",
220
+ toolCalls: "#fb923c",
221
+ toolDefs: "#f472b6",
222
+ overhead: "#9ca3af",
216
223
  }
217
224
 
218
225
  const breakdownSegments = () => {
@@ -245,11 +252,13 @@ const StatusDialog = (props: { api: TuiPluginApi; s: StatusDetail }) => {
245
252
  })
246
253
 
247
254
  if (d.conversationTokens > 0)
248
- segs.push({ label: "Conversation", tokens: d.conversationTokens, color: t().textMuted })
255
+ segs.push({ label: "Conversation", tokens: d.conversationTokens, color: COLORS.conversation })
249
256
  if (d.toolCallTokens > 0)
250
- segs.push({ label: "Tool Calls", tokens: d.toolCallTokens, color: "#6b7280" })
257
+ segs.push({ label: "Tool Calls", tokens: d.toolCallTokens, color: COLORS.toolCalls })
251
258
  if (d.toolDefinitionTokens > 0)
252
- segs.push({ label: "Tool Defs + Overhead", tokens: d.toolDefinitionTokens, color: "#9ca3af" })
259
+ segs.push({ label: "Tool Defs", tokens: d.toolDefinitionTokens, color: COLORS.toolDefs })
260
+ if (d.overheadTokens > 0)
261
+ segs.push({ label: "Overhead", tokens: d.overheadTokens, color: COLORS.overhead })
253
262
 
254
263
  return { segs, total }
255
264
  }
@@ -23,11 +23,17 @@ function relativeTime(ms: number): string {
23
23
 
24
24
  // Token breakdown segment colors (hardcoded hex values)
25
25
  const COLORS = {
26
- system: "#c084fc", // Purple-ish
27
- compartments: "#60a5fa", // Blue-ish
28
- facts: "#fbbf24", // Yellow/orange
29
- memories: "#34d399", // Green
30
- conversation: "#9ca3af", // Gray (will use theme.textMuted)
26
+ // Cool / structured — injected by the plugin into message[0]
27
+ system: "#c084fc", // Purple
28
+ compartments: "#60a5fa", // Blue
29
+ facts: "#fbbf24", // Yellow/orange
30
+ memories: "#34d399", // Green
31
+ // Warm / user-facing — regular chat and tool traffic. Grouped visually
32
+ // by hue family so the user reads them as a related block.
33
+ conversation: "#f87171", // Red
34
+ toolCalls: "#fb923c", // Orange
35
+ toolDefs: "#f472b6", // Pink
36
+ overhead: "#9ca3af", // Gray — catch-all residual
31
37
  }
32
38
 
33
39
  interface TokenSegment {
@@ -95,7 +101,7 @@ const TokenBreakdown = (props: {
95
101
  result.push({
96
102
  key: "conv",
97
103
  tokens: s.conversationTokens,
98
- color: props.theme.textMuted,
104
+ color: COLORS.conversation,
99
105
  label: "Conversation",
100
106
  })
101
107
  }
@@ -106,19 +112,35 @@ const TokenBreakdown = (props: {
106
112
  result.push({
107
113
  key: "tool-calls",
108
114
  tokens: s.toolCallTokens,
109
- color: "#6b7280",
115
+ color: COLORS.toolCalls,
110
116
  label: "Tool Calls",
111
117
  })
112
118
  }
113
119
 
114
- // Tool Definitions = tool schemas sent separately by OpenCode
115
- // (residual: inputTokens - system - messagesBlock - toolCalls)
120
+ // Tool Definitions = measured description + JSON-schema parameters for
121
+ // each tool OpenCode sends in the `tools` request parameter, populated
122
+ // by the `tool.definition` plugin hook keyed by {provider, model, agent}.
123
+ // Zero until the first turn measures the active agent's tool set.
116
124
  if (s.toolDefinitionTokens > 0) {
117
125
  result.push({
118
126
  key: "tool-defs",
119
127
  tokens: s.toolDefinitionTokens,
120
- color: COLORS.conversation,
121
- label: "Tool Defs + Overhead",
128
+ color: COLORS.toolDefs,
129
+ label: "Tool Defs",
130
+ })
131
+ }
132
+
133
+ // Overhead = residual between input tokens and everything measured above.
134
+ // Captures provider-side JSON wrapping around the tools array,
135
+ // tool_choice/cache-control markers, and tokenizer imprecision. Before
136
+ // the first turn's tool.definition measurement lands, the real tool
137
+ // schema cost also shows up here.
138
+ if (s.overheadTokens > 0) {
139
+ result.push({
140
+ key: "overhead",
141
+ tokens: s.overheadTokens,
142
+ color: COLORS.overhead,
143
+ label: "Overhead",
122
144
  })
123
145
  }
124
146