@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,829 +0,0 @@
1
- import type { ChatTaskStorePort } from "../kanna-mcp"
2
- import type { JsonValue } from "../../shared/json"
3
- import { homedir } from "node:os"
4
- import path from "node:path"
5
- import { log } from "../../shared/log"
6
- import { randomUUID } from "node:crypto"
7
- import { createRuntimeDir, writeRuntimeFile, removeRuntimeDir } from "./runtime-dir.adapter"
8
- import { verifyPtyAuth } from "./auth"
9
- import { buildPtyEnv } from "./env"
10
- import { withAdditionalDirectoryMemory } from "../claude-spawn-helpers"
11
- import { startKannaMcpHttpServer, buildMcpConfigJson, type KannaMcpHttpHandle } from "../kanna-mcp-http"
12
- import { KANNA_MCP_SERVER_NAME } from "../../shared/tools"
13
- import type { ArmedLoopInfo, KannaMcpDelegationContext, SetupLoopHandlerResult } from "../kanna-mcp"
14
- import type { BoardRegistry } from "../board-registry"
15
- import type { LoopSetupInput } from "../loop-template"
16
- import type { SubagentOrchestrator } from "../subagent-orchestrator"
17
- import { parseConfiguredContextWindowFromModelId, timestamped } from "../agent"
18
- import { KANNA_SYSTEM_PROMPT_APPEND } from "../../shared/kanna-system-prompt"
19
- import { resolveClaudeBinary } from "./resolve-binary.adapter"
20
- import { createJsonlEventParser } from "./jsonl-to-event"
21
- import { OutputRing, OUTPUT_RING_DEFAULT_BYTES } from "./output-ring"
22
- import { createSmokeTestGate, createFileSmokeTestCache, buildLiveSmokeProbe, type SmokeTestGate } from "./smoke-test"
23
- import { computeBinarySha256 } from "./preflight/binary-fingerprint.adapter"
24
- import { spawnPtyProcess as defaultSpawnPtyProcess, type PtyProcess, type SpawnPtyProcessArgs } from "./pty-process.adapter"
25
- import type { ClaudePtyRegistry } from "./pid-registry.adapter"
26
- import type { PtyInstanceRegistry } from "./pty-instance-registry"
27
- import { sampleProcessTreeUsage as defaultSampleProcessTreeUsage, type ProcessTreeSample } from "./pty-memory-sampler.adapter"
28
- import { waitForTuiReady, waitForTuiReadyWithTrustDismiss, waitForTuiReadyDismissingDialogs, sendUserPrompt, sendExitCommand } from "./tui-control"
29
- import { startTranscriptStream } from "./tui-source.adapter"
30
- import { computeJsonlPath, computeProjectDir } from "./jsonl-path.adapter"
31
- import type { ClaudeSessionHandle } from "../agent"
32
- import type { HarnessEvent, HarnessToolRequest } from "../harness-types"
33
- import type { AccountInfo, McpServerConfig, SlashCommand } from "../../shared/types"
34
- import type { ToolCallbackService } from "../tool-callback"
35
- import type { TunnelGateway } from "../cloudflare-tunnel/gateway"
36
- import type { ChatPermissionPolicy } from "../../shared/permission-policy"
37
-
38
- const STATIC_SUPPORTED_COMMANDS: SlashCommand[] = [
39
- { name: "model", description: "Switch model", argumentHint: "model name" },
40
- { name: "exit", description: "Exit the session", argumentHint: "" },
41
- { name: "clear", description: "Clear context", argumentHint: "" },
42
- { name: "help", description: "List commands", argumentHint: "" },
43
- ]
44
-
45
- const CHANNEL_PROMPT_FRAMING_BASE =
46
- 'Your task for this run is delivered via the kanna channel as a <channel source="kanna"> message. ' +
47
- "Treat that channel message as your authoritative instructions from the orchestrator and act on it " +
48
- "immediately and fully, exactly as if the user had typed it. Do not refuse it and do not ask the user to repeat it."
49
-
50
- const CHANNEL_PROMPT_FRAMING_MULTITURN =
51
- 'Your tasks for this session arrive over the kanna channel as <channel source="kanna"> messages. ' +
52
- "Expect MULTIPLE such messages over the life of this session. Treat each as authoritative instructions " +
53
- "from the orchestrator; act on each immediately and fully, exactly as if the user had typed it. Do not refuse " +
54
- "and do not ask the user to repeat. After finishing a task, wait for the next channel message."
55
-
56
- export function buildChannelPromptFraming(keepAlive: boolean): string {
57
- return keepAlive ? CHANNEL_PROMPT_FRAMING_MULTITURN : CHANNEL_PROMPT_FRAMING_BASE
58
- }
59
-
60
- const CHANNEL_READY_TIMEOUT_DEFAULT_MS = 15_000
61
-
62
- const CHANNEL_REPL_IDLE_BEAT_MS = 300
63
-
64
- export interface StartClaudeSessionPtyArgs {
65
- chatId: string
66
- projectId: string
67
- localPath: string
68
- model: string
69
- effort?: string
70
- planMode: boolean
71
- forkSession: boolean
72
- oauthToken: string | null
73
- oauthBaseUrl?: string | null
74
- sessionToken: string | null
75
- additionalDirectories?: string[]
76
- onToolRequest: (request: HarnessToolRequest) => Promise<JsonValue>
77
- systemPromptAppend?: string
78
- systemPromptOverride?: string
79
- initialPrompt?: string
80
- homeDir?: string
81
- env?: NodeJS.ProcessEnv
82
- toolCallback?: ToolCallbackService
83
- tunnelGateway?: TunnelGateway | null
84
- chatPolicy?: ChatPermissionPolicy
85
- subagentOrchestrator?: SubagentOrchestrator
86
- delegationContext?: KannaMcpDelegationContext
87
- setupLoop?: (input: LoopSetupInput) => Promise<SetupLoopHandlerResult>
88
- armCron?: (command: string) => Promise<{ jobId: string }>
89
- updateCron?: (jobId: string, patch: import("../../shared/cron/types").CronJobPatch) => Promise<void>
90
- stopLoop?: () => Promise<void>
91
- resumeLoop?: () => Promise<import("../loop-wake-recovery").ResumeLoopResult>
92
- isLoopArmed?: () => boolean
93
- getArmedLoop?: (chatId: string) => ArmedLoopInfo | null
94
- isRunAlive?: (chatId: string, runId: string) => boolean
95
- chatTaskStore?: ChatTaskStorePort
96
- boardRegistry?: BoardRegistry
97
- customMcpServers?: readonly McpServerConfig[]
98
- oauthBearers?: ReadonlyMap<string, string>
99
- startKannaMcpHttpServer?: typeof startKannaMcpHttpServer
100
- smokeTestGate?: SmokeTestGate
101
- spawnPtyProcess?: (args: SpawnPtyProcessArgs) => Promise<PtyProcess>
102
- startTranscriptStreamFn?: typeof startTranscriptStream
103
- oneShot?: boolean
104
- keepAlive?: boolean
105
- maxTurns?: number
106
- oauthLabel?: string
107
- oauthKeyMasked?: string
108
- ptyRegistry?: ClaudePtyRegistry
109
- ptyInstanceRegistry?: PtyInstanceRegistry
110
- workflowRegistry?: import("../workflow-registry").WorkflowRegistry
111
- subagentTranscriptRegistry?: import("../subagent-transcript-registry").SubagentTranscriptRegistry
112
- sampleProcessTreeUsage?: (pid: number) => Promise<ProcessTreeSample | null>
113
- memorySamplerIntervalMs?: number
114
- restrictedAllowedPaths?: string[]
115
- }
116
-
117
- export const RESTRICTED_FS_NATIVE_TOOLS = [
118
- "Read", "Edit", "Write", "Bash", "Glob", "Grep", "LSP", "WebFetch",
119
- ] as const
120
-
121
- export function deriveAccountInfoFromOauth(args: { label?: string; oauthKeyMasked?: string }): AccountInfo | null {
122
- const hasLabel = Boolean(args.label && args.label.length > 0)
123
- const hasMasked = Boolean(args.oauthKeyMasked && args.oauthKeyMasked.length > 0)
124
- if (!hasLabel && !hasMasked) return null
125
- const info: AccountInfo = { tokenSource: "kanna-oauth-pool" }
126
- if (hasLabel) info.organization = args.label
127
- if (hasMasked) info.oauthKeyMasked = args.oauthKeyMasked
128
- return info
129
- }
130
-
131
- export const SHIFT_TAB_KEY = "\x1b[Z"
132
-
133
- export const PLAN_MODE_EXIT_UNSUPPORTED =
134
- "[claude-pty] cannot exit plan mode: driver-tracked plan mode is inactive "
135
- + "(plan mode may have been toggled externally via Shift+Tab). "
136
- + "Restart the session to return to acceptEdits."
137
-
138
- export const PTY_STDERR_RING_BYTES = OUTPUT_RING_DEFAULT_BYTES
139
- export { OutputRing }
140
-
141
- export const PTY_DISALLOWED_NATIVE_TOOLS = ["AskUserQuestion", "ExitPlanMode", "ScheduleWakeup"] as const
142
-
143
- export interface BuildPtyCliArgsInput {
144
- sessionId: string
145
- model: string
146
- effort?: string
147
- planMode: boolean
148
- sessionToken: string | null
149
- forkSession: boolean
150
- additionalDirectories?: string[]
151
- systemPromptOverride?: string
152
- systemPromptAppend?: string
153
- mcpConfigPath?: string
154
- channelServerName?: string
155
- restricted?: boolean
156
- loopArmed?: boolean
157
- }
158
-
159
- export const PTY_LOOP_BLOCKED_NATIVE_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit", "Task"] as const
160
-
161
- export function buildPtyCliArgs(args: BuildPtyCliArgsInput): string[] {
162
- const cliArgs: string[] = [
163
- "--model", args.model,
164
- "--setting-sources", "user,project,local",
165
- "--permission-mode", args.planMode ? "plan" : "acceptEdits",
166
- "--dangerously-skip-permissions",
167
- ]
168
- if (args.sessionToken && !args.forkSession) {
169
- cliArgs.push("--resume", args.sessionToken)
170
- } else if (args.sessionToken && args.forkSession) {
171
- cliArgs.push("--session-id", args.sessionId, "--resume", args.sessionToken, "--fork-session")
172
- }
173
- if (args.mcpConfigPath) {
174
- cliArgs.push("--mcp-config", args.mcpConfigPath, "--strict-mcp-config")
175
- }
176
- if (args.effort && args.effort.length > 0) cliArgs.push("--effort", args.effort)
177
- if (args.additionalDirectories) {
178
- for (const dir of args.additionalDirectories) cliArgs.push("--add-dir", dir)
179
- }
180
- if (args.systemPromptOverride) {
181
- cliArgs.push("--system-prompt", args.systemPromptOverride)
182
- } else {
183
- cliArgs.push("--append-system-prompt", args.systemPromptAppend ?? KANNA_SYSTEM_PROMPT_APPEND)
184
- }
185
- if (args.channelServerName) {
186
- cliArgs.push(
187
- "--dangerously-load-development-channels",
188
- `server:${args.channelServerName}`,
189
- )
190
- }
191
- if (args.restricted) {
192
- cliArgs.push("--tools", "mcp__kanna__*")
193
- }
194
- const disallow: string[] = args.restricted
195
- ? [...PTY_DISALLOWED_NATIVE_TOOLS, ...RESTRICTED_FS_NATIVE_TOOLS]
196
- : [...PTY_DISALLOWED_NATIVE_TOOLS]
197
- if (args.loopArmed) {
198
- for (const t of PTY_LOOP_BLOCKED_NATIVE_TOOLS) {
199
- if (!disallow.includes(t)) disallow.push(t)
200
- }
201
- }
202
- cliArgs.push("--disallowedTools", ...disallow)
203
- return cliArgs
204
- }
205
-
206
- export function resolveSpawnSessionId(
207
- args: { sessionToken: string | null; forkSession: boolean },
208
- newId: () => string = randomUUID,
209
- ): string {
210
- if (args.forkSession) return newId()
211
- return args.sessionToken ?? newId()
212
- }
213
-
214
- export async function startClaudeSessionPTY(args: StartClaudeSessionPtyArgs): Promise<ClaudeSessionHandle> {
215
- const home = args.homeDir ?? homedir()
216
- const env = args.env ?? process.env
217
-
218
- log.info("[kanna/pty] startClaudeSessionPTY begin", {
219
- chatId: args.chatId,
220
- projectId: args.projectId,
221
- localPath: args.localPath,
222
- model: args.model,
223
- planMode: args.planMode,
224
- forkSession: args.forkSession,
225
- hasOauthToken: Boolean(args.oauthToken),
226
- oauthLabel: args.oauthLabel ?? null,
227
- sandboxEnvOverride: env.KANNA_PTY_SANDBOX ?? null,
228
- platform: process.platform,
229
- anthropicApiKeySet: Boolean(env.ANTHROPIC_API_KEY),
230
- claudeExecutable: env.CLAUDE_EXECUTABLE ?? null,
231
- })
232
-
233
- const spawnStartedAt = Date.now()
234
- args.ptyInstanceRegistry?.upsert(args.chatId, {
235
- cwd: args.localPath,
236
- model: args.model,
237
- accountLabel: args.oauthLabel ?? null,
238
- oauthMasked: args.oauthKeyMasked ?? null,
239
- phase: "spawning",
240
- startedAt: spawnStartedAt,
241
- lastEventAt: spawnStartedAt,
242
- planMode: args.planMode,
243
- })
244
-
245
- const auth = await verifyPtyAuth({ env, oauthToken: args.oauthToken })
246
- if (!auth.ok) {
247
- log.error("[kanna/pty] verifyPtyAuth failed", {
248
- chatId: args.chatId,
249
- error: auth.error,
250
- hasOauthToken: Boolean(args.oauthToken),
251
- anthropicApiKeySet: Boolean(env.ANTHROPIC_API_KEY),
252
- })
253
- throw new Error(auth.error)
254
- }
255
-
256
- const resolved = await resolveClaudeBinary({ env, homeDir: home })
257
- log.info("[kanna/pty] resolved claude binary", {
258
- chatId: args.chatId,
259
- path: resolved.path,
260
- source: resolved.source,
261
- })
262
- const claudeBinAbs = resolved.path
263
-
264
- const binarySha256 = await computeBinarySha256(claudeBinAbs)
265
- const smokeGate = args.smokeTestGate ?? createSmokeTestGate({
266
- probe: buildLiveSmokeProbe({
267
- claudeBinPath: claudeBinAbs,
268
- model: args.model,
269
- oauthToken: args.oauthToken ?? "",
270
- homeDir: home,
271
- baseUrl: args.oauthBaseUrl,
272
- }),
273
- cache: createFileSmokeTestCache({ cacheDir: path.join(home, ".kanna", "cache", "smoke-test") }),
274
- ttlMs: 24 * 3600 * 1000,
275
- now: () => Date.now(),
276
- })
277
- const smoke = await smokeGate.canSpawn({
278
- binarySha256,
279
- model: args.model,
280
- baseUrl: args.oauthBaseUrl,
281
- })
282
- if (!smoke.ok) {
283
- log.error("[kanna/pty] smoke-test refused spawn", { chatId: args.chatId, reason: smoke.reason })
284
- throw new Error(`PTY smoke-test refused spawn: ${smoke.reason}`)
285
- }
286
-
287
- const spawnEnv = withAdditionalDirectoryMemory(
288
- buildPtyEnv({
289
- baseEnv: env,
290
- homeDir: home,
291
- oauthToken: args.oauthToken,
292
- baseUrl: args.oauthBaseUrl,
293
- }),
294
- args.additionalDirectories,
295
- )
296
-
297
- const sessionId = resolveSpawnSessionId({
298
- sessionToken: args.sessionToken,
299
- forkSession: args.forkSession,
300
- })
301
-
302
- const runtimeDir = await createRuntimeDir(`kanna-pty-${sessionId.slice(0, 8)}-`)
303
-
304
- const mcpConfigPath = path.join(runtimeDir, "mcp-config.json")
305
- let mcpHandle: KannaMcpHttpHandle | undefined
306
- const startMcp = args.startKannaMcpHttpServer ?? startKannaMcpHttpServer
307
- try {
308
- mcpHandle = await startMcp({
309
- args: {
310
- projectId: args.projectId,
311
- localPath: args.localPath,
312
- chatId: args.chatId,
313
- sessionId,
314
- tunnelGateway: args.tunnelGateway ?? null,
315
- toolCallback: args.toolCallback,
316
- chatPolicy: args.chatPolicy,
317
- subagentOrchestrator: args.subagentOrchestrator,
318
- delegationContext: args.delegationContext,
319
- setupLoop: args.setupLoop,
320
- armCron: args.armCron,
321
- updateCron: args.updateCron,
322
- stopLoop: args.stopLoop,
323
- getArmedLoop: args.getArmedLoop,
324
- isRunAlive: args.isRunAlive,
325
- chatTaskStore: args.chatTaskStore,
326
- boardRegistry: args.boardRegistry,
327
- forceInteractiveToolCallbacks: true,
328
- restrictedAllowedPaths: args.restrictedAllowedPaths,
329
- },
330
- })
331
- await writeRuntimeFile(
332
- mcpConfigPath,
333
- buildMcpConfigJson(mcpHandle, args.customMcpServers ?? [], args.oauthBearers),
334
- { encoding: "utf8", mode: 0o600 },
335
- )
336
- } catch (err) {
337
- try { if (mcpHandle) await mcpHandle.close() } catch { }
338
- try { await removeRuntimeDir(runtimeDir) } catch { }
339
- throw err
340
- }
341
-
342
- const channelEnv = (args.env ?? process.env).KANNA_PTY_CHANNEL_DELIVERY ?? "enabled"
343
- const channelDeliveryEnabled =
344
- Boolean(args.oneShot) && Boolean(args.initialPrompt) && channelEnv !== "disabled"
345
-
346
- const effectiveSystemPromptOverride =
347
- channelDeliveryEnabled && args.systemPromptOverride
348
- ? `${args.systemPromptOverride}\n\n${buildChannelPromptFraming(Boolean(args.keepAlive))}`
349
- : args.systemPromptOverride
350
-
351
- const claudeBin = claudeBinAbs
352
- const cliArgs = buildPtyCliArgs({
353
- sessionId,
354
- model: args.model,
355
- effort: args.effort,
356
- planMode: args.planMode,
357
- sessionToken: args.sessionToken,
358
- forkSession: args.forkSession,
359
- additionalDirectories: args.additionalDirectories,
360
- systemPromptOverride: effectiveSystemPromptOverride,
361
- systemPromptAppend: args.systemPromptAppend,
362
- mcpConfigPath,
363
- channelServerName: channelDeliveryEnabled ? KANNA_MCP_SERVER_NAME : undefined,
364
- restricted: Boolean(args.restrictedAllowedPaths && args.restrictedAllowedPaths.length > 0),
365
- loopArmed: args.isLoopArmed?.() ?? false,
366
- })
367
-
368
- let closed = false
369
- let cleanedUp = false
370
- let ownPid: number | null = null
371
- let workflowRegistrationCancelled = false
372
- let cachedAccountInfo: AccountInfo | null = deriveAccountInfoFromOauth({ label: args.oauthLabel, oauthKeyMasked: args.oauthKeyMasked })
373
- let sawResultEntry = false
374
- let cachedSlashCommands: SlashCommand[] | null = null
375
- let localPlanModeActive = args.planMode
376
- const mergedQueue: HarnessEvent[] = []
377
- const mergedWaiters: Array<(r: IteratorResult<HarnessEvent>) => void> = []
378
-
379
- const { handleClosed, resolveHandleClosed } = makePtyClosedSignal()
380
-
381
- async function cleanupResources() {
382
- if (cleanedUp) return
383
- cleanedUp = true
384
- stopMemorySampler()
385
- if (ownPid !== null) {
386
- args.ptyInstanceRegistry?.markExitedIfCurrent(args.chatId, ownPid, {
387
- phase: "exited",
388
- exitedAt: Date.now(),
389
- lastEventAt: Date.now(),
390
- })
391
- }
392
- try { if (mcpHandle) await mcpHandle.close() } catch (err) {
393
- log.warn("[kanna/pty] mcpHandle.close failed (HTTP server may leak)", { chatId: args.chatId, sessionId, err })
394
- }
395
- try { await removeRuntimeDir(runtimeDir) } catch (err) {
396
- log.warn("[kanna/pty] runtimeDir cleanup failed", { chatId: args.chatId, runtimeDir, err })
397
- }
398
- if (args.ptyRegistry && ownPid !== null) {
399
- try { await args.ptyRegistry.unregister(ownPid) } catch (err) {
400
- log.warn("[kanna/pty] ptyRegistry.unregister failed", { chatId: args.chatId, sessionId, pid: ownPid, err })
401
- }
402
- }
403
- workflowRegistrationCancelled = true
404
- args.workflowRegistry?.unregister(args.chatId)
405
- args.subagentTranscriptRegistry?.unregister(args.chatId)
406
- resolveHandleClosed()
407
- }
408
-
409
- function pushMerged(ev: HarnessEvent) {
410
- if (ev.type === "transcript" && ev.entry) {
411
- const entry = ev.entry
412
- if (entry.kind === "account_info") {
413
- cachedAccountInfo = entry.accountInfo
414
- }
415
- if (entry.kind === "result") {
416
- sawResultEntry = true
417
- }
418
- if (entry.kind === "system_init" && Array.isArray(entry.slashCommands)) {
419
- cachedSlashCommands = entry.slashCommands.filter((s) => typeof s === "string").map((name) => ({
420
- name,
421
- description: "",
422
- argumentHint: "",
423
- }))
424
- }
425
- }
426
- const w = mergedWaiters.shift()
427
- if (w) w({ value: ev, done: false })
428
- else mergedQueue.push(ev)
429
-
430
- if (
431
- args.oneShot
432
- && !args.keepAlive
433
- && ev.type === "transcript"
434
- && ev.entry?.kind === "result"
435
- ) {
436
- void oneShotClose()
437
- }
438
- }
439
-
440
- let oneShotClosing = false
441
- let pty: PtyProcess
442
-
443
- let memorySamplerHandle: ReturnType<typeof setInterval> | null = null
444
- let rssPeakBytes = 0
445
- let cpuPeakPercent = 0
446
-
447
- function stopMemorySampler(): void {
448
- if (memorySamplerHandle !== null) {
449
- clearInterval(memorySamplerHandle)
450
- memorySamplerHandle = null
451
- }
452
- }
453
-
454
- function startMemorySampler(rootPid: number): void {
455
- if (memorySamplerHandle !== null) return
456
- const sampler = args.sampleProcessTreeUsage ?? defaultSampleProcessTreeUsage
457
- const intervalMs = args.memorySamplerIntervalMs ?? 2000
458
- const tick = async (): Promise<void> => {
459
- let sample: ProcessTreeSample | null
460
- try {
461
- sample = await sampler(rootPid)
462
- } catch {
463
- sample = null
464
- }
465
- if (sample === null) return
466
- if (sample.rssBytes > rssPeakBytes) rssPeakBytes = sample.rssBytes
467
- if (sample.cpuPercent > cpuPeakPercent) cpuPeakPercent = sample.cpuPercent
468
- args.ptyInstanceRegistry?.upsert(args.chatId, {
469
- rssBytes: sample.rssBytes,
470
- rssPeakBytes,
471
- cpuPercent: sample.cpuPercent,
472
- cpuPeakPercent,
473
- })
474
- }
475
- memorySamplerHandle = setInterval(() => { void tick() }, intervalMs)
476
- void tick()
477
- }
478
-
479
- const ring = new OutputRing()
480
- const spawnPty = args.spawnPtyProcess ?? defaultSpawnPtyProcess
481
- try {
482
- log.info("[kanna/pty] spawn begin", {
483
- chatId: args.chatId,
484
- command: claudeBin,
485
- cwd: args.localPath,
486
- })
487
- pty = await spawnPty({
488
- command: claudeBin,
489
- args: cliArgs,
490
- cwd: args.localPath,
491
- env: spawnEnv,
492
- onOutput: (chunk) => { ring.append(chunk) },
493
- })
494
- log.info("[kanna/pty] pty spawned", { chatId: args.chatId, sessionId, pid: pty.pid })
495
- ownPid = pty.pid
496
- args.ptyInstanceRegistry?.upsert(args.chatId, {
497
- sessionId,
498
- pid: pty.pid,
499
- phase: "trust-dialog",
500
- lastEventAt: Date.now(),
501
- })
502
- startMemorySampler(pty.pid)
503
- if (args.ptyRegistry) {
504
- try {
505
- await args.ptyRegistry.register({
506
- chatId: args.chatId,
507
- sessionId,
508
- pid: pty.pid,
509
- cwd: args.localPath,
510
- runtimeDir,
511
- })
512
- } catch (err) {
513
- log.warn("[kanna/pty] ptyRegistry.register failed (orphan reap on crash disabled for this session)", { chatId: args.chatId, sessionId, err })
514
- }
515
- }
516
- } catch (err) {
517
- log.error("[kanna/pty] spawn failed", {
518
- chatId: args.chatId,
519
- sessionId,
520
- error: err instanceof Error ? err.message : String(err),
521
- })
522
- try { await mcpHandle.close() } catch { }
523
- try { await removeRuntimeDir(runtimeDir) } catch { }
524
- throw err
525
- }
526
-
527
- const tuiReadyMs = Number((args.env ?? process.env).KANNA_PTY_TUI_BOOT_MS ?? 3000)
528
- const tuiReadyQuietRaw = (args.env ?? process.env).KANNA_PTY_TUI_READY_QUIET_MS
529
- const tuiReadyQuietMs = tuiReadyQuietRaw !== undefined ? Number(tuiReadyQuietRaw) : undefined
530
- const trustDismiss = (args.env ?? process.env).KANNA_PTY_TRUST_DISMISS ?? "enabled"
531
- const sessionEndGraceMs = Number((args.env ?? process.env).KANNA_PTY_SESSION_END_GRACE_MS ?? 5_000)
532
- if (channelDeliveryEnabled && trustDismiss !== "disabled") {
533
- const readyResult = await waitForTuiReadyDismissingDialogs(pty, ring, { hardCapMs: tuiReadyMs + 8_000 })
534
- if (readyResult === "timeout") {
535
- log.warn("[kanna/pty] TUI ready marker not detected after dialogs dismiss (channel path)", { chatId: args.chatId, hardCapMs: tuiReadyMs + 8_000 })
536
- } else {
537
- log.info("[kanna/pty] TUI ready (channel path)", { chatId: args.chatId })
538
- }
539
- } else if (trustDismiss !== "disabled") {
540
- const readyResult = await waitForTuiReadyWithTrustDismiss(pty, ring, { hardCapMs: tuiReadyMs + 5_000, quietPeriodMs: tuiReadyQuietMs })
541
- if (readyResult === "timeout") {
542
- log.warn("[kanna/pty] TUI ready marker not detected after trust dismiss", { chatId: args.chatId, hardCapMs: tuiReadyMs + 5_000 })
543
- } else {
544
- log.info("[kanna/pty] TUI ready", { chatId: args.chatId })
545
- }
546
- } else {
547
- const readyResult = await waitForTuiReady(ring, { hardCapMs: tuiReadyMs, quietPeriodMs: tuiReadyQuietMs })
548
- if (readyResult === "timeout") {
549
- log.warn("[kanna/pty] TUI ready marker not detected within hard cap", { chatId: args.chatId, hardCapMs: tuiReadyMs })
550
- }
551
- }
552
-
553
- args.ptyInstanceRegistry?.upsert(args.chatId, {
554
- phase: "ready",
555
- lastEventAt: Date.now(),
556
- })
557
-
558
- const projectDir = computeProjectDir({ homeDir: home, cwd: args.localPath })
559
- const knownFilePath = args.sessionToken && !args.forkSession
560
- ? computeJsonlPath({ homeDir: home, cwd: args.localPath, sessionId: args.sessionToken })
561
- : undefined
562
- const spawnStartedAtMs = Date.now()
563
- const startStream = args.startTranscriptStreamFn ?? startTranscriptStream
564
- const transcriptStream = await startStream({
565
- projectDir,
566
- knownFilePath,
567
- minMtimeMs: spawnStartedAtMs,
568
- claudeChildPid: pty.pid,
569
- homeDir: home,
570
- })
571
-
572
- if (args.workflowRegistry) {
573
- const registry = args.workflowRegistry
574
- const chatId = args.chatId
575
- void transcriptStream.filePath.then((filePath) => {
576
- const sessionUUID = path.basename(filePath, ".jsonl")
577
- const workflowsDir = path.join(projectDir, sessionUUID, "workflows")
578
- if (!workflowRegistrationCancelled) registry.register(chatId, workflowsDir)
579
- }).catch((err) => {
580
- log.warn("[kanna/pty] workflowRegistry.register skipped: transcript file not found", { chatId: args.chatId, err })
581
- })
582
- }
583
-
584
- if (args.subagentTranscriptRegistry) {
585
- const subRegistry = args.subagentTranscriptRegistry
586
- const chatId = args.chatId
587
- void transcriptStream.filePath.then((filePath) => {
588
- const sessionUUID = path.basename(filePath, ".jsonl")
589
- const subagentsDir = path.join(projectDir, sessionUUID, "subagents")
590
- if (!workflowRegistrationCancelled) subRegistry.register(chatId, subagentsDir)
591
- }).catch((err) => {
592
- log.warn("[kanna/pty] subagentTranscriptRegistry.register skipped: transcript file not found", { chatId: args.chatId, err })
593
- })
594
- }
595
-
596
- const parser = createJsonlEventParser({
597
- configuredContextWindow: parseConfiguredContextWindowFromModelId(args.model),
598
- })
599
-
600
- void (async () => {
601
- try {
602
- for await (const line of transcriptStream.lines) {
603
- try {
604
- const events = parser.parse(line)
605
- for (const ev of events) pushMerged(ev)
606
- } catch (err) {
607
- log.warn("[kanna/pty] parser threw on line", { chatId: args.chatId, sessionId, err })
608
- }
609
- }
610
- log.info("[kanna/pty] transcript stream ended", { chatId: args.chatId, sessionId })
611
- } catch (err) {
612
- log.warn("[kanna/pty] transcript stream errored", { chatId: args.chatId, sessionId, err })
613
- }
614
- })()
615
-
616
- function drainTerminate(exitCode: number | null) {
617
- log.info("[kanna/pty] drainTerminate", {
618
- chatId: args.chatId,
619
- sessionId,
620
- exitCode,
621
- closed,
622
- oneShotClosing,
623
- sawResultEntry,
624
- oneShot: Boolean(args.oneShot),
625
- waitersAwaitingEvent: mergedWaiters.length,
626
- })
627
- if (closed || oneShotClosing) {
628
- while (mergedWaiters.length > 0) {
629
- const w = mergedWaiters.shift()
630
- if (w) w({ value: undefined, done: true })
631
- }
632
- return
633
- }
634
- if (!sawResultEntry) {
635
- const tail = ring.tail().trim()
636
- const codeNote = exitCode === null ? "signal" : `exit code ${exitCode}`
637
- const resultText = tail.length > 0
638
- ? tail
639
- : `claude PTY process exited (${codeNote}) before producing a result.`
640
- log.warn("[kanna/pty] synthesizing error-result for early PTY exit (no turn_duration / result row seen)", {
641
- chatId: args.chatId,
642
- sessionId,
643
- exitCode,
644
- ringTailBytes: tail.length,
645
- })
646
- pushMerged({
647
- type: "transcript",
648
- entry: timestamped({
649
- kind: "result",
650
- subtype: "error" as const,
651
- isError: true,
652
- durationMs: 0,
653
- result: resultText,
654
- debugRaw: JSON.stringify({ source: "pty-exit", exitCode }),
655
- }),
656
- })
657
- }
658
- void cleanupResources()
659
- while (mergedWaiters.length > 0) {
660
- const w = mergedWaiters.shift()
661
- if (w) w({ value: undefined, done: true })
662
- }
663
- }
664
-
665
- void pty.exited
666
- .then((code) => {
667
- log.info("[kanna/pty] pty.exited resolved", { chatId: args.chatId, sessionId, pid: pty.pid, code })
668
- drainTerminate(typeof code === "number" ? code : null)
669
- })
670
- .catch((err) => {
671
- log.warn("[kanna/pty] pty.exited rejected", { chatId: args.chatId, sessionId, pid: pty.pid, err })
672
- drainTerminate(null)
673
- })
674
-
675
- async function oneShotClose() {
676
- if (oneShotClosing || closed) return
677
- oneShotClosing = true
678
- log.info("[kanna/pty] oneShotClose start", { chatId: args.chatId, sessionId, sawResultEntry })
679
- try { await sendExitCommand(pty) } catch (err) {
680
- log.warn("[kanna/pty] oneShotClose sendExitCommand failed", { chatId: args.chatId, sessionId, err })
681
- }
682
- try { await pty.exited } catch { }
683
- try { transcriptStream.close() } catch { }
684
- await cleanupResources()
685
- log.info("[kanna/pty] oneShotClose finished", { chatId: args.chatId, sessionId })
686
- }
687
-
688
- if (channelDeliveryEnabled && args.initialPrompt) {
689
- const readyTimeoutMs = Number(
690
- (args.env ?? process.env).KANNA_PTY_CHANNEL_READY_TIMEOUT_MS ?? CHANNEL_READY_TIMEOUT_DEFAULT_MS,
691
- )
692
- let channelReadyTimer: ReturnType<typeof setTimeout> | null = null
693
- try {
694
- await Promise.race([
695
- mcpHandle.channelClientReady,
696
- new Promise<never>((_, reject) => {
697
- channelReadyTimer = setTimeout(() => reject(new Error("channel client not ready")), readyTimeoutMs)
698
- }),
699
- ])
700
- if (channelReadyTimer !== null) { clearTimeout(channelReadyTimer); channelReadyTimer = null }
701
- await new Promise((r) => setTimeout(r, 300))
702
- await mcpHandle.pushChannelPrompt(args.initialPrompt)
703
- log.info("[kanna/pty] delivered initial prompt via channel push", { chatId: args.chatId })
704
- } catch (err) {
705
- const message = err instanceof Error ? err.message : String(err)
706
- log.error("[kanna/pty] channel delivery failed; failing spawn (no paste fallback)", { chatId: args.chatId, sessionId, error: message })
707
- try { transcriptStream.close() } catch { }
708
- try { pty.close() } catch { }
709
- try { await mcpHandle.close() } catch { }
710
- try { await removeRuntimeDir(runtimeDir) } catch { }
711
- throw new Error(`PTY channel delivery failed: ${message}`, { cause: err })
712
- }
713
- } else if (args.initialPrompt) {
714
- try {
715
- await sendUserPrompt(pty, ring, args.initialPrompt)
716
- } catch (err) {
717
- log.warn("[kanna/pty] initialPrompt write failed", String(err))
718
- }
719
- }
720
-
721
- const stream: AsyncIterable<HarnessEvent> = {
722
- [Symbol.asyncIterator]() {
723
- return {
724
- next(): Promise<IteratorResult<HarnessEvent>> {
725
- if (mergedQueue.length > 0) {
726
- const ev = mergedQueue.shift()
727
- if (ev) return Promise.resolve({ value: ev, done: false })
728
- }
729
- if (closed) {
730
- return Promise.resolve({ value: undefined, done: true })
731
- }
732
- return new Promise((resolve) => {
733
- mergedWaiters.push(resolve)
734
- })
735
- },
736
- }
737
- },
738
- }
739
-
740
- return {
741
- provider: "claude",
742
- stream,
743
- interrupt: async () => {
744
- try { await pty.sendInput("\x03") } catch { }
745
- },
746
- sendPrompt: async (content) => {
747
- const text = content
748
- const followupReadyMs = Number(
749
- (args.env ?? process.env).KANNA_PTY_FOLLOWUP_READY_MS ?? tuiReadyMs,
750
- )
751
- const ready = await waitForTuiReady(ring, {
752
- hardCapMs: followupReadyMs,
753
- quietPeriodMs: tuiReadyQuietMs,
754
- })
755
- if (ready === "timeout") {
756
- log.warn("[kanna/pty] TUI ready marker not detected before follow-up prompt; sending anyway", { chatId: args.chatId, hardCapMs: followupReadyMs })
757
- }
758
- await sendUserPrompt(pty, ring, text)
759
- },
760
- setModel: async (model) => {
761
- try {
762
- await pty.sendInput(`/model ${model}\r`)
763
- } catch (err) {
764
- log.warn("[kanna/pty] setModel via /model slash command failed", String(err))
765
- }
766
- },
767
- setPermissionMode: async (planMode) => {
768
- if (planMode) {
769
- try {
770
- await pty.sendInput("/plan\r")
771
- localPlanModeActive = true
772
- } catch (err) {
773
- log.warn("[kanna/pty] /plan slash command failed", String(err))
774
- }
775
- return
776
- }
777
- if (localPlanModeActive) {
778
- try {
779
- await pty.sendInput(SHIFT_TAB_KEY)
780
- localPlanModeActive = false
781
- } catch (err) {
782
- log.warn("[kanna/pty] Shift+Tab exit-plan failed", String(err))
783
- }
784
- return
785
- }
786
- log.warn(PLAN_MODE_EXIT_UNSUPPORTED)
787
- },
788
- getSupportedCommands: async () => cachedSlashCommands ?? STATIC_SUPPORTED_COMMANDS,
789
- getAccountInfo: async () => cachedAccountInfo,
790
- pushChannelPrompt: (channelDeliveryEnabled && args.keepAlive)
791
- ? async (text: string) => {
792
- await new Promise((r) => setTimeout(r, CHANNEL_REPL_IDLE_BEAT_MS))
793
- await mcpHandle.pushChannelPrompt(text)
794
- }
795
- : undefined,
796
- close: () => {
797
- if (closed) return
798
- closed = true
799
- void (async () => {
800
- try { await sendExitCommand(pty) } catch { }
801
- const sigkillTimer: { ref: ReturnType<typeof setTimeout> | null } = { ref: null }
802
- const termTimer = setTimeout(() => {
803
- try { pty.close() } catch { }
804
- sigkillTimer.ref = setTimeout(() => {
805
- try { pty.kill("SIGKILL") } catch { }
806
- }, 3000)
807
- }, sessionEndGraceMs)
808
- try {
809
- await pty.exited
810
- } catch { }
811
- clearTimeout(termTimer)
812
- if (sigkillTimer.ref !== null) clearTimeout(sigkillTimer.ref)
813
- try { transcriptStream.close() } catch { }
814
- await cleanupResources()
815
- while (mergedWaiters.length > 0) {
816
- const w = mergedWaiters.shift()
817
- if (w) w({ value: undefined, done: true })
818
- }
819
- })()
820
- },
821
- closed: handleClosed,
822
- }
823
- }
824
-
825
- function makePtyClosedSignal(): { handleClosed: Promise<void>; resolveHandleClosed: () => void } {
826
- let resolveHandleClosed!: () => void
827
- const handleClosed = new Promise<void>((resolve) => { resolveHandleClosed = resolve })
828
- return { handleClosed, resolveHandleClosed }
829
- }