@gajae-code/coding-agent 0.8.2 → 0.9.1

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 (214) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/types/append-only-mode.d.ts +31 -0
  3. package/dist/types/capability/rule.d.ts +8 -0
  4. package/dist/types/cli/fixture-report.d.ts +20 -0
  5. package/dist/types/cli/fixture-report.test.d.ts +1 -0
  6. package/dist/types/cli/nofile-limit.d.ts +11 -0
  7. package/dist/types/cli/update-cli.d.ts +22 -0
  8. package/dist/types/config/settings-schema.d.ts +119 -1
  9. package/dist/types/discovery/index.d.ts +1 -0
  10. package/dist/types/export/html/template.generated.d.ts +1 -1
  11. package/dist/types/export/ttsr.d.ts +14 -3
  12. package/dist/types/gjc-runtime/deep-interview-state.d.ts +5 -2
  13. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +7 -0
  14. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  15. package/dist/types/gjc-runtime/team-runtime.d.ts +26 -0
  16. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  17. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +42 -0
  18. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +119 -13
  19. package/dist/types/goals/runtime.d.ts +1 -0
  20. package/dist/types/hooks/native-skill-hook.d.ts +12 -0
  21. package/dist/types/modes/components/assistant-message.d.ts +5 -1
  22. package/dist/types/modes/components/status-line.d.ts +9 -0
  23. package/dist/types/modes/components/thinking-selector.d.ts +2 -2
  24. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  25. package/dist/types/modes/controllers/event-controller.d.ts +5 -0
  26. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  27. package/dist/types/modes/interactive-mode.d.ts +4 -0
  28. package/dist/types/modes/rpc/rpc-mode.d.ts +1 -3
  29. package/dist/types/modes/rpc/rpc-socket-security.d.ts +3 -0
  30. package/dist/types/modes/rpc/rpc-types.d.ts +14 -0
  31. package/dist/types/modes/shared/agent-wire/event-contract.d.ts +19 -0
  32. package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +23 -1
  33. package/dist/types/modes/types.d.ts +3 -0
  34. package/dist/types/modes/utils/injected-user-submission.d.ts +52 -0
  35. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  36. package/dist/types/notifications/config-commands.d.ts +9 -0
  37. package/dist/types/notifications/config.d.ts +22 -0
  38. package/dist/types/notifications/index.d.ts +2 -0
  39. package/dist/types/notifications/lifecycle-commands.d.ts +1 -0
  40. package/dist/types/notifications/lifecycle-control-runtime.d.ts +2 -1
  41. package/dist/types/notifications/reply-sent-store.d.ts +53 -0
  42. package/dist/types/notifications/rich-draft.d.ts +68 -0
  43. package/dist/types/notifications/rich-render.d.ts +90 -0
  44. package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
  45. package/dist/types/notifications/telegram-daemon.d.ts +30 -0
  46. package/dist/types/notifications/telegram-reference.d.ts +7 -0
  47. package/dist/types/notifications/threaded-render.d.ts +14 -0
  48. package/dist/types/sdk.d.ts +3 -0
  49. package/dist/types/session/agent-session.d.ts +43 -1
  50. package/dist/types/session/file-mention-pruning.d.ts +8 -0
  51. package/dist/types/session/messages.d.ts +4 -0
  52. package/dist/types/session/session-manager.d.ts +17 -1
  53. package/dist/types/skill-state/active-state.d.ts +18 -1
  54. package/dist/types/system-prompt.d.ts +18 -2
  55. package/dist/types/task/executor.d.ts +1 -0
  56. package/dist/types/task/fork-context-budget.d.ts +7 -0
  57. package/dist/types/task/token-log.d.ts +30 -0
  58. package/dist/types/task/token-log.test.d.ts +1 -0
  59. package/dist/types/tools/bisect.d.ts +81 -0
  60. package/dist/types/tools/index.d.ts +1 -0
  61. package/dist/types/tools/search.d.ts +2 -2
  62. package/dist/types/utils/file-mentions.d.ts +14 -0
  63. package/dist/types/utils/git.d.ts +32 -0
  64. package/dist/types/utils/pasted-image-path.d.ts +30 -0
  65. package/dist/types/web/scrapers/youtube.d.ts +8 -0
  66. package/package.json +8 -11
  67. package/src/append-only-mode.ts +58 -0
  68. package/src/autoresearch/prompt.md +1 -1
  69. package/src/autoresearch/tools/log-experiment.ts +1 -1
  70. package/src/capability/rule.ts +9 -0
  71. package/src/cli/config-cli.ts +27 -3
  72. package/src/cli/fixture-report.test.ts +230 -0
  73. package/src/cli/fixture-report.ts +171 -0
  74. package/src/cli/nofile-limit.ts +48 -0
  75. package/src/cli/notify-cli.ts +11 -9
  76. package/src/cli/update-cli.ts +85 -9
  77. package/src/cli.ts +26 -0
  78. package/src/commands/contribution-prep.ts +1 -1
  79. package/src/config/keybindings.ts +1 -1
  80. package/src/config/settings-schema.ts +76 -1
  81. package/src/defaults/gjc/skills/deep-interview/SKILL.md +12 -13
  82. package/src/defaults/gjc/skills/ralplan/SKILL.md +30 -93
  83. package/src/defaults/gjc/skills/team/SKILL.md +16 -13
  84. package/src/defaults/gjc/skills/ultragoal/SKILL.md +59 -4
  85. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +30 -0
  86. package/src/defaults/gjc-defaults.ts +9 -0
  87. package/src/discovery/helpers.ts +10 -0
  88. package/src/discovery/index.ts +1 -0
  89. package/src/export/html/template.generated.ts +1 -1
  90. package/src/export/html/template.js +60 -2
  91. package/src/export/ttsr.ts +96 -18
  92. package/src/extensibility/extensions/runner.ts +181 -219
  93. package/src/extensibility/gjc-plugins/runtime-adapters.ts +156 -44
  94. package/src/gjc-runtime/deep-interview-state.ts +31 -2
  95. package/src/gjc-runtime/launch-tmux.ts +6 -0
  96. package/src/gjc-runtime/session-state-sidecar.ts +208 -13
  97. package/src/gjc-runtime/state-writer.ts +15 -0
  98. package/src/gjc-runtime/team-runtime.ts +175 -19
  99. package/src/gjc-runtime/tmux-common.ts +1 -0
  100. package/src/gjc-runtime/ultragoal-guard.ts +55 -66
  101. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +315 -0
  102. package/src/gjc-runtime/ultragoal-runtime.ts +1602 -143
  103. package/src/gjc-runtime/workflow-manifest.generated.json +113 -2
  104. package/src/gjc-runtime/workflow-manifest.ts +74 -1
  105. package/src/goals/runtime.ts +10 -2
  106. package/src/hindsight/backend.ts +1 -3
  107. package/src/hooks/native-skill-hook.ts +95 -4
  108. package/src/internal-urls/docs-index.generated.ts +18 -9
  109. package/src/lsp/client.ts +1 -0
  110. package/src/main.ts +43 -2
  111. package/src/memories/index.ts +12 -1
  112. package/src/modes/components/assistant-message.ts +241 -82
  113. package/src/modes/components/custom-editor.ts +1 -0
  114. package/src/modes/components/footer.ts +7 -2
  115. package/src/modes/components/status-line.ts +167 -54
  116. package/src/modes/components/thinking-selector.ts +10 -8
  117. package/src/modes/components/tool-execution.ts +44 -19
  118. package/src/modes/controllers/command-controller.ts +39 -2
  119. package/src/modes/controllers/event-controller.ts +77 -49
  120. package/src/modes/controllers/extension-ui-controller.ts +8 -1
  121. package/src/modes/controllers/input-controller.ts +13 -18
  122. package/src/modes/controllers/selector-controller.ts +41 -0
  123. package/src/modes/interactive-mode.ts +14 -1
  124. package/src/modes/rpc/rpc-mode.ts +32 -12
  125. package/src/modes/rpc/rpc-socket-security.ts +13 -1
  126. package/src/modes/rpc/rpc-types.ts +9 -0
  127. package/src/modes/shared/agent-wire/command-validation.ts +4 -0
  128. package/src/modes/shared/agent-wire/event-contract.ts +22 -0
  129. package/src/modes/shared/agent-wire/event-envelope.ts +82 -0
  130. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  131. package/src/modes/types.ts +3 -0
  132. package/src/modes/utils/injected-user-submission.ts +94 -0
  133. package/src/modes/utils/ui-helpers.ts +17 -4
  134. package/src/notifications/config-commands.ts +20 -0
  135. package/src/notifications/config.ts +43 -4
  136. package/src/notifications/index.ts +105 -9
  137. package/src/notifications/lifecycle-commands.ts +37 -16
  138. package/src/notifications/lifecycle-control-runtime.ts +8 -3
  139. package/src/notifications/reply-sent-store.ts +134 -0
  140. package/src/notifications/rich-draft.ts +107 -0
  141. package/src/notifications/rich-render.ts +142 -0
  142. package/src/notifications/telegram-daemon-cli.ts +54 -7
  143. package/src/notifications/telegram-daemon-control.ts +2 -2
  144. package/src/notifications/telegram-daemon.ts +390 -56
  145. package/src/notifications/telegram-reference.ts +17 -0
  146. package/src/notifications/threaded-render.ts +45 -2
  147. package/src/prompts/agents/architect.md +19 -10
  148. package/src/prompts/agents/critic.md +16 -18
  149. package/src/prompts/agents/executor.md +14 -11
  150. package/src/prompts/agents/planner.md +18 -13
  151. package/src/prompts/ci-green-request.md +3 -8
  152. package/src/prompts/goals/goal-continuation.md +0 -5
  153. package/src/prompts/goals/goal-mode-active.md +0 -5
  154. package/src/prompts/system/agent-creation-architect.md +2 -2
  155. package/src/prompts/system/plan-mode-active.md +1 -1
  156. package/src/prompts/system/plan-mode-subagent.md +3 -3
  157. package/src/prompts/system/project-prompt.md +0 -13
  158. package/src/prompts/system/subagent-system-prompt.md +3 -2
  159. package/src/prompts/system/system-prompt.md +26 -19
  160. package/src/prompts/system/volatile-project-context.md +10 -0
  161. package/src/prompts/system/web-search.md +4 -7
  162. package/src/prompts/tools/apply-patch.md +6 -0
  163. package/src/prompts/tools/ast-edit.md +1 -0
  164. package/src/prompts/tools/bash.md +1 -1
  165. package/src/prompts/tools/bisect.md +22 -0
  166. package/src/prompts/tools/browser.md +1 -1
  167. package/src/prompts/tools/cron.md +0 -2
  168. package/src/prompts/tools/eval.md +2 -2
  169. package/src/prompts/tools/find.md +1 -1
  170. package/src/prompts/tools/github.md +6 -6
  171. package/src/prompts/tools/goal.md +1 -3
  172. package/src/prompts/tools/image-gen.md +12 -5
  173. package/src/prompts/tools/irc.md +5 -4
  174. package/src/prompts/tools/job.md +2 -2
  175. package/src/prompts/tools/lsp.md +1 -1
  176. package/src/prompts/tools/monitor.md +2 -2
  177. package/src/prompts/tools/patch.md +2 -2
  178. package/src/prompts/tools/replace.md +2 -21
  179. package/src/prompts/tools/rewind.md +1 -1
  180. package/src/prompts/tools/search-tool-bm25.md +6 -11
  181. package/src/prompts/tools/search.md +1 -1
  182. package/src/prompts/tools/skill.md +3 -2
  183. package/src/prompts/tools/ssh.md +2 -1
  184. package/src/prompts/tools/subagent.md +2 -2
  185. package/src/prompts/tools/task.md +4 -5
  186. package/src/prompts/tools/todo-write.md +1 -1
  187. package/src/prompts/tools/vim.md +1 -0
  188. package/src/prompts/tools/web-search.md +2 -0
  189. package/src/prompts/tools/write.md +8 -2
  190. package/src/sdk.ts +68 -30
  191. package/src/session/agent-session.ts +734 -107
  192. package/src/session/file-mention-pruning.ts +64 -0
  193. package/src/session/messages.ts +4 -0
  194. package/src/session/session-manager.ts +59 -2
  195. package/src/skill-state/active-state.ts +141 -5
  196. package/src/skill-state/deep-interview-mutation-guard.ts +67 -3
  197. package/src/slash-commands/builtin-registry.ts +143 -1
  198. package/src/system-prompt.ts +57 -16
  199. package/src/task/executor.ts +125 -20
  200. package/src/task/fork-context-advisory.ts +2 -7
  201. package/src/task/fork-context-budget.ts +9 -0
  202. package/src/task/index.ts +4 -3
  203. package/src/task/output-manager.ts +4 -2
  204. package/src/task/token-log.test.ts +188 -0
  205. package/src/task/token-log.ts +179 -0
  206. package/src/tools/bisect.ts +367 -0
  207. package/src/tools/index.ts +3 -0
  208. package/src/tools/output-meta.ts +116 -4
  209. package/src/tools/search.ts +8 -2
  210. package/src/tools/skill.ts +15 -0
  211. package/src/utils/file-mentions.ts +39 -7
  212. package/src/utils/git.ts +73 -0
  213. package/src/utils/pasted-image-path.ts +170 -0
  214. package/src/web/scrapers/youtube.ts +24 -12
@@ -0,0 +1,188 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { appendFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { GJC_SESSION_ACTIVITY_FILE, sessionRoot } from "../gjc-runtime/session-layout";
6
+ import {
7
+ computeCacheHitRate,
8
+ persistTaskTokenLog,
9
+ readTaskTokenLogs,
10
+ resolveTaskTokenLogDir,
11
+ taskTokenLogFromChat,
12
+ taskTokenLogFromUsage,
13
+ } from "./token-log";
14
+
15
+ async function withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
16
+ const dir = await mkdtemp(join(tmpdir(), "gjc-token-log-"));
17
+ try {
18
+ return await fn(dir);
19
+ } finally {
20
+ await rm(dir, { recursive: true, force: true });
21
+ }
22
+ }
23
+
24
+ describe("task token log", () => {
25
+ it("maps raw chat buckets without folding cache into input", () => {
26
+ const log = taskTokenLogFromChat(
27
+ {
28
+ inputTokens: 100,
29
+ outputTokens: 20,
30
+ cachedInputTokens: 30,
31
+ cacheWriteTokens: 5,
32
+ costUsd: 0.25,
33
+ },
34
+ {
35
+ subagentId: "root",
36
+ agent: "main",
37
+ turn: 1,
38
+ at: "2026-01-01T00:00:00.000Z",
39
+ model: "model-a",
40
+ },
41
+ );
42
+
43
+ expect(log.input).toBe(100);
44
+ expect(log.output).toBe(20);
45
+ expect(log.cacheRead).toBe(30);
46
+ expect(log.cacheWrite).toBe(5);
47
+ expect(log.totalTokens).toBe(155);
48
+ expect(log.cost).toBe(0.25);
49
+ expect(log.model).toBe("model-a");
50
+ });
51
+
52
+ it("maps zero-cache chat buckets without inventing cache tokens", () => {
53
+ const log = taskTokenLogFromChat(
54
+ { inputTokens: 7, outputTokens: 3, cachedInputTokens: 0, cacheWriteTokens: 0 },
55
+ { subagentId: "root", turn: 2, at: "2026-01-01T00:00:02.000Z" },
56
+ );
57
+
58
+ expect(log.input).toBe(7);
59
+ expect(log.output).toBe(3);
60
+ expect(log.cacheRead).toBe(0);
61
+ expect(log.cacheWrite).toBe(0);
62
+ expect(log.totalTokens).toBe(10);
63
+ });
64
+
65
+ it("keeps large cache-only reads out of input and hits the cache-rate upper boundary", () => {
66
+ const log = taskTokenLogFromChat(
67
+ { inputTokens: 0, outputTokens: 2, cachedInputTokens: 1_000_000_000, cacheWriteTokens: 0 },
68
+ { subagentId: "root", turn: 3, at: "2026-01-01T00:00:03.000Z" },
69
+ );
70
+
71
+ expect(log.input).toBe(0);
72
+ expect(log.output).toBe(2);
73
+ expect(log.cacheRead).toBe(1_000_000_000);
74
+ expect(log.cacheWrite).toBe(0);
75
+ expect(log.totalTokens).toBe(1_000_000_002);
76
+ expect(computeCacheHitRate(log.input, log.cacheRead)).toBe(1);
77
+ });
78
+
79
+ it("persists and reads a jsonl round trip", async () => {
80
+ await withTempDir(async dir => {
81
+ const first = taskTokenLogFromChat(
82
+ { inputTokens: 1, outputTokens: 2, cachedInputTokens: 3, cacheWriteTokens: 4 },
83
+ { subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z" },
84
+ );
85
+ const second = taskTokenLogFromChat(
86
+ { inputTokens: 5, outputTokens: 6, cachedInputTokens: 7, cacheWriteTokens: 8 },
87
+ { subagentId: "child", agent: "executor", turn: 1, at: "2026-01-01T00:00:01.000Z" },
88
+ );
89
+
90
+ await persistTaskTokenLog(first, { dir });
91
+ await persistTaskTokenLog(second, { dir });
92
+
93
+ expect(await readTaskTokenLogs(dir)).toEqual([first, second]);
94
+ });
95
+ });
96
+
97
+ it("prefers the current session manager over a stale latest-active session", async () => {
98
+ await withTempDir(async cwd => {
99
+ const currentSessionId = "current-session";
100
+ const staleSessionId = "stale-session";
101
+ for (const [sessionId, updatedAt] of [
102
+ [currentSessionId, "2026-01-01T00:00:00.000Z"],
103
+ [staleSessionId, "2026-01-01T00:00:10.000Z"],
104
+ ] as const) {
105
+ const markerPath = join(sessionRoot(cwd, sessionId), GJC_SESSION_ACTIVITY_FILE);
106
+ await mkdir(join(markerPath, ".."), { recursive: true });
107
+ await writeFile(
108
+ markerPath,
109
+ `${JSON.stringify({ session_id: sessionId, updated_at: updatedAt })}\n`,
110
+ "utf-8",
111
+ );
112
+ }
113
+
114
+ await expect(resolveTaskTokenLogDir(cwd, undefined, "")).resolves.toBe(
115
+ join(sessionRoot(cwd, staleSessionId), "token-logs"),
116
+ );
117
+ await expect(resolveTaskTokenLogDir(cwd, { getSessionId: () => currentSessionId }, "")).resolves.toBe(
118
+ join(sessionRoot(cwd, currentSessionId), "token-logs"),
119
+ );
120
+ });
121
+ });
122
+
123
+ it("missing token-log file reads as empty", async () => {
124
+ await withTempDir(async dir => {
125
+ expect(await readTaskTokenLogs(dir)).toEqual([]);
126
+ });
127
+ });
128
+
129
+ it("subtracts cache from the aggregate ChatUsageSnapshot input bucket", () => {
130
+ // buildUsageSnapshot sets inputTokens = rawInput + cacheRead + cacheWrite.
131
+ // taskTokenLogFromUsage must recover the raw (cache-excluded) input.
132
+ const log = taskTokenLogFromUsage(
133
+ {
134
+ inputTokens: 135,
135
+ outputTokens: 20,
136
+ totalTokens: 155,
137
+ cachedInputTokens: 30,
138
+ cacheWriteTokens: 5,
139
+ reasoningOutputTokens: 0,
140
+ },
141
+ { subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z", cost: { usd: 0.5 } },
142
+ );
143
+
144
+ expect(log.input).toBe(100);
145
+ expect(log.cacheRead).toBe(30);
146
+ expect(log.cacheWrite).toBe(5);
147
+ expect(log.output).toBe(20);
148
+ expect(log.totalTokens).toBe(155);
149
+ expect(log.cost).toBe(0.5);
150
+ });
151
+
152
+ it("clamps aggregate input underflow to zero", () => {
153
+ const log = taskTokenLogFromUsage(
154
+ {
155
+ inputTokens: 10,
156
+ outputTokens: 1,
157
+ totalTokens: 10,
158
+ cachedInputTokens: 8,
159
+ cacheWriteTokens: 5,
160
+ reasoningOutputTokens: undefined,
161
+ },
162
+ { subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z" },
163
+ );
164
+
165
+ expect(log.input).toBe(0);
166
+ });
167
+
168
+ it("skips a torn or corrupt jsonl line instead of throwing", async () => {
169
+ await withTempDir(async dir => {
170
+ const valid = taskTokenLogFromChat(
171
+ { inputTokens: 1, outputTokens: 2, cachedInputTokens: 3, cacheWriteTokens: 4 },
172
+ { subagentId: "root", turn: 1, at: "2026-01-01T00:00:00.000Z" },
173
+ );
174
+ await persistTaskTokenLog(valid, { dir });
175
+ // A partially-flushed / corrupt trailing line must not abort the read.
176
+ await appendFile(join(dir, "token-log.jsonl"), '{"subagentId":"root","turn":2,', "utf-8");
177
+
178
+ expect(await readTaskTokenLogs(dir)).toEqual([valid]);
179
+ });
180
+ });
181
+
182
+ it("throws when a non-empty log has no valid entries", async () => {
183
+ await withTempDir(async dir => {
184
+ await appendFile(join(dir, "token-log.jsonl"), "not json\n{}\n", "utf-8");
185
+ await expect(readTaskTokenLogs(dir)).rejects.toThrow(/corrupt token-log/);
186
+ });
187
+ });
188
+ });
@@ -0,0 +1,179 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import type { ChatUsageSnapshot, CostEstimate } from "@gajae-code/agent-core";
4
+ import { sessionRoot } from "../gjc-runtime/session-layout";
5
+ import { resolveGjcSessionForRead, SessionResolutionError } from "../gjc-runtime/session-resolution";
6
+ import type { TaskTokenLog, TaskTokenMetrics } from "./types";
7
+
8
+ const TOKEN_LOG_FILE = "token-log.jsonl";
9
+
10
+ export interface TaskTokenLogChatBuckets {
11
+ readonly inputTokens: number;
12
+ readonly outputTokens: number;
13
+ readonly cachedInputTokens: number;
14
+ readonly cacheWriteTokens: number;
15
+ readonly costUsd?: number;
16
+ }
17
+
18
+ export interface TaskTokenLogSessionManager {
19
+ getSessionId(): string;
20
+ }
21
+
22
+ export interface TaskTokenLogContext {
23
+ readonly subagentId: string;
24
+ readonly agent?: string;
25
+ readonly turn: number;
26
+ readonly at: string;
27
+ readonly model?: string;
28
+ }
29
+
30
+ export function taskTokenLogFromChat(chat: TaskTokenLogChatBuckets, ctx: TaskTokenLogContext): TaskTokenLog {
31
+ const input = chat.inputTokens;
32
+ const output = chat.outputTokens;
33
+ const cacheRead = chat.cachedInputTokens;
34
+ const cacheWrite = chat.cacheWriteTokens;
35
+ return {
36
+ subagentId: ctx.subagentId,
37
+ ...(ctx.agent ? { agent: ctx.agent } : {}),
38
+ turn: ctx.turn,
39
+ at: ctx.at,
40
+ input,
41
+ output,
42
+ cacheRead,
43
+ cacheWrite,
44
+ totalTokens: input + output + cacheRead + cacheWrite,
45
+ ...(chat.costUsd !== undefined ? { cost: chat.costUsd } : {}),
46
+ ...(ctx.model ? { model: ctx.model } : {}),
47
+ };
48
+ }
49
+
50
+ export function taskTokenLogFromUsage(
51
+ usage: ChatUsageSnapshot,
52
+ ctx: TaskTokenLogContext & { readonly cost?: CostEstimate },
53
+ ): TaskTokenLog {
54
+ const costUsd = ctx.cost && "usd" in ctx.cost ? ctx.cost.usd : undefined;
55
+ const cacheRead = usage.cachedInputTokens ?? 0;
56
+ const cacheWrite = usage.cacheWriteTokens ?? 0;
57
+ // `ChatUsageSnapshot.inputTokens` is the aggregate input-class bucket
58
+ // (raw input + cacheRead + cacheWrite; see buildUsageSnapshot in the agent
59
+ // telemetry layer). `TaskTokenLog.input` must be the cost-bearing bucket that
60
+ // EXCLUDES cache, so subtract the cache buckets back out (clamped at 0).
61
+ const input = Math.max(0, usage.inputTokens - cacheRead - cacheWrite);
62
+ return taskTokenLogFromChat(
63
+ {
64
+ inputTokens: input,
65
+ outputTokens: usage.outputTokens,
66
+ cachedInputTokens: cacheRead,
67
+ cacheWriteTokens: cacheWrite,
68
+ ...(costUsd !== undefined ? { costUsd } : {}),
69
+ },
70
+ ctx,
71
+ );
72
+ }
73
+
74
+ export async function resolveTaskTokenLogDir(
75
+ cwd: string,
76
+ sessionManager: TaskTokenLogSessionManager | undefined,
77
+ envSessionId: string | undefined = process.env.GJC_SESSION_ID,
78
+ ): Promise<string | undefined> {
79
+ // Prefer the canonical SessionManager id so root turns land in the SAME
80
+ // `<session>/token-logs` dir the task executor uses for subagent turns and
81
+ // that `gjc --fixture <id>` reads from. Fall back to the env/latest-active
82
+ // session only when no manager id is available (e.g. lifecycle launches where
83
+ // the SDK adopts a pre-allocated id internally). Never let a best-effort
84
+ // telemetry side channel crash startup — swallow every SessionResolutionError.
85
+ const managerId = sessionManager?.getSessionId();
86
+ if (managerId) return path.join(sessionRoot(cwd, managerId), "token-logs");
87
+ try {
88
+ const session = await resolveGjcSessionForRead(cwd, { envSessionId });
89
+ return path.join(session.sessionRoot, "token-logs");
90
+ } catch (error) {
91
+ if (error instanceof SessionResolutionError) return undefined;
92
+ throw error;
93
+ }
94
+ }
95
+ export async function persistTaskTokenLog(entry: TaskTokenLog, opts: { dir: string }): Promise<void> {
96
+ await fs.mkdir(opts.dir, { recursive: true });
97
+ await fs.appendFile(path.join(opts.dir, TOKEN_LOG_FILE), `${JSON.stringify(entry)}\n`, "utf-8");
98
+ }
99
+
100
+ export async function readTaskTokenLogs(dir: string): Promise<TaskTokenLog[]> {
101
+ let raw: string;
102
+ try {
103
+ raw = await fs.readFile(path.join(dir, TOKEN_LOG_FILE), "utf-8");
104
+ } catch (error) {
105
+ if (isNodeError(error) && error.code === "ENOENT") return [];
106
+ throw error;
107
+ }
108
+ const logs: TaskTokenLog[] = [];
109
+ let skipped = 0;
110
+ for (const line of raw.split("\n")) {
111
+ if (line.trim().length === 0) continue;
112
+ let parsed: unknown;
113
+ try {
114
+ parsed = JSON.parse(line);
115
+ } catch {
116
+ // Skip a torn/partially-flushed tail line rather than aborting the
117
+ // entire read; concurrent appends can leave an incomplete final line.
118
+ skipped += 1;
119
+ continue;
120
+ }
121
+ if (isTaskTokenLog(parsed)) logs.push(parsed);
122
+ else skipped += 1;
123
+ }
124
+ // A torn tail line alongside valid entries is tolerated, but a non-empty file
125
+ // that yields zero valid entries is corrupt — surface it loudly rather than
126
+ // returning [] (which a caller would render as a misleading all-zero report).
127
+ if (logs.length === 0 && skipped > 0) {
128
+ throw new Error(`corrupt token-log: ${skipped} unparseable line(s) and no valid entries in ${dir}`);
129
+ }
130
+ return logs;
131
+ }
132
+
133
+ export function computeTaskTokenMetrics(logs: readonly TaskTokenLog[]): TaskTokenMetrics {
134
+ let inputTokens = 0;
135
+ let outputTokens = 0;
136
+ let cacheReadTokens = 0;
137
+ let cacheWriteTokens = 0;
138
+ let totalTokens = 0;
139
+ for (const log of logs) {
140
+ inputTokens += log.input;
141
+ outputTokens += log.output;
142
+ cacheReadTokens += log.cacheRead;
143
+ cacheWriteTokens += log.cacheWrite;
144
+ totalTokens += log.totalTokens;
145
+ }
146
+ return {
147
+ turns: logs.length,
148
+ inputTokens,
149
+ outputTokens,
150
+ cacheReadTokens,
151
+ cacheWriteTokens,
152
+ totalTokens,
153
+ cacheHitRate: computeCacheHitRate(inputTokens, cacheReadTokens),
154
+ };
155
+ }
156
+
157
+ export function computeCacheHitRate(input: number, cacheRead: number): number {
158
+ const denominator = input + cacheRead;
159
+ return denominator <= 0 ? 0 : cacheRead / denominator;
160
+ }
161
+
162
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
163
+ return error instanceof Error && "code" in error;
164
+ }
165
+
166
+ function isTaskTokenLog(value: unknown): value is TaskTokenLog {
167
+ if (value === null || typeof value !== "object") return false;
168
+ const record = value as Record<string, unknown>;
169
+ return (
170
+ typeof record.subagentId === "string" &&
171
+ typeof record.turn === "number" &&
172
+ typeof record.at === "string" &&
173
+ typeof record.input === "number" &&
174
+ typeof record.output === "number" &&
175
+ typeof record.cacheRead === "number" &&
176
+ typeof record.cacheWrite === "number" &&
177
+ typeof record.totalTokens === "number"
178
+ );
179
+ }