@cuongtran001/kanna 1.63.2 → 1.64.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 (211) hide show
  1. package/README.md +5 -9
  2. package/dist/client/assets/{BoardsPage-BJalB-bc.js → BoardsPage--7VsnbXM.js} +1 -1
  3. package/dist/client/assets/BoardsRoutePage-Hkl_oQv0.js +1 -0
  4. package/dist/client/assets/ChatPage-h-VTkkkt.js +93 -0
  5. package/dist/client/assets/{CronJobRow-CPbO_jwU.js → CronJobRow-eb86NFF7.js} +1 -1
  6. package/dist/client/assets/{CronJobsPage-Ck3SNgGi.js → CronJobsPage-B0NHa3ss.js} +1 -1
  7. package/dist/client/assets/{FilePreviewSheet-XEuUtDPn.js → FilePreviewSheet-DOEK5SRs.js} +2 -2
  8. package/dist/client/assets/{FlowSurface-BCbeMTJe.js → FlowSurface-DPotbbWr.js} +1 -1
  9. package/dist/client/assets/{GenUIBlock-BJy_dfs-.js → GenUIBlock-BNZfT-Vk.js} +2 -2
  10. package/dist/client/assets/{KannaSocketProvider-D7Lzlt35.js → KannaSocketProvider-DtKR165f.js} +1 -1
  11. package/dist/client/assets/{LocalProjectsPage-9LJkQjhT.js → LocalProjectsPage-DNLMYHFL.js} +1 -1
  12. package/dist/client/assets/SettingsPage-B3bF5Xse.js +9 -0
  13. package/dist/client/assets/{StackBoardsRoutePage-t_EaFNG4.js → StackBoardsRoutePage-DplbkiEr.js} +1 -1
  14. package/dist/client/assets/{ToolCallMessage-RbF9XT2B.js → ToolCallMessage-BJQRDH0E.js} +1 -1
  15. package/dist/client/assets/{VChartSurface-CKuPelsn.js → VChartSurface-gDrFdsRD.js} +1 -1
  16. package/dist/client/assets/{WorkflowsPage-B6qUmQA3.js → WorkflowsPage-RDNDgswE.js} +1 -1
  17. package/dist/client/assets/{WorkflowsSection-DxO0S4An.js → WorkflowsSection-8tzLFcmi.js} +1 -1
  18. package/dist/client/assets/browser-uLao0xGx.js +3 -0
  19. package/dist/client/assets/{chart-renderer-DjYZvNxh.js → chart-renderer-Bp9ekEsh.js} +1 -1
  20. package/dist/client/assets/{chatSounds-FD2DQzF6.js → chatSounds-4LPsoFE0.js} +1 -1
  21. package/dist/client/assets/chevron-left-BXDenZrj.js +1 -0
  22. package/dist/client/assets/{clipboard.adapter-GN9nnYak.js → clipboard.adapter-V4RFuBQg.js} +1 -1
  23. package/dist/client/assets/{columnStyle-DjA7hA9k.js → columnStyle-CPsUAD1y.js} +1 -1
  24. package/dist/client/assets/{createScopedStore-BoLB9Rhc.js → createScopedStore-vi_qBrnv.js} +1 -1
  25. package/dist/client/assets/{dist-CimtOoGK.js → dist-CQv2I5w0.js} +1 -1
  26. package/dist/client/assets/editor-icons-DSEWhYdt.js +1 -0
  27. package/dist/client/assets/{ellipsis-Cd7sO0LO.js → ellipsis-Dw447mDf.js} +1 -1
  28. package/dist/client/assets/{flow-kinds-CW2TU_83.js → flow-kinds-Dctbq8tY.js} +1 -1
  29. package/dist/client/assets/{folder-C7sXCaHj.js → folder-K_gAJ1DI.js} +1 -1
  30. package/dist/client/assets/{formatters-wSVyvDFZ.js → formatters-BGORekd-.js} +1 -1
  31. package/dist/client/assets/{genui-EzBIAYFE.js → genui-buyzerHE.js} +1 -1
  32. package/dist/client/assets/{globe-xmRpmpma.js → globe-D0uBqjk3.js} +1 -1
  33. package/dist/client/assets/{host-DCaA9W-Z.js → host-Bmj1x1Ur.js} +1 -1
  34. package/dist/client/assets/{index-CDqjh7nt.js → index-BioMVpYd.js} +7 -7
  35. package/dist/client/assets/index-CxDE9VOy.css +1 -0
  36. package/dist/client/assets/{info-D4MMvZFe.js → info-XTOdbyHd.js} +1 -1
  37. package/dist/client/assets/{input-C4fvDiQ4.js → input-BdIYegqO.js} +1 -1
  38. package/dist/client/assets/{lexicalToReact-sPUyOWk6.js → lexicalToReact-DqgmPzHx.js} +3 -3
  39. package/dist/client/assets/{log-aTaDVF_e.js → log-CKbWpKMT.js} +1 -1
  40. package/dist/client/assets/{monitor-_CRKcV1h.js → monitor-CGybOpir.js} +1 -1
  41. package/dist/client/assets/{pencil-fNBeUUCf.js → pencil-B8yLmanR.js} +1 -1
  42. package/dist/client/assets/{pendingActionsStore-COTh4Ztq.js → pendingActionsStore-CdwEvP-X.js} +1 -1
  43. package/dist/client/assets/{play-BAmkJfn6.js → play-BmOQO8Wo.js} +1 -1
  44. package/dist/client/assets/{plus-c75CkNEO.js → plus-wpoGLqmM.js} +1 -1
  45. package/dist/client/assets/{popover-CHM4skzh.js → popover-B9W07k-v.js} +1 -1
  46. package/dist/client/assets/{pushClient-BJH_ITRB.js → pushClient-C3BFEIhM.js} +1 -1
  47. package/dist/client/assets/runDetached-B3NSxNTV.js +1 -0
  48. package/dist/client/assets/{segmented-control-DNOwRupT.js → segmented-control-D1eTj6vZ.js} +1 -1
  49. package/dist/client/assets/{select-dLHiAls6.js → select-rChS1ZvA.js} +1 -1
  50. package/dist/client/assets/{settings-header-button-CGKCzFyk.js → settings-header-button-D0i3JySg.js} +1 -1
  51. package/dist/client/assets/{sidebarPendingActions-Dyp80Lv_.js → sidebarPendingActions-BKojWYqD.js} +1 -1
  52. package/dist/client/assets/{square-pen-B1IxmScM.js → square-pen-C_M6NWE6.js} +1 -1
  53. package/dist/client/assets/{state-mark-BhTZfcYe.js → state-mark-D71tSKL1.js} +1 -1
  54. package/dist/client/assets/{status-pill-CwhJhaMy.js → status-pill-IuanKijZ.js} +1 -1
  55. package/dist/client/assets/{terminal-Bnj7qFGw.js → terminal-ChU65qN4.js} +1 -1
  56. package/dist/client/assets/{timer.adapter-DHt2zwsL.js → timer.adapter-DxStl6YQ.js} +1 -1
  57. package/dist/client/assets/{tools-3gJcj3hK.js → tools-DcwSZn_Y.js} +1 -1
  58. package/dist/client/assets/{triangle-alert-CWrc0CIO.js → triangle-alert-CVNaunqf.js} +1 -1
  59. package/dist/client/assets/useAppGlobalState-MUKjQL55.js +1 -0
  60. package/dist/client/assets/useKannaState-DiOIS_Z6.js +5 -0
  61. package/dist/client/assets/useTheme-DpVQlqks.js +1 -0
  62. package/dist/client/index.html +35 -35
  63. package/package.json +5 -2
  64. package/src/server/agent-coordinator-types.ts +0 -6
  65. package/src/server/agent-coordinator.ts +6 -45
  66. package/src/server/agent.compaction.test.ts +1 -13
  67. package/src/server/agent.oauth-account-info.test.ts +0 -3
  68. package/src/server/agent.oauth-release.test.ts +0 -2
  69. package/src/server/agent.oauth-rotation.test.ts +0 -9
  70. package/src/server/agent.oauth-token-share-cap.test.ts +0 -1
  71. package/src/server/agent.openrouter-model.test.ts +0 -2
  72. package/src/server/agent.openrouter-watchdog.test.ts +0 -3
  73. package/src/server/agent.sdk-workflow-register.test.ts +2 -64
  74. package/src/server/agent.test.ts +1 -319
  75. package/src/server/app-settings.test.ts +17 -20
  76. package/src/server/app-settings.ts +22 -37
  77. package/src/server/auth.test.ts +5 -1
  78. package/src/server/auto-continue/e2e.test.ts +0 -1
  79. package/src/server/background-task-output-registry.test.ts +1 -1
  80. package/src/server/background-task-output-registry.ts +1 -1
  81. package/src/server/claude-cancel-handler.test.ts +0 -58
  82. package/src/server/claude-cancel-handler.ts +1 -17
  83. package/src/server/claude-harness-stream.test.ts +134 -0
  84. package/src/server/{claude-pty/jsonl-path.test.ts → claude-projects-path.adapter.test.ts} +1 -19
  85. package/src/server/{claude-pty/jsonl-path.adapter.ts → claude-projects-path.adapter.ts} +0 -11
  86. package/src/server/claude-send-command.test.ts +0 -1
  87. package/src/server/claude-session-config-helpers.test.ts +1 -90
  88. package/src/server/claude-session-config-helpers.ts +1 -38
  89. package/src/server/claude-session-error-handler.test.ts +0 -1
  90. package/src/server/claude-session-lifecycle.test.ts +2 -36
  91. package/src/server/claude-session-lifecycle.ts +2 -8
  92. package/src/server/claude-session-runner.test.ts +8 -36
  93. package/src/server/claude-session-runner.ts +1 -26
  94. package/src/server/claude-session-spawner.test.ts +5 -64
  95. package/src/server/claude-session-spawner.ts +49 -115
  96. package/src/server/claude-session-start.test.ts +1 -14
  97. package/src/server/claude-session-start.ts +0 -8
  98. package/src/server/claude-session-state-queries.test.ts +1 -2
  99. package/src/server/claude-session-state.test.ts +0 -1
  100. package/src/server/claude-session-state.ts +0 -4
  101. package/src/server/claude-subagent-wiring.test.ts +2 -68
  102. package/src/server/claude-subagent-wiring.ts +3 -50
  103. package/src/server/claude-turn-starter-types.ts +0 -3
  104. package/src/server/claude-turn-starter.test.ts +0 -28
  105. package/src/server/claude-turn-starter.ts +0 -5
  106. package/src/server/event-store.test.ts +0 -2
  107. package/src/server/events.ts +1 -2
  108. package/src/server/harness-types.ts +1 -2
  109. package/src/server/http-api-routes.ts +27 -103
  110. package/src/server/http-dispatcher.test.ts +7 -9
  111. package/src/server/http-dispatcher.ts +16 -3
  112. package/src/server/http-file-response.ts +92 -0
  113. package/src/server/import-subagent-drill.e2e.test.ts +1 -1
  114. package/src/server/imported-subagents-dir.test.ts +1 -1
  115. package/src/server/imported-subagents-dir.ts +1 -1
  116. package/src/server/kanna-mcp.test.ts +0 -75
  117. package/src/server/kanna-mcp.ts +1 -47
  118. package/src/server/server.test.ts +5 -7
  119. package/src/server/server.ts +3 -22
  120. package/src/server/subagent-orchestrator.test.ts +0 -1
  121. package/src/server/subagent-provider-run.test.ts +48 -10
  122. package/src/server/subagent-provider-run.ts +12 -3
  123. package/src/server/tool-callback.test.ts +1 -1
  124. package/src/server/tus-uploads.adapter.ts +142 -0
  125. package/src/server/uploads.adapter.ts +133 -9
  126. package/src/server/uploads.test.ts +295 -120
  127. package/src/server/uploads.ts +1 -1
  128. package/src/server/ws-router-agent-ctrl.test.ts +0 -45
  129. package/src/server/ws-router-agent-ctrl.ts +1 -31
  130. package/src/server/ws-router-broadcast.ts +0 -27
  131. package/src/server/ws-router-defaults.test.ts +2 -3
  132. package/src/server/ws-router-defaults.ts +2 -3
  133. package/src/server/ws-router-envelope.ts +0 -15
  134. package/src/server/ws-router-observability.test.ts +1 -1
  135. package/src/server/ws-router-settings.test.ts +2 -2
  136. package/src/server/ws-router.test.ts +2 -2
  137. package/src/server/ws-router.ts +0 -8
  138. package/src/shared/app-settings-types.ts +9 -20
  139. package/src/shared/claude-api-error-classification.ts +1 -1
  140. package/src/shared/protocol.ts +1 -11
  141. package/src/shared/settings/uploads.test.ts +8 -0
  142. package/src/shared/settings/uploads.ts +2 -2
  143. package/dist/client/assets/BoardsRoutePage-BTTjJX-Y.js +0 -1
  144. package/dist/client/assets/ChatPage-DH8wtQyD.js +0 -93
  145. package/dist/client/assets/SettingsPage-CKrZ6dGQ.js +0 -9
  146. package/dist/client/assets/chevron-left-CJ0qv9kF.js +0 -1
  147. package/dist/client/assets/editor-icons-CONDQZof.js +0 -1
  148. package/dist/client/assets/index-C-UYaDk6.css +0 -1
  149. package/dist/client/assets/runDetached-DqQ3k2ji.js +0 -1
  150. package/dist/client/assets/useAppGlobalState-DKOGrO5l.js +0 -1
  151. package/dist/client/assets/useKannaState-D7R_-dM6.js +0 -5
  152. package/dist/client/assets/useTheme-B57LIKyx.js +0 -1
  153. package/src/server/agent.pty-rotation.test.ts +0 -322
  154. package/src/server/claude-pty/auth.test.ts +0 -38
  155. package/src/server/claude-pty/auth.ts +0 -17
  156. package/src/server/claude-pty/claude-session-registry.adapter.ts +0 -68
  157. package/src/server/claude-pty/claude-session-registry.test.ts +0 -149
  158. package/src/server/claude-pty/driver.test.ts +0 -1153
  159. package/src/server/claude-pty/driver.ts +0 -829
  160. package/src/server/claude-pty/env.test.ts +0 -79
  161. package/src/server/claude-pty/env.ts +0 -20
  162. package/src/server/claude-pty/jsonl-to-event.test.ts +0 -882
  163. package/src/server/claude-pty/jsonl-to-event.ts +0 -357
  164. package/src/server/claude-pty/parity-matrix.test.ts +0 -437
  165. package/src/server/claude-pty/pid-registry.adapter.ts +0 -164
  166. package/src/server/claude-pty/pid-registry.test.ts +0 -178
  167. package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +0 -20
  168. package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +0 -32
  169. package/src/server/claude-pty/pty-instance-registry.test.ts +0 -208
  170. package/src/server/claude-pty/pty-instance-registry.ts +0 -172
  171. package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +0 -103
  172. package/src/server/claude-pty/pty-memory-sampler.adapter.ts +0 -86
  173. package/src/server/claude-pty/pty-process.adapter.ts +0 -60
  174. package/src/server/claude-pty/pty-process.test.ts +0 -49
  175. package/src/server/claude-pty/resolve-binary.adapter.ts +0 -106
  176. package/src/server/claude-pty/resolve-binary.test.ts +0 -118
  177. package/src/server/claude-pty/runtime-dir.adapter.ts +0 -19
  178. package/src/server/claude-pty/sandbox/detect.adapter.ts +0 -17
  179. package/src/server/claude-pty/sandbox/detect.test.ts +0 -17
  180. package/src/server/claude-pty/sandbox/platform.test.ts +0 -35
  181. package/src/server/claude-pty/sandbox/platform.ts +0 -27
  182. package/src/server/claude-pty/sandbox/preflight-io.adapter.ts +0 -14
  183. package/src/server/claude-pty/sandbox/preflight.test.ts +0 -69
  184. package/src/server/claude-pty/sandbox/preflight.ts +0 -44
  185. package/src/server/claude-pty/sandbox/profile-linux.adapter.ts +0 -63
  186. package/src/server/claude-pty/sandbox/profile-linux.test.ts +0 -90
  187. package/src/server/claude-pty/sandbox/profile-macos.adapter.ts +0 -72
  188. package/src/server/claude-pty/sandbox/profile-macos.test.ts +0 -68
  189. package/src/server/claude-pty/sandbox/wrap.adapter.ts +0 -58
  190. package/src/server/claude-pty/sandbox/wrap.test.ts +0 -82
  191. package/src/server/claude-pty/settings-writer.adapter.ts +0 -23
  192. package/src/server/claude-pty/settings-writer.test.ts +0 -22
  193. package/src/server/claude-pty/smoke-test-io.adapter.ts +0 -28
  194. package/src/server/claude-pty/smoke-test.test.ts +0 -199
  195. package/src/server/claude-pty/smoke-test.ts +0 -182
  196. package/src/server/claude-pty/tui-control.test.ts +0 -300
  197. package/src/server/claude-pty/tui-control.ts +0 -174
  198. package/src/server/claude-pty/tui-source.adapter.test.ts +0 -374
  199. package/src/server/claude-pty/tui-source.adapter.ts +0 -294
  200. package/src/server/http-server.adapter.test.ts +0 -18
  201. package/src/server/http-server.adapter.ts +0 -39
  202. package/src/server/kanna-mcp-http.test.ts +0 -274
  203. package/src/server/kanna-mcp-http.ts +0 -194
  204. package/src/server/kanna-mcp-tools/ask-user-question.test.ts +0 -108
  205. package/src/server/kanna-mcp-tools/ask-user-question.ts +0 -55
  206. package/src/server/kanna-mcp-tools/exit-plan-mode.test.ts +0 -61
  207. package/src/server/kanna-mcp-tools/exit-plan-mode.ts +0 -49
  208. package/src/server/mcp-zod-compat.adapter.ts +0 -20
  209. package/src/shared/pty-instance.ts +0 -43
  210. /package/src/server/{claude-pty/output-ring.test.ts → output-ring.test.ts} +0 -0
  211. /package/src/server/{claude-pty/output-ring.ts → output-ring.ts} +0 -0
@@ -1,882 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { parseJsonlLine, createJsonlEventParser } from "./jsonl-to-event"
3
- import type { HarnessEvent } from "../harness-types"
4
-
5
- describe("parseJsonlLine", () => {
6
- test("ignores empty lines", () => {
7
- expect(parseJsonlLine("")).toEqual([])
8
- expect(parseJsonlLine(" ")).toEqual([])
9
- })
10
-
11
- test("ignores malformed JSON (logs but does not throw)", () => {
12
- expect(parseJsonlLine("{not json")).toEqual([])
13
- })
14
-
15
- test("system.init → session_token event", () => {
16
- const line = JSON.stringify({
17
- type: "system",
18
- subtype: "init",
19
- session_id: "sess-1",
20
- model: "claude-sonnet-4-6",
21
- })
22
- const events = parseJsonlLine(line)
23
- const sessionTokenEvent = events.find((e) => e.type === "session_token")
24
- expect(sessionTokenEvent).toBeDefined()
25
- expect(sessionTokenEvent?.sessionToken).toBe("sess-1")
26
- })
27
-
28
- test("assistant message → transcript event with assistant role", () => {
29
- const line = JSON.stringify({
30
- type: "assistant",
31
- message: {
32
- role: "assistant",
33
- content: [{ type: "text", text: "hello" }],
34
- },
35
- })
36
- const events = parseJsonlLine(line)
37
- const transcriptEvents = events.filter((e) => e.type === "transcript")
38
- expect(transcriptEvents.length).toBeGreaterThan(0)
39
- })
40
-
41
- test("on-disk apiError reason survives onto the api_error transcript entry", () => {
42
- const line = JSON.stringify({
43
- type: "assistant",
44
- uuid: "bbfa6969-b743-4219-8cee-8f6fef2bb688",
45
- message: {
46
- model: "<synthetic>",
47
- role: "assistant",
48
- content: [{ type: "text", text: "API Error: 400 Claude Code 2.1.272 does not support this model; version 2.1.280 or newer is required." }],
49
- },
50
- requestId: "req_011CfKjVZB7wvfHVmLXQfJSU",
51
- apiError: "claude_code_version_too_old",
52
- error: "invalid_request",
53
- isApiErrorMessage: true,
54
- apiErrorStatus: 400,
55
- })
56
- const entries = parseJsonlLine(line).flatMap((e) => (e.type === "transcript" && e.entry ? [e.entry] : []))
57
- const apiError = entries.find((entry) => entry.kind === "api_error")
58
- if (apiError?.kind !== "api_error") throw new Error("expected api_error")
59
- expect(apiError.status).toBe(400)
60
- expect(apiError.apiErrorReason).toBe("claude_code_version_too_old")
61
- })
62
-
63
- test("system.rate_limit subtype → rate_limit event", () => {
64
- const line = JSON.stringify({
65
- type: "system",
66
- subtype: "rate_limit",
67
- resetAt: 1748800000000,
68
- tz: "PT",
69
- })
70
- const events = parseJsonlLine(line)
71
- const rl = events.find((e) => e.type === "rate_limit")
72
- expect(rl).toBeDefined()
73
- expect(rl?.rateLimit?.tz).toBe("PT")
74
- })
75
-
76
- test("system.informational without rate-limit content → no rate_limit event", () => {
77
- const line = JSON.stringify({
78
- type: "system",
79
- subtype: "informational",
80
- content: "Remote Control failed to connect",
81
- })
82
- const events = parseJsonlLine(line)
83
- const rl = events.find((e) => e.type === "rate_limit")
84
- expect(rl).toBeUndefined()
85
- })
86
-
87
- test("sidechain (subagent) line → no events", () => {
88
- const line = JSON.stringify({
89
- type: "assistant",
90
- isSidechain: true,
91
- session_id: "sub-sess",
92
- message: { role: "assistant", content: [{ type: "text", text: "subagent thinking" }] },
93
- })
94
- expect(parseJsonlLine(line)).toEqual([])
95
- })
96
- })
97
-
98
- describe("createJsonlEventParser", () => {
99
- function emitTypes(events: HarnessEvent[]): string[] {
100
- return events.map((e) => e.type)
101
- }
102
-
103
- test("D3: emits session_token for every line carrying a session_id (not only system/init)", () => {
104
- const parser = createJsonlEventParser()
105
- const initLine = JSON.stringify({
106
- type: "system",
107
- subtype: "init",
108
- session_id: "sess-A",
109
- })
110
- const assistantLine = JSON.stringify({
111
- type: "assistant",
112
- session_id: "sess-A",
113
- message: { id: "msg-1", role: "assistant", content: [{ type: "text", text: "hi" }] },
114
- })
115
- const initEvents = parser.parse(initLine)
116
- const assistantEvents = parser.parse(assistantLine)
117
- expect(initEvents.find((e) => e.type === "session_token")?.sessionToken).toBe("sess-A")
118
- expect(assistantEvents.find((e) => e.type === "session_token")?.sessionToken).toBe("sess-A")
119
- })
120
-
121
- test("D3: lines without session_id do not emit session_token", () => {
122
- const parser = createJsonlEventParser()
123
- const noSession = JSON.stringify({ type: "assistant", message: { role: "assistant", content: [] } })
124
- const events = parser.parse(noSession)
125
- expect(events.find((e) => e.type === "session_token")).toBeUndefined()
126
- })
127
-
128
- test("D3: emits session_token for camelCase sessionId (real transcript shape)", () => {
129
- const parser = createJsonlEventParser()
130
- const realLine = JSON.stringify({
131
- type: "assistant",
132
- sessionId: "real-sess-1",
133
- message: { id: "msg-9", role: "assistant", content: [{ type: "text", text: "hi" }] },
134
- })
135
- const events = parser.parse(realLine)
136
- expect(events.find((e) => e.type === "session_token")?.sessionToken).toBe("real-sess-1")
137
- })
138
-
139
- test("D2: SDK-native rate_limit_event message → rate_limit event via detector", () => {
140
- const parser = createJsonlEventParser()
141
- const line = JSON.stringify({
142
- type: "rate_limit_event",
143
- rate_limit_info: {
144
- status: "rejected",
145
- resetsAt: 1_748_800_000,
146
- },
147
- })
148
- const events = parser.parse(line)
149
- const rl = events.find((e) => e.type === "rate_limit")
150
- expect(rl).toBeDefined()
151
- expect(rl?.rateLimit?.resetAt).toBe(1_748_800_000_000)
152
- })
153
-
154
- test("D2: rate_limit_event with status != rejected → no event", () => {
155
- const parser = createJsonlEventParser()
156
- const line = JSON.stringify({
157
- type: "rate_limit_event",
158
- rate_limit_info: { status: "allowed", resetsAt: 1_748_800_000 },
159
- })
160
- const events = parser.parse(line)
161
- expect(events.find((e) => e.type === "rate_limit")).toBeUndefined()
162
- })
163
-
164
- test("D2: legacy system/rate_limit shape still recognised", () => {
165
- const parser = createJsonlEventParser()
166
- const line = JSON.stringify({
167
- type: "system",
168
- subtype: "rate_limit",
169
- resetAt: 1748800000000,
170
- tz: "PT",
171
- })
172
- const events = parser.parse(line)
173
- const rl = events.find((e) => e.type === "rate_limit")
174
- expect(rl).toBeDefined()
175
- expect(rl?.rateLimit?.tz).toBe("PT")
176
- })
177
-
178
- test("D1: assistant message with usage → context_window_updated transcript", () => {
179
- const parser = createJsonlEventParser()
180
- const line = JSON.stringify({
181
- type: "assistant",
182
- message: {
183
- id: "msg-usage-1",
184
- role: "assistant",
185
- content: [{ type: "text", text: "hi" }],
186
- },
187
- usage: {
188
- input_tokens: 100,
189
- cache_creation_input_tokens: 0,
190
- cache_read_input_tokens: 0,
191
- output_tokens: 25,
192
- },
193
- })
194
- const events = parser.parse(line)
195
- const ctxEvents = events.filter(
196
- (e) => e.type === "transcript" && (e.entry as { kind?: string }).kind === "context_window_updated",
197
- )
198
- expect(ctxEvents).toHaveLength(1)
199
- })
200
-
201
- test("D1: duplicate assistant usage id is deduped", () => {
202
- const parser = createJsonlEventParser()
203
- const line = JSON.stringify({
204
- type: "assistant",
205
- message: {
206
- id: "msg-dedup",
207
- role: "assistant",
208
- content: [{ type: "text", text: "hi" }],
209
- },
210
- usage: { input_tokens: 50, output_tokens: 10 },
211
- })
212
- const first = parser.parse(line)
213
- const second = parser.parse(line)
214
- const firstCtx = first.filter(
215
- (e) => e.type === "transcript" && (e.entry as { kind?: string }).kind === "context_window_updated",
216
- )
217
- const secondCtx = second.filter(
218
- (e) => e.type === "transcript" && (e.entry as { kind?: string }).kind === "context_window_updated",
219
- )
220
- expect(firstCtx).toHaveLength(1)
221
- expect(secondCtx).toHaveLength(0)
222
- })
223
-
224
- test("D1: on-disk transcript nests usage under message.usage → context_window_updated", () => {
225
- const parser = createJsonlEventParser()
226
- const line = JSON.stringify({
227
- type: "assistant",
228
- message: {
229
- id: "msg-nested-1",
230
- role: "assistant",
231
- content: [{ type: "text", text: "hi" }],
232
- usage: {
233
- input_tokens: 14334,
234
- cache_creation_input_tokens: 15367,
235
- cache_read_input_tokens: 17117,
236
- output_tokens: 435,
237
- },
238
- },
239
- })
240
- const events = parser.parse(line)
241
- const ctxEvents = events.filter(
242
- (e) => e.type === "transcript" && (e.entry as { kind?: string }).kind === "context_window_updated",
243
- )
244
- expect(ctxEvents).toHaveLength(1)
245
- const usage = (ctxEvents[0] as { entry: { usage: { usedTokens: number } } }).entry.usage
246
- expect(usage.usedTokens).toBe(14334 + 15367 + 17117 + 435)
247
- })
248
-
249
- test("D1: result message after assistant emits final context_window_updated", () => {
250
- const parser = createJsonlEventParser()
251
- parser.parse(JSON.stringify({
252
- type: "assistant",
253
- message: { id: "msg-1", role: "assistant", content: [{ type: "text", text: "hi" }] },
254
- usage: { input_tokens: 80, output_tokens: 20 },
255
- }))
256
- const resultLine = JSON.stringify({
257
- type: "result",
258
- subtype: "success",
259
- result: "done",
260
- isError: false,
261
- durationMs: 1000,
262
- usage: { input_tokens: 80, output_tokens: 20 },
263
- modelUsage: {
264
- "claude-sonnet-4-6": { contextWindow: 200000, inputTokens: 80, outputTokens: 20 },
265
- },
266
- })
267
- const events = parser.parse(resultLine)
268
- const ctxEvents = events.filter(
269
- (e) => e.type === "transcript" && (e.entry as { kind?: string }).kind === "context_window_updated",
270
- )
271
- expect(ctxEvents).toHaveLength(1)
272
- })
273
-
274
- test("D1: 1M context window floor preserved when modelUsage reports 200k", () => {
275
- const parser = createJsonlEventParser({ configuredContextWindow: 1_000_000 })
276
- parser.parse(JSON.stringify({
277
- type: "assistant",
278
- message: { id: "msg-1m", role: "assistant", content: [{ type: "text", text: "hi" }] },
279
- usage: { input_tokens: 100, output_tokens: 50 },
280
- }))
281
- const resultLine = JSON.stringify({
282
- type: "result",
283
- subtype: "success",
284
- result: "done",
285
- isError: false,
286
- durationMs: 500,
287
- usage: { input_tokens: 100, output_tokens: 50 },
288
- modelUsage: { "claude-sonnet-4-6": { contextWindow: 200000 } },
289
- })
290
- const events = parser.parse(resultLine)
291
- const ctx = events.find(
292
- (e): e is Extract<HarnessEvent, { type: "transcript" }> =>
293
- e.type === "transcript" && (e.entry as { kind?: string }).kind === "context_window_updated",
294
- )
295
- const usage = (ctx?.entry as { usage?: { maxTokens?: number } } | undefined)?.usage
296
- expect(usage?.maxTokens).toBe(1_000_000)
297
- })
298
-
299
- test("emitTypes helper produces deterministic order across calls", () => {
300
- const parser = createJsonlEventParser()
301
- const types = emitTypes(parser.parse(JSON.stringify({
302
- type: "system",
303
- subtype: "init",
304
- session_id: "sess-X",
305
- })))
306
- expect(types[0]).toBe("session_token")
307
- })
308
-
309
- test("sidechain result → no transcript result entry and no session_token", () => {
310
- const parser = createJsonlEventParser()
311
- const line = JSON.stringify({
312
- type: "result",
313
- isSidechain: true,
314
- session_id: "sub-sess",
315
- subtype: "success",
316
- result: "subagent done",
317
- isError: false,
318
- duration_ms: 1000,
319
- })
320
- const events = parser.parse(line)
321
- expect(events).toEqual([])
322
- })
323
-
324
- test("sidechain turn_duration → no synthesized result entry", () => {
325
- const parser = createJsonlEventParser()
326
- const line = JSON.stringify({
327
- type: "system",
328
- subtype: "turn_duration",
329
- isSidechain: true,
330
- session_id: "sub-sess",
331
- durationMs: 1234,
332
- })
333
- const events = parser.parse(line)
334
- const resultEntries = events.filter(
335
- (e) => e.type === "transcript" && (e.entry as { kind?: string }).kind === "result",
336
- )
337
- expect(resultEntries).toEqual([])
338
- expect(events.find((e) => e.type === "session_token")).toBeUndefined()
339
- })
340
-
341
- test("non-sidechain turn_duration still synthesizes a result (regression guard)", () => {
342
- const parser = createJsonlEventParser()
343
- const line = JSON.stringify({
344
- type: "system",
345
- subtype: "turn_duration",
346
- session_id: "main-sess",
347
- durationMs: 1234,
348
- })
349
- const events = parser.parse(line)
350
- const resultEntries = events.filter(
351
- (e) => e.type === "transcript" && (e.entry as { kind?: string }).kind === "result",
352
- )
353
- expect(resultEntries).toHaveLength(1)
354
- })
355
-
356
- describe("background auto-wake filtering", () => {
357
- function makeMetaUser(content: string): string {
358
- return JSON.stringify({
359
- type: "user",
360
- isMeta: true,
361
- message: { role: "user", content },
362
- })
363
- }
364
- function makeRealUser(text: string): string {
365
- return JSON.stringify({
366
- type: "user",
367
- message: { role: "user", content: text },
368
- })
369
- }
370
- function makeAssistant(text: string): string {
371
- return JSON.stringify({
372
- type: "assistant",
373
- message: { role: "assistant", content: [{ type: "text", text }] },
374
- })
375
- }
376
- function makeResult(): string {
377
- return JSON.stringify({
378
- type: "result",
379
- subtype: "success",
380
- isError: false,
381
- duration_ms: 100,
382
- result: "",
383
- })
384
- }
385
- function makeTurnDuration(): string {
386
- return JSON.stringify({
387
- type: "system",
388
- subtype: "turn_duration",
389
- session_id: "main-sess",
390
- durationMs: 100,
391
- })
392
- }
393
- function resultEntries(events: HarnessEvent[]) {
394
- return events.filter(
395
- (e): e is Extract<HarnessEvent, { type: "transcript" }> =>
396
- e.type === "transcript" && (e.entry as { kind?: string }).kind === "result",
397
- )
398
- }
399
-
400
- test("auto-wake: meta user at turn boundary → drop the synthetic user line", () => {
401
- const parser = createJsonlEventParser()
402
- parser.parse(makeRealUser("hi"))
403
- parser.parse(makeAssistant("hello"))
404
- parser.parse(makeResult())
405
- const events = parser.parse(makeMetaUser("<task-notification>bash done</task-notification>"))
406
- const userEntries = events.filter(
407
- (e) => e.type === "transcript" && (e.entry as { kind?: string }).kind === "user_prompt",
408
- )
409
- expect(userEntries).toEqual([])
410
- })
411
-
412
- test("auto-wake: result following meta user at turn boundary is dropped", () => {
413
- const parser = createJsonlEventParser()
414
- parser.parse(makeRealUser("hi"))
415
- parser.parse(makeAssistant("hello"))
416
- parser.parse(makeResult())
417
- parser.parse(makeMetaUser("<task-notification>bash done</task-notification>"))
418
- parser.parse(makeAssistant("acknowledged"))
419
- const events = parser.parse(makeResult())
420
- expect(resultEntries(events)).toEqual([])
421
- })
422
-
423
- test("auto-wake: turn_duration following meta user at turn boundary is dropped", () => {
424
- const parser = createJsonlEventParser()
425
- parser.parse(makeRealUser("hi"))
426
- parser.parse(makeAssistant("hello"))
427
- parser.parse(makeTurnDuration())
428
- parser.parse(makeMetaUser("<task-notification>bash done</task-notification>"))
429
- parser.parse(makeAssistant("acknowledged"))
430
- const events = parser.parse(makeTurnDuration())
431
- expect(resultEntries(events)).toEqual([])
432
- })
433
-
434
- test("mid-turn meta user (e.g. FileRead metadata) does NOT drop the next result", () => {
435
- const parser = createJsonlEventParser()
436
- parser.parse(makeRealUser("read a file"))
437
- parser.parse(makeAssistant("calling FileRead"))
438
- parser.parse(makeMetaUser("<file-metadata>...</file-metadata>"))
439
- parser.parse(makeAssistant("done"))
440
- const events = parser.parse(makeResult())
441
- expect(resultEntries(events).length).toBeGreaterThan(0)
442
- })
443
-
444
- test("auto-wake chain: two consecutive wakes both dropped, real turn after still emits", () => {
445
- const parser = createJsonlEventParser()
446
- parser.parse(makeRealUser("hi"))
447
- parser.parse(makeAssistant("hello"))
448
- parser.parse(makeResult())
449
- parser.parse(makeMetaUser("<task-notification>A done</task-notification>"))
450
- parser.parse(makeAssistant("ack A"))
451
- expect(resultEntries(parser.parse(makeResult()))).toEqual([])
452
- parser.parse(makeMetaUser("<task-notification>B done</task-notification>"))
453
- parser.parse(makeAssistant("ack B"))
454
- expect(resultEntries(parser.parse(makeResult()))).toEqual([])
455
- parser.parse(makeRealUser("status?"))
456
- parser.parse(makeAssistant("all good"))
457
- expect(resultEntries(parser.parse(makeResult())).length).toBeGreaterThan(0)
458
- })
459
-
460
- test("auto-wake: assistant text inside a wake is still emitted (user sees model output)", () => {
461
- const parser = createJsonlEventParser()
462
- parser.parse(makeRealUser("hi"))
463
- parser.parse(makeAssistant("hello"))
464
- parser.parse(makeResult())
465
- parser.parse(makeMetaUser("<task-notification>bash done</task-notification>"))
466
- const events = parser.parse(makeAssistant("bash exited 0"))
467
- const transcript = events.filter((e) => e.type === "transcript")
468
- expect(transcript.length).toBeGreaterThan(0)
469
- })
470
- })
471
-
472
- describe("stop_reason turn-end synthesis (claude ≥2.1.x, no system rows)", () => {
473
- function makeRealUser(text: string): string {
474
- return JSON.stringify({
475
- type: "user",
476
- sessionId: "sess-sr",
477
- message: { role: "user", content: text },
478
- })
479
- }
480
- function makeAssistantRow(args: {
481
- id: string
482
- stopReason: string | null
483
- block: Record<string, unknown>
484
- }): string {
485
- return JSON.stringify({
486
- type: "assistant",
487
- sessionId: "sess-sr",
488
- message: {
489
- id: args.id,
490
- role: "assistant",
491
- stop_reason: args.stopReason,
492
- content: [args.block],
493
- },
494
- })
495
- }
496
- function makeToolResultUser(toolUseId: string): string {
497
- return JSON.stringify({
498
- type: "user",
499
- sessionId: "sess-sr",
500
- message: {
501
- role: "user",
502
- content: [{ type: "tool_result", tool_use_id: toolUseId, content: "ok" }],
503
- },
504
- })
505
- }
506
- function makeLastPrompt(): string {
507
- return JSON.stringify({
508
- type: "last-prompt",
509
- lastPrompt: "hi",
510
- leafUuid: "leaf-1",
511
- sessionId: "sess-sr",
512
- })
513
- }
514
- function makeMetaUser(content: string): string {
515
- return JSON.stringify({
516
- type: "user",
517
- isMeta: true,
518
- sessionId: "sess-sr",
519
- message: { role: "user", content },
520
- })
521
- }
522
- function makeTurnDuration(): string {
523
- return JSON.stringify({
524
- type: "system",
525
- subtype: "turn_duration",
526
- sessionId: "sess-sr",
527
- durationMs: 1234,
528
- })
529
- }
530
- function resultEntries(events: HarnessEvent[]) {
531
- return events.filter(
532
- (e): e is Extract<HarnessEvent, { type: "transcript" }> =>
533
- e.type === "transcript" && (e.entry as { kind?: string }).kind === "result",
534
- )
535
- }
536
-
537
- test("new-format turn: result synthesized once, flushed on the checkpoint row", () => {
538
- const parser = createJsonlEventParser()
539
- expect(resultEntries(parser.parse(makeRealUser("list files")))).toEqual([])
540
- expect(resultEntries(parser.parse(makeAssistantRow({
541
- id: "msg_tool",
542
- stopReason: "tool_use",
543
- block: { type: "tool_use", id: "tu_1", name: "Glob", input: { pattern: "*" } },
544
- })))).toEqual([])
545
- expect(resultEntries(parser.parse(makeToolResultUser("tu_1")))).toEqual([])
546
- expect(resultEntries(parser.parse(makeAssistantRow({
547
- id: "msg_end",
548
- stopReason: "end_turn",
549
- block: { type: "thinking", thinking: "done thinking" },
550
- })))).toEqual([])
551
- expect(resultEntries(parser.parse(makeAssistantRow({
552
- id: "msg_end",
553
- stopReason: "end_turn",
554
- block: { type: "text", text: "all done" },
555
- })))).toEqual([])
556
- const flushed = parser.parse(makeLastPrompt())
557
- expect(resultEntries(flushed)).toHaveLength(1)
558
- const entry = resultEntries(flushed)[0]?.entry as { subtype?: string; isError?: boolean }
559
- expect(entry.subtype).toBe("success")
560
- expect(entry.isError).toBe(false)
561
- })
562
-
563
- test("tool_use stop_reason never synthesizes a result", () => {
564
- const parser = createJsonlEventParser()
565
- parser.parse(makeRealUser("hi"))
566
- parser.parse(makeAssistantRow({
567
- id: "msg_t",
568
- stopReason: "tool_use",
569
- block: { type: "tool_use", id: "tu_2", name: "Read", input: {} },
570
- }))
571
- expect(resultEntries(parser.parse(makeLastPrompt()))).toEqual([])
572
- })
573
-
574
- test("old format: turn_duration after end_turn rows emits exactly one result (the real one)", () => {
575
- const parser = createJsonlEventParser()
576
- parser.parse(makeRealUser("hi"))
577
- parser.parse(makeAssistantRow({
578
- id: "msg_old",
579
- stopReason: "end_turn",
580
- block: { type: "text", text: "hello" },
581
- }))
582
- const events = parser.parse(makeTurnDuration())
583
- const results = resultEntries(events)
584
- expect(results).toHaveLength(1)
585
- expect((results[0]?.entry as { durationMs?: number }).durationMs).toBe(1234)
586
- expect(resultEntries(parser.parse(makeLastPrompt()))).toEqual([])
587
- })
588
-
589
- test("SDK shape: result row directly after end_turn rows emits exactly one result", () => {
590
- const parser = createJsonlEventParser()
591
- parser.parse(makeRealUser("hi"))
592
- parser.parse(makeAssistantRow({
593
- id: "msg_sdk",
594
- stopReason: "end_turn",
595
- block: { type: "text", text: "hello" },
596
- }))
597
- const events = parser.parse(JSON.stringify({
598
- type: "result",
599
- subtype: "success",
600
- isError: false,
601
- duration_ms: 777,
602
- result: "hello",
603
- }))
604
- const results = resultEntries(events)
605
- expect(results).toHaveLength(1)
606
- expect((results[0]?.entry as { durationMs?: number }).durationMs).toBe(777)
607
- })
608
-
609
- test("late turn_duration after a synthesized flush is swallowed; next turn unaffected", () => {
610
- const parser = createJsonlEventParser()
611
- parser.parse(makeRealUser("hi"))
612
- parser.parse(makeAssistantRow({
613
- id: "msg_a",
614
- stopReason: "end_turn",
615
- block: { type: "text", text: "first" },
616
- }))
617
- expect(resultEntries(parser.parse(makeLastPrompt()))).toHaveLength(1)
618
- expect(resultEntries(parser.parse(makeTurnDuration()))).toEqual([])
619
- parser.parse(makeRealUser("again"))
620
- parser.parse(makeAssistantRow({
621
- id: "msg_b",
622
- stopReason: "end_turn",
623
- block: { type: "text", text: "second" },
624
- }))
625
- expect(resultEntries(parser.parse(makeLastPrompt()))).toHaveLength(1)
626
- })
627
-
628
- test("auto-wake under new format: wake turn's synthesized result is dropped", () => {
629
- const parser = createJsonlEventParser()
630
- parser.parse(makeRealUser("hi"))
631
- parser.parse(makeAssistantRow({
632
- id: "msg_1",
633
- stopReason: "end_turn",
634
- block: { type: "text", text: "hello" },
635
- }))
636
- expect(resultEntries(parser.parse(makeLastPrompt()))).toHaveLength(1)
637
- const wakeEvents = parser.parse(makeMetaUser("<task-notification>bash done</task-notification>"))
638
- expect(wakeEvents.filter(
639
- (e) => e.type === "transcript" && (e.entry as { kind?: string }).kind === "user_prompt",
640
- )).toEqual([])
641
- parser.parse(makeAssistantRow({
642
- id: "msg_wake",
643
- stopReason: "end_turn",
644
- block: { type: "text", text: "ack" },
645
- }))
646
- expect(resultEntries(parser.parse(makeLastPrompt()))).toEqual([])
647
- parser.parse(makeRealUser("status?"))
648
- parser.parse(makeAssistantRow({
649
- id: "msg_2",
650
- stopReason: "end_turn",
651
- block: { type: "text", text: "fine" },
652
- }))
653
- expect(resultEntries(parser.parse(makeLastPrompt()))).toHaveLength(1)
654
- })
655
-
656
- test("keep-alive channel push under new format: its turn-end result emits", () => {
657
- const parser = createJsonlEventParser()
658
- parser.parse(JSON.stringify({
659
- type: "user",
660
- isMeta: true,
661
- sessionId: "sess-sr",
662
- message: { role: "user", content: '<channel source="kanna">do the task</channel>' },
663
- }))
664
- parser.parse(makeAssistantRow({
665
- id: "msg_ch",
666
- stopReason: "end_turn",
667
- block: { type: "text", text: "DONE" },
668
- }))
669
- expect(resultEntries(parser.parse(makeLastPrompt()))).toHaveLength(1)
670
- })
671
-
672
- test("synthesized result carries usage from the final assistant message (CLI ≥2.1.x path)", () => {
673
- const parser = createJsonlEventParser()
674
- parser.parse(makeRealUser("list files"))
675
- const assistantWithUsage = JSON.stringify({
676
- type: "assistant",
677
- sessionId: "sess-sr",
678
- message: {
679
- id: "msg_usage_flush",
680
- role: "assistant",
681
- stop_reason: "end_turn",
682
- content: [{ type: "text", text: "here are the files" }],
683
- usage: {
684
- input_tokens: 1234,
685
- cache_creation_input_tokens: 100,
686
- cache_read_input_tokens: 50,
687
- output_tokens: 42,
688
- },
689
- },
690
- })
691
- parser.parse(assistantWithUsage)
692
- const flushed = parser.parse(makeLastPrompt())
693
- const results = resultEntries(flushed)
694
- expect(results).toHaveLength(1)
695
- const entry = results[0]?.entry as {
696
- kind?: string
697
- subtype?: string
698
- usage?: { inputTokens?: number; outputTokens?: number; cachedInputTokens?: number }
699
- }
700
- expect(entry.subtype).toBe("success")
701
- expect(entry.usage?.inputTokens).toBe(1384)
702
- expect(entry.usage?.outputTokens).toBe(42)
703
- expect(entry.usage?.cachedInputTokens).toBe(50)
704
- })
705
-
706
- test("synthesized result resets usage tracking; next turn starts clean", () => {
707
- const parser = createJsonlEventParser()
708
- parser.parse(makeRealUser("turn 1"))
709
- const assistantRow = JSON.stringify({
710
- type: "assistant",
711
- sessionId: "sess-sr",
712
- message: {
713
- id: "msg_t1",
714
- role: "assistant",
715
- stop_reason: "end_turn",
716
- content: [{ type: "text", text: "done" }],
717
- usage: {
718
- input_tokens: 99,
719
- cache_creation_input_tokens: 0,
720
- cache_read_input_tokens: 0,
721
- output_tokens: 11,
722
- },
723
- },
724
- })
725
- parser.parse(assistantRow)
726
- const turn1Results = resultEntries(parser.parse(makeLastPrompt()))
727
- expect(turn1Results).toHaveLength(1)
728
- const t1Entry = turn1Results[0]?.entry as { usage?: { inputTokens?: number } }
729
- expect(t1Entry.usage?.inputTokens).toBe(99)
730
-
731
- parser.parse(makeRealUser("turn 2"))
732
- const assistantNoUsage = JSON.stringify({
733
- type: "assistant",
734
- sessionId: "sess-sr",
735
- message: {
736
- id: "msg_t2",
737
- role: "assistant",
738
- stop_reason: "end_turn",
739
- content: [{ type: "text", text: "also done" }],
740
- },
741
- })
742
- parser.parse(assistantNoUsage)
743
- const turn2Results = resultEntries(parser.parse(makeLastPrompt()))
744
- expect(turn2Results).toHaveLength(1)
745
- const t2Entry = turn2Results[0]?.entry as { usage?: unknown }
746
- expect(t2Entry.usage).toBeUndefined()
747
- })
748
-
749
- test("sidechain row still triggers the pending flush (result not delayed)", () => {
750
- const parser = createJsonlEventParser()
751
- parser.parse(makeRealUser("hi"))
752
- parser.parse(makeAssistantRow({
753
- id: "msg_main",
754
- stopReason: "end_turn",
755
- block: { type: "text", text: "spawned a subagent earlier" },
756
- }))
757
- const events = parser.parse(JSON.stringify({
758
- type: "assistant",
759
- isSidechain: true,
760
- sessionId: "sub-sess",
761
- message: { id: "msg_side", role: "assistant", content: [{ type: "text", text: "side" }] },
762
- }))
763
- expect(resultEntries(events)).toHaveLength(1)
764
- })
765
- })
766
-
767
- describe("keep-alive channel-push exemption", () => {
768
- function makeChannelUser(content: string): string {
769
- return JSON.stringify({
770
- type: "user",
771
- isMeta: true,
772
- message: { role: "user", content },
773
- })
774
- }
775
- function makeChannelUserBlocks(text: string): string {
776
- return JSON.stringify({
777
- type: "user",
778
- isMeta: true,
779
- message: { role: "user", content: [{ type: "text", text }] },
780
- })
781
- }
782
- function makeAssistant(text: string): string {
783
- return JSON.stringify({
784
- type: "assistant",
785
- message: { role: "assistant", content: [{ type: "text", text }] },
786
- })
787
- }
788
- function makeMetaUser(content: string): string {
789
- return JSON.stringify({
790
- type: "user",
791
- isMeta: true,
792
- message: { role: "user", content },
793
- })
794
- }
795
- function makeTurnDuration(): string {
796
- return JSON.stringify({
797
- type: "system",
798
- subtype: "turn_duration",
799
- session_id: "main-sess",
800
- durationMs: 100,
801
- })
802
- }
803
- function resultEntries(events: HarnessEvent[]) {
804
- return events.filter(
805
- (e): e is Extract<HarnessEvent, { type: "transcript" }> =>
806
- e.type === "transcript" && (e.entry as { kind?: string }).kind === "result",
807
- )
808
- }
809
-
810
- test("turn 1: channel push at boundary is NOT an auto-wake — its result emits", () => {
811
- const parser = createJsonlEventParser()
812
- parser.parse(makeChannelUser('<channel source="kanna">do the task</channel>'))
813
- parser.parse(makeAssistant("DONE"))
814
- const events = parser.parse(makeTurnDuration())
815
- expect(resultEntries(events).length).toBeGreaterThan(0)
816
- })
817
-
818
- test("turn 2: a second channel push after turn 1 also emits its result", () => {
819
- const parser = createJsonlEventParser()
820
- parser.parse(makeChannelUser('<channel source="kanna">turn one</channel>'))
821
- parser.parse(makeAssistant("DONE A"))
822
- expect(resultEntries(parser.parse(makeTurnDuration())).length).toBeGreaterThan(0)
823
- parser.parse(makeChannelUser('<channel source="kanna">turn two</channel>'))
824
- parser.parse(makeAssistant("DONE B"))
825
- expect(resultEntries(parser.parse(makeTurnDuration())).length).toBeGreaterThan(0)
826
- })
827
-
828
- test("channel push with array content blocks is also exempted", () => {
829
- const parser = createJsonlEventParser()
830
- parser.parse(makeChannelUserBlocks('<channel source="kanna">block form</channel>'))
831
- parser.parse(makeAssistant("DONE"))
832
- expect(resultEntries(parser.parse(makeTurnDuration())).length).toBeGreaterThan(0)
833
- })
834
-
835
- test("regression: a genuine task-notification wake after a channel turn is still dropped", () => {
836
- const parser = createJsonlEventParser()
837
- parser.parse(makeChannelUser('<channel source="kanna">turn one</channel>'))
838
- parser.parse(makeAssistant("DONE"))
839
- expect(resultEntries(parser.parse(makeTurnDuration())).length).toBeGreaterThan(0)
840
- parser.parse(makeMetaUser("<task-notification>bash done</task-notification>"))
841
- parser.parse(makeAssistant("ack"))
842
- expect(resultEntries(parser.parse(makeTurnDuration()))).toEqual([])
843
- })
844
- })
845
-
846
- describe("nested_memory → memory_loaded", () => {
847
- const memoryPaths = (events: HarnessEvent[]): string[] =>
848
- events
849
- .filter((e) => e.type === "transcript")
850
- .map((e) => e.entry)
851
- .filter((entry) => entry?.kind === "memory_loaded")
852
- .map((entry) => (entry as { path: string }).path)
853
-
854
- const resultCount = (events: HarnessEvent[]): number =>
855
- events.filter((e) => e.type === "transcript" && e.entry?.kind === "result").length
856
-
857
- const nestedMemory = (path: unknown): string =>
858
- JSON.stringify({ type: "nested_memory", attachment: { type: "nested_memory", path } })
859
-
860
- test("emits one memory_loaded entry carrying the attachment path", () => {
861
- const parser = createJsonlEventParser()
862
- const paths = memoryPaths(parser.parse(nestedMemory("/repo/.claude/rules/pattern_id.md")))
863
- expect(paths).toEqual(["/repo/.claude/rules/pattern_id.md"])
864
- })
865
-
866
- test("malformed nested_memory (missing/blank/non-string path) emits nothing, does not throw", () => {
867
- const parser = createJsonlEventParser()
868
- expect(memoryPaths(parser.parse(JSON.stringify({ type: "nested_memory" })))).toEqual([])
869
- expect(memoryPaths(parser.parse(nestedMemory("")))).toEqual([])
870
- expect(memoryPaths(parser.parse(nestedMemory(42)))).toEqual([])
871
- })
872
-
873
- test("does not disturb turn-state (mid-turn nested_memory keeps the result emitting)", () => {
874
- const parser = createJsonlEventParser()
875
- parser.parse(JSON.stringify({ type: "user", message: { role: "user", content: "hi" } }))
876
- parser.parse(nestedMemory("/repo/CLAUDE.md"))
877
- parser.parse(JSON.stringify({ type: "assistant", message: { role: "assistant", content: [{ type: "text", text: "answer" }] } }))
878
- const resultLine = JSON.stringify({ type: "system", subtype: "turn_duration", duration_ms: 5 })
879
- expect(resultCount(parser.parse(resultLine))).toBeGreaterThan(0)
880
- })
881
- })
882
- })