@sayknow-cli/coding-agent 0.3.12 → 0.3.15

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 (248) hide show
  1. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  2. package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
  3. package/dist/types/cli/classify-install-target.d.ts +0 -10
  4. package/dist/types/cli/daemon-cli.d.ts +2 -0
  5. package/dist/types/cli/initial-message.d.ts +1 -1
  6. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  7. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  8. package/dist/types/cli/notify-cli.d.ts +3 -1
  9. package/dist/types/cli/session-picker.d.ts +4 -3
  10. package/dist/types/cli/update-cli.d.ts +22 -3
  11. package/dist/types/cli.d.ts +5 -0
  12. package/dist/types/commands/daemon.d.ts +6 -2
  13. package/dist/types/commands/launch.d.ts +1 -0
  14. package/dist/types/commands/notify.d.ts +6 -0
  15. package/dist/types/commands/web-search.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
  18. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
  19. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
  20. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  21. package/dist/types/commit/analysis/summary.d.ts +1 -1
  22. package/dist/types/commit/changelog/generate.d.ts +3 -3
  23. package/dist/types/commit/changelog/index.d.ts +2 -2
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  26. package/dist/types/commit/shared-llm.d.ts +9 -9
  27. package/dist/types/config/keybindings.d.ts +134 -129
  28. package/dist/types/config/model-profile-activation.d.ts +1 -0
  29. package/dist/types/config/model-registry.d.ts +35 -35
  30. package/dist/types/config/models-config-schema.d.ts +111 -111
  31. package/dist/types/config/settings-schema.d.ts +56 -16
  32. package/dist/types/config/settings.d.ts +2 -0
  33. package/dist/types/coordinator/contract.d.ts +1 -1
  34. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  35. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  36. package/dist/types/coordinator-mcp/server.d.ts +79 -6
  37. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  38. package/dist/types/daemon/control-types.d.ts +10 -0
  39. package/dist/types/daemon/operator-contract.d.ts +47 -0
  40. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  41. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
  42. package/dist/types/discovery/helpers.d.ts +1 -1
  43. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  44. package/dist/types/edit/modes/patch.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  46. package/dist/types/goals/tools/goal-tool.d.ts +6 -6
  47. package/dist/types/i18n/messages/en.d.ts +8 -8
  48. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  49. package/dist/types/lsp/index.d.ts +9 -9
  50. package/dist/types/lsp/types.d.ts +9 -9
  51. package/dist/types/main.d.ts +66 -2
  52. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  53. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  54. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  56. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  57. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  58. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  59. package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
  60. package/dist/types/modes/components/session-selector.d.ts +22 -9
  61. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  62. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  63. package/dist/types/modes/components/status-line.d.ts +0 -10
  64. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  65. package/dist/types/modes/components/user-message.d.ts +2 -1
  66. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  67. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  68. package/dist/types/modes/interactive-mode.d.ts +24 -2
  69. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  70. package/dist/types/modes/print-mode.d.ts +3 -4
  71. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  72. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  73. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  74. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  75. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  76. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  77. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  78. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  79. package/dist/types/modes/types.d.ts +28 -2
  80. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  81. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  82. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  83. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  84. package/dist/types/notifications/config.d.ts +34 -0
  85. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  86. package/dist/types/notifications/index.d.ts +27 -0
  87. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  88. package/dist/types/notifications/notification-service.d.ts +144 -0
  89. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  90. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  91. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  92. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  93. package/dist/types/notifications/topic-registry.d.ts +24 -4
  94. package/dist/types/registry/agent-registry.d.ts +2 -0
  95. package/dist/types/rlm/index.d.ts +1 -1
  96. package/dist/types/sdk.d.ts +2 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +40 -3
  99. package/dist/types/session/cache-economics.d.ts +79 -0
  100. package/dist/types/session/context-estimation.d.ts +31 -0
  101. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  102. package/dist/types/session/session-manager.d.ts +124 -4
  103. package/dist/types/session/session-storage.d.ts +155 -13
  104. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  105. package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
  106. package/dist/types/skc-runtime/state-schema.d.ts +19 -19
  107. package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
  108. package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  109. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
  110. package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
  111. package/dist/types/task/agents.d.ts +2 -2
  112. package/dist/types/task/executor.d.ts +3 -0
  113. package/dist/types/task/receipt.d.ts +3 -1
  114. package/dist/types/task/types.d.ts +35 -27
  115. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  116. package/dist/types/tools/bash.d.ts +1 -1
  117. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  118. package/dist/types/tools/browser.d.ts +28 -28
  119. package/dist/types/tools/computer.d.ts +20 -20
  120. package/dist/types/tools/cron.d.ts +2 -2
  121. package/dist/types/tools/debug.d.ts +40 -40
  122. package/dist/types/tools/gh.d.ts +10 -10
  123. package/dist/types/tools/image-gen.d.ts +2 -2
  124. package/dist/types/tools/index.d.ts +58 -6
  125. package/dist/types/tools/monitor.d.ts +6 -6
  126. package/dist/types/tools/skill-discovery.d.ts +4 -4
  127. package/dist/types/tools/subagent.d.ts +12 -12
  128. package/dist/types/tools/telegram-send.d.ts +7 -0
  129. package/dist/types/tools/todo-write.d.ts +8 -8
  130. package/dist/types/utils/external-editor.d.ts +2 -0
  131. package/dist/types/web/insane/bridge.d.ts +9 -9
  132. package/dist/types/web/search/index.d.ts +4 -4
  133. package/package.json +8 -8
  134. package/scripts/g004-tmux-smoke.ts +196 -36
  135. package/src/cli/daemon-cli.ts +19 -29
  136. package/src/cli/malloc-env-guard.test.ts +104 -0
  137. package/src/cli/malloc-env-guard.ts +149 -0
  138. package/src/cli/notify-cli.ts +78 -14
  139. package/src/cli/session-picker.ts +23 -40
  140. package/src/cli/update-cli.ts +56 -37
  141. package/src/cli.ts +51 -10
  142. package/src/commands/daemon.ts +19 -5
  143. package/src/commands/harness.ts +515 -47
  144. package/src/commands/launch.ts +11 -3
  145. package/src/commands/notify.ts +5 -1
  146. package/src/commands/session.ts +6 -1
  147. package/src/config/keybindings.ts +5 -0
  148. package/src/config/model-profile-activation.ts +1 -1
  149. package/src/config/model-profiles.ts +14 -14
  150. package/src/config/settings-schema.ts +41 -1
  151. package/src/config/settings.ts +96 -45
  152. package/src/coordinator/contract.ts +1 -0
  153. package/src/coordinator-mcp/model-preset.ts +100 -0
  154. package/src/coordinator-mcp/policy.ts +30 -0
  155. package/src/coordinator-mcp/server.ts +2958 -430
  156. package/src/coordinator-mcp/session-reaper.ts +129 -0
  157. package/src/daemon/control-types.ts +11 -0
  158. package/src/daemon/operator-contract.ts +121 -0
  159. package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  160. package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  161. package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  162. package/src/extensibility/extensions/runner.ts +1 -1
  163. package/src/extensibility/extensions/types.ts +3 -1
  164. package/src/internal-urls/docs-index.generated.ts +10 -9
  165. package/src/internal-urls/local-protocol.ts +29 -10
  166. package/src/main.ts +301 -80
  167. package/src/modes/acp/acp-agent.ts +730 -100
  168. package/src/modes/acp/acp-mode.ts +29 -2
  169. package/src/modes/components/assistant-message.ts +32 -3
  170. package/src/modes/components/bash-execution.ts +43 -13
  171. package/src/modes/components/custom-editor.ts +5 -0
  172. package/src/modes/components/footer.ts +3 -3
  173. package/src/modes/components/hook-selector.ts +12 -0
  174. package/src/modes/components/irc-sidebar.ts +206 -0
  175. package/src/modes/components/pet-selector.ts +55 -0
  176. package/src/modes/components/sayknow-pet-widget.ts +360 -0
  177. package/src/modes/components/session-selector.ts +222 -221
  178. package/src/modes/components/settings-selector.ts +10 -0
  179. package/src/modes/components/status-line/segments.ts +14 -7
  180. package/src/modes/components/status-line/types.ts +1 -1
  181. package/src/modes/components/status-line.ts +4 -171
  182. package/src/modes/components/tool-execution.ts +47 -6
  183. package/src/modes/components/user-message.ts +26 -9
  184. package/src/modes/controllers/command-controller.ts +28 -2
  185. package/src/modes/controllers/event-controller.ts +159 -36
  186. package/src/modes/controllers/extension-ui-controller.ts +37 -18
  187. package/src/modes/controllers/input-controller.ts +8 -2
  188. package/src/modes/controllers/selector-controller.ts +62 -15
  189. package/src/modes/interactive-mode.ts +199 -7
  190. package/src/modes/irc-observation-ledger.ts +127 -0
  191. package/src/modes/print-mode.ts +228 -92
  192. package/src/modes/rpc/rpc-client.ts +58 -1
  193. package/src/modes/rpc/rpc-mode.ts +286 -85
  194. package/src/modes/rpc/rpc-types.ts +15 -1
  195. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  196. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  197. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  198. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  199. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  200. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  201. package/src/modes/types.ts +34 -2
  202. package/src/modes/utils/context-usage.ts +56 -92
  203. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  204. package/src/modes/utils/irc-message.ts +138 -0
  205. package/src/modes/utils/ui-helpers.ts +216 -50
  206. package/src/notifications/config.ts +44 -0
  207. package/src/notifications/daemon-paths.ts +8 -0
  208. package/src/notifications/index.ts +535 -75
  209. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  210. package/src/notifications/notification-service.ts +710 -0
  211. package/src/notifications/rate-limit-pool.ts +46 -12
  212. package/src/notifications/telegram-daemon-control.ts +27 -2
  213. package/src/notifications/telegram-daemon.ts +635 -94
  214. package/src/notifications/telegram-reference.ts +73 -13
  215. package/src/notifications/topic-registry.ts +86 -9
  216. package/src/prompts/system/irc-peer-roster.md +1 -0
  217. package/src/prompts/system/subagent-system-prompt.md +1 -0
  218. package/src/prompts/system/system-prompt.md +1 -0
  219. package/src/prompts/tools/irc.md +1 -1
  220. package/src/registry/agent-registry.ts +3 -0
  221. package/src/sdk.ts +52 -1
  222. package/src/secrets/obfuscator.ts +4 -2
  223. package/src/session/agent-session.ts +670 -217
  224. package/src/session/cache-economics.ts +275 -0
  225. package/src/session/context-estimation.ts +91 -0
  226. package/src/session/provider-safety-stop.ts +9 -0
  227. package/src/session/session-dump-format.ts +6 -0
  228. package/src/session/session-manager.ts +809 -39
  229. package/src/session/session-storage.ts +582 -38
  230. package/src/skc-runtime/session-state-sidecar.ts +756 -135
  231. package/src/skc-runtime/tmux-common.ts +26 -7
  232. package/src/skc-runtime/tmux-gc.ts +52 -9
  233. package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
  234. package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
  235. package/src/skc-runtime/tmux-sessions.ts +871 -68
  236. package/src/slash-commands/builtin-registry.ts +153 -9
  237. package/src/slash-commands/helpers/context-report.ts +26 -4
  238. package/src/task/executor.ts +12 -3
  239. package/src/task/index.ts +8 -0
  240. package/src/task/receipt.ts +7 -2
  241. package/src/task/types.ts +26 -0
  242. package/src/tools/ask-answer-registry.ts +25 -0
  243. package/src/tools/browser/tab-supervisor.ts +63 -6
  244. package/src/tools/browser.ts +8 -3
  245. package/src/tools/index.ts +81 -6
  246. package/src/tools/sqlite-reader.ts +8 -4
  247. package/src/tui/output-block.ts +18 -6
  248. package/src/utils/external-editor.ts +6 -1
@@ -0,0 +1,275 @@
1
+ import type { Usage } from "@sayknow-cli/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 SKC
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 SKC 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 SKC 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 SKC 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,91 @@
1
+ /**
2
+ * Session-neutral fixed-context (non-message) token estimation.
3
+ *
4
+ * Lives in the session layer so load-bearing session/compaction behavior
5
+ * (`AgentSession` context estimates) never depends on a presentation module.
6
+ * UI surfaces (`modes/utils/context-usage.ts`, status line) import from here
7
+ * as well, so every surface reports the same numbers.
8
+ */
9
+ import { estimateTextTokensHeuristic } from "@sayknow-cli/agent-core/compaction";
10
+ import type { Skill } from "../extensibility/skills";
11
+ import type { Tool } from "../tools";
12
+ import type { AgentSession } from "./agent-session";
13
+
14
+ export function estimateSkillsTokens(skills: readonly Skill[]): number {
15
+ const fragments: string[] = [];
16
+ for (const skill of skills) {
17
+ // "- name: description\n" wire framing tokenizes ~identically to the
18
+ // concatenated form, so encode each piece separately and sum.
19
+ fragments.push(skill.name, skill.description);
20
+ }
21
+ return estimateTextTokensHeuristic(fragments);
22
+ }
23
+
24
+ export function estimateToolSchemaTokens(
25
+ tools: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">>,
26
+ ): number {
27
+ const fragments: string[] = [];
28
+ for (const tool of tools) {
29
+ fragments.push(tool.name, tool.description);
30
+ try {
31
+ fragments.push(JSON.stringify(tool.parameters ?? {}));
32
+ } catch {
33
+ // Schema may contain functions or cycles; ignore.
34
+ }
35
+ }
36
+ return estimateTextTokensHeuristic(fragments);
37
+ }
38
+
39
+ /**
40
+ * Compute just the NON-MESSAGE token total: system prompt (with its skills
41
+ * section subtracted, since skills are tokenized separately) + system context
42
+ * (the rest of the system-prompt array) + tools + skills.
43
+ *
44
+ * Exposed so callers like `StatusLineComponent` can cache the non-message
45
+ * total separately from the message total. Non-message inputs (skills,
46
+ * tools, system prompt) change rarely; the message list grows on every
47
+ * streaming turn. Splitting the two lets the caller refresh each on its own
48
+ * cadence — non-message recomputed only when the inputs identity changes,
49
+ * messages walked incrementally as new entries append.
50
+ */
51
+ export function computeNonMessageTokens(session: AgentSession): number {
52
+ const parts = computeNonMessageBreakdown(session);
53
+ return (
54
+ parts.systemPromptTokens + parts.systemContextTokens + parts.rulesTokens + parts.toolsTokens + parts.skillsTokens
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Shared helper for the four non-message token totals. Single source of truth
60
+ * for `computeNonMessageTokens` (session estimates + status-line incremental
61
+ * cache) and `computeContextBreakdown` (/context panel). The split avoids
62
+ * drift between the surfaces — they MUST report the same numbers.
63
+ */
64
+ export function computeNonMessageBreakdown(session: AgentSession): {
65
+ rulesTokens: number;
66
+ skillsTokens: number;
67
+ toolsTokens: number;
68
+ systemContextTokens: number;
69
+ systemPromptTokens: number;
70
+ } {
71
+ const skillsTokens = estimateSkillsTokens(session.skills ?? []);
72
+ const toolsTokens = estimateToolSchemaTokens(session.agent?.state?.tools ?? []);
73
+ const systemPromptParts = session.systemPrompt ?? [];
74
+ const rulesTokens = estimateRulesTokens(systemPromptParts);
75
+ const systemContextTokens = estimateTextTokensHeuristic(systemPromptParts.slice(1));
76
+ const systemPromptTokens = Math.max(
77
+ 0,
78
+ estimateTextTokensHeuristic(systemPromptParts[0] ?? "") - skillsTokens - rulesTokens,
79
+ );
80
+ return { rulesTokens, skillsTokens, toolsTokens, systemContextTokens, systemPromptTokens };
81
+ }
82
+
83
+ function estimateRulesTokens(systemPromptParts: readonly string[]): number {
84
+ const fragments: string[] = [];
85
+ for (const part of systemPromptParts) {
86
+ for (const match of part.matchAll(/<rules>[\s\S]*?<\/rules>/g)) {
87
+ fragments.push(match[0]);
88
+ }
89
+ }
90
+ return fragments.length === 0 ? 0 : estimateTextTokensHeuristic(fragments);
91
+ }
@@ -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 "@sayknow-cli/agent-core";
5
5
  import { INTENT_FIELD } from "@sayknow-cli/agent-core";
6
6
  import type { AssistantMessage, Model } from "@sayknow-cli/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}`);