@cuongtran001/kanna 1.63.2 → 1.63.3

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 (199) hide show
  1. package/README.md +5 -9
  2. package/dist/client/assets/{BoardsPage-BJalB-bc.js → BoardsPage-AckCuswi.js} +1 -1
  3. package/dist/client/assets/BoardsRoutePage-DzKwKy3f.js +1 -0
  4. package/dist/client/assets/ChatPage-DsaH8CTz.js +93 -0
  5. package/dist/client/assets/{CronJobRow-CPbO_jwU.js → CronJobRow-Bx9gRqKW.js} +1 -1
  6. package/dist/client/assets/{CronJobsPage-Ck3SNgGi.js → CronJobsPage-v0-2X1oX.js} +1 -1
  7. package/dist/client/assets/{FilePreviewSheet-XEuUtDPn.js → FilePreviewSheet-B6SDxSD0.js} +2 -2
  8. package/dist/client/assets/{FlowSurface-BCbeMTJe.js → FlowSurface-CLC-ZFar.js} +1 -1
  9. package/dist/client/assets/{GenUIBlock-BJy_dfs-.js → GenUIBlock-CC79Ujro.js} +2 -2
  10. package/dist/client/assets/{KannaSocketProvider-D7Lzlt35.js → KannaSocketProvider-B2sHsWP5.js} +1 -1
  11. package/dist/client/assets/{LocalProjectsPage-9LJkQjhT.js → LocalProjectsPage-DYBkcEAm.js} +1 -1
  12. package/dist/client/assets/SettingsPage-CZqMkMRW.js +9 -0
  13. package/dist/client/assets/{StackBoardsRoutePage-t_EaFNG4.js → StackBoardsRoutePage-Clviz0hX.js} +1 -1
  14. package/dist/client/assets/{ToolCallMessage-RbF9XT2B.js → ToolCallMessage-BfNbWGmc.js} +1 -1
  15. package/dist/client/assets/{VChartSurface-CKuPelsn.js → VChartSurface-CUbxyBo3.js} +1 -1
  16. package/dist/client/assets/{WorkflowsPage-B6qUmQA3.js → WorkflowsPage-rengzb97.js} +1 -1
  17. package/dist/client/assets/{WorkflowsSection-DxO0S4An.js → WorkflowsSection-DuXdLsMO.js} +1 -1
  18. package/dist/client/assets/{chart-renderer-DjYZvNxh.js → chart-renderer-D7ukB2wg.js} +1 -1
  19. package/dist/client/assets/{chatSounds-FD2DQzF6.js → chatSounds-BLg2xPDZ.js} +1 -1
  20. package/dist/client/assets/chevron-left-RbFvmeyK.js +1 -0
  21. package/dist/client/assets/{clipboard.adapter-GN9nnYak.js → clipboard.adapter-CgdNwn-1.js} +1 -1
  22. package/dist/client/assets/{columnStyle-DjA7hA9k.js → columnStyle-CMCF0ds6.js} +1 -1
  23. package/dist/client/assets/{createScopedStore-BoLB9Rhc.js → createScopedStore-BrD5NHWx.js} +1 -1
  24. package/dist/client/assets/{dist-CimtOoGK.js → dist-DNSTHlSo.js} +1 -1
  25. package/dist/client/assets/editor-icons-DY4uKeEY.js +1 -0
  26. package/dist/client/assets/{ellipsis-Cd7sO0LO.js → ellipsis-OHg0x_x1.js} +1 -1
  27. package/dist/client/assets/{flow-kinds-CW2TU_83.js → flow-kinds-Ki2DP9nP.js} +1 -1
  28. package/dist/client/assets/{folder-C7sXCaHj.js → folder-CnWzWIG5.js} +1 -1
  29. package/dist/client/assets/{formatters-wSVyvDFZ.js → formatters-BGORekd-.js} +1 -1
  30. package/dist/client/assets/{genui-EzBIAYFE.js → genui-C9i6j0XZ.js} +1 -1
  31. package/dist/client/assets/{globe-xmRpmpma.js → globe-BjR1tyE4.js} +1 -1
  32. package/dist/client/assets/{host-DCaA9W-Z.js → host-D9tJwZXl.js} +1 -1
  33. package/dist/client/assets/{index-CDqjh7nt.js → index-CesRxZ0X.js} +7 -7
  34. package/dist/client/assets/index-CxDE9VOy.css +1 -0
  35. package/dist/client/assets/{info-D4MMvZFe.js → info-DNYekoE-.js} +1 -1
  36. package/dist/client/assets/{input-C4fvDiQ4.js → input-DgW9uRZN.js} +1 -1
  37. package/dist/client/assets/{lexicalToReact-sPUyOWk6.js → lexicalToReact-CZvp9Pgz.js} +3 -3
  38. package/dist/client/assets/{log-aTaDVF_e.js → log-DnEvbp3E.js} +1 -1
  39. package/dist/client/assets/{monitor-_CRKcV1h.js → monitor-qbWfUdBG.js} +1 -1
  40. package/dist/client/assets/{pencil-fNBeUUCf.js → pencil-C965rrpd.js} +1 -1
  41. package/dist/client/assets/{pendingActionsStore-COTh4Ztq.js → pendingActionsStore-CxhI19V9.js} +1 -1
  42. package/dist/client/assets/{play-BAmkJfn6.js → play-uofLR7yT.js} +1 -1
  43. package/dist/client/assets/{plus-c75CkNEO.js → plus-D4CI6rj1.js} +1 -1
  44. package/dist/client/assets/{popover-CHM4skzh.js → popover-Bc646hKn.js} +1 -1
  45. package/dist/client/assets/{pushClient-BJH_ITRB.js → pushClient-CxnHu9h4.js} +1 -1
  46. package/dist/client/assets/runDetached-DfQXHAaY.js +1 -0
  47. package/dist/client/assets/{segmented-control-DNOwRupT.js → segmented-control-CKGX29Kl.js} +1 -1
  48. package/dist/client/assets/{select-dLHiAls6.js → select-BjeajpVt.js} +1 -1
  49. package/dist/client/assets/{settings-header-button-CGKCzFyk.js → settings-header-button-BYvHR6Xl.js} +1 -1
  50. package/dist/client/assets/{sidebarPendingActions-Dyp80Lv_.js → sidebarPendingActions-xgMTnfRY.js} +1 -1
  51. package/dist/client/assets/{square-pen-B1IxmScM.js → square-pen-DmppQdTd.js} +1 -1
  52. package/dist/client/assets/{state-mark-BhTZfcYe.js → state-mark-C3kEL1hX.js} +1 -1
  53. package/dist/client/assets/{status-pill-CwhJhaMy.js → status-pill-Bo1Nb6HX.js} +1 -1
  54. package/dist/client/assets/{terminal-Bnj7qFGw.js → terminal-DfCF-J4p.js} +1 -1
  55. package/dist/client/assets/{timer.adapter-DHt2zwsL.js → timer.adapter-C2RhP4d6.js} +1 -1
  56. package/dist/client/assets/{tools-3gJcj3hK.js → tools-t5T_3UBg.js} +1 -1
  57. package/dist/client/assets/{triangle-alert-CWrc0CIO.js → triangle-alert-CYQhobyY.js} +1 -1
  58. package/dist/client/assets/useAppGlobalState-DUa3pkCF.js +1 -0
  59. package/dist/client/assets/useKannaState-FcieR5qb.js +5 -0
  60. package/dist/client/assets/useTheme-D9sKDPEm.js +1 -0
  61. package/dist/client/index.html +35 -35
  62. package/package.json +2 -2
  63. package/src/server/agent-coordinator-types.ts +0 -6
  64. package/src/server/agent-coordinator.ts +6 -45
  65. package/src/server/agent.compaction.test.ts +1 -13
  66. package/src/server/agent.oauth-account-info.test.ts +0 -3
  67. package/src/server/agent.oauth-release.test.ts +0 -2
  68. package/src/server/agent.oauth-rotation.test.ts +0 -9
  69. package/src/server/agent.oauth-token-share-cap.test.ts +0 -1
  70. package/src/server/agent.openrouter-model.test.ts +0 -2
  71. package/src/server/agent.openrouter-watchdog.test.ts +0 -3
  72. package/src/server/agent.sdk-workflow-register.test.ts +2 -64
  73. package/src/server/agent.test.ts +1 -319
  74. package/src/server/app-settings.test.ts +14 -17
  75. package/src/server/app-settings.ts +22 -37
  76. package/src/server/auto-continue/e2e.test.ts +0 -1
  77. package/src/server/background-task-output-registry.test.ts +1 -1
  78. package/src/server/background-task-output-registry.ts +1 -1
  79. package/src/server/claude-cancel-handler.test.ts +0 -58
  80. package/src/server/claude-cancel-handler.ts +1 -17
  81. package/src/server/claude-harness-stream.test.ts +134 -0
  82. package/src/server/{claude-pty/jsonl-path.test.ts → claude-projects-path.adapter.test.ts} +1 -19
  83. package/src/server/{claude-pty/jsonl-path.adapter.ts → claude-projects-path.adapter.ts} +0 -11
  84. package/src/server/claude-send-command.test.ts +0 -1
  85. package/src/server/claude-session-config-helpers.test.ts +1 -90
  86. package/src/server/claude-session-config-helpers.ts +1 -38
  87. package/src/server/claude-session-error-handler.test.ts +0 -1
  88. package/src/server/claude-session-lifecycle.test.ts +2 -36
  89. package/src/server/claude-session-lifecycle.ts +2 -8
  90. package/src/server/claude-session-runner.test.ts +8 -36
  91. package/src/server/claude-session-runner.ts +1 -26
  92. package/src/server/claude-session-spawner.test.ts +5 -64
  93. package/src/server/claude-session-spawner.ts +49 -115
  94. package/src/server/claude-session-start.test.ts +1 -14
  95. package/src/server/claude-session-start.ts +0 -8
  96. package/src/server/claude-session-state-queries.test.ts +1 -2
  97. package/src/server/claude-session-state.test.ts +0 -1
  98. package/src/server/claude-session-state.ts +0 -4
  99. package/src/server/claude-subagent-wiring.test.ts +2 -68
  100. package/src/server/claude-subagent-wiring.ts +3 -50
  101. package/src/server/claude-turn-starter-types.ts +0 -3
  102. package/src/server/claude-turn-starter.test.ts +0 -28
  103. package/src/server/claude-turn-starter.ts +0 -5
  104. package/src/server/event-store.test.ts +0 -2
  105. package/src/server/events.ts +1 -2
  106. package/src/server/harness-types.ts +1 -2
  107. package/src/server/import-subagent-drill.e2e.test.ts +1 -1
  108. package/src/server/imported-subagents-dir.test.ts +1 -1
  109. package/src/server/imported-subagents-dir.ts +1 -1
  110. package/src/server/kanna-mcp.test.ts +0 -75
  111. package/src/server/kanna-mcp.ts +1 -47
  112. package/src/server/server.test.ts +5 -7
  113. package/src/server/server.ts +2 -18
  114. package/src/server/subagent-orchestrator.test.ts +0 -1
  115. package/src/server/subagent-provider-run.test.ts +48 -10
  116. package/src/server/subagent-provider-run.ts +12 -3
  117. package/src/server/tool-callback.test.ts +1 -1
  118. package/src/server/ws-router-agent-ctrl.test.ts +0 -45
  119. package/src/server/ws-router-agent-ctrl.ts +1 -31
  120. package/src/server/ws-router-broadcast.ts +0 -27
  121. package/src/server/ws-router-defaults.test.ts +2 -3
  122. package/src/server/ws-router-defaults.ts +2 -3
  123. package/src/server/ws-router-envelope.ts +0 -15
  124. package/src/server/ws-router-observability.test.ts +1 -1
  125. package/src/server/ws-router-settings.test.ts +2 -2
  126. package/src/server/ws-router.test.ts +2 -2
  127. package/src/server/ws-router.ts +0 -8
  128. package/src/shared/app-settings-types.ts +9 -20
  129. package/src/shared/claude-api-error-classification.ts +1 -1
  130. package/src/shared/protocol.ts +1 -11
  131. package/dist/client/assets/BoardsRoutePage-BTTjJX-Y.js +0 -1
  132. package/dist/client/assets/ChatPage-DH8wtQyD.js +0 -93
  133. package/dist/client/assets/SettingsPage-CKrZ6dGQ.js +0 -9
  134. package/dist/client/assets/chevron-left-CJ0qv9kF.js +0 -1
  135. package/dist/client/assets/editor-icons-CONDQZof.js +0 -1
  136. package/dist/client/assets/index-C-UYaDk6.css +0 -1
  137. package/dist/client/assets/runDetached-DqQ3k2ji.js +0 -1
  138. package/dist/client/assets/useAppGlobalState-DKOGrO5l.js +0 -1
  139. package/dist/client/assets/useKannaState-D7R_-dM6.js +0 -5
  140. package/dist/client/assets/useTheme-B57LIKyx.js +0 -1
  141. package/src/server/agent.pty-rotation.test.ts +0 -322
  142. package/src/server/claude-pty/auth.test.ts +0 -38
  143. package/src/server/claude-pty/auth.ts +0 -17
  144. package/src/server/claude-pty/claude-session-registry.adapter.ts +0 -68
  145. package/src/server/claude-pty/claude-session-registry.test.ts +0 -149
  146. package/src/server/claude-pty/driver.test.ts +0 -1153
  147. package/src/server/claude-pty/driver.ts +0 -829
  148. package/src/server/claude-pty/env.test.ts +0 -79
  149. package/src/server/claude-pty/env.ts +0 -20
  150. package/src/server/claude-pty/jsonl-to-event.test.ts +0 -882
  151. package/src/server/claude-pty/jsonl-to-event.ts +0 -357
  152. package/src/server/claude-pty/parity-matrix.test.ts +0 -437
  153. package/src/server/claude-pty/pid-registry.adapter.ts +0 -164
  154. package/src/server/claude-pty/pid-registry.test.ts +0 -178
  155. package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +0 -20
  156. package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +0 -32
  157. package/src/server/claude-pty/pty-instance-registry.test.ts +0 -208
  158. package/src/server/claude-pty/pty-instance-registry.ts +0 -172
  159. package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +0 -103
  160. package/src/server/claude-pty/pty-memory-sampler.adapter.ts +0 -86
  161. package/src/server/claude-pty/pty-process.adapter.ts +0 -60
  162. package/src/server/claude-pty/pty-process.test.ts +0 -49
  163. package/src/server/claude-pty/resolve-binary.adapter.ts +0 -106
  164. package/src/server/claude-pty/resolve-binary.test.ts +0 -118
  165. package/src/server/claude-pty/runtime-dir.adapter.ts +0 -19
  166. package/src/server/claude-pty/sandbox/detect.adapter.ts +0 -17
  167. package/src/server/claude-pty/sandbox/detect.test.ts +0 -17
  168. package/src/server/claude-pty/sandbox/platform.test.ts +0 -35
  169. package/src/server/claude-pty/sandbox/platform.ts +0 -27
  170. package/src/server/claude-pty/sandbox/preflight-io.adapter.ts +0 -14
  171. package/src/server/claude-pty/sandbox/preflight.test.ts +0 -69
  172. package/src/server/claude-pty/sandbox/preflight.ts +0 -44
  173. package/src/server/claude-pty/sandbox/profile-linux.adapter.ts +0 -63
  174. package/src/server/claude-pty/sandbox/profile-linux.test.ts +0 -90
  175. package/src/server/claude-pty/sandbox/profile-macos.adapter.ts +0 -72
  176. package/src/server/claude-pty/sandbox/profile-macos.test.ts +0 -68
  177. package/src/server/claude-pty/sandbox/wrap.adapter.ts +0 -58
  178. package/src/server/claude-pty/sandbox/wrap.test.ts +0 -82
  179. package/src/server/claude-pty/settings-writer.adapter.ts +0 -23
  180. package/src/server/claude-pty/settings-writer.test.ts +0 -22
  181. package/src/server/claude-pty/smoke-test-io.adapter.ts +0 -28
  182. package/src/server/claude-pty/smoke-test.test.ts +0 -199
  183. package/src/server/claude-pty/smoke-test.ts +0 -182
  184. package/src/server/claude-pty/tui-control.test.ts +0 -300
  185. package/src/server/claude-pty/tui-control.ts +0 -174
  186. package/src/server/claude-pty/tui-source.adapter.test.ts +0 -374
  187. package/src/server/claude-pty/tui-source.adapter.ts +0 -294
  188. package/src/server/http-server.adapter.test.ts +0 -18
  189. package/src/server/http-server.adapter.ts +0 -39
  190. package/src/server/kanna-mcp-http.test.ts +0 -274
  191. package/src/server/kanna-mcp-http.ts +0 -194
  192. package/src/server/kanna-mcp-tools/ask-user-question.test.ts +0 -108
  193. package/src/server/kanna-mcp-tools/ask-user-question.ts +0 -55
  194. package/src/server/kanna-mcp-tools/exit-plan-mode.test.ts +0 -61
  195. package/src/server/kanna-mcp-tools/exit-plan-mode.ts +0 -49
  196. package/src/server/mcp-zod-compat.adapter.ts +0 -20
  197. package/src/shared/pty-instance.ts +0 -43
  198. /package/src/server/{claude-pty/output-ring.test.ts → output-ring.test.ts} +0 -0
  199. /package/src/server/{claude-pty/output-ring.ts → output-ring.ts} +0 -0
@@ -1,437 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { mkdtemp, rm, writeFile, appendFile, mkdir } from "node:fs/promises"
3
- import { tmpdir } from "node:os"
4
- import path from "node:path"
5
- import { createClaudeHarnessStream } from "../agent"
6
- import type { ClaudeRawSdkMessage } from "../agent"
7
- import { createJsonlEventParser } from "./jsonl-to-event"
8
- import { startTranscriptStream } from "./tui-source.adapter"
9
- import type { HarnessEvent } from "../harness-types"
10
- import type { CompactBoundaryMetadata } from "../../shared/transcript-types"
11
- import type { ModelPrice } from "../../shared/token-pricing"
12
-
13
-
14
- function fakeQuery(messages: unknown[]): AsyncGenerator<ClaudeRawSdkMessage> {
15
- return (async function* () {
16
- for (const m of messages) yield m as ClaudeRawSdkMessage
17
- })()
18
- }
19
-
20
- function normalize(events: HarnessEvent[]): unknown[] {
21
- return events.map((ev) => {
22
- if (ev.type === "transcript") {
23
- const { _id: _i, createdAt: _c, ...rest } = ev.entry as unknown as Record<string, unknown> & {
24
- _id?: string
25
- createdAt?: number
26
- }
27
- return { type: ev.type, entry: rest }
28
- }
29
- return ev
30
- })
31
- }
32
-
33
- async function collectSdk(messages: unknown[], configuredContextWindow?: number): Promise<HarnessEvent[]> {
34
- const events: HarnessEvent[] = []
35
- for await (const ev of createClaudeHarnessStream(fakeQuery(messages), configuredContextWindow)) {
36
- events.push(ev)
37
- }
38
- return events
39
- }
40
-
41
- async function ptyEventsViaTranscriptStream(messages: unknown[], configuredContextWindow?: number): Promise<HarnessEvent[]> {
42
- const tmpDir = await mkdtemp(path.join(tmpdir(), "kanna-parity-"))
43
- const projectDir = path.join(tmpDir, "projects", "fake")
44
- await mkdir(projectDir, { recursive: true })
45
- const filePath = path.join(projectDir, "fixture.jsonl")
46
- await writeFile(filePath, "")
47
- const stream = await startTranscriptStream({ projectDir, knownFilePath: filePath, firstFileTimeoutMs: 2000 })
48
- const parser = createJsonlEventParser({ configuredContextWindow })
49
- const events: HarnessEvent[] = []
50
- const writeAll = (async () => {
51
- for (const m of messages) {
52
- await appendFile(filePath, `${JSON.stringify(m) }\n`)
53
- await new Promise<void>((r) => setTimeout(r, 30))
54
- }
55
- await appendFile(filePath, '{"type":"__parity_sentinel__"}\n')
56
- })()
57
- const collectDone = (async () => {
58
- for await (const line of stream.lines) {
59
- let parsed: { type?: string }
60
- try { parsed = JSON.parse(line) as { type?: string } } catch { continue }
61
- if (parsed.type === "__parity_sentinel__") break
62
- for (const ev of parser.parse(line)) events.push(ev)
63
- }
64
- })()
65
- await writeAll
66
- await collectDone
67
- stream.close()
68
- await rm(tmpDir, { recursive: true, force: true })
69
- return events
70
- }
71
-
72
- async function assertSameEvents(messages: unknown[], configuredContextWindow?: number): Promise<void> {
73
- const sdk = await collectSdk(messages, configuredContextWindow)
74
- const pty = await ptyEventsViaTranscriptStream(messages, configuredContextWindow)
75
- expect(normalize(pty)).toEqual(normalize(sdk))
76
- }
77
-
78
- describe("SDK ↔ PTY HarnessEvent equivalence matrix", () => {
79
- test("simple turn: system/init → assistant → result", async () => {
80
- await assertSameEvents([
81
- {
82
- type: "system",
83
- subtype: "init",
84
- session_id: "sess-1",
85
- model: "claude-sonnet-4-6",
86
- tools: [],
87
- mcp_servers: [],
88
- slash_commands: [],
89
- cwd: "/tmp",
90
- permissionMode: "acceptEdits",
91
- apiKeySource: "none",
92
- claude_code_version: "0.0.0",
93
- output_style: "default",
94
- skills: [],
95
- plugins: [],
96
- uuid: "u1",
97
- },
98
- {
99
- type: "assistant",
100
- session_id: "sess-1",
101
- message: {
102
- id: "msg-1",
103
- role: "assistant",
104
- content: [{ type: "text", text: "hi" }],
105
- },
106
- usage: { input_tokens: 100, output_tokens: 25 },
107
- uuid: "u2",
108
- },
109
- {
110
- type: "result",
111
- subtype: "success",
112
- session_id: "sess-1",
113
- result: "done",
114
- is_error: false,
115
- duration_ms: 500,
116
- usage: { input_tokens: 100, output_tokens: 25 },
117
- modelUsage: { "claude-sonnet-4-6": { contextWindow: 200000 } },
118
- uuid: "u3",
119
- },
120
- ])
121
- })
122
-
123
- test("rate_limit_event (SDK-native shape)", async () => {
124
- await assertSameEvents([
125
- {
126
- type: "rate_limit_event",
127
- session_id: "sess-rl",
128
- rate_limit_info: { status: "rejected", resetsAt: 1_748_800_000 },
129
- },
130
- ])
131
- })
132
-
133
- test("prompt-too-long error result", async () => {
134
- await assertSameEvents([
135
- {
136
- type: "result",
137
- subtype: "error",
138
- session_id: "sess-err",
139
- is_error: true,
140
- result: "prompt is too long",
141
- duration_ms: 0,
142
- uuid: "u-err",
143
- },
144
- ])
145
- })
146
-
147
- test("multiple assistant messages dedupe on usage id (same id seen twice)", async () => {
148
- await assertSameEvents([
149
- {
150
- type: "assistant",
151
- session_id: "sess-d",
152
- message: { id: "dup", role: "assistant", content: [{ type: "text", text: "a" }] },
153
- usage: { input_tokens: 10, output_tokens: 3 },
154
- },
155
- {
156
- type: "assistant",
157
- session_id: "sess-d",
158
- message: { id: "dup", role: "assistant", content: [{ type: "text", text: "a" }] },
159
- usage: { input_tokens: 10, output_tokens: 3 },
160
- },
161
- ])
162
- })
163
-
164
- test("1M context window floor preserved against modelUsage.contextWindow=200k", async () => {
165
- await assertSameEvents(
166
- [
167
- {
168
- type: "assistant",
169
- session_id: "sess-1m",
170
- message: { id: "msg-1m", role: "assistant", content: [{ type: "text", text: "x" }] },
171
- usage: { input_tokens: 100, output_tokens: 50 },
172
- },
173
- {
174
- type: "result",
175
- subtype: "success",
176
- session_id: "sess-1m",
177
- is_error: false,
178
- duration_ms: 500,
179
- usage: { input_tokens: 100, output_tokens: 50 },
180
- modelUsage: { "claude-sonnet-4-6": { contextWindow: 200000 } },
181
- },
182
- ],
183
- 1_000_000,
184
- )
185
- })
186
-
187
- test("session_token emitted from every message carrying session_id", async () => {
188
- await assertSameEvents([
189
- { type: "system", subtype: "init", session_id: "a", model: "m", tools: [], mcp_servers: [], slash_commands: [], cwd: "/", permissionMode: "acceptEdits", apiKeySource: "none", claude_code_version: "0", output_style: "d", skills: [], plugins: [], uuid: "1" },
190
- { type: "assistant", session_id: "a", message: { id: "m1", role: "assistant", content: [] } },
191
- { type: "result", subtype: "success", session_id: "a", is_error: false, duration_ms: 0, uuid: "r" },
192
- ])
193
- })
194
-
195
- test("rate-limit api_error + error result: result body scrubbed (no duplicate text)", async () => {
196
- const limitText = "You've hit your limit · resets 11:10pm (Asia/Saigon)"
197
- const sdk = await collectSdk([
198
- {
199
- type: "assistant",
200
- session_id: "sess-rl",
201
- uuid: "msg-err",
202
- isApiErrorMessage: true,
203
- message: {
204
- model: "<synthetic>",
205
- content: [{ type: "text", text: limitText }],
206
- },
207
- },
208
- {
209
- type: "result",
210
- subtype: "error",
211
- session_id: "sess-rl",
212
- is_error: true,
213
- result: limitText,
214
- duration_ms: 2000,
215
- uuid: "r-err",
216
- },
217
- ])
218
- const apiErrorEntries = sdk.flatMap((ev) =>
219
- ev.type === "transcript" && ev.entry && ev.entry.kind === "api_error" ? [ev.entry] : [],
220
- )
221
- const resultEntries = sdk.flatMap((ev) =>
222
- ev.type === "transcript" && ev.entry && ev.entry.kind === "result" ? [ev.entry] : [],
223
- )
224
- expect(apiErrorEntries).toHaveLength(1)
225
- expect(apiErrorEntries[0].text).toContain("hit your limit")
226
- expect(resultEntries).toHaveLength(1)
227
- expect(resultEntries[0].isError).toBe(true)
228
- expect(resultEntries[0].result).toBe("")
229
- expect(resultEntries[0].durationMs).toBe(2000)
230
- })
231
-
232
- test("compact_boundary turn does not produce phantom context_window_updated", async () => {
233
- await assertSameEvents([
234
- {
235
- type: "system",
236
- subtype: "compact_boundary",
237
- session_id: "sess-c",
238
- compact_metadata: { trigger: "auto", pre_tokens: 50000 },
239
- uuid: "cb",
240
- },
241
- {
242
- type: "result",
243
- subtype: "success",
244
- session_id: "sess-c",
245
- is_error: false,
246
- duration_ms: 100,
247
- usage: { input_tokens: 1000, cache_read_input_tokens: 49000, output_tokens: 0 },
248
- uuid: "r-c",
249
- },
250
- ])
251
- })
252
-
253
- test("both wire spellings of compact metadata reach the entry identically", async () => {
254
- const snakeCaseFromSdkStream = {
255
- type: "system",
256
- subtype: "compact_boundary",
257
- session_id: "sess-d",
258
- compact_metadata: { trigger: "auto", pre_tokens: 206978, post_tokens: 7509, duration_ms: 143806 },
259
- uuid: "cb-snake",
260
- }
261
- const camelCaseFromCliTranscript = {
262
- type: "system",
263
- subtype: "compact_boundary",
264
- session_id: "sess-d",
265
- compactMetadata: { trigger: "auto", preTokens: 206978, postTokens: 7509, durationMs: 143806 },
266
- uuid: "cb-camel",
267
- }
268
- const expected: CompactBoundaryMetadata = {
269
- trigger: "auto",
270
- preTokens: 206978,
271
- postTokens: 7509,
272
- durationMs: 143806,
273
- }
274
- const boundaryMetadata = (events: HarnessEvent[]): (CompactBoundaryMetadata | undefined)[] =>
275
- events.flatMap((event) =>
276
- event.type === "transcript" && event.entry.kind === "compact_boundary"
277
- ? [event.entry.compactMetadata]
278
- : [],
279
- )
280
-
281
- for (const message of [snakeCaseFromSdkStream, camelCaseFromCliTranscript]) {
282
- await assertSameEvents([message])
283
- expect(boundaryMetadata(await collectSdk([message]))).toEqual([expected])
284
- expect(boundaryMetadata(await ptyEventsViaTranscriptStream([message]))).toEqual([expected])
285
- }
286
- })
287
- })
288
-
289
- describe("createClaudeHarnessStream cost attachment", () => {
290
- test("claude result cost is attached to the final-turn snapshot", async () => {
291
- const messages = [
292
- {
293
- type: "assistant",
294
- session_id: "sess-cost",
295
- message: { id: "m1", role: "assistant", content: [] },
296
- usage: { input_tokens: 100, output_tokens: 50 },
297
- },
298
- {
299
- type: "result",
300
- subtype: "success",
301
- session_id: "sess-cost",
302
- is_error: false,
303
- total_cost_usd: 0.0123,
304
- usage: { input_tokens: 100, output_tokens: 50 },
305
- duration_ms: 10,
306
- num_turns: 1,
307
- result: "ok",
308
- },
309
- ]
310
-
311
- const events: HarnessEvent[] = []
312
- for await (const ev of createClaudeHarnessStream(fakeQuery(messages))) {
313
- events.push(ev)
314
- }
315
-
316
- const cwuEntries = events.flatMap((ev) => {
317
- if (ev.type !== "transcript") return []
318
- const entry = ev.entry
319
- return entry && entry.kind === "context_window_updated" ? [entry] : []
320
- })
321
- const lastCwu = cwuEntries.at(-1)
322
- expect(lastCwu).toBeDefined()
323
- expect(lastCwu!.usage.costUsd).toBeCloseTo(0.0123, 6)
324
- })
325
-
326
- test("openrouter turn gets computed cost from a price resolver", async () => {
327
- const messages = [
328
- {
329
- type: "assistant",
330
- session_id: "sess-or",
331
- message: { id: "m1", role: "assistant", content: [] },
332
- usage: { input_tokens: 1_000_000, output_tokens: 0 },
333
- },
334
- {
335
- type: "result",
336
- subtype: "success",
337
- session_id: "sess-or",
338
- is_error: false,
339
- usage: { input_tokens: 1_000_000, output_tokens: 0 },
340
- duration_ms: 10,
341
- num_turns: 1,
342
- result: "ok",
343
- },
344
- ]
345
-
346
- const price: ModelPrice = { inputPerMTok: 3, outputPerMTok: 15 }
347
- const events: HarnessEvent[] = []
348
- for await (const ev of createClaudeHarnessStream(fakeQuery(messages), undefined, () => price)) {
349
- events.push(ev)
350
- }
351
-
352
- const cwuEntries = events.flatMap((ev) => {
353
- if (ev.type !== "transcript") return []
354
- const entry = ev.entry
355
- return entry && entry.kind === "context_window_updated" ? [entry] : []
356
- })
357
- const lastCwu = cwuEntries.at(-1)
358
- expect(lastCwu).toBeDefined()
359
- expect(lastCwu!.usage.costUsd).toBeCloseTo(3, 6)
360
- })
361
- })
362
-
363
- describe("result entry usage + cost enrichment", () => {
364
- test("claude: result entry carries usage tokens and provider costUsd", async () => {
365
- const messages = [
366
- {
367
- type: "assistant",
368
- session_id: "sess-ru",
369
- message: { id: "m1", role: "assistant", content: [] },
370
- usage: { input_tokens: 200, output_tokens: 80 },
371
- },
372
- {
373
- type: "result",
374
- subtype: "success",
375
- session_id: "sess-ru",
376
- is_error: false,
377
- total_cost_usd: 0.02,
378
- usage: { input_tokens: 200, output_tokens: 80 },
379
- duration_ms: 100,
380
- num_turns: 1,
381
- result: "done",
382
- },
383
- ]
384
-
385
- const events: HarnessEvent[] = []
386
- for await (const ev of createClaudeHarnessStream(fakeQuery(messages))) {
387
- events.push(ev)
388
- }
389
-
390
- const resultEntries = events.flatMap((ev) =>
391
- ev.type === "transcript" && ev.entry && ev.entry.kind === "result" ? [ev.entry] : [],
392
- )
393
- expect(resultEntries).toHaveLength(1)
394
- const resultEntry = resultEntries[0]
395
- expect(resultEntry.costUsd).toBeCloseTo(0.02, 6)
396
- expect(resultEntry.usage).toBeDefined()
397
- expect(resultEntry.usage?.outputTokens).toBe(80)
398
- expect(resultEntry.usage?.inputTokens).toBe(200)
399
- })
400
-
401
- test("openrouter: result entry carries computed cost and usage tokens from resolver", async () => {
402
- const messages = [
403
- {
404
- type: "assistant",
405
- session_id: "sess-oru",
406
- message: { id: "m1", role: "assistant", content: [] },
407
- usage: { input_tokens: 1_000_000, output_tokens: 0 },
408
- },
409
- {
410
- type: "result",
411
- subtype: "success",
412
- session_id: "sess-oru",
413
- is_error: false,
414
- usage: { input_tokens: 1_000_000, output_tokens: 0 },
415
- duration_ms: 10,
416
- num_turns: 1,
417
- result: "ok",
418
- },
419
- ]
420
-
421
- const price: ModelPrice = { inputPerMTok: 3, outputPerMTok: 15 }
422
- const events: HarnessEvent[] = []
423
- for await (const ev of createClaudeHarnessStream(fakeQuery(messages), undefined, () => price)) {
424
- events.push(ev)
425
- }
426
-
427
- const resultEntries = events.flatMap((ev) =>
428
- ev.type === "transcript" && ev.entry && ev.entry.kind === "result" ? [ev.entry] : [],
429
- )
430
- expect(resultEntries).toHaveLength(1)
431
- const resultEntry = resultEntries[0]
432
- expect(resultEntry.costUsd).toBeCloseTo(3, 6)
433
- expect(resultEntry.usage).toBeDefined()
434
- expect(resultEntry.usage?.inputTokens).toBe(1_000_000)
435
- expect(resultEntry.usage?.outputTokens).toBeUndefined()
436
- })
437
- })
@@ -1,164 +0,0 @@
1
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises"
2
- import path from "node:path"
3
- import process from "node:process"
4
- import { isJsonArray, isJsonObject, safeJsonParse, type JsonValue } from "../../shared/json"
5
-
6
- export interface ClaudePtyEntry {
7
- chatId: string
8
- sessionId: string
9
- pid: number
10
- cwd: string
11
- runtimeDir: string
12
- createdAt: number
13
- }
14
-
15
- interface RegistryFile {
16
- entries: ClaudePtyEntry[]
17
- }
18
-
19
- export class ClaudePtyRegistry {
20
- private readonly filePath: string
21
- private entries: ClaudePtyEntry[] = []
22
- private loaded = false
23
- private writeQueue: Promise<void> = Promise.resolve()
24
-
25
- constructor(filePath: string) {
26
- this.filePath = filePath
27
- }
28
-
29
- async register(entry: Omit<ClaudePtyEntry, "createdAt">): Promise<void> {
30
- await this.loadIfNeeded()
31
- const next = this.entries.filter((existing) => existing.pid !== entry.pid)
32
- next.push({ ...entry, createdAt: Date.now() })
33
- this.entries = next
34
- await this.persist()
35
- }
36
-
37
- async unregister(pid: number): Promise<void> {
38
- await this.loadIfNeeded()
39
- this.entries = this.entries.filter((entry) => entry.pid !== pid)
40
- await this.persist()
41
- }
42
-
43
- async reapStale(): Promise<ClaudePtyEntry[]> {
44
- const stored = await this.readFromDisk()
45
- if (stored.length === 0) {
46
- this.entries = []
47
- this.loaded = true
48
- return []
49
- }
50
- for (const entry of stored) {
51
- await killProcessTree(entry.pid)
52
- if (entry.runtimeDir && entry.runtimeDir.length > 0) {
53
- try { await rm(entry.runtimeDir, { recursive: true, force: true }) } catch {
54
- }
55
- }
56
- }
57
- this.entries = []
58
- this.loaded = true
59
- await this.persist()
60
- return stored
61
- }
62
-
63
- private async loadIfNeeded() {
64
- if (this.loaded) return
65
- this.entries = await this.readFromDisk()
66
- this.loaded = true
67
- }
68
-
69
- private async readFromDisk(): Promise<ClaudePtyEntry[]> {
70
- let raw: string
71
- try {
72
- raw = await readFile(this.filePath, "utf8")
73
- } catch {
74
- return []
75
- }
76
- const parsed = safeJsonParse(raw)
77
- if (parsed === null || !isJsonObject(parsed) || !isJsonArray(parsed.entries)) return []
78
- return parsed.entries.filter(isValidEntry)
79
- }
80
-
81
- private async persist() {
82
- const snapshot: RegistryFile = { entries: [...this.entries] }
83
- const serialized = JSON.stringify(snapshot)
84
- this.writeQueue = this.writeQueue
85
- .catch(() => undefined)
86
- .then(async () => {
87
- await mkdir(path.dirname(this.filePath), { recursive: true })
88
- await writeFile(this.filePath, serialized, "utf8")
89
- })
90
- await this.writeQueue
91
- }
92
- }
93
-
94
- function isValidEntry(value: JsonValue): value is JsonValue & ClaudePtyEntry {
95
- if (!isJsonObject(value)) return false
96
- return (
97
- typeof value.chatId === "string"
98
- && typeof value.sessionId === "string"
99
- && typeof value.pid === "number"
100
- && Number.isFinite(value.pid)
101
- && typeof value.cwd === "string"
102
- && typeof value.runtimeDir === "string"
103
- && typeof value.createdAt === "number"
104
- )
105
- }
106
-
107
- export async function killProcessTree(pid: number): Promise<void> {
108
- if (process.platform === "win32") return
109
- if (!Number.isFinite(pid) || pid <= 0) return
110
-
111
- const targets = [pid, ...(await collectDescendants(pid))]
112
- for (const target of targets.reverse()) {
113
- try {
114
- process.kill(target, "SIGKILL")
115
- } catch {
116
- }
117
- }
118
- }
119
-
120
- async function collectDescendants(root: number): Promise<number[]> {
121
- let childrenByParent: Map<number, number[]>
122
- try {
123
- const proc = Bun.spawn(["ps", "-A", "-o", "pid=,ppid="], {
124
- stdout: "pipe",
125
- stderr: "ignore",
126
- stdin: "ignore",
127
- })
128
- const text = await new Response(proc.stdout).text()
129
- await proc.exited
130
- childrenByParent = parsePidPpid(text)
131
- } catch {
132
- return []
133
- }
134
-
135
- const descendants: number[] = []
136
- const queue = [root]
137
- const seen = new Set<number>([root])
138
- while (queue.length > 0) {
139
- const parent = queue.shift()
140
- if (parent === undefined) break
141
- for (const child of childrenByParent.get(parent) ?? []) {
142
- if (seen.has(child)) continue
143
- seen.add(child)
144
- descendants.push(child)
145
- queue.push(child)
146
- }
147
- }
148
- return descendants
149
- }
150
-
151
- function parsePidPpid(psOutput: string): Map<number, number[]> {
152
- const childrenByParent = new Map<number, number[]>()
153
- for (const line of psOutput.split("\n")) {
154
- const match = line.trim().match(/^(\d+)\s+(\d+)$/)
155
- if (!match) continue
156
- const pid = Number.parseInt(match[1] ?? "", 10)
157
- const ppid = Number.parseInt(match[2] ?? "", 10)
158
- if (!Number.isFinite(pid) || !Number.isFinite(ppid)) continue
159
- const siblings = childrenByParent.get(ppid)
160
- if (siblings) siblings.push(pid)
161
- else childrenByParent.set(ppid, [pid])
162
- }
163
- return childrenByParent
164
- }