@gajae-code/coding-agent 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/cli/classify-install-target.d.ts +0 -10
  3. package/dist/types/cli/daemon-cli.d.ts +2 -0
  4. package/dist/types/cli/initial-message.d.ts +1 -1
  5. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  6. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  7. package/dist/types/cli/notify-cli.d.ts +3 -1
  8. package/dist/types/cli/session-picker.d.ts +4 -3
  9. package/dist/types/cli/update-cli.d.ts +22 -3
  10. package/dist/types/cli.d.ts +4 -0
  11. package/dist/types/commands/daemon.d.ts +6 -2
  12. package/dist/types/commands/launch.d.ts +1 -0
  13. package/dist/types/commands/notify.d.ts +6 -0
  14. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  15. package/dist/types/commit/analysis/summary.d.ts +1 -1
  16. package/dist/types/commit/changelog/generate.d.ts +1 -1
  17. package/dist/types/commit/changelog/index.d.ts +2 -2
  18. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  19. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  20. package/dist/types/config/keybindings.d.ts +5 -0
  21. package/dist/types/config/model-profile-activation.d.ts +1 -0
  22. package/dist/types/config/settings-schema.d.ts +47 -7
  23. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  24. package/dist/types/coordinator-mcp/server.d.ts +67 -6
  25. package/dist/types/daemon/control-types.d.ts +10 -0
  26. package/dist/types/daemon/operator-contract.d.ts +47 -0
  27. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  28. package/dist/types/discovery/helpers.d.ts +1 -1
  29. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  30. package/dist/types/gjc-runtime/launch-tmux.d.ts +26 -7
  31. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +34 -3
  32. package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
  33. package/dist/types/gjc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +309 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +49 -17
  36. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  37. package/dist/types/main.d.ts +66 -2
  38. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  39. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  40. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  41. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  42. package/dist/types/modes/components/gajae-pet-widget.d.ts +59 -0
  43. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  44. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  45. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  46. package/dist/types/modes/components/session-selector.d.ts +22 -9
  47. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  48. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  49. package/dist/types/modes/components/user-message.d.ts +2 -1
  50. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  51. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  52. package/dist/types/modes/interactive-mode.d.ts +24 -2
  53. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  54. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  55. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  56. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  57. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  58. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  59. package/dist/types/modes/types.d.ts +28 -2
  60. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  61. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  62. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  63. package/dist/types/notifications/config.d.ts +34 -0
  64. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  65. package/dist/types/notifications/index.d.ts +27 -0
  66. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  67. package/dist/types/notifications/notification-service.d.ts +144 -0
  68. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  69. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  70. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  71. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  72. package/dist/types/notifications/topic-registry.d.ts +24 -4
  73. package/dist/types/registry/agent-registry.d.ts +2 -0
  74. package/dist/types/rlm/index.d.ts +1 -1
  75. package/dist/types/sdk.d.ts +2 -0
  76. package/dist/types/secrets/obfuscator.d.ts +1 -1
  77. package/dist/types/session/agent-session.d.ts +26 -2
  78. package/dist/types/session/cache-economics.d.ts +79 -0
  79. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  80. package/dist/types/session/session-manager.d.ts +124 -4
  81. package/dist/types/session/session-storage.d.ts +155 -13
  82. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  83. package/dist/types/task/executor.d.ts +3 -0
  84. package/dist/types/task/receipt.d.ts +3 -1
  85. package/dist/types/task/types.d.ts +8 -0
  86. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  87. package/dist/types/tools/ask.d.ts +25 -1
  88. package/dist/types/tools/bash.d.ts +1 -1
  89. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  90. package/dist/types/tools/index.d.ts +58 -6
  91. package/dist/types/tools/telegram-send.d.ts +7 -0
  92. package/dist/types/utils/external-editor.d.ts +2 -0
  93. package/package.json +9 -9
  94. package/scripts/compile-args.ts +8 -2
  95. package/scripts/g004-tmux-smoke.ts +196 -36
  96. package/src/cli/daemon-cli.ts +19 -29
  97. package/src/cli/malloc-env-guard.test.ts +104 -0
  98. package/src/cli/malloc-env-guard.ts +149 -0
  99. package/src/cli/notify-cli.ts +78 -14
  100. package/src/cli/session-picker.ts +23 -40
  101. package/src/cli/update-cli.ts +56 -37
  102. package/src/cli.ts +50 -10
  103. package/src/commands/daemon.ts +19 -5
  104. package/src/commands/harness.ts +515 -47
  105. package/src/commands/launch.ts +11 -3
  106. package/src/commands/notify.ts +5 -1
  107. package/src/commands/session.ts +6 -1
  108. package/src/config/keybindings.ts +5 -0
  109. package/src/config/model-profile-activation.ts +1 -1
  110. package/src/config/model-profiles.ts +23 -23
  111. package/src/config/settings-schema.ts +41 -1
  112. package/src/coordinator-mcp/model-preset.ts +100 -0
  113. package/src/coordinator-mcp/server.ts +2653 -425
  114. package/src/daemon/control-types.ts +11 -0
  115. package/src/daemon/operator-contract.ts +121 -0
  116. package/src/deep-interview/render-middleware.ts +67 -2
  117. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  118. package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  119. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  120. package/src/gjc-runtime/launch-tmux.ts +702 -135
  121. package/src/gjc-runtime/session-state-sidecar.ts +756 -135
  122. package/src/gjc-runtime/team-runtime.ts +6 -0
  123. package/src/gjc-runtime/tmux-common.ts +26 -7
  124. package/src/gjc-runtime/tmux-gc.ts +52 -9
  125. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +185 -0
  126. package/src/gjc-runtime/tmux-owner-isolation.ts +2060 -0
  127. package/src/gjc-runtime/tmux-sessions.ts +871 -68
  128. package/src/internal-urls/docs-index.generated.ts +9 -8
  129. package/src/internal-urls/local-protocol.ts +29 -10
  130. package/src/main.ts +298 -75
  131. package/src/modes/acp/acp-agent.ts +728 -98
  132. package/src/modes/acp/acp-mode.ts +29 -2
  133. package/src/modes/components/assistant-message.ts +32 -3
  134. package/src/modes/components/bash-execution.ts +43 -13
  135. package/src/modes/components/custom-editor.ts +5 -0
  136. package/src/modes/components/gajae-pet-widget.ts +360 -0
  137. package/src/modes/components/hook-selector.ts +12 -0
  138. package/src/modes/components/irc-sidebar.ts +206 -0
  139. package/src/modes/components/pet-selector.ts +55 -0
  140. package/src/modes/components/session-selector.ts +222 -221
  141. package/src/modes/components/settings-selector.ts +10 -0
  142. package/src/modes/components/tool-execution.ts +47 -6
  143. package/src/modes/components/user-message.ts +26 -9
  144. package/src/modes/controllers/command-controller.ts +28 -2
  145. package/src/modes/controllers/event-controller.ts +159 -36
  146. package/src/modes/controllers/extension-ui-controller.ts +36 -17
  147. package/src/modes/controllers/input-controller.ts +8 -2
  148. package/src/modes/controllers/selector-controller.ts +62 -15
  149. package/src/modes/interactive-mode.ts +183 -7
  150. package/src/modes/irc-observation-ledger.ts +127 -0
  151. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  152. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  153. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  154. package/src/modes/types.ts +34 -2
  155. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  156. package/src/modes/utils/irc-message.ts +138 -0
  157. package/src/modes/utils/ui-helpers.ts +216 -50
  158. package/src/notifications/config.ts +44 -0
  159. package/src/notifications/daemon-paths.ts +8 -0
  160. package/src/notifications/index.ts +535 -75
  161. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  162. package/src/notifications/notification-service.ts +710 -0
  163. package/src/notifications/rate-limit-pool.ts +46 -12
  164. package/src/notifications/telegram-daemon-cli.ts +2 -0
  165. package/src/notifications/telegram-daemon-control.ts +27 -2
  166. package/src/notifications/telegram-daemon.ts +635 -94
  167. package/src/notifications/telegram-reference.ts +73 -13
  168. package/src/notifications/topic-registry.ts +86 -9
  169. package/src/prompts/system/irc-peer-roster.md +1 -0
  170. package/src/prompts/system/subagent-system-prompt.md +1 -0
  171. package/src/prompts/system/system-prompt.md +1 -0
  172. package/src/prompts/tools/irc.md +1 -1
  173. package/src/registry/agent-registry.ts +3 -0
  174. package/src/sdk.ts +52 -1
  175. package/src/secrets/obfuscator.ts +4 -2
  176. package/src/session/agent-session.ts +396 -145
  177. package/src/session/cache-economics.ts +275 -0
  178. package/src/session/provider-safety-stop.ts +9 -0
  179. package/src/session/session-dump-format.ts +6 -0
  180. package/src/session/session-manager.ts +809 -39
  181. package/src/session/session-storage.ts +582 -38
  182. package/src/slash-commands/builtin-registry.ts +143 -9
  183. package/src/task/executor.ts +12 -3
  184. package/src/task/index.ts +8 -0
  185. package/src/task/receipt.ts +7 -2
  186. package/src/task/types.ts +26 -0
  187. package/src/tools/ask-answer-registry.ts +25 -0
  188. package/src/tools/ask.ts +355 -33
  189. package/src/tools/browser/tab-supervisor.ts +63 -6
  190. package/src/tools/browser.ts +8 -3
  191. package/src/tools/index.ts +81 -6
  192. package/src/tools/sqlite-reader.ts +8 -4
  193. package/src/tui/output-block.ts +18 -6
  194. package/src/utils/external-editor.ts +6 -1
@@ -0,0 +1,275 @@
1
+ import type { Usage } from "@gajae-code/ai";
2
+
3
+ const TOKENS_PER_MILLION = 1_000_000;
4
+ const MATERIAL_COST_USD = 0.01;
5
+ const MATERIAL_CACHE_TOKENS = 10_000;
6
+ const EXPENSIVE_MISS_COST_USD = 0.05;
7
+ const LARGE_INPUT_TOKENS = 20_000;
8
+ const LARGE_CACHE_WRITE_TOKENS = 25_000;
9
+ const TRANSCRIPT_WARNING_CAP = 3;
10
+
11
+ export interface CacheEconomicsPricing {
12
+ input: number;
13
+ cacheRead: number;
14
+ cacheWrite: number;
15
+ }
16
+
17
+ export interface CacheEconomicsModelCost extends CacheEconomicsPricing {
18
+ output: number;
19
+ }
20
+
21
+ export type CacheEconomicsBasis =
22
+ | { kind: "persisted-aggregate"; costBreakdown: Usage["cost"] }
23
+ | { kind: "current-model-estimate"; pricing: CacheEconomicsPricing };
24
+
25
+ export interface CacheEconomicsUsage {
26
+ input: number;
27
+ output?: number;
28
+ cacheRead: number;
29
+ cacheWrite: number;
30
+ total?: number;
31
+ cost?: {
32
+ input?: number;
33
+ output?: number;
34
+ cacheRead?: number;
35
+ cacheWrite?: number;
36
+ total?: number;
37
+ };
38
+ }
39
+
40
+ export interface CacheMissCostSummary {
41
+ inputTokens: number;
42
+ cacheReadTokens: number;
43
+ cacheWriteTokens: number;
44
+ inputCostUsd: number;
45
+ cacheReadCostUsd: number;
46
+ cacheWriteCostUsd: number;
47
+ cacheHitRate: number | undefined;
48
+ missPremiumUsd: number | undefined;
49
+ }
50
+
51
+ /**
52
+ * How a detected cache-miss pattern is attributed, per issue #2020.
53
+ *
54
+ * - `actionable`: usage evidence points to a user-controllable cause and GJC
55
+ * has a concrete remediation.
56
+ * - `diagnostic-only`: a miss pattern is observed but the cause is not
57
+ * determinable from usage alone; describe what is unknown, assert no cause.
58
+ * - `provider-side-suspected`: the provider returned no cache activity, so the
59
+ * miss cannot be attributed to the user's prompt; not user-actionable.
60
+ */
61
+ export type CacheMissAttribution = "actionable" | "diagnostic-only" | "provider-side-suspected";
62
+
63
+ export interface CacheBehaviorWarning {
64
+ code: "expensive_cache_miss" | "cache_write_spike" | "provider_side_cache_miss";
65
+ attribution: CacheMissAttribution;
66
+ reason: string;
67
+ /** Concrete remediation — present only when `attribution` is `actionable`. */
68
+ nextStep?: string;
69
+ /** What GJC cannot determine — present for non-actionable attributions. */
70
+ unknown?: string;
71
+ costUsd: number;
72
+ }
73
+
74
+ export interface CacheWarningBuildState {
75
+ warningsEmitted: number;
76
+ }
77
+
78
+ function finiteNonNegative(value: unknown): value is number {
79
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
80
+ }
81
+
82
+ function positiveFinite(value: number | undefined): value is number {
83
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
84
+ }
85
+
86
+ function currentModelBucketCost(
87
+ tokens: number,
88
+ persistedCostUsd: number | undefined,
89
+ pricePerMillionTokens: number | undefined,
90
+ ): number | undefined {
91
+ if (persistedCostUsd !== undefined) {
92
+ return finiteNonNegative(persistedCostUsd) ? persistedCostUsd : undefined;
93
+ }
94
+ if (!positiveFinite(tokens) || !positiveFinite(pricePerMillionTokens)) return 0;
95
+ return (tokens / TOKENS_PER_MILLION) * pricePerMillionTokens;
96
+ }
97
+
98
+ function persistedAggregateCosts(costBreakdown: Usage["cost"] | undefined): [number, number, number] | undefined {
99
+ if (!costBreakdown) return undefined;
100
+ const { input, cacheRead, cacheWrite, output, total } = costBreakdown;
101
+ if (![input, cacheRead, cacheWrite, output, total].every(finiteNonNegative)) return undefined;
102
+ return [input, cacheRead, cacheWrite];
103
+ }
104
+
105
+ function hasMaterialEvidence(usage: CacheEconomicsUsage, costs: readonly number[]): boolean {
106
+ const tokenEvidence = usage.input + usage.cacheRead + usage.cacheWrite >= MATERIAL_CACHE_TOKENS;
107
+ const costEvidence = costs.some(cost => cost >= MATERIAL_COST_USD);
108
+ return tokenEvidence || costEvidence;
109
+ }
110
+
111
+ export function computeCacheMissCostSummary(
112
+ usage: CacheEconomicsUsage | undefined,
113
+ basis: CacheEconomicsBasis,
114
+ ): CacheMissCostSummary | undefined {
115
+ if (!usage) return undefined;
116
+
117
+ const costs =
118
+ basis.kind === "persisted-aggregate"
119
+ ? persistedAggregateCosts(basis.costBreakdown)
120
+ : [
121
+ currentModelBucketCost(usage.input, usage.cost?.input, basis.pricing.input),
122
+ currentModelBucketCost(usage.cacheRead, usage.cost?.cacheRead, basis.pricing.cacheRead),
123
+ currentModelBucketCost(usage.cacheWrite, usage.cost?.cacheWrite, basis.pricing.cacheWrite),
124
+ ];
125
+ if (!costs) return undefined;
126
+ const [inputCostUsd, cacheReadCostUsd, cacheWriteCostUsd] = costs;
127
+ if (inputCostUsd === undefined || cacheReadCostUsd === undefined || cacheWriteCostUsd === undefined)
128
+ return undefined;
129
+ if (usage.input <= 0 && cacheWriteCostUsd <= 0) return undefined;
130
+ if (!hasMaterialEvidence(usage, [inputCostUsd, cacheReadCostUsd, cacheWriteCostUsd])) return undefined;
131
+ if (inputCostUsd <= 0 && cacheWriteCostUsd <= 0) return undefined;
132
+
133
+ const totalReusableInput = usage.input + usage.cacheRead;
134
+ const cacheHitRate = totalReusableInput > 0 ? usage.cacheRead / totalReusableInput : undefined;
135
+ const missPremiumUsd =
136
+ basis.kind === "current-model-estimate" &&
137
+ positiveFinite(basis.pricing.input) &&
138
+ positiveFinite(basis.pricing.cacheRead) &&
139
+ basis.pricing.input > basis.pricing.cacheRead &&
140
+ usage.input > 0
141
+ ? ((basis.pricing.input - basis.pricing.cacheRead) * usage.input) / TOKENS_PER_MILLION
142
+ : undefined;
143
+
144
+ return {
145
+ inputTokens: usage.input,
146
+ cacheReadTokens: usage.cacheRead,
147
+ cacheWriteTokens: usage.cacheWrite,
148
+ inputCostUsd,
149
+ cacheReadCostUsd,
150
+ cacheWriteCostUsd,
151
+ cacheHitRate,
152
+ missPremiumUsd: positiveFinite(missPremiumUsd) ? missPremiumUsd : undefined,
153
+ };
154
+ }
155
+
156
+ function formatUsd(cost: number): string {
157
+ return cost < 0.01 ? `$${cost.toFixed(4)}` : `$${cost.toFixed(2)}`;
158
+ }
159
+
160
+ function formatPercent(value: number): string {
161
+ return `${(value * 100).toFixed(1)}%`;
162
+ }
163
+
164
+ export function formatCacheMissSummaryLines(summary: CacheMissCostSummary): string[] {
165
+ const lines = [`Uncached Input Cost: ${formatUsd(summary.inputCostUsd)}`];
166
+ if (summary.missPremiumUsd !== undefined) {
167
+ lines.push(`Estimated Miss Premium: ${formatUsd(summary.missPremiumUsd)} vs cache-read pricing`);
168
+ }
169
+ if (summary.cacheHitRate !== undefined) {
170
+ lines.push(`Cache Hit Rate: ${formatPercent(summary.cacheHitRate)}`);
171
+ }
172
+ if (summary.cacheWriteCostUsd >= MATERIAL_COST_USD) {
173
+ lines.push(`Cache Write Cost: ${formatUsd(summary.cacheWriteCostUsd)}`);
174
+ }
175
+ return lines;
176
+ }
177
+
178
+ export function buildCacheBehaviorWarning(
179
+ usage: Usage | undefined,
180
+ model: { cost: CacheEconomicsModelCost } | undefined | null,
181
+ ): CacheBehaviorWarning | undefined {
182
+ const summary = model
183
+ ? computeCacheMissCostSummary(usage, { kind: "current-model-estimate", pricing: model.cost })
184
+ : undefined;
185
+ if (!summary) return undefined;
186
+ if (
187
+ summary.inputTokens >= LARGE_INPUT_TOKENS &&
188
+ (summary.cacheHitRate === undefined || summary.cacheHitRate < 0.25) &&
189
+ summary.inputCostUsd >= EXPENSIVE_MISS_COST_USD
190
+ ) {
191
+ // Attribute by observable cache activity rather than asserting a cause (#2020).
192
+ const noCacheActivity = summary.cacheReadTokens <= 0 && summary.cacheWriteTokens <= 0;
193
+ if (noCacheActivity) {
194
+ // The provider returned zero cache read/write tokens for a large, costly
195
+ // prompt: the miss cannot be traced to the user's prefix, and keeping a
196
+ // stable prefix cannot help when nothing is being cached at all.
197
+ return {
198
+ code: "provider_side_cache_miss",
199
+ attribution: "provider-side-suspected",
200
+ reason: `large uncached input with no cache activity (${formatUsd(summary.inputCostUsd)})`,
201
+ unknown:
202
+ "the provider reported no cache reads or writes, so GJC cannot tell whether prompt caching is unsupported, missing cache-control fields, or provider-side eviction",
203
+ costUsd: summary.inputCostUsd,
204
+ };
205
+ }
206
+ if (summary.cacheWriteTokens > 0) {
207
+ // Writes are landing but reuse is low: the cacheable prefix is churning
208
+ // between turns, which is user-controllable.
209
+ return {
210
+ code: "expensive_cache_miss",
211
+ attribution: "actionable",
212
+ reason: `large uncached input with low cache reuse (${formatUsd(summary.inputCostUsd)})`,
213
+ nextStep: "keep the stable prefix; avoid rereading unchanged context before the next turn",
214
+ costUsd: summary.inputCostUsd,
215
+ };
216
+ }
217
+ // Reads happened but no writes this turn: part of the prefix was reused, yet
218
+ // the large new input was not cached. From usage alone GJC cannot tell new
219
+ // content apart from a provider-side gap, so it reports without asserting.
220
+ return {
221
+ code: "expensive_cache_miss",
222
+ attribution: "diagnostic-only",
223
+ reason: `large uncached input with partial cache reuse (${formatUsd(summary.inputCostUsd)})`,
224
+ unknown:
225
+ "the uncached tokens may be genuinely new content or a provider-side gap; usage alone cannot attribute a single cause",
226
+ costUsd: summary.inputCostUsd,
227
+ };
228
+ }
229
+ if (
230
+ summary.cacheWriteTokens >= LARGE_CACHE_WRITE_TOKENS &&
231
+ summary.cacheWriteCostUsd >= EXPENSIVE_MISS_COST_USD &&
232
+ // Only claim reuse is insufficient when reads actually fail to cover the
233
+ // writes; a large write that is being read back is healthy, not a spike.
234
+ summary.cacheReadTokens < summary.cacheWriteTokens
235
+ ) {
236
+ return {
237
+ code: "cache_write_spike",
238
+ attribution: "actionable",
239
+ reason: `large cache write without enough matching reads (${formatUsd(summary.cacheWriteCostUsd)})`,
240
+ nextStep: "make the next turn reuse the same context; avoid changing system or tool prefixes",
241
+ costUsd: summary.cacheWriteCostUsd,
242
+ };
243
+ }
244
+ return undefined;
245
+ }
246
+
247
+ /**
248
+ * Render a cache warning as a single transcript line. Actionable attributions
249
+ * carry a concrete next step; non-actionable ones state what is unknown and,
250
+ * for provider-side patterns, that the miss is not user-actionable — never
251
+ * asserting a cause or blaming the provider (#2020).
252
+ */
253
+ export function formatCacheWarningLine(warning: CacheBehaviorWarning): string {
254
+ if (warning.attribution === "actionable" && warning.nextStep) {
255
+ return `Cache warning: ${warning.reason}; next step: ${warning.nextStep}.`;
256
+ }
257
+ if (warning.attribution === "provider-side-suspected") {
258
+ const detail = warning.unknown ? ` — ${warning.unknown}` : "";
259
+ return `Cache notice: ${warning.reason}; provider-side suspected / not user-actionable${detail}.`;
260
+ }
261
+ const detail = warning.unknown ? `; ${warning.unknown}` : "";
262
+ return `Cache notice: ${warning.reason}${detail}.`;
263
+ }
264
+
265
+ export function buildCacheEconomicsWarning(
266
+ usage: Usage | undefined,
267
+ model: { cost: CacheEconomicsModelCost } | undefined | null,
268
+ state: CacheWarningBuildState,
269
+ ): string | undefined {
270
+ if (state.warningsEmitted >= TRANSCRIPT_WARNING_CAP) return undefined;
271
+ const warning = buildCacheBehaviorWarning(usage, model);
272
+ if (!warning) return undefined;
273
+ state.warningsEmitted += 1;
274
+ return formatCacheWarningLine(warning);
275
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Recognizes legacy provider safety-stop labels persisted before `errorKind`.
3
+ * Keep this anchored and conservative so incidental transient-error prose stays retryable.
4
+ */
5
+ export function isLegacyProviderSafetyStopMessage(errorMessage: string): boolean {
6
+ return /^(?:refusal(?: \([^()\s\r\n](?:[^()\r\n]*[^()\s\r\n])?\))?(?:: \S(?:[^\r\n]*\S)?)?|content flagged by safety filters|blocked under anthropic's usage policy\.|provider finish_reason: content_filter)[ \t]*(?![\s\S])/i.test(
7
+ errorMessage,
8
+ );
9
+ }
@@ -4,6 +4,7 @@
4
4
  import type { AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
5
5
  import { INTENT_FIELD } from "@gajae-code/agent-core";
6
6
  import type { AssistantMessage, Model } from "@gajae-code/ai";
7
+ import { buildCacheEconomicsWarning, type CacheWarningBuildState } from "./cache-economics";
7
8
  import {
8
9
  type BashExecutionMessage,
9
10
  type BranchSummaryMessage,
@@ -137,6 +138,7 @@ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): st
137
138
  lines.push("\n");
138
139
  }
139
140
 
141
+ const cacheWarningState: CacheWarningBuildState = { warningsEmitted: 0 };
140
142
  for (const msg of options.messages) {
141
143
  if (msg.role === "user" || msg.role === "developer") {
142
144
  lines.push(msg.role === "developer" ? "## Developer\n" : "## User\n");
@@ -172,6 +174,10 @@ export function formatSessionDumpText(options: FormatSessionDumpTextOptions): st
172
174
  lines.push("<" + "/invoke>\n");
173
175
  }
174
176
  }
177
+ const cacheWarning = buildCacheEconomicsWarning(assistantMsg.usage, model, cacheWarningState);
178
+ if (cacheWarning) {
179
+ lines.push(cacheWarning);
180
+ }
175
181
  lines.push("");
176
182
  } else if (msg.role === "toolResult") {
177
183
  lines.push(`### Tool Result: ${msg.toolName}`);