@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,437 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdtemp, rm, writeFile, appendFile, mkdir } from "node:fs/promises"
|
|
3
|
-
import { tmpdir } from "node:os"
|
|
4
|
-
import path from "node:path"
|
|
5
|
-
import { createClaudeHarnessStream } from "../agent"
|
|
6
|
-
import type { ClaudeRawSdkMessage } from "../agent"
|
|
7
|
-
import { createJsonlEventParser } from "./jsonl-to-event"
|
|
8
|
-
import { startTranscriptStream } from "./tui-source.adapter"
|
|
9
|
-
import type { HarnessEvent } from "../harness-types"
|
|
10
|
-
import type { CompactBoundaryMetadata } from "../../shared/transcript-types"
|
|
11
|
-
import type { ModelPrice } from "../../shared/token-pricing"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function fakeQuery(messages: unknown[]): AsyncGenerator<ClaudeRawSdkMessage> {
|
|
15
|
-
return (async function* () {
|
|
16
|
-
for (const m of messages) yield m as ClaudeRawSdkMessage
|
|
17
|
-
})()
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function normalize(events: HarnessEvent[]): unknown[] {
|
|
21
|
-
return events.map((ev) => {
|
|
22
|
-
if (ev.type === "transcript") {
|
|
23
|
-
const { _id: _i, createdAt: _c, ...rest } = ev.entry as unknown as Record<string, unknown> & {
|
|
24
|
-
_id?: string
|
|
25
|
-
createdAt?: number
|
|
26
|
-
}
|
|
27
|
-
return { type: ev.type, entry: rest }
|
|
28
|
-
}
|
|
29
|
-
return ev
|
|
30
|
-
})
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function collectSdk(messages: unknown[], configuredContextWindow?: number): Promise<HarnessEvent[]> {
|
|
34
|
-
const events: HarnessEvent[] = []
|
|
35
|
-
for await (const ev of createClaudeHarnessStream(fakeQuery(messages), configuredContextWindow)) {
|
|
36
|
-
events.push(ev)
|
|
37
|
-
}
|
|
38
|
-
return events
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function ptyEventsViaTranscriptStream(messages: unknown[], configuredContextWindow?: number): Promise<HarnessEvent[]> {
|
|
42
|
-
const tmpDir = await mkdtemp(path.join(tmpdir(), "kanna-parity-"))
|
|
43
|
-
const projectDir = path.join(tmpDir, "projects", "fake")
|
|
44
|
-
await mkdir(projectDir, { recursive: true })
|
|
45
|
-
const filePath = path.join(projectDir, "fixture.jsonl")
|
|
46
|
-
await writeFile(filePath, "")
|
|
47
|
-
const stream = await startTranscriptStream({ projectDir, knownFilePath: filePath, firstFileTimeoutMs: 2000 })
|
|
48
|
-
const parser = createJsonlEventParser({ configuredContextWindow })
|
|
49
|
-
const events: HarnessEvent[] = []
|
|
50
|
-
const writeAll = (async () => {
|
|
51
|
-
for (const m of messages) {
|
|
52
|
-
await appendFile(filePath, `${JSON.stringify(m) }\n`)
|
|
53
|
-
await new Promise<void>((r) => setTimeout(r, 30))
|
|
54
|
-
}
|
|
55
|
-
await appendFile(filePath, '{"type":"__parity_sentinel__"}\n')
|
|
56
|
-
})()
|
|
57
|
-
const collectDone = (async () => {
|
|
58
|
-
for await (const line of stream.lines) {
|
|
59
|
-
let parsed: { type?: string }
|
|
60
|
-
try { parsed = JSON.parse(line) as { type?: string } } catch { continue }
|
|
61
|
-
if (parsed.type === "__parity_sentinel__") break
|
|
62
|
-
for (const ev of parser.parse(line)) events.push(ev)
|
|
63
|
-
}
|
|
64
|
-
})()
|
|
65
|
-
await writeAll
|
|
66
|
-
await collectDone
|
|
67
|
-
stream.close()
|
|
68
|
-
await rm(tmpDir, { recursive: true, force: true })
|
|
69
|
-
return events
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function assertSameEvents(messages: unknown[], configuredContextWindow?: number): Promise<void> {
|
|
73
|
-
const sdk = await collectSdk(messages, configuredContextWindow)
|
|
74
|
-
const pty = await ptyEventsViaTranscriptStream(messages, configuredContextWindow)
|
|
75
|
-
expect(normalize(pty)).toEqual(normalize(sdk))
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
describe("SDK ↔ PTY HarnessEvent equivalence matrix", () => {
|
|
79
|
-
test("simple turn: system/init → assistant → result", async () => {
|
|
80
|
-
await assertSameEvents([
|
|
81
|
-
{
|
|
82
|
-
type: "system",
|
|
83
|
-
subtype: "init",
|
|
84
|
-
session_id: "sess-1",
|
|
85
|
-
model: "claude-sonnet-4-6",
|
|
86
|
-
tools: [],
|
|
87
|
-
mcp_servers: [],
|
|
88
|
-
slash_commands: [],
|
|
89
|
-
cwd: "/tmp",
|
|
90
|
-
permissionMode: "acceptEdits",
|
|
91
|
-
apiKeySource: "none",
|
|
92
|
-
claude_code_version: "0.0.0",
|
|
93
|
-
output_style: "default",
|
|
94
|
-
skills: [],
|
|
95
|
-
plugins: [],
|
|
96
|
-
uuid: "u1",
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
type: "assistant",
|
|
100
|
-
session_id: "sess-1",
|
|
101
|
-
message: {
|
|
102
|
-
id: "msg-1",
|
|
103
|
-
role: "assistant",
|
|
104
|
-
content: [{ type: "text", text: "hi" }],
|
|
105
|
-
},
|
|
106
|
-
usage: { input_tokens: 100, output_tokens: 25 },
|
|
107
|
-
uuid: "u2",
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
type: "result",
|
|
111
|
-
subtype: "success",
|
|
112
|
-
session_id: "sess-1",
|
|
113
|
-
result: "done",
|
|
114
|
-
is_error: false,
|
|
115
|
-
duration_ms: 500,
|
|
116
|
-
usage: { input_tokens: 100, output_tokens: 25 },
|
|
117
|
-
modelUsage: { "claude-sonnet-4-6": { contextWindow: 200000 } },
|
|
118
|
-
uuid: "u3",
|
|
119
|
-
},
|
|
120
|
-
])
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
test("rate_limit_event (SDK-native shape)", async () => {
|
|
124
|
-
await assertSameEvents([
|
|
125
|
-
{
|
|
126
|
-
type: "rate_limit_event",
|
|
127
|
-
session_id: "sess-rl",
|
|
128
|
-
rate_limit_info: { status: "rejected", resetsAt: 1_748_800_000 },
|
|
129
|
-
},
|
|
130
|
-
])
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
test("prompt-too-long error result", async () => {
|
|
134
|
-
await assertSameEvents([
|
|
135
|
-
{
|
|
136
|
-
type: "result",
|
|
137
|
-
subtype: "error",
|
|
138
|
-
session_id: "sess-err",
|
|
139
|
-
is_error: true,
|
|
140
|
-
result: "prompt is too long",
|
|
141
|
-
duration_ms: 0,
|
|
142
|
-
uuid: "u-err",
|
|
143
|
-
},
|
|
144
|
-
])
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
test("multiple assistant messages dedupe on usage id (same id seen twice)", async () => {
|
|
148
|
-
await assertSameEvents([
|
|
149
|
-
{
|
|
150
|
-
type: "assistant",
|
|
151
|
-
session_id: "sess-d",
|
|
152
|
-
message: { id: "dup", role: "assistant", content: [{ type: "text", text: "a" }] },
|
|
153
|
-
usage: { input_tokens: 10, output_tokens: 3 },
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
type: "assistant",
|
|
157
|
-
session_id: "sess-d",
|
|
158
|
-
message: { id: "dup", role: "assistant", content: [{ type: "text", text: "a" }] },
|
|
159
|
-
usage: { input_tokens: 10, output_tokens: 3 },
|
|
160
|
-
},
|
|
161
|
-
])
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
test("1M context window floor preserved against modelUsage.contextWindow=200k", async () => {
|
|
165
|
-
await assertSameEvents(
|
|
166
|
-
[
|
|
167
|
-
{
|
|
168
|
-
type: "assistant",
|
|
169
|
-
session_id: "sess-1m",
|
|
170
|
-
message: { id: "msg-1m", role: "assistant", content: [{ type: "text", text: "x" }] },
|
|
171
|
-
usage: { input_tokens: 100, output_tokens: 50 },
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
type: "result",
|
|
175
|
-
subtype: "success",
|
|
176
|
-
session_id: "sess-1m",
|
|
177
|
-
is_error: false,
|
|
178
|
-
duration_ms: 500,
|
|
179
|
-
usage: { input_tokens: 100, output_tokens: 50 },
|
|
180
|
-
modelUsage: { "claude-sonnet-4-6": { contextWindow: 200000 } },
|
|
181
|
-
},
|
|
182
|
-
],
|
|
183
|
-
1_000_000,
|
|
184
|
-
)
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
test("session_token emitted from every message carrying session_id", async () => {
|
|
188
|
-
await assertSameEvents([
|
|
189
|
-
{ type: "system", subtype: "init", session_id: "a", model: "m", tools: [], mcp_servers: [], slash_commands: [], cwd: "/", permissionMode: "acceptEdits", apiKeySource: "none", claude_code_version: "0", output_style: "d", skills: [], plugins: [], uuid: "1" },
|
|
190
|
-
{ type: "assistant", session_id: "a", message: { id: "m1", role: "assistant", content: [] } },
|
|
191
|
-
{ type: "result", subtype: "success", session_id: "a", is_error: false, duration_ms: 0, uuid: "r" },
|
|
192
|
-
])
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
test("rate-limit api_error + error result: result body scrubbed (no duplicate text)", async () => {
|
|
196
|
-
const limitText = "You've hit your limit · resets 11:10pm (Asia/Saigon)"
|
|
197
|
-
const sdk = await collectSdk([
|
|
198
|
-
{
|
|
199
|
-
type: "assistant",
|
|
200
|
-
session_id: "sess-rl",
|
|
201
|
-
uuid: "msg-err",
|
|
202
|
-
isApiErrorMessage: true,
|
|
203
|
-
message: {
|
|
204
|
-
model: "<synthetic>",
|
|
205
|
-
content: [{ type: "text", text: limitText }],
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
type: "result",
|
|
210
|
-
subtype: "error",
|
|
211
|
-
session_id: "sess-rl",
|
|
212
|
-
is_error: true,
|
|
213
|
-
result: limitText,
|
|
214
|
-
duration_ms: 2000,
|
|
215
|
-
uuid: "r-err",
|
|
216
|
-
},
|
|
217
|
-
])
|
|
218
|
-
const apiErrorEntries = sdk.flatMap((ev) =>
|
|
219
|
-
ev.type === "transcript" && ev.entry && ev.entry.kind === "api_error" ? [ev.entry] : [],
|
|
220
|
-
)
|
|
221
|
-
const resultEntries = sdk.flatMap((ev) =>
|
|
222
|
-
ev.type === "transcript" && ev.entry && ev.entry.kind === "result" ? [ev.entry] : [],
|
|
223
|
-
)
|
|
224
|
-
expect(apiErrorEntries).toHaveLength(1)
|
|
225
|
-
expect(apiErrorEntries[0].text).toContain("hit your limit")
|
|
226
|
-
expect(resultEntries).toHaveLength(1)
|
|
227
|
-
expect(resultEntries[0].isError).toBe(true)
|
|
228
|
-
expect(resultEntries[0].result).toBe("")
|
|
229
|
-
expect(resultEntries[0].durationMs).toBe(2000)
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
test("compact_boundary turn does not produce phantom context_window_updated", async () => {
|
|
233
|
-
await assertSameEvents([
|
|
234
|
-
{
|
|
235
|
-
type: "system",
|
|
236
|
-
subtype: "compact_boundary",
|
|
237
|
-
session_id: "sess-c",
|
|
238
|
-
compact_metadata: { trigger: "auto", pre_tokens: 50000 },
|
|
239
|
-
uuid: "cb",
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
type: "result",
|
|
243
|
-
subtype: "success",
|
|
244
|
-
session_id: "sess-c",
|
|
245
|
-
is_error: false,
|
|
246
|
-
duration_ms: 100,
|
|
247
|
-
usage: { input_tokens: 1000, cache_read_input_tokens: 49000, output_tokens: 0 },
|
|
248
|
-
uuid: "r-c",
|
|
249
|
-
},
|
|
250
|
-
])
|
|
251
|
-
})
|
|
252
|
-
|
|
253
|
-
test("both wire spellings of compact metadata reach the entry identically", async () => {
|
|
254
|
-
const snakeCaseFromSdkStream = {
|
|
255
|
-
type: "system",
|
|
256
|
-
subtype: "compact_boundary",
|
|
257
|
-
session_id: "sess-d",
|
|
258
|
-
compact_metadata: { trigger: "auto", pre_tokens: 206978, post_tokens: 7509, duration_ms: 143806 },
|
|
259
|
-
uuid: "cb-snake",
|
|
260
|
-
}
|
|
261
|
-
const camelCaseFromCliTranscript = {
|
|
262
|
-
type: "system",
|
|
263
|
-
subtype: "compact_boundary",
|
|
264
|
-
session_id: "sess-d",
|
|
265
|
-
compactMetadata: { trigger: "auto", preTokens: 206978, postTokens: 7509, durationMs: 143806 },
|
|
266
|
-
uuid: "cb-camel",
|
|
267
|
-
}
|
|
268
|
-
const expected: CompactBoundaryMetadata = {
|
|
269
|
-
trigger: "auto",
|
|
270
|
-
preTokens: 206978,
|
|
271
|
-
postTokens: 7509,
|
|
272
|
-
durationMs: 143806,
|
|
273
|
-
}
|
|
274
|
-
const boundaryMetadata = (events: HarnessEvent[]): (CompactBoundaryMetadata | undefined)[] =>
|
|
275
|
-
events.flatMap((event) =>
|
|
276
|
-
event.type === "transcript" && event.entry.kind === "compact_boundary"
|
|
277
|
-
? [event.entry.compactMetadata]
|
|
278
|
-
: [],
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
for (const message of [snakeCaseFromSdkStream, camelCaseFromCliTranscript]) {
|
|
282
|
-
await assertSameEvents([message])
|
|
283
|
-
expect(boundaryMetadata(await collectSdk([message]))).toEqual([expected])
|
|
284
|
-
expect(boundaryMetadata(await ptyEventsViaTranscriptStream([message]))).toEqual([expected])
|
|
285
|
-
}
|
|
286
|
-
})
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
describe("createClaudeHarnessStream cost attachment", () => {
|
|
290
|
-
test("claude result cost is attached to the final-turn snapshot", async () => {
|
|
291
|
-
const messages = [
|
|
292
|
-
{
|
|
293
|
-
type: "assistant",
|
|
294
|
-
session_id: "sess-cost",
|
|
295
|
-
message: { id: "m1", role: "assistant", content: [] },
|
|
296
|
-
usage: { input_tokens: 100, output_tokens: 50 },
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
type: "result",
|
|
300
|
-
subtype: "success",
|
|
301
|
-
session_id: "sess-cost",
|
|
302
|
-
is_error: false,
|
|
303
|
-
total_cost_usd: 0.0123,
|
|
304
|
-
usage: { input_tokens: 100, output_tokens: 50 },
|
|
305
|
-
duration_ms: 10,
|
|
306
|
-
num_turns: 1,
|
|
307
|
-
result: "ok",
|
|
308
|
-
},
|
|
309
|
-
]
|
|
310
|
-
|
|
311
|
-
const events: HarnessEvent[] = []
|
|
312
|
-
for await (const ev of createClaudeHarnessStream(fakeQuery(messages))) {
|
|
313
|
-
events.push(ev)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const cwuEntries = events.flatMap((ev) => {
|
|
317
|
-
if (ev.type !== "transcript") return []
|
|
318
|
-
const entry = ev.entry
|
|
319
|
-
return entry && entry.kind === "context_window_updated" ? [entry] : []
|
|
320
|
-
})
|
|
321
|
-
const lastCwu = cwuEntries.at(-1)
|
|
322
|
-
expect(lastCwu).toBeDefined()
|
|
323
|
-
expect(lastCwu!.usage.costUsd).toBeCloseTo(0.0123, 6)
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
test("openrouter turn gets computed cost from a price resolver", async () => {
|
|
327
|
-
const messages = [
|
|
328
|
-
{
|
|
329
|
-
type: "assistant",
|
|
330
|
-
session_id: "sess-or",
|
|
331
|
-
message: { id: "m1", role: "assistant", content: [] },
|
|
332
|
-
usage: { input_tokens: 1_000_000, output_tokens: 0 },
|
|
333
|
-
},
|
|
334
|
-
{
|
|
335
|
-
type: "result",
|
|
336
|
-
subtype: "success",
|
|
337
|
-
session_id: "sess-or",
|
|
338
|
-
is_error: false,
|
|
339
|
-
usage: { input_tokens: 1_000_000, output_tokens: 0 },
|
|
340
|
-
duration_ms: 10,
|
|
341
|
-
num_turns: 1,
|
|
342
|
-
result: "ok",
|
|
343
|
-
},
|
|
344
|
-
]
|
|
345
|
-
|
|
346
|
-
const price: ModelPrice = { inputPerMTok: 3, outputPerMTok: 15 }
|
|
347
|
-
const events: HarnessEvent[] = []
|
|
348
|
-
for await (const ev of createClaudeHarnessStream(fakeQuery(messages), undefined, () => price)) {
|
|
349
|
-
events.push(ev)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const cwuEntries = events.flatMap((ev) => {
|
|
353
|
-
if (ev.type !== "transcript") return []
|
|
354
|
-
const entry = ev.entry
|
|
355
|
-
return entry && entry.kind === "context_window_updated" ? [entry] : []
|
|
356
|
-
})
|
|
357
|
-
const lastCwu = cwuEntries.at(-1)
|
|
358
|
-
expect(lastCwu).toBeDefined()
|
|
359
|
-
expect(lastCwu!.usage.costUsd).toBeCloseTo(3, 6)
|
|
360
|
-
})
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
describe("result entry usage + cost enrichment", () => {
|
|
364
|
-
test("claude: result entry carries usage tokens and provider costUsd", async () => {
|
|
365
|
-
const messages = [
|
|
366
|
-
{
|
|
367
|
-
type: "assistant",
|
|
368
|
-
session_id: "sess-ru",
|
|
369
|
-
message: { id: "m1", role: "assistant", content: [] },
|
|
370
|
-
usage: { input_tokens: 200, output_tokens: 80 },
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
type: "result",
|
|
374
|
-
subtype: "success",
|
|
375
|
-
session_id: "sess-ru",
|
|
376
|
-
is_error: false,
|
|
377
|
-
total_cost_usd: 0.02,
|
|
378
|
-
usage: { input_tokens: 200, output_tokens: 80 },
|
|
379
|
-
duration_ms: 100,
|
|
380
|
-
num_turns: 1,
|
|
381
|
-
result: "done",
|
|
382
|
-
},
|
|
383
|
-
]
|
|
384
|
-
|
|
385
|
-
const events: HarnessEvent[] = []
|
|
386
|
-
for await (const ev of createClaudeHarnessStream(fakeQuery(messages))) {
|
|
387
|
-
events.push(ev)
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const resultEntries = events.flatMap((ev) =>
|
|
391
|
-
ev.type === "transcript" && ev.entry && ev.entry.kind === "result" ? [ev.entry] : [],
|
|
392
|
-
)
|
|
393
|
-
expect(resultEntries).toHaveLength(1)
|
|
394
|
-
const resultEntry = resultEntries[0]
|
|
395
|
-
expect(resultEntry.costUsd).toBeCloseTo(0.02, 6)
|
|
396
|
-
expect(resultEntry.usage).toBeDefined()
|
|
397
|
-
expect(resultEntry.usage?.outputTokens).toBe(80)
|
|
398
|
-
expect(resultEntry.usage?.inputTokens).toBe(200)
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
test("openrouter: result entry carries computed cost and usage tokens from resolver", async () => {
|
|
402
|
-
const messages = [
|
|
403
|
-
{
|
|
404
|
-
type: "assistant",
|
|
405
|
-
session_id: "sess-oru",
|
|
406
|
-
message: { id: "m1", role: "assistant", content: [] },
|
|
407
|
-
usage: { input_tokens: 1_000_000, output_tokens: 0 },
|
|
408
|
-
},
|
|
409
|
-
{
|
|
410
|
-
type: "result",
|
|
411
|
-
subtype: "success",
|
|
412
|
-
session_id: "sess-oru",
|
|
413
|
-
is_error: false,
|
|
414
|
-
usage: { input_tokens: 1_000_000, output_tokens: 0 },
|
|
415
|
-
duration_ms: 10,
|
|
416
|
-
num_turns: 1,
|
|
417
|
-
result: "ok",
|
|
418
|
-
},
|
|
419
|
-
]
|
|
420
|
-
|
|
421
|
-
const price: ModelPrice = { inputPerMTok: 3, outputPerMTok: 15 }
|
|
422
|
-
const events: HarnessEvent[] = []
|
|
423
|
-
for await (const ev of createClaudeHarnessStream(fakeQuery(messages), undefined, () => price)) {
|
|
424
|
-
events.push(ev)
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const resultEntries = events.flatMap((ev) =>
|
|
428
|
-
ev.type === "transcript" && ev.entry && ev.entry.kind === "result" ? [ev.entry] : [],
|
|
429
|
-
)
|
|
430
|
-
expect(resultEntries).toHaveLength(1)
|
|
431
|
-
const resultEntry = resultEntries[0]
|
|
432
|
-
expect(resultEntry.costUsd).toBeCloseTo(3, 6)
|
|
433
|
-
expect(resultEntry.usage).toBeDefined()
|
|
434
|
-
expect(resultEntry.usage?.inputTokens).toBe(1_000_000)
|
|
435
|
-
expect(resultEntry.usage?.outputTokens).toBeUndefined()
|
|
436
|
-
})
|
|
437
|
-
})
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
import process from "node:process"
|
|
4
|
-
import { isJsonArray, isJsonObject, safeJsonParse, type JsonValue } from "../../shared/json"
|
|
5
|
-
|
|
6
|
-
export interface ClaudePtyEntry {
|
|
7
|
-
chatId: string
|
|
8
|
-
sessionId: string
|
|
9
|
-
pid: number
|
|
10
|
-
cwd: string
|
|
11
|
-
runtimeDir: string
|
|
12
|
-
createdAt: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface RegistryFile {
|
|
16
|
-
entries: ClaudePtyEntry[]
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class ClaudePtyRegistry {
|
|
20
|
-
private readonly filePath: string
|
|
21
|
-
private entries: ClaudePtyEntry[] = []
|
|
22
|
-
private loaded = false
|
|
23
|
-
private writeQueue: Promise<void> = Promise.resolve()
|
|
24
|
-
|
|
25
|
-
constructor(filePath: string) {
|
|
26
|
-
this.filePath = filePath
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async register(entry: Omit<ClaudePtyEntry, "createdAt">): Promise<void> {
|
|
30
|
-
await this.loadIfNeeded()
|
|
31
|
-
const next = this.entries.filter((existing) => existing.pid !== entry.pid)
|
|
32
|
-
next.push({ ...entry, createdAt: Date.now() })
|
|
33
|
-
this.entries = next
|
|
34
|
-
await this.persist()
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async unregister(pid: number): Promise<void> {
|
|
38
|
-
await this.loadIfNeeded()
|
|
39
|
-
this.entries = this.entries.filter((entry) => entry.pid !== pid)
|
|
40
|
-
await this.persist()
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async reapStale(): Promise<ClaudePtyEntry[]> {
|
|
44
|
-
const stored = await this.readFromDisk()
|
|
45
|
-
if (stored.length === 0) {
|
|
46
|
-
this.entries = []
|
|
47
|
-
this.loaded = true
|
|
48
|
-
return []
|
|
49
|
-
}
|
|
50
|
-
for (const entry of stored) {
|
|
51
|
-
await killProcessTree(entry.pid)
|
|
52
|
-
if (entry.runtimeDir && entry.runtimeDir.length > 0) {
|
|
53
|
-
try { await rm(entry.runtimeDir, { recursive: true, force: true }) } catch {
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
this.entries = []
|
|
58
|
-
this.loaded = true
|
|
59
|
-
await this.persist()
|
|
60
|
-
return stored
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private async loadIfNeeded() {
|
|
64
|
-
if (this.loaded) return
|
|
65
|
-
this.entries = await this.readFromDisk()
|
|
66
|
-
this.loaded = true
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private async readFromDisk(): Promise<ClaudePtyEntry[]> {
|
|
70
|
-
let raw: string
|
|
71
|
-
try {
|
|
72
|
-
raw = await readFile(this.filePath, "utf8")
|
|
73
|
-
} catch {
|
|
74
|
-
return []
|
|
75
|
-
}
|
|
76
|
-
const parsed = safeJsonParse(raw)
|
|
77
|
-
if (parsed === null || !isJsonObject(parsed) || !isJsonArray(parsed.entries)) return []
|
|
78
|
-
return parsed.entries.filter(isValidEntry)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
private async persist() {
|
|
82
|
-
const snapshot: RegistryFile = { entries: [...this.entries] }
|
|
83
|
-
const serialized = JSON.stringify(snapshot)
|
|
84
|
-
this.writeQueue = this.writeQueue
|
|
85
|
-
.catch(() => undefined)
|
|
86
|
-
.then(async () => {
|
|
87
|
-
await mkdir(path.dirname(this.filePath), { recursive: true })
|
|
88
|
-
await writeFile(this.filePath, serialized, "utf8")
|
|
89
|
-
})
|
|
90
|
-
await this.writeQueue
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function isValidEntry(value: JsonValue): value is JsonValue & ClaudePtyEntry {
|
|
95
|
-
if (!isJsonObject(value)) return false
|
|
96
|
-
return (
|
|
97
|
-
typeof value.chatId === "string"
|
|
98
|
-
&& typeof value.sessionId === "string"
|
|
99
|
-
&& typeof value.pid === "number"
|
|
100
|
-
&& Number.isFinite(value.pid)
|
|
101
|
-
&& typeof value.cwd === "string"
|
|
102
|
-
&& typeof value.runtimeDir === "string"
|
|
103
|
-
&& typeof value.createdAt === "number"
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function killProcessTree(pid: number): Promise<void> {
|
|
108
|
-
if (process.platform === "win32") return
|
|
109
|
-
if (!Number.isFinite(pid) || pid <= 0) return
|
|
110
|
-
|
|
111
|
-
const targets = [pid, ...(await collectDescendants(pid))]
|
|
112
|
-
for (const target of targets.reverse()) {
|
|
113
|
-
try {
|
|
114
|
-
process.kill(target, "SIGKILL")
|
|
115
|
-
} catch {
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async function collectDescendants(root: number): Promise<number[]> {
|
|
121
|
-
let childrenByParent: Map<number, number[]>
|
|
122
|
-
try {
|
|
123
|
-
const proc = Bun.spawn(["ps", "-A", "-o", "pid=,ppid="], {
|
|
124
|
-
stdout: "pipe",
|
|
125
|
-
stderr: "ignore",
|
|
126
|
-
stdin: "ignore",
|
|
127
|
-
})
|
|
128
|
-
const text = await new Response(proc.stdout).text()
|
|
129
|
-
await proc.exited
|
|
130
|
-
childrenByParent = parsePidPpid(text)
|
|
131
|
-
} catch {
|
|
132
|
-
return []
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const descendants: number[] = []
|
|
136
|
-
const queue = [root]
|
|
137
|
-
const seen = new Set<number>([root])
|
|
138
|
-
while (queue.length > 0) {
|
|
139
|
-
const parent = queue.shift()
|
|
140
|
-
if (parent === undefined) break
|
|
141
|
-
for (const child of childrenByParent.get(parent) ?? []) {
|
|
142
|
-
if (seen.has(child)) continue
|
|
143
|
-
seen.add(child)
|
|
144
|
-
descendants.push(child)
|
|
145
|
-
queue.push(child)
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return descendants
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function parsePidPpid(psOutput: string): Map<number, number[]> {
|
|
152
|
-
const childrenByParent = new Map<number, number[]>()
|
|
153
|
-
for (const line of psOutput.split("\n")) {
|
|
154
|
-
const match = line.trim().match(/^(\d+)\s+(\d+)$/)
|
|
155
|
-
if (!match) continue
|
|
156
|
-
const pid = Number.parseInt(match[1] ?? "", 10)
|
|
157
|
-
const ppid = Number.parseInt(match[2] ?? "", 10)
|
|
158
|
-
if (!Number.isFinite(pid) || !Number.isFinite(ppid)) continue
|
|
159
|
-
const siblings = childrenByParent.get(ppid)
|
|
160
|
-
if (siblings) siblings.push(pid)
|
|
161
|
-
else childrenByParent.set(ppid, [pid])
|
|
162
|
-
}
|
|
163
|
-
return childrenByParent
|
|
164
|
-
}
|