@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.
- package/README.md +5 -9
- package/dist/client/assets/{BoardsPage-BJalB-bc.js → BoardsPage-AckCuswi.js} +1 -1
- package/dist/client/assets/BoardsRoutePage-DzKwKy3f.js +1 -0
- package/dist/client/assets/ChatPage-DsaH8CTz.js +93 -0
- package/dist/client/assets/{CronJobRow-CPbO_jwU.js → CronJobRow-Bx9gRqKW.js} +1 -1
- package/dist/client/assets/{CronJobsPage-Ck3SNgGi.js → CronJobsPage-v0-2X1oX.js} +1 -1
- package/dist/client/assets/{FilePreviewSheet-XEuUtDPn.js → FilePreviewSheet-B6SDxSD0.js} +2 -2
- package/dist/client/assets/{FlowSurface-BCbeMTJe.js → FlowSurface-CLC-ZFar.js} +1 -1
- package/dist/client/assets/{GenUIBlock-BJy_dfs-.js → GenUIBlock-CC79Ujro.js} +2 -2
- package/dist/client/assets/{KannaSocketProvider-D7Lzlt35.js → KannaSocketProvider-B2sHsWP5.js} +1 -1
- package/dist/client/assets/{LocalProjectsPage-9LJkQjhT.js → LocalProjectsPage-DYBkcEAm.js} +1 -1
- package/dist/client/assets/SettingsPage-CZqMkMRW.js +9 -0
- package/dist/client/assets/{StackBoardsRoutePage-t_EaFNG4.js → StackBoardsRoutePage-Clviz0hX.js} +1 -1
- package/dist/client/assets/{ToolCallMessage-RbF9XT2B.js → ToolCallMessage-BfNbWGmc.js} +1 -1
- package/dist/client/assets/{VChartSurface-CKuPelsn.js → VChartSurface-CUbxyBo3.js} +1 -1
- package/dist/client/assets/{WorkflowsPage-B6qUmQA3.js → WorkflowsPage-rengzb97.js} +1 -1
- package/dist/client/assets/{WorkflowsSection-DxO0S4An.js → WorkflowsSection-DuXdLsMO.js} +1 -1
- package/dist/client/assets/{chart-renderer-DjYZvNxh.js → chart-renderer-D7ukB2wg.js} +1 -1
- package/dist/client/assets/{chatSounds-FD2DQzF6.js → chatSounds-BLg2xPDZ.js} +1 -1
- package/dist/client/assets/chevron-left-RbFvmeyK.js +1 -0
- package/dist/client/assets/{clipboard.adapter-GN9nnYak.js → clipboard.adapter-CgdNwn-1.js} +1 -1
- package/dist/client/assets/{columnStyle-DjA7hA9k.js → columnStyle-CMCF0ds6.js} +1 -1
- package/dist/client/assets/{createScopedStore-BoLB9Rhc.js → createScopedStore-BrD5NHWx.js} +1 -1
- package/dist/client/assets/{dist-CimtOoGK.js → dist-DNSTHlSo.js} +1 -1
- package/dist/client/assets/editor-icons-DY4uKeEY.js +1 -0
- package/dist/client/assets/{ellipsis-Cd7sO0LO.js → ellipsis-OHg0x_x1.js} +1 -1
- package/dist/client/assets/{flow-kinds-CW2TU_83.js → flow-kinds-Ki2DP9nP.js} +1 -1
- package/dist/client/assets/{folder-C7sXCaHj.js → folder-CnWzWIG5.js} +1 -1
- package/dist/client/assets/{formatters-wSVyvDFZ.js → formatters-BGORekd-.js} +1 -1
- package/dist/client/assets/{genui-EzBIAYFE.js → genui-C9i6j0XZ.js} +1 -1
- package/dist/client/assets/{globe-xmRpmpma.js → globe-BjR1tyE4.js} +1 -1
- package/dist/client/assets/{host-DCaA9W-Z.js → host-D9tJwZXl.js} +1 -1
- package/dist/client/assets/{index-CDqjh7nt.js → index-CesRxZ0X.js} +7 -7
- package/dist/client/assets/index-CxDE9VOy.css +1 -0
- package/dist/client/assets/{info-D4MMvZFe.js → info-DNYekoE-.js} +1 -1
- package/dist/client/assets/{input-C4fvDiQ4.js → input-DgW9uRZN.js} +1 -1
- package/dist/client/assets/{lexicalToReact-sPUyOWk6.js → lexicalToReact-CZvp9Pgz.js} +3 -3
- package/dist/client/assets/{log-aTaDVF_e.js → log-DnEvbp3E.js} +1 -1
- package/dist/client/assets/{monitor-_CRKcV1h.js → monitor-qbWfUdBG.js} +1 -1
- package/dist/client/assets/{pencil-fNBeUUCf.js → pencil-C965rrpd.js} +1 -1
- package/dist/client/assets/{pendingActionsStore-COTh4Ztq.js → pendingActionsStore-CxhI19V9.js} +1 -1
- package/dist/client/assets/{play-BAmkJfn6.js → play-uofLR7yT.js} +1 -1
- package/dist/client/assets/{plus-c75CkNEO.js → plus-D4CI6rj1.js} +1 -1
- package/dist/client/assets/{popover-CHM4skzh.js → popover-Bc646hKn.js} +1 -1
- package/dist/client/assets/{pushClient-BJH_ITRB.js → pushClient-CxnHu9h4.js} +1 -1
- package/dist/client/assets/runDetached-DfQXHAaY.js +1 -0
- package/dist/client/assets/{segmented-control-DNOwRupT.js → segmented-control-CKGX29Kl.js} +1 -1
- package/dist/client/assets/{select-dLHiAls6.js → select-BjeajpVt.js} +1 -1
- package/dist/client/assets/{settings-header-button-CGKCzFyk.js → settings-header-button-BYvHR6Xl.js} +1 -1
- package/dist/client/assets/{sidebarPendingActions-Dyp80Lv_.js → sidebarPendingActions-xgMTnfRY.js} +1 -1
- package/dist/client/assets/{square-pen-B1IxmScM.js → square-pen-DmppQdTd.js} +1 -1
- package/dist/client/assets/{state-mark-BhTZfcYe.js → state-mark-C3kEL1hX.js} +1 -1
- package/dist/client/assets/{status-pill-CwhJhaMy.js → status-pill-Bo1Nb6HX.js} +1 -1
- package/dist/client/assets/{terminal-Bnj7qFGw.js → terminal-DfCF-J4p.js} +1 -1
- package/dist/client/assets/{timer.adapter-DHt2zwsL.js → timer.adapter-C2RhP4d6.js} +1 -1
- package/dist/client/assets/{tools-3gJcj3hK.js → tools-t5T_3UBg.js} +1 -1
- package/dist/client/assets/{triangle-alert-CWrc0CIO.js → triangle-alert-CYQhobyY.js} +1 -1
- package/dist/client/assets/useAppGlobalState-DUa3pkCF.js +1 -0
- package/dist/client/assets/useKannaState-FcieR5qb.js +5 -0
- package/dist/client/assets/useTheme-D9sKDPEm.js +1 -0
- package/dist/client/index.html +35 -35
- package/package.json +2 -2
- package/src/server/agent-coordinator-types.ts +0 -6
- package/src/server/agent-coordinator.ts +6 -45
- package/src/server/agent.compaction.test.ts +1 -13
- package/src/server/agent.oauth-account-info.test.ts +0 -3
- package/src/server/agent.oauth-release.test.ts +0 -2
- package/src/server/agent.oauth-rotation.test.ts +0 -9
- package/src/server/agent.oauth-token-share-cap.test.ts +0 -1
- package/src/server/agent.openrouter-model.test.ts +0 -2
- package/src/server/agent.openrouter-watchdog.test.ts +0 -3
- package/src/server/agent.sdk-workflow-register.test.ts +2 -64
- package/src/server/agent.test.ts +1 -319
- package/src/server/app-settings.test.ts +14 -17
- package/src/server/app-settings.ts +22 -37
- package/src/server/auto-continue/e2e.test.ts +0 -1
- package/src/server/background-task-output-registry.test.ts +1 -1
- package/src/server/background-task-output-registry.ts +1 -1
- package/src/server/claude-cancel-handler.test.ts +0 -58
- package/src/server/claude-cancel-handler.ts +1 -17
- package/src/server/claude-harness-stream.test.ts +134 -0
- package/src/server/{claude-pty/jsonl-path.test.ts → claude-projects-path.adapter.test.ts} +1 -19
- package/src/server/{claude-pty/jsonl-path.adapter.ts → claude-projects-path.adapter.ts} +0 -11
- package/src/server/claude-send-command.test.ts +0 -1
- package/src/server/claude-session-config-helpers.test.ts +1 -90
- package/src/server/claude-session-config-helpers.ts +1 -38
- package/src/server/claude-session-error-handler.test.ts +0 -1
- package/src/server/claude-session-lifecycle.test.ts +2 -36
- package/src/server/claude-session-lifecycle.ts +2 -8
- package/src/server/claude-session-runner.test.ts +8 -36
- package/src/server/claude-session-runner.ts +1 -26
- package/src/server/claude-session-spawner.test.ts +5 -64
- package/src/server/claude-session-spawner.ts +49 -115
- package/src/server/claude-session-start.test.ts +1 -14
- package/src/server/claude-session-start.ts +0 -8
- package/src/server/claude-session-state-queries.test.ts +1 -2
- package/src/server/claude-session-state.test.ts +0 -1
- package/src/server/claude-session-state.ts +0 -4
- package/src/server/claude-subagent-wiring.test.ts +2 -68
- package/src/server/claude-subagent-wiring.ts +3 -50
- package/src/server/claude-turn-starter-types.ts +0 -3
- package/src/server/claude-turn-starter.test.ts +0 -28
- package/src/server/claude-turn-starter.ts +0 -5
- package/src/server/event-store.test.ts +0 -2
- package/src/server/events.ts +1 -2
- package/src/server/harness-types.ts +1 -2
- package/src/server/import-subagent-drill.e2e.test.ts +1 -1
- package/src/server/imported-subagents-dir.test.ts +1 -1
- package/src/server/imported-subagents-dir.ts +1 -1
- package/src/server/kanna-mcp.test.ts +0 -75
- package/src/server/kanna-mcp.ts +1 -47
- package/src/server/server.test.ts +5 -7
- package/src/server/server.ts +2 -18
- package/src/server/subagent-orchestrator.test.ts +0 -1
- package/src/server/subagent-provider-run.test.ts +48 -10
- package/src/server/subagent-provider-run.ts +12 -3
- package/src/server/tool-callback.test.ts +1 -1
- package/src/server/ws-router-agent-ctrl.test.ts +0 -45
- package/src/server/ws-router-agent-ctrl.ts +1 -31
- package/src/server/ws-router-broadcast.ts +0 -27
- package/src/server/ws-router-defaults.test.ts +2 -3
- package/src/server/ws-router-defaults.ts +2 -3
- package/src/server/ws-router-envelope.ts +0 -15
- package/src/server/ws-router-observability.test.ts +1 -1
- package/src/server/ws-router-settings.test.ts +2 -2
- package/src/server/ws-router.test.ts +2 -2
- package/src/server/ws-router.ts +0 -8
- package/src/shared/app-settings-types.ts +9 -20
- package/src/shared/claude-api-error-classification.ts +1 -1
- package/src/shared/protocol.ts +1 -11
- package/dist/client/assets/BoardsRoutePage-BTTjJX-Y.js +0 -1
- package/dist/client/assets/ChatPage-DH8wtQyD.js +0 -93
- package/dist/client/assets/SettingsPage-CKrZ6dGQ.js +0 -9
- package/dist/client/assets/chevron-left-CJ0qv9kF.js +0 -1
- package/dist/client/assets/editor-icons-CONDQZof.js +0 -1
- package/dist/client/assets/index-C-UYaDk6.css +0 -1
- package/dist/client/assets/runDetached-DqQ3k2ji.js +0 -1
- package/dist/client/assets/useAppGlobalState-DKOGrO5l.js +0 -1
- package/dist/client/assets/useKannaState-D7R_-dM6.js +0 -5
- package/dist/client/assets/useTheme-B57LIKyx.js +0 -1
- package/src/server/agent.pty-rotation.test.ts +0 -322
- package/src/server/claude-pty/auth.test.ts +0 -38
- package/src/server/claude-pty/auth.ts +0 -17
- package/src/server/claude-pty/claude-session-registry.adapter.ts +0 -68
- package/src/server/claude-pty/claude-session-registry.test.ts +0 -149
- package/src/server/claude-pty/driver.test.ts +0 -1153
- package/src/server/claude-pty/driver.ts +0 -829
- package/src/server/claude-pty/env.test.ts +0 -79
- package/src/server/claude-pty/env.ts +0 -20
- package/src/server/claude-pty/jsonl-to-event.test.ts +0 -882
- package/src/server/claude-pty/jsonl-to-event.ts +0 -357
- package/src/server/claude-pty/parity-matrix.test.ts +0 -437
- package/src/server/claude-pty/pid-registry.adapter.ts +0 -164
- package/src/server/claude-pty/pid-registry.test.ts +0 -178
- package/src/server/claude-pty/preflight/binary-fingerprint.adapter.ts +0 -20
- package/src/server/claude-pty/preflight/binary-fingerprint.test.ts +0 -32
- package/src/server/claude-pty/pty-instance-registry.test.ts +0 -208
- package/src/server/claude-pty/pty-instance-registry.ts +0 -172
- package/src/server/claude-pty/pty-memory-sampler.adapter.test.ts +0 -103
- package/src/server/claude-pty/pty-memory-sampler.adapter.ts +0 -86
- package/src/server/claude-pty/pty-process.adapter.ts +0 -60
- package/src/server/claude-pty/pty-process.test.ts +0 -49
- package/src/server/claude-pty/resolve-binary.adapter.ts +0 -106
- package/src/server/claude-pty/resolve-binary.test.ts +0 -118
- package/src/server/claude-pty/runtime-dir.adapter.ts +0 -19
- package/src/server/claude-pty/sandbox/detect.adapter.ts +0 -17
- package/src/server/claude-pty/sandbox/detect.test.ts +0 -17
- package/src/server/claude-pty/sandbox/platform.test.ts +0 -35
- package/src/server/claude-pty/sandbox/platform.ts +0 -27
- package/src/server/claude-pty/sandbox/preflight-io.adapter.ts +0 -14
- package/src/server/claude-pty/sandbox/preflight.test.ts +0 -69
- package/src/server/claude-pty/sandbox/preflight.ts +0 -44
- package/src/server/claude-pty/sandbox/profile-linux.adapter.ts +0 -63
- package/src/server/claude-pty/sandbox/profile-linux.test.ts +0 -90
- package/src/server/claude-pty/sandbox/profile-macos.adapter.ts +0 -72
- package/src/server/claude-pty/sandbox/profile-macos.test.ts +0 -68
- package/src/server/claude-pty/sandbox/wrap.adapter.ts +0 -58
- package/src/server/claude-pty/sandbox/wrap.test.ts +0 -82
- package/src/server/claude-pty/settings-writer.adapter.ts +0 -23
- package/src/server/claude-pty/settings-writer.test.ts +0 -22
- package/src/server/claude-pty/smoke-test-io.adapter.ts +0 -28
- package/src/server/claude-pty/smoke-test.test.ts +0 -199
- package/src/server/claude-pty/smoke-test.ts +0 -182
- package/src/server/claude-pty/tui-control.test.ts +0 -300
- package/src/server/claude-pty/tui-control.ts +0 -174
- package/src/server/claude-pty/tui-source.adapter.test.ts +0 -374
- package/src/server/claude-pty/tui-source.adapter.ts +0 -294
- package/src/server/http-server.adapter.test.ts +0 -18
- package/src/server/http-server.adapter.ts +0 -39
- package/src/server/kanna-mcp-http.test.ts +0 -274
- package/src/server/kanna-mcp-http.ts +0 -194
- package/src/server/kanna-mcp-tools/ask-user-question.test.ts +0 -108
- package/src/server/kanna-mcp-tools/ask-user-question.ts +0 -55
- package/src/server/kanna-mcp-tools/exit-plan-mode.test.ts +0 -61
- package/src/server/kanna-mcp-tools/exit-plan-mode.ts +0 -49
- package/src/server/mcp-zod-compat.adapter.ts +0 -20
- package/src/shared/pty-instance.ts +0 -43
- /package/src/server/{claude-pty/output-ring.test.ts → output-ring.test.ts} +0 -0
- /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
|
-
}
|