@meetquinn/animactl 0.1.2 → 0.1.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 (289) hide show
  1. package/README.md +11 -6
  2. package/dist/server/{runtime/activity-payload.d.ts → activities/format.d.ts} +4 -1
  3. package/dist/server/activities/format.d.ts.map +1 -0
  4. package/dist/server/{runtime/activity-payload.js → activities/format.js} +16 -1
  5. package/dist/server/agents/agent-skills.d.ts +7 -3
  6. package/dist/server/agents/agent-skills.d.ts.map +1 -1
  7. package/dist/server/agents/agent-skills.js +243 -38
  8. package/dist/server/agents/agent-slack.service.d.ts +15 -0
  9. package/dist/server/agents/agent-slack.service.d.ts.map +1 -1
  10. package/dist/server/agents/agent-slack.service.js +43 -6
  11. package/dist/server/agents/default-skills.d.ts +9 -0
  12. package/dist/server/agents/default-skills.d.ts.map +1 -0
  13. package/dist/server/agents/default-skills.js +52 -0
  14. package/dist/server/cli/runtime-cli.d.ts +7 -1
  15. package/dist/server/cli/runtime-cli.d.ts.map +1 -1
  16. package/dist/server/cli/runtime-cli.js +107 -3
  17. package/dist/server/cli/services-cli.d.ts.map +1 -1
  18. package/dist/server/cli/services-cli.js +39 -0
  19. package/dist/server/inbox/slack-subscriber.d.ts +2 -0
  20. package/dist/server/inbox/slack-subscriber.d.ts.map +1 -1
  21. package/dist/server/inbox/slack-subscriber.js +28 -2
  22. package/dist/server/inbox/slack-subscription.service.d.ts.map +1 -1
  23. package/dist/server/inbox/slack-subscription.service.js +13 -1
  24. package/dist/server/inbox/wake-queue.service.d.ts +3 -1
  25. package/dist/server/inbox/wake-queue.service.d.ts.map +1 -1
  26. package/dist/server/inbox/wake-queue.service.js +5 -3
  27. package/dist/server/provider-usage/http.d.ts +1 -0
  28. package/dist/server/provider-usage/http.d.ts.map +1 -1
  29. package/dist/server/provider-usage/http.js +23 -1
  30. package/dist/server/provider-usage/provider-usage.service.d.ts +1 -4
  31. package/dist/server/provider-usage/provider-usage.service.d.ts.map +1 -1
  32. package/dist/server/provider-usage/provider-usage.service.js +1 -18
  33. package/dist/server/provider-usage/providers/claude.d.ts +1 -1
  34. package/dist/server/provider-usage/providers/claude.d.ts.map +1 -1
  35. package/dist/server/provider-usage/providers/claude.js +1 -1
  36. package/dist/server/provider-usage/providers/codex.d.ts +1 -1
  37. package/dist/server/provider-usage/providers/codex.d.ts.map +1 -1
  38. package/dist/server/provider-usage/providers/codex.js +1 -1
  39. package/dist/server/provider-usage/providers/kimi.d.ts +1 -1
  40. package/dist/server/provider-usage/providers/kimi.d.ts.map +1 -1
  41. package/dist/server/provider-usage/providers/kimi.js +4 -4
  42. package/dist/server/provider-usage/result.d.ts +5 -0
  43. package/dist/server/provider-usage/result.d.ts.map +1 -0
  44. package/dist/server/provider-usage/result.js +18 -0
  45. package/dist/server/providers/active-runtime.d.ts +1 -1
  46. package/dist/server/providers/active-runtime.d.ts.map +1 -1
  47. package/dist/server/providers/claude-events.d.ts +1 -1
  48. package/dist/server/providers/claude-events.d.ts.map +1 -1
  49. package/dist/server/providers/claude-events.js +2 -2
  50. package/dist/server/providers/claude.d.ts +1 -1
  51. package/dist/server/providers/claude.d.ts.map +1 -1
  52. package/dist/server/providers/claude.js +2 -2
  53. package/dist/server/providers/codex-app-server.d.ts +1 -1
  54. package/dist/server/providers/codex-app-server.d.ts.map +1 -1
  55. package/dist/server/providers/codex-app-server.js +1 -1
  56. package/dist/server/providers/codex-events.js +1 -1
  57. package/dist/server/providers/codex.d.ts +1 -1
  58. package/dist/server/providers/codex.d.ts.map +1 -1
  59. package/dist/server/providers/codex.js +2 -2
  60. package/dist/server/{runtime/provider-contract.d.ts → providers/contract.d.ts} +1 -1
  61. package/dist/server/providers/contract.d.ts.map +1 -0
  62. package/dist/server/providers/factory.d.ts +1 -1
  63. package/dist/server/providers/factory.d.ts.map +1 -1
  64. package/dist/server/providers/kimi-events.d.ts +1 -1
  65. package/dist/server/providers/kimi-events.d.ts.map +1 -1
  66. package/dist/server/providers/kimi-events.js +1 -1
  67. package/dist/server/providers/kimi.d.ts +1 -1
  68. package/dist/server/providers/kimi.d.ts.map +1 -1
  69. package/dist/server/providers/kimi.js +3 -2
  70. package/dist/server/providers/reasoning-events.js +1 -1
  71. package/dist/server/runtime/active-run-control.d.ts +19 -0
  72. package/dist/server/runtime/active-run-control.d.ts.map +1 -0
  73. package/dist/server/runtime/active-run-control.js +89 -0
  74. package/dist/server/runtime/activity.js +1 -1
  75. package/dist/server/runtime/agent-runner.d.ts +1 -1
  76. package/dist/server/runtime/agent-runner.d.ts.map +1 -1
  77. package/dist/server/runtime/delivery-prompt.d.ts +6 -0
  78. package/dist/server/runtime/delivery-prompt.d.ts.map +1 -1
  79. package/dist/server/runtime/delivery-prompt.js +53 -34
  80. package/dist/server/runtime/followup-appender.d.ts +20 -0
  81. package/dist/server/runtime/followup-appender.d.ts.map +1 -0
  82. package/dist/server/runtime/followup-appender.js +128 -0
  83. package/dist/server/runtime/host.d.ts +2 -0
  84. package/dist/server/runtime/host.d.ts.map +1 -1
  85. package/dist/server/runtime/host.js +8 -0
  86. package/dist/server/runtime/provider-runner.d.ts +16 -0
  87. package/dist/server/runtime/provider-runner.d.ts.map +1 -0
  88. package/dist/server/runtime/provider-runner.js +86 -0
  89. package/dist/server/runtime/runtime-bridge.d.ts +1 -1
  90. package/dist/server/runtime/runtime-bridge.d.ts.map +1 -1
  91. package/dist/server/runtime/runtime-session.service.d.ts +1 -1
  92. package/dist/server/runtime/runtime-session.service.d.ts.map +1 -1
  93. package/dist/server/runtime/runtime-session.service.js +1 -1
  94. package/dist/server/runtime/runtime-worker.d.ts +2 -10
  95. package/dist/server/runtime/runtime-worker.d.ts.map +1 -1
  96. package/dist/server/runtime/runtime-worker.js +60 -286
  97. package/dist/server/runtime-management/managed-runtime.d.ts.map +1 -0
  98. package/dist/server/runtime-management/runtime-release.d.ts.map +1 -0
  99. package/dist/server/{runtime → runtime-management}/runtime-upgrade.d.ts +1 -0
  100. package/dist/server/runtime-management/runtime-upgrade.d.ts.map +1 -0
  101. package/dist/server/{runtime → runtime-management}/runtime-upgrade.js +14 -0
  102. package/dist/server/services/system.service.d.ts +41 -0
  103. package/dist/server/services/system.service.d.ts.map +1 -0
  104. package/dist/server/services/system.service.js +154 -0
  105. package/dist/server/settings/settings.service.d.ts +2 -1
  106. package/dist/server/settings/settings.service.d.ts.map +1 -1
  107. package/dist/server/settings/settings.service.js +9 -1
  108. package/dist/server/slack/shortcuts.d.ts +0 -3
  109. package/dist/server/slack/shortcuts.d.ts.map +1 -1
  110. package/dist/server/slack/shortcuts.js +0 -3
  111. package/dist/server/slack-interactions/shortcut-ids.d.ts +4 -0
  112. package/dist/server/slack-interactions/shortcut-ids.d.ts.map +1 -0
  113. package/dist/server/slack-interactions/shortcut-ids.js +3 -0
  114. package/dist/server/slack-interactions/shortcut-views.d.ts +65 -0
  115. package/dist/server/slack-interactions/shortcut-views.d.ts.map +1 -0
  116. package/dist/server/slack-interactions/shortcut-views.js +211 -0
  117. package/dist/server/{slack → slack-interactions}/shortcut.service.d.ts +2 -47
  118. package/dist/server/slack-interactions/shortcut.service.d.ts.map +1 -0
  119. package/dist/server/slack-interactions/shortcut.service.js +227 -0
  120. package/dist/server/storage/jsonl-log.d.ts +9 -0
  121. package/dist/server/storage/jsonl-log.d.ts.map +1 -1
  122. package/dist/server/storage/jsonl-log.js +36 -1
  123. package/dist/server/storage/schema/server.store.d.ts +5 -0
  124. package/dist/server/storage/schema/server.store.d.ts.map +1 -1
  125. package/dist/server/storage/schema/server.store.js +2 -1
  126. package/dist/server/storage/schema/wake-queue.store.d.ts +15 -1
  127. package/dist/server/storage/schema/wake-queue.store.d.ts.map +1 -1
  128. package/dist/server/storage/schema/wake-queue.store.js +5 -3
  129. package/dist/server/tests/agent-config.test.d.ts +2 -0
  130. package/dist/server/tests/agent-config.test.d.ts.map +1 -0
  131. package/dist/server/tests/agent-config.test.js +229 -0
  132. package/dist/server/tests/agent-runtime.test.d.ts +2 -0
  133. package/dist/server/tests/agent-runtime.test.d.ts.map +1 -0
  134. package/dist/server/tests/agent-runtime.test.js +1110 -0
  135. package/dist/server/tests/agent-seed-memory.test.d.ts +2 -0
  136. package/dist/server/tests/agent-seed-memory.test.d.ts.map +1 -0
  137. package/dist/server/tests/agent-seed-memory.test.js +16 -0
  138. package/dist/server/tests/agent-skills.test.d.ts +2 -0
  139. package/dist/server/tests/agent-skills.test.d.ts.map +1 -0
  140. package/dist/server/tests/agent-skills.test.js +140 -0
  141. package/dist/server/tests/anima-home.d.ts +2 -0
  142. package/dist/server/tests/anima-home.d.ts.map +1 -0
  143. package/dist/server/tests/anima-home.js +4 -0
  144. package/dist/server/tests/cli-file.test.d.ts +2 -0
  145. package/dist/server/tests/cli-file.test.d.ts.map +1 -0
  146. package/dist/server/tests/cli-file.test.js +596 -0
  147. package/dist/server/tests/cli-message.test.d.ts +2 -0
  148. package/dist/server/tests/cli-message.test.d.ts.map +1 -0
  149. package/dist/server/tests/cli-message.test.js +1196 -0
  150. package/dist/server/tests/client-error-routes.test.d.ts +2 -0
  151. package/dist/server/tests/client-error-routes.test.d.ts.map +1 -0
  152. package/dist/server/tests/client-error-routes.test.js +163 -0
  153. package/dist/server/tests/config.test.d.ts +2 -0
  154. package/dist/server/tests/config.test.d.ts.map +1 -0
  155. package/dist/server/tests/config.test.js +497 -0
  156. package/dist/server/tests/default-skills.test.d.ts +2 -0
  157. package/dist/server/tests/default-skills.test.d.ts.map +1 -0
  158. package/dist/server/tests/default-skills.test.js +57 -0
  159. package/dist/server/tests/helpers/inbox.d.ts +10 -0
  160. package/dist/server/tests/helpers/inbox.d.ts.map +1 -0
  161. package/dist/server/tests/helpers/inbox.js +21 -0
  162. package/dist/server/tests/helpers/slack.d.ts +11 -0
  163. package/dist/server/tests/helpers/slack.d.ts.map +1 -0
  164. package/dist/server/tests/helpers/slack.js +17 -0
  165. package/dist/server/tests/helpers/state.d.ts +19 -0
  166. package/dist/server/tests/helpers/state.d.ts.map +1 -0
  167. package/dist/server/tests/helpers/state.js +69 -0
  168. package/dist/server/tests/inbox-slack-events.test.d.ts +2 -0
  169. package/dist/server/tests/inbox-slack-events.test.d.ts.map +1 -0
  170. package/dist/server/tests/inbox-slack-events.test.js +271 -0
  171. package/dist/server/tests/inbox.test.d.ts +2 -0
  172. package/dist/server/tests/inbox.test.d.ts.map +1 -0
  173. package/dist/server/tests/inbox.test.js +144 -0
  174. package/dist/server/tests/interactive-ask.test.d.ts +2 -0
  175. package/dist/server/tests/interactive-ask.test.d.ts.map +1 -0
  176. package/dist/server/tests/interactive-ask.test.js +387 -0
  177. package/dist/server/tests/kb.test.d.ts +2 -0
  178. package/dist/server/tests/kb.test.d.ts.map +1 -0
  179. package/dist/server/tests/kb.test.js +371 -0
  180. package/dist/server/tests/message.service.test.d.ts +2 -0
  181. package/dist/server/tests/message.service.test.d.ts.map +1 -0
  182. package/dist/server/tests/message.service.test.js +151 -0
  183. package/dist/server/tests/messages.test.d.ts +2 -0
  184. package/dist/server/tests/messages.test.d.ts.map +1 -0
  185. package/dist/server/tests/messages.test.js +12 -0
  186. package/dist/server/tests/prompt-attachments.test.d.ts +2 -0
  187. package/dist/server/tests/prompt-attachments.test.d.ts.map +1 -0
  188. package/dist/server/tests/prompt-attachments.test.js +274 -0
  189. package/dist/server/tests/provider-usage.test.d.ts +2 -0
  190. package/dist/server/tests/provider-usage.test.d.ts.map +1 -0
  191. package/dist/server/tests/provider-usage.test.js +97 -0
  192. package/dist/server/tests/reminders.test.d.ts +2 -0
  193. package/dist/server/tests/reminders.test.d.ts.map +1 -0
  194. package/dist/server/tests/reminders.test.js +383 -0
  195. package/dist/server/tests/runtime-cli.test.d.ts +2 -0
  196. package/dist/server/tests/runtime-cli.test.d.ts.map +1 -0
  197. package/dist/server/tests/runtime-cli.test.js +21 -0
  198. package/dist/server/tests/runtime-upgrade.test.d.ts +2 -0
  199. package/dist/server/tests/runtime-upgrade.test.d.ts.map +1 -0
  200. package/dist/server/tests/runtime-upgrade.test.js +307 -0
  201. package/dist/server/tests/runtime-worker.test.d.ts +2 -0
  202. package/dist/server/tests/runtime-worker.test.d.ts.map +1 -0
  203. package/dist/server/tests/runtime-worker.test.js +1021 -0
  204. package/dist/server/tests/runtime.test.d.ts +2 -0
  205. package/dist/server/tests/runtime.test.d.ts.map +1 -0
  206. package/dist/server/tests/runtime.test.js +354 -0
  207. package/dist/server/tests/services.test.d.ts +2 -0
  208. package/dist/server/tests/services.test.d.ts.map +1 -0
  209. package/dist/server/tests/services.test.js +636 -0
  210. package/dist/server/tests/slack-files.test.d.ts +2 -0
  211. package/dist/server/tests/slack-files.test.d.ts.map +1 -0
  212. package/dist/server/tests/slack-files.test.js +118 -0
  213. package/dist/server/tests/slack-shortcuts.test.d.ts +2 -0
  214. package/dist/server/tests/slack-shortcuts.test.d.ts.map +1 -0
  215. package/dist/server/tests/slack-shortcuts.test.js +310 -0
  216. package/dist/server/tests/slack.test.d.ts +2 -0
  217. package/dist/server/tests/slack.test.d.ts.map +1 -0
  218. package/dist/server/tests/slack.test.js +213 -0
  219. package/dist/server/tests/state-cache.test.d.ts +2 -0
  220. package/dist/server/tests/state-cache.test.d.ts.map +1 -0
  221. package/dist/server/tests/state-cache.test.js +184 -0
  222. package/dist/server/tests/subscriptions.test.d.ts +2 -0
  223. package/dist/server/tests/subscriptions.test.d.ts.map +1 -0
  224. package/dist/server/tests/subscriptions.test.js +292 -0
  225. package/dist/server/tests/system-service.test.d.ts +2 -0
  226. package/dist/server/tests/system-service.test.d.ts.map +1 -0
  227. package/dist/server/tests/system-service.test.js +20 -0
  228. package/dist/server/tests/url-routes.test.d.ts +2 -0
  229. package/dist/server/tests/url-routes.test.d.ts.map +1 -0
  230. package/dist/server/tests/url-routes.test.js +217 -0
  231. package/dist/server/tests/web-api.test.d.ts +2 -0
  232. package/dist/server/tests/web-api.test.d.ts.map +1 -0
  233. package/dist/server/tests/web-api.test.js +1759 -0
  234. package/dist/server/web/app.d.ts.map +1 -1
  235. package/dist/server/web/app.js +2 -0
  236. package/dist/server/web/client-error-routes.d.ts +3 -0
  237. package/dist/server/web/client-error-routes.d.ts.map +1 -0
  238. package/dist/server/web/client-error-routes.js +101 -0
  239. package/dist/server/web/system-routes.d.ts.map +1 -1
  240. package/dist/server/web/system-routes.js +21 -131
  241. package/dist/shared/agent-config.d.ts +8 -18
  242. package/dist/shared/agent-config.d.ts.map +1 -1
  243. package/dist/shared/agent-config.js +3 -4
  244. package/dist/shared/inbox.d.ts +27 -0
  245. package/dist/shared/inbox.d.ts.map +1 -1
  246. package/dist/shared/inbox.js +1 -0
  247. package/dist/shared/server-info.d.ts +6 -5
  248. package/dist/shared/server-info.d.ts.map +1 -1
  249. package/dist/shared/server-info.js +3 -1
  250. package/dist/shared/server-settings.d.ts +6 -0
  251. package/dist/shared/server-settings.d.ts.map +1 -1
  252. package/dist/shared/server-settings.js +1 -0
  253. package/dist/shared/skills.d.ts +24 -3
  254. package/dist/shared/skills.d.ts.map +1 -1
  255. package/dist/web/assets/activity-DHjip8tY.js +7 -0
  256. package/dist/web/assets/clipboard-D2NSC52v.js +1 -0
  257. package/dist/web/assets/{external-link-Dt0qTpmV.js → external-link-Bhr1WdRS.js} +1 -1
  258. package/dist/web/assets/{file-text-Dd9ys6py.js → file-text-CYMA_SMA.js} +1 -1
  259. package/dist/web/assets/{index-CzAQnbuO.js → index-CVpew409.js} +5 -5
  260. package/dist/web/assets/index-mh0uJTkY.css +2 -0
  261. package/dist/web/assets/kb-DDVpYkT9.js +35 -0
  262. package/dist/web/assets/profile-CBy6GNzp.js +1 -0
  263. package/dist/web/assets/reminders-Bbwb9QNT.js +1 -0
  264. package/dist/web/favicon.svg +1 -1
  265. package/dist/web/index.html +2 -2
  266. package/package.json +1 -1
  267. package/templates/runtime-standing-prompt.md +16 -2
  268. package/templates/skills/find-skills/SKILL.md +133 -0
  269. package/dist/server/runtime/activity-payload.d.ts.map +0 -1
  270. package/dist/server/runtime/activity-text.d.ts +0 -4
  271. package/dist/server/runtime/activity-text.d.ts.map +0 -1
  272. package/dist/server/runtime/activity-text.js +0 -16
  273. package/dist/server/runtime/managed-runtime.d.ts.map +0 -1
  274. package/dist/server/runtime/provider-contract.d.ts.map +0 -1
  275. package/dist/server/runtime/runtime-release.d.ts.map +0 -1
  276. package/dist/server/runtime/runtime-upgrade.d.ts.map +0 -1
  277. package/dist/server/slack/shortcut.service.d.ts.map +0 -1
  278. package/dist/server/slack/shortcut.service.js +0 -493
  279. package/dist/web/assets/activity-BP0C81IV.js +0 -7
  280. package/dist/web/assets/globe-DmDZSgDP.js +0 -1
  281. package/dist/web/assets/index-DKecrvN0.css +0 -2
  282. package/dist/web/assets/kb-3wFeP2Jt.js +0 -32
  283. package/dist/web/assets/profile-D7wPZXW_.js +0 -1
  284. package/dist/web/assets/reminders-DOhroVwo.js +0 -1
  285. /package/dist/server/{runtime/provider-contract.js → providers/contract.js} +0 -0
  286. /package/dist/server/{runtime → runtime-management}/managed-runtime.d.ts +0 -0
  287. /package/dist/server/{runtime → runtime-management}/managed-runtime.js +0 -0
  288. /package/dist/server/{runtime → runtime-management}/runtime-release.d.ts +0 -0
  289. /package/dist/server/{runtime → runtime-management}/runtime-release.js +0 -0
@@ -0,0 +1,1110 @@
1
+ import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { createAgentRuntime } from '../providers/factory.js';
7
+ import { CLAUDE_DISALLOWED_TOOLS } from '../providers/claude.js';
8
+ import { AgentRuntimeBridge } from '../runtime/runtime-bridge.js';
9
+ import { makeSlackEvent } from './helpers/slack.js';
10
+ import { ingestEvent } from './helpers/inbox.js';
11
+ import { allActivities, loadState } from './helpers/state.js';
12
+ import { activitiesForInboxItemWindow } from '../runtime/item-activities.js';
13
+ import { defaultAgentRegistryService } from '../agents/agent.service.js';
14
+ import { withAnimaHome } from './anima-home.js';
15
+ async function runtimeInput(runtime, context, state) {
16
+ return new AgentRuntimeBridge(runtime).runInput({
17
+ context,
18
+ profile: { displayName: 'Anima' },
19
+ session: state?.sessions[context.agentId],
20
+ });
21
+ }
22
+ async function runtimeFollowupInput(runtime, activeContext, context, _state) {
23
+ return new AgentRuntimeBridge(runtime).followupInput({ activeContext, context });
24
+ }
25
+ test('codex-cli app-server transport starts a turn and appends subscription follow-up input', async () => {
26
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
27
+ let runtime;
28
+ try {
29
+ await withAnimaHome(stateDir, async () => {
30
+ const callsPath = join(stateDir, 'codex-app-server-calls.jsonl');
31
+ const fakeCodex = join(stateDir, 'codex');
32
+ await writeFile(fakeCodex, [
33
+ '#!/usr/bin/env node',
34
+ "import { appendFileSync } from 'node:fs';",
35
+ "import readline from 'node:readline';",
36
+ "const rl = readline.createInterface({ input: process.stdin });",
37
+ "const send = (message) => process.stdout.write(JSON.stringify(message) + '\\n');",
38
+ "let turnCount = 0;",
39
+ "rl.on('line', (line) => {",
40
+ " const msg = JSON.parse(line);",
41
+ " appendFileSync(process.env.CALLS_PATH, JSON.stringify(msg) + '\\n');",
42
+ " if (msg.method === 'initialize') {",
43
+ " send({ id: msg.id, result: { userAgent: 'fake-codex' } });",
44
+ " return;",
45
+ " }",
46
+ " if (msg.method === 'initialized') return;",
47
+ " if (msg.method === 'thread/start') {",
48
+ " if (msg.params.approvalPolicy !== 'never') process.exit(30);",
49
+ " if (msg.params.sandbox !== 'danger-full-access') process.exit(31);",
50
+ " if (msg.params.model !== 'gpt-test') process.exit(32);",
51
+ " if (msg.params.config.model_reasoning_effort !== 'xhigh') process.exit(33);",
52
+ " if (msg.params.config.model_reasoning_summary !== 'auto') process.exit(330);",
53
+ " if (!msg.params.developerInstructions.includes('You are Anima, general-purpose Anima agent.')) process.exit(34);",
54
+ " if (!msg.params.developerInstructions.includes('anima message send --channel')) process.exit(35);",
55
+ " send({ id: msg.id, result: { thread: { id: 'codex-thread-1', cwd: process.cwd(), cliVersion: 'test' } } });",
56
+ " return;",
57
+ " }",
58
+ " if (msg.method === 'thread/resume') process.exit(36);",
59
+ " if (msg.method === 'turn/start') {",
60
+ " turnCount += 1;",
61
+ " const prompt = msg.params.input[0].text;",
62
+ " if (prompt.includes('You are Anima, general-purpose Anima agent.')) process.exit(37);",
63
+ " if (!prompt.includes('New Slack message:')) process.exit(38);",
64
+ " if ('cwd' in msg.params || 'model' in msg.params || 'effort' in msg.params) process.exit(39);",
65
+ " if (turnCount === 1) {",
66
+ " if (prompt.includes('fresh session after rotate')) {",
67
+ " send({ id: msg.id, result: { turn: { id: 'turn-fresh', status: 'inProgress', items: [], itemsView: 'full', error: null, startedAt: 5, completedAt: null, durationMs: null } } });",
68
+ " send({ method: 'item/agentMessage/delta', params: { threadId: 'codex-thread-1', turnId: 'turn-fresh', itemId: 'item-fresh', delta: 'handled fresh' } });",
69
+ " send({ method: 'turn/completed', params: { threadId: 'codex-thread-1', turn: { id: 'turn-fresh', status: 'completed', items: [], itemsView: 'full', error: null, startedAt: 5, completedAt: 6, durationMs: 1000 } } });",
70
+ " return;",
71
+ " }",
72
+ " if (!prompt.includes('first message')) process.exit(40);",
73
+ " send({ id: msg.id, result: { turn: { id: 'turn-1', status: 'inProgress', items: [], itemsView: 'full', error: null, startedAt: 1, completedAt: null, durationMs: null } } });",
74
+ " process.stdout.write('not-json from codex app-server\\n' + JSON.stringify({ method: 'item/started', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'cmd-1', type: 'commandExecution', command: \"/bin/zsh -lc \\'sed -n 1,20p server/providers/codex.ts\\'\" } } }) + '\\n');",
75
+ " send({ method: 'item/completed', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'cmd-1', type: 'commandExecution', command: \"/bin/zsh -lc 'sed -n 1,20p server/providers/codex.ts'\", exitCode: 0, aggregatedOutput: 'ok' } } });",
76
+ " send({ method: 'item/started', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'cmd-anima-1', type: 'commandExecution', command: \"/bin/zsh -lc 'anima message react --channel C1 --message-ts 1.2 --name white_check_mark'\" } } });",
77
+ " send({ method: 'item/completed', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'cmd-anima-1', type: 'commandExecution', command: \"/bin/zsh -lc 'anima message react --channel C1 --message-ts 1.2 --name white_check_mark'\", exitCode: 0, aggregatedOutput: 'reaction added successfully.' } } });",
78
+ " send({ method: 'item/started', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'cmd-2', type: 'commandExecution', command: 'pnpm missing-script' } } });",
79
+ " send({ method: 'item/completed', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'cmd-2', type: 'commandExecution', command: 'pnpm missing-script', exitCode: 1, aggregatedOutput: 'ERR_PNPM_NO_SCRIPT Missing script' } } });",
80
+ " send({ method: 'item/started', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'web-1', type: 'webSearch', action: { type: 'search', query: 'activity log display query' } } } });",
81
+ " send({ method: 'item/completed', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'web-1', type: 'webSearch', action: { type: 'search', query: 'activity log display query' }, status: 'completed' } } });",
82
+ " send({ method: 'item/started', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'task-parent-1', type: 'mcpToolCall', server: 'codex', tool: 'Agent' } } });",
83
+ " send({ method: 'item/started', params: { threadId: 'codex-child-thread', turnId: 'turn-1', item: { id: 'cmd-sub-1', type: 'commandExecution', command: 'cat package.json', parentToolCallId: 'task-parent-1', subRunId: 'codex-child-1', agent_nickname: 'Pascal', agent_role: 'explorer', depth: 1 } } });",
84
+ " send({ method: 'item/agentMessage/delta', params: { threadId: 'codex-child-thread', turnId: 'turn-1', itemId: 'child-text-1', delta: 'child draft', parentToolCallId: 'task-parent-1', subRunId: 'codex-child-1', agent_nickname: 'Pascal', agent_role: 'explorer', depth: 1 } });",
85
+ " send({ method: 'item/started', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'compact-1', type: 'contextCompaction' } } });",
86
+ " send({ method: 'item/completed', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'compact-1', type: 'contextCompaction', status: 'completed' } } });",
87
+ " send({ method: 'item/started', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'reasoning-1', type: 'reasoning', summary: [], content: [] } } });",
88
+ " send({ method: 'item/reasoning/summaryPartAdded', params: { threadId: 'codex-thread-1', turnId: 'turn-1', itemId: 'reasoning-1', summaryIndex: 0 } });",
89
+ " send({ method: 'item/reasoning/summaryTextDelta', params: { threadId: 'codex-thread-1', turnId: 'turn-1', itemId: 'reasoning-1', summaryIndex: 0, delta: 'Inspecting runtime events.' } });",
90
+ " send({ method: 'item/reasoning/textDelta', params: { threadId: 'codex-thread-1', turnId: 'turn-1', itemId: 'reasoning-1', contentIndex: 0, delta: 'raw reasoning for open models' } });",
91
+ " send({ method: 'item/completed', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { id: 'reasoning-1', type: 'reasoning', summary: ['Inspecting runtime events.'], content: ['raw reasoning for open models'] } } });",
92
+ " send({ method: 'turn/plan/updated', params: { threadId: 'codex-thread-1', turnId: 'turn-1', explanation: 'Plan changed', plan: [{ step: 'Read provider', status: 'completed' }, { step: 'Record events', status: 'inProgress' }] } });",
93
+ " send({ method: 'turn/diff/updated', params: { threadId: 'codex-thread-1', turnId: 'turn-1', diff: 'diff --git a/server/providers/codex.ts b/server/providers/codex.ts' } });",
94
+ " send({ method: 'rawResponseItem/completed', params: { threadId: 'codex-thread-1', turnId: 'turn-1', item: { type: 'reasoning', summary: [{ text: 'summary' }], content: [{ type: 'reasoning_text', text: 'do not store raw' }], encrypted_content: 'secret-ciphertext' } } });",
95
+ " send({ method: 'account/rateLimits/updated', params: { rateLimits: { limitId: 'primary', limitName: 'Primary', primary: { usedPercent: 42, windowDurationMins: 300, resetsAt: 1770000000 }, secondary: null, planType: 'pro', rateLimitReachedType: null } } });",
96
+ " send({ method: 'model/rerouted', params: { threadId: 'codex-thread-1', turnId: 'turn-1', fromModel: 'gpt-test', toModel: 'gpt-fallback', reason: 'unavailable' } });",
97
+ " send({ method: 'warning', params: { threadId: 'codex-thread-1', message: 'non-fatal warning' } });",
98
+ " send({ method: 'item/agentMessage/delta', params: { threadId: 'codex-thread-1', turnId: 'turn-1', itemId: 'item-1', delta: 'handled first' } });",
99
+ " return;",
100
+ " }",
101
+ " if (!prompt.includes('third message')) process.exit(43);",
102
+ " send({ id: msg.id, result: { turn: { id: 'turn-2', status: 'inProgress', items: [], itemsView: 'full', error: null, startedAt: 3, completedAt: null, durationMs: null } } });",
103
+ " send({ method: 'item/agentMessage/delta', params: { threadId: 'codex-thread-1', turnId: 'turn-2', itemId: 'item-2', delta: 'handled third' } });",
104
+ " send({ method: 'turn/completed', params: { threadId: 'codex-thread-1', turn: { id: 'turn-2', status: 'completed', items: [], itemsView: 'full', error: null, startedAt: 3, completedAt: 4, durationMs: 1000 } } });",
105
+ " return;",
106
+ " }",
107
+ " if (msg.method === 'turn/steer') {",
108
+ " if (msg.params.expectedTurnId !== 'turn-1') process.exit(41);",
109
+ " if (!msg.params.input[0].text.includes('second message')) process.exit(42);",
110
+ " send({ id: msg.id, result: { turnId: 'turn-1' } });",
111
+ " send({ method: 'item/agentMessage/delta', params: { threadId: 'codex-thread-1', turnId: 'turn-1', itemId: 'item-1', delta: ' + appended second' } });",
112
+ " send({ method: 'thread/tokenUsage/updated', params: { threadId: 'codex-thread-1', turnId: 'turn-1', tokenUsage: { last: { inputTokens: 1111, cachedInputTokens: 222, outputTokens: 33, reasoningOutputTokens: 44, totalTokens: 1366 }, total: { inputTokens: 2111, cachedInputTokens: 333, outputTokens: 55, reasoningOutputTokens: 66, totalTokens: 2166 }, modelContextWindow: 200000 } } });",
113
+ " send({ method: 'turn/completed', params: { threadId: 'codex-thread-1', turn: { id: 'turn-1', status: 'completed', model: 'gpt-test', usage: { inputTokens: 1111, cachedInputTokens: 222, outputTokens: 33, totalTokens: 1366 }, items: [], itemsView: 'full', error: null, startedAt: 1, completedAt: 2, durationMs: 1000 } } });",
114
+ " }",
115
+ "});",
116
+ '',
117
+ ].join('\n'), 'utf8');
118
+ await chmod(fakeCodex, 0o755);
119
+ const config = { agentId: 'anima', stateDir };
120
+ const firstCtx = await ingestEvent(makeSlackEvent({
121
+ channelId: 'D-anima',
122
+ teamId: 'T-demo',
123
+ text: 'first message',
124
+ userId: 'U1',
125
+ }), config);
126
+ const secondCtx = await ingestEvent(makeSlackEvent({
127
+ channelId: 'D-anima',
128
+ teamId: 'T-demo',
129
+ text: 'second message',
130
+ userId: 'U1',
131
+ }), config);
132
+ runtime = createAgentRuntime({
133
+ env: runtimeTestEnv(stateDir, { CALLS_PATH: callsPath }),
134
+ kind: 'codex-cli',
135
+ model: 'gpt-test',
136
+ reasoningEffort: 'xhigh',
137
+ });
138
+ const runPromise = runtime.run(await runtimeInput(runtime, firstCtx, await loadState()));
139
+ await waitFor(async () => (await readFile(callsPath, 'utf8')).includes('"method":"turn/start"'));
140
+ assert.deepEqual(await runtime.appendToActiveRun(await runtimeFollowupInput(runtime, firstCtx, secondCtx, await loadState())), { accepted: true, text: 'appended to turn-1' });
141
+ assert.equal((await runPromise).text, 'handled first + appended second');
142
+ const calls = (await readFile(callsPath, 'utf8')).trim().split('\n').map((line) => JSON.parse(line));
143
+ assert.ok(calls.some((call) => call.method === 'turn/steer'));
144
+ const stateAfterRun = await loadState();
145
+ assert.equal(stateAfterRun.sessions.anima?.current?.id, 'codex-thread-1');
146
+ const activities = await activitiesForInboxItemWindow('anima', firstCtx.item.id);
147
+ const started = activities.find((activity) => activity.type === 'tool.call.started' && activity.payload?.['providerToolId'] === 'cmd-1');
148
+ const failed = activities.find((activity) => activity.type === 'tool.call.failed' && activity.payload?.['providerToolId'] === 'cmd-2');
149
+ assert.equal(started?.payload?.['tool'], 'codex.shell');
150
+ assert.equal(started?.payload?.['command'], 'sed -n 1,20p server/providers/codex.ts');
151
+ assert.ok(activities.some((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'codex.protocol.invalid_json'));
152
+ assert.equal(failed?.payload?.['tool'], 'codex.shell');
153
+ assert.equal(failed?.payload?.['command'], 'pnpm missing-script');
154
+ assert.match(String(failed?.payload?.['error']), /ERR_PNPM_NO_SCRIPT/);
155
+ const webSearch = activities.find((activity) => activity.type === 'tool.call.started' && activity.payload?.['providerToolId'] === 'web-1');
156
+ assert.equal(webSearch?.payload?.['tool'], 'codex.webSearch');
157
+ assert.equal(webSearch?.payload?.['query'], 'activity log display query');
158
+ assert.equal(webSearch?.payload?.['target'], 'activity log display query');
159
+ const subagentTool = activities.find((activity) => activity.type === 'tool.call.started' && activity.payload?.['providerToolId'] === 'cmd-sub-1');
160
+ assert.equal(subagentTool?.payload?.['parentToolCallId'], 'task-parent-1');
161
+ assert.equal(subagentTool?.payload?.['subRunId'], 'codex-child-1');
162
+ assert.equal(subagentTool?.payload?.['name'], 'Pascal');
163
+ assert.equal(subagentTool?.payload?.['role'], 'explorer');
164
+ assert.equal(subagentTool?.payload?.['depth'], 1);
165
+ const childText = activities.find((activity) => activity.type === 'agent.text' && activity.payload?.['subRunId'] === 'codex-child-1');
166
+ assert.equal(childText?.payload?.['text'], 'child draft');
167
+ assert.equal(childText?.payload?.['parentToolCallId'], 'task-parent-1');
168
+ assert.equal(activities.some((activity) => activity.payload?.['providerToolId'] === 'cmd-anima-1'), false);
169
+ const allRunActivities = allActivities(stateAfterRun);
170
+ const compactStarted = allRunActivities.find((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'codex.compact.started');
171
+ const compactCompleted = allRunActivities.find((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'codex.compact.completed');
172
+ const stats = allRunActivities.find((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'codex.session.stats');
173
+ assert.ok(compactStarted);
174
+ assert.ok(compactCompleted);
175
+ assert.equal(stats?.payload?.['model'], 'gpt-test');
176
+ assert.equal(stats?.payload?.['inputTokens'], 1111);
177
+ assert.equal(stats?.payload?.['cacheReadInputTokens'], 222);
178
+ assert.equal(stats?.payload?.['outputTokens'], 33);
179
+ assert.equal(stats?.payload?.['totalTokens'], 1366);
180
+ assert.equal(stats?.payload?.['terminalReason'], 'completed');
181
+ for (const hiddenEventType of [
182
+ 'codex.context.stats',
183
+ 'codex.reasoning.started',
184
+ 'codex.reasoning.summary_delta',
185
+ 'provider.reasoning',
186
+ 'codex.reasoning.completed',
187
+ 'codex.plan.updated',
188
+ 'codex.diff.updated',
189
+ 'codex.raw_response_item.completed',
190
+ 'codex.item.commandExecution.outputDelta',
191
+ ]) {
192
+ assert.equal(activities.some((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === hiddenEventType), false);
193
+ }
194
+ const rateLimits = activities.find((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'codex.rate_limits.updated');
195
+ assert.deepEqual(rateLimits?.payload?.['primary'], { usedPercent: 42, windowDurationMins: 300, resetsAt: 1770000000 });
196
+ const rerouted = activities.find((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'codex.model.rerouted');
197
+ assert.equal(rerouted?.payload?.['toModel'], 'gpt-fallback');
198
+ const warning = activities.find((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'codex.warning');
199
+ assert.equal(warning?.payload?.['message'], 'non-fatal warning');
200
+ const thirdCtx = await ingestEvent(makeSlackEvent({
201
+ channelId: 'D-anima',
202
+ teamId: 'T-demo',
203
+ text: 'third message',
204
+ userId: 'U1',
205
+ }), config);
206
+ assert.equal((await runtime.run(await runtimeInput(runtime, thirdCtx, await loadState()))).text, 'handled third');
207
+ const finalCalls = (await readFile(callsPath, 'utf8')).trim().split('\n').map((line) => JSON.parse(line));
208
+ assert.equal(finalCalls.filter((call) => call.method === 'initialize').length, 1);
209
+ assert.equal(finalCalls.filter((call) => call.method === 'thread/start').length, 1);
210
+ assert.equal(finalCalls.some((call) => call.method === 'thread/resume'), false);
211
+ const fourthCtx = await ingestEvent(makeSlackEvent({
212
+ channelId: 'D-anima',
213
+ teamId: 'T-demo',
214
+ text: 'fresh session after rotate',
215
+ userId: 'U1',
216
+ }), config);
217
+ await defaultAgentRegistryService.serviceFor('anima').rotateSession();
218
+ assert.equal((await runtime.run(await runtimeInput(runtime, fourthCtx, await loadState()))).text, 'handled fresh');
219
+ const postRotateState = await loadState();
220
+ assert.deepEqual(await providerSessionStartedPayload(fourthCtx.item.id), { kind: 'codex-cli', resumed: false });
221
+ assert.ok(postRotateState.sessions.anima?.archived?.some((session) => session.kind === 'codex-cli' && session.id === 'codex-thread-1'));
222
+ const postRotateCalls = (await readFile(callsPath, 'utf8')).trim().split('\n').map((line) => JSON.parse(line));
223
+ assert.equal(postRotateCalls.filter((call) => call.method === 'initialize').length, 2);
224
+ assert.equal(postRotateCalls.filter((call) => call.method === 'thread/start').length, 2);
225
+ assert.equal(postRotateCalls.some((call) => call.method === 'thread/resume'), false);
226
+ });
227
+ }
228
+ finally {
229
+ await runtime?.close?.();
230
+ await rm(stateDir, { force: true, recursive: true });
231
+ }
232
+ });
233
+ test('codex-cli app-server transport fails when process exits before turn completion', async () => {
234
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
235
+ try {
236
+ await withAnimaHome(stateDir, async () => {
237
+ const fakeCodex = join(stateDir, 'codex');
238
+ await writeFile(fakeCodex, [
239
+ '#!/usr/bin/env node',
240
+ "import readline from 'node:readline';",
241
+ "const rl = readline.createInterface({ input: process.stdin });",
242
+ "const send = (message) => process.stdout.write(JSON.stringify(message) + '\\n');",
243
+ "rl.on('line', (line) => {",
244
+ " const msg = JSON.parse(line);",
245
+ " if (msg.method === 'initialize') {",
246
+ " send({ id: msg.id, result: { userAgent: 'fake-codex' } });",
247
+ " return;",
248
+ " }",
249
+ " if (msg.method === 'initialized') return;",
250
+ " if (msg.method === 'thread/start') {",
251
+ " send({ id: msg.id, result: { thread: { id: 'codex-thread-1', cwd: process.cwd(), cliVersion: 'test' } } });",
252
+ " return;",
253
+ " }",
254
+ " if (msg.method === 'turn/start') {",
255
+ " send({ id: msg.id, result: { turn: { id: 'turn-1', status: 'inProgress', items: [], itemsView: 'full', error: null, startedAt: 1, completedAt: null, durationMs: null } } });",
256
+ " setTimeout(() => process.exit(0), 10);",
257
+ " }",
258
+ "});",
259
+ '',
260
+ ].join('\n'), 'utf8');
261
+ await chmod(fakeCodex, 0o755);
262
+ const ctx = await ingestEvent(makeSlackEvent({
263
+ channelId: 'D-anima',
264
+ teamId: 'T-demo',
265
+ text: 'first message',
266
+ userId: 'U1',
267
+ }), { agentId: 'anima', stateDir });
268
+ const runtime = createAgentRuntime({
269
+ env: runtimeTestEnv(stateDir),
270
+ kind: 'codex-cli',
271
+ });
272
+ await assert.rejects(withTimeout(runtime.run(await runtimeInput(runtime, ctx, await loadState())), 1_000), /exited before completing active requests/);
273
+ const activities = await activitiesForInboxItemWindow('anima', ctx.item.id);
274
+ assert.ok(activities.some((activity) => activity.type === 'runtime.failed'));
275
+ });
276
+ }
277
+ finally {
278
+ await rm(stateDir, { force: true, recursive: true });
279
+ }
280
+ });
281
+ test('claude-code runtime streams activity, persists Claude session metadata, and resumes it', async () => {
282
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
283
+ const previousClaudeProjectsDir = process.env.CLAUDE_PROJECTS_DIR;
284
+ try {
285
+ await withAnimaHome(stateDir, async () => {
286
+ const callsPath = join(stateDir, 'claude-calls.jsonl');
287
+ const claudeProjectsDir = join(stateDir, 'claude-projects');
288
+ const claudeSubagentCwd = '/tmp/anima-claude-subagent-cwd';
289
+ const claudeProjectRoot = join(claudeProjectsDir, claudeSubagentCwd.replace(/\/+$/, '').replaceAll('/', '-'));
290
+ const claudeProjectDir = join(claudeProjectRoot, 'claude-session-1', 'subagents');
291
+ const claudeParentTranscriptLog = join(claudeProjectRoot, 'claude-session-1.jsonl');
292
+ const claudeResultSubagentLog = join(claudeProjectDir, 'agent-claude-child-result.jsonl');
293
+ process.env.CLAUDE_PROJECTS_DIR = claudeProjectsDir;
294
+ await mkdir(claudeProjectDir, { recursive: true });
295
+ await writeFile(join(claudeProjectDir, 'agent-claude-child-meta.meta.json'), `${JSON.stringify({ agentType: 'general-purpose', description: 'metadata child', toolUseId: 'toolu_parent_task' })}\n`, 'utf8');
296
+ await writeFile(join(claudeProjectDir, 'agent-claude-child-meta.jsonl'), `${JSON.stringify({ agentId: 'claude-child-meta', type: 'assistant', message: { content: [{ type: 'tool_use', id: 'toolu_child_meta_read', name: 'Read' }] } })}\n`, 'utf8');
297
+ await writeFile(join(claudeProjectDir, 'agent-claude-child-result.meta.json'), `${JSON.stringify({ agentType: 'general-purpose', description: 'result child', toolUseId: 'toolu_result_task' })}\n`, 'utf8');
298
+ const fakeClaude = join(stateDir, 'claude');
299
+ await writeFile(fakeClaude, [
300
+ '#!/usr/bin/env node',
301
+ "import { appendFileSync, readFileSync, writeFileSync } from 'node:fs';",
302
+ "import readline from 'node:readline';",
303
+ "const argv = process.argv.slice(2);",
304
+ "const resumeIndex = argv.indexOf('--resume');",
305
+ "const systemPromptFileIndex = argv.indexOf('--system-prompt-file');",
306
+ "const systemPromptFile = systemPromptFileIndex === -1 ? '' : argv[systemPromptFileIndex + 1];",
307
+ "const systemPrompt = systemPromptFile ? readFileSync(systemPromptFile, 'utf8') : '';",
308
+ 'if (argv.includes("-p")) process.exit(41);',
309
+ 'if (argv.includes("--append-system-prompt")) process.exit(58);',
310
+ 'if (!argv.includes("--verbose")) process.exit(42);',
311
+ 'if (!argv.includes("--include-partial-messages")) process.exit(60);',
312
+ 'if (!argv.includes("--include-hook-events")) process.exit(61);',
313
+ `if (argv[argv.indexOf("--disallowedTools") + 1] !== ${JSON.stringify(CLAUDE_DISALLOWED_TOOLS.join(','))}) process.exit(62);`,
314
+ 'if (argv[argv.indexOf("--output-format") + 1] !== "stream-json") process.exit(43);',
315
+ 'if (argv[argv.indexOf("--permission-mode") + 1] !== "bypassPermissions") process.exit(44);',
316
+ 'if (argv[argv.indexOf("--model") + 1] !== "opus") process.exit(56);',
317
+ 'if (argv[argv.indexOf("--effort") + 1] !== "xhigh") process.exit(57);',
318
+ 'if (process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW !== "200000") process.exit(59);',
319
+ 'if (!systemPrompt.includes("You are Anima, general-purpose Anima agent.")) process.exit(53);',
320
+ 'if (!systemPrompt.includes("anima message send --channel")) process.exit(54);',
321
+ 'console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "claude-session-1", cwd: process.env.CLAUDE_SUBAGENT_CWD, claude_code_version: "test", model: "opus", permissionMode: "bypassPermissions", tools: ["Read", "Bash"], mcp_servers: ["filesystem"], agents: ["Explore"], skills: ["frontend"], plugins: ["Browser"], memory_paths: ["/tmp/MEMORY.md"] }));',
322
+ 'const rl = readline.createInterface({ input: process.stdin });',
323
+ 'let count = 0;',
324
+ "rl.on('line', (line) => {",
325
+ ' count += 1;',
326
+ ' const msg = JSON.parse(line);',
327
+ ' const prompt = msg.message.content[0].text;',
328
+ ' appendFileSync(process.env.CALLS_PATH, JSON.stringify({ argv, count, prompt }) + "\\n");',
329
+ ' if (!prompt.includes("What did I ask?")) process.exit(45);',
330
+ ' if (!prompt.includes("New Slack message:")) process.exit(46);',
331
+ ' if (prompt.includes("\\\"currentEvent\\\"")) process.exit(47);',
332
+ ' if (prompt.includes("You are Anima, general-purpose Anima agent.")) process.exit(51);',
333
+ ' if (prompt.includes("Reply command")) process.exit(52);',
334
+ ' if (count === 2) {',
335
+ ' if (prompt.includes("Recovery context:")) process.exit(49);',
336
+ ' console.log(JSON.stringify({ type: "assistant", message: { usage: { input_tokens: 30, cache_read_input_tokens: 300, cache_creation_input_tokens: 7 }, content: [{ type: "text", text: "checking resumed Claude context" }] }, session_id: "claude-session-1" }));',
337
+ ' console.log(JSON.stringify({ type: "result", subtype: "success", result: "second run", session_id: "claude-session-1", duration_ms: 1200, duration_api_ms: 900, ttft_ms: 42, num_turns: 1, usage: { cache_read_input_tokens: 1234, output_tokens: 12, server_tool_use: { web_search_requests: 1, web_fetch_requests: 2 } }, modelUsage: { "claude-opus-test": { contextWindow: 200000, maxOutputTokens: 32000, costUSD: 0.05 } }, permission_denials: [{ tool_name: "Bash" }], terminal_reason: "completed", fast_mode_state: "disabled" }));',
338
+ ' return;',
339
+ ' }',
340
+ ' if (resumeIndex !== -1) process.exit(48);',
341
+ ' if (prompt.includes("Recovery context:")) process.exit(50);',
342
+ ' console.log(JSON.stringify({ type: "stream_event", event: { type: "message_start", message: { id: "msg_1", model: "claude-opus-test", usage: { input_tokens: 9, cache_read_input_tokens: 90 } } }, ttft_ms: 42, session_id: "claude-session-1" }));',
343
+ ' console.log(JSON.stringify({ type: "stream_event", event: { type: "content_block_start", index: 0, content_block: { type: "tool_use", id: "toolu_read_1", name: "Read", input: {}, caller: { type: "model" } } }, session_id: "claude-session-1" }));',
344
+ ' console.log(JSON.stringify({ type: "stream_event", event: { type: "content_block_delta", index: 1, delta: { type: "thinking_delta", thinking: "checking the file first" } }, session_id: "claude-session-1" }));',
345
+ ' console.log(JSON.stringify({ type: "stream_event", event: { type: "message_delta", delta: { stop_reason: "tool_use", context_management: { applied_edits: [{ type: "clear_tool_uses_20250919" }] } }, usage: { output_tokens: 3 } }, session_id: "claude-session-1" }));',
346
+ ' console.log(JSON.stringify({ type: "rate_limit_event", rate_limit_info: { status: "allowed_warning", rateLimitType: "seven_day", resetsAt: "2026-05-21T00:00:00Z", utilization: 0.26, isUsingOverage: false }, session_id: "claude-session-1" }));',
347
+ ' console.log(JSON.stringify({ type: "assistant", message: { usage: { input_tokens: 10, cache_read_input_tokens: 100, cache_creation_input_tokens: 5 }, content: [{ type: "tool_use", id: "toolu_read_1", name: "Read", input: { file_path: "/tmp/context.md" } }] }, session_id: "claude-session-1" }));',
348
+ ' console.log(JSON.stringify({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: "toolu_read_1", content: "file contents should stay out of agent text", is_error: false }] }, session_id: "claude-session-1" }));',
349
+ ' console.log(JSON.stringify({ type: "assistant", message: { usage: { input_tokens: 11, cache_read_input_tokens: 200, cache_creation_input_tokens: 6 }, content: [{ type: "tool_use", id: "toolu_anima_1", name: "Bash", input: { command: "ANIMA_HOME=/tmp/anima anima file send --channel C1 /tmp/image.png", description: "Upload file" } }] }, session_id: "claude-session-1" }));',
350
+ ' console.log(JSON.stringify({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: "toolu_anima_1", content: "uploaded successfully", is_error: false }] }, session_id: "claude-session-1" }));',
351
+ ' console.log(JSON.stringify({ type: "assistant", message: { usage: { input_tokens: 11, cache_read_input_tokens: 200, cache_creation_input_tokens: 6 }, content: [{ type: "tool_use", id: "toolu_parent_task", name: "Task", input: { description: "Research child" } }] }, session_id: "claude-session-1" }));',
352
+ ' console.log(JSON.stringify({ type: "assistant", parent_tool_use_id: "toolu_parent_task", agentId: "claude-child-1", attributionAgent: "researcher", slug: "child-researcher", message: { usage: { input_tokens: 11, cache_read_input_tokens: 200, cache_creation_input_tokens: 6 }, content: [{ type: "tool_use", id: "toolu_child_read", name: "Read", input: { file_path: "/tmp/child.md" } }] }, session_id: "claude-session-1" }));',
353
+ ' console.log(JSON.stringify({ type: "assistant", parent_tool_use_id: "toolu_parent_task", agentId: "claude-child-1", attributionAgent: "researcher", slug: "child-researcher", message: { usage: { input_tokens: 12, cache_read_input_tokens: 220, cache_creation_input_tokens: 8 }, content: [{ type: "text", text: "child draft summary" }] }, session_id: "claude-session-1" }));',
354
+ ' console.log(JSON.stringify({ type: "assistant", agentId: "claude-child-meta", attributionAgent: "general-purpose", message: { usage: { input_tokens: 11, cache_read_input_tokens: 200, cache_creation_input_tokens: 6 }, content: [{ type: "tool_use", id: "toolu_child_meta_read", name: "Read", input: { file_path: "/tmp/child-meta.md" } }] } }));',
355
+ ' console.log(JSON.stringify({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: "toolu_bash_1", content: "", is_error: false, tool_use_result: { stdout: "command output", stderr: "", interrupted: false, isImage: false, noOutputExpected: false } }] }, session_id: "claude-session-1" }));',
356
+ ' console.log(JSON.stringify({ type: "assistant", message: { content: [{ type: "tool_use", id: "toolu_result_task", name: "Agent", input: { description: "Result child", prompt: "Read the child file." } }] }, session_id: "claude-session-1" }));',
357
+ ' console.log(JSON.stringify({ type: "assistant", message: { content: [{ type: "tool_use", id: "toolu_child_result_read", name: "Read", input: { file_path: "/tmp/child-result.md" } }] }, session_id: "claude-session-1" }));',
358
+ ' setTimeout(() => {',
359
+ ' writeFileSync(process.env.CLAUDE_RESULT_SUBAGENT_LOG, [',
360
+ ' JSON.stringify({ agentId: "claude-child-result", type: "user", message: { role: "user", content: "Read the child file." }, cwd: process.env.CLAUDE_SUBAGENT_CWD, sessionId: "claude-session-1" }),',
361
+ ' JSON.stringify({ agentId: "claude-child-result", attributionAgent: "general-purpose", type: "assistant", message: { content: [{ type: "tool_use", id: "toolu_child_result_read", name: "Read", input: { file_path: "/tmp/child-result.md" } }] }, cwd: process.env.CLAUDE_SUBAGENT_CWD, sessionId: "claude-session-1" }),',
362
+ ' JSON.stringify({ agentId: "claude-child-result", type: "user", message: { content: [{ type: "tool_result", tool_use_id: "toolu_child_result_read", content: "child result contents", is_error: false }] }, cwd: process.env.CLAUDE_SUBAGENT_CWD, sessionId: "claude-session-1" }),',
363
+ ' JSON.stringify({ agentId: "claude-child-result", attributionAgent: "general-purpose", type: "assistant", message: { content: [{ type: "text", text: "child result summary" }] }, cwd: process.env.CLAUDE_SUBAGENT_CWD, sessionId: "claude-session-1" }),',
364
+ ' ].join("\\n") + "\\n", "utf8");',
365
+ ' writeFileSync(process.env.CLAUDE_PARENT_TRANSCRIPT_LOG, JSON.stringify({ type: "user", cwd: process.env.CLAUDE_SUBAGENT_CWD, sessionId: "claude-session-1", message: { content: [{ type: "tool_result", tool_use_id: "toolu_result_task", content: "child result done", is_error: false, tool_use_result: { stdout: "child result done" } }] }, toolUseResult: { status: "completed", agentId: "claude-child-result", agentType: "general-purpose" } }) + "\\n", "utf8");',
366
+ ' console.log(JSON.stringify({ type: "assistant", message: { usage: { input_tokens: 12, cache_read_input_tokens: 220, cache_creation_input_tokens: 8 }, content: [{ type: "text", text: "checking via Claude" }] }, session_id: "claude-session-1" }));',
367
+ ' console.log(JSON.stringify({ type: "system", subtype: "status", status: "compacting", session_id: "claude-session-1" }));',
368
+ ' console.log(JSON.stringify({ type: "system", subtype: "compact_boundary", session_id: "claude-session-1" }));',
369
+ ' console.log(JSON.stringify({ type: "result", subtype: "success", result: "first run", session_id: "claude-session-1", duration_ms: 1200, duration_api_ms: 900, ttft_ms: 42, num_turns: 1, usage: { cache_read_input_tokens: 1000, output_tokens: 10, server_tool_use: { web_search_requests: 1, web_fetch_requests: 2 } }, modelUsage: { "claude-opus-test": { contextWindow: 200000, maxOutputTokens: 32000, costUSD: 0.05 } }, permission_denials: [{ tool_name: "Bash" }], terminal_reason: "completed", fast_mode_state: "disabled" }));',
370
+ ' }, 20);',
371
+ ' return;',
372
+ '});',
373
+ '',
374
+ ].join('\n'), 'utf8');
375
+ await chmod(fakeClaude, 0o755);
376
+ const config = { agentId: 'anima', stateDir };
377
+ await ingestEvent(makeSlackEvent({
378
+ channelId: 'D-anima',
379
+ teamId: 'T-demo',
380
+ text: 'previous context',
381
+ userId: 'U1',
382
+ }), config);
383
+ const firstCtx = await ingestEvent(makeSlackEvent({
384
+ channelId: 'D-anima',
385
+ teamId: 'T-demo',
386
+ text: 'What did I ask?',
387
+ userId: 'U1',
388
+ }), config);
389
+ const runtime = createAgentRuntime({
390
+ env: {
391
+ CALLS_PATH: callsPath,
392
+ ...runtimeTestEnv(stateDir, {
393
+ CLAUDE_RESULT_SUBAGENT_LOG: claudeResultSubagentLog,
394
+ CLAUDE_PARENT_TRANSCRIPT_LOG: claudeParentTranscriptLog,
395
+ CLAUDE_SUBAGENT_CWD: claudeSubagentCwd,
396
+ }),
397
+ },
398
+ kind: 'claude-code',
399
+ model: 'opus',
400
+ reasoningEffort: 'xhigh',
401
+ });
402
+ assert.equal((await runtime.run(await runtimeInput(runtime, firstCtx, await loadState()))).text, 'first run');
403
+ const stateAfterFirst = await loadState();
404
+ assert.equal(stateAfterFirst.sessions.anima?.current?.id, 'claude-session-1');
405
+ const firstActivities = await activitiesForInboxItemWindow('anima', firstCtx.item.id);
406
+ assert.deepEqual(await providerSessionStartedPayload(firstCtx.item.id), { kind: 'claude-code', resumed: false });
407
+ assert.equal(firstActivities.find((activity) => activity.type === 'agent.text' && !activity.payload?.['subRunId'])?.payload?.['text'], 'checking via Claude');
408
+ const providerToolActivity = firstActivities.find((activity) => activity.payload?.['tool'] === 'claude.Read');
409
+ assert.equal(providerToolActivity?.type, 'tool.call.started');
410
+ assert.equal(providerToolActivity?.payload?.['target'], '/tmp/context.md');
411
+ assert.equal(providerToolActivity?.payload?.['providerToolId'], 'toolu_read_1');
412
+ const childToolActivity = firstActivities.find((activity) => activity.payload?.['providerToolId'] === 'toolu_child_read');
413
+ assert.equal(childToolActivity?.type, 'tool.call.started');
414
+ assert.equal(childToolActivity?.payload?.['parentToolCallId'], 'toolu_parent_task');
415
+ assert.equal(childToolActivity?.payload?.['subRunId'], 'claude-child-1');
416
+ assert.equal(childToolActivity?.payload?.['role'], 'researcher');
417
+ assert.equal(childToolActivity?.payload?.['name'], 'child-researcher');
418
+ assert.equal(childToolActivity?.payload?.['depth'], 1);
419
+ const childAgentText = firstActivities.find((activity) => activity.type === 'agent.text' && activity.payload?.['subRunId'] === 'claude-child-1');
420
+ assert.equal(childAgentText?.payload?.['text'], 'child draft summary');
421
+ assert.equal(childAgentText?.payload?.['parentToolCallId'], 'toolu_parent_task');
422
+ const metaChildToolActivity = firstActivities.find((activity) => activity.payload?.['providerToolId'] === 'toolu_child_meta_read');
423
+ assert.equal(metaChildToolActivity?.type, 'tool.call.started');
424
+ assert.equal(metaChildToolActivity?.payload?.['parentToolCallId'], 'toolu_parent_task');
425
+ assert.equal(metaChildToolActivity?.payload?.['subRunId'], 'claude-child-meta');
426
+ assert.equal(metaChildToolActivity?.payload?.['role'], 'general-purpose');
427
+ assert.equal(metaChildToolActivity?.payload?.['name'], 'metadata child');
428
+ const resultChildTools = firstActivities.filter((activity) => activity.payload?.['providerToolId'] === 'toolu_child_result_read');
429
+ assert.equal(resultChildTools.length, 1);
430
+ assert.equal(resultChildTools[0]?.type, 'tool.call.started');
431
+ assert.equal(resultChildTools[0]?.payload?.['parentToolCallId'], 'toolu_result_task');
432
+ assert.equal(resultChildTools[0]?.payload?.['subRunId'], 'claude-child-result');
433
+ assert.equal(resultChildTools[0]?.payload?.['role'], 'general-purpose');
434
+ assert.equal(resultChildTools[0]?.payload?.['name'], 'result child');
435
+ const resultChildText = firstActivities.find((activity) => activity.type === 'agent.text' && activity.payload?.['subRunId'] === 'claude-child-result');
436
+ assert.equal(resultChildText?.payload?.['text'], 'child result summary');
437
+ assert.equal(resultChildText?.payload?.['parentToolCallId'], 'toolu_result_task');
438
+ assert.equal(allActivities(stateAfterFirst).some((activity) => activity.payload?.['providerToolId'] === 'toolu_anima_1'), false);
439
+ assert.equal(allActivities(stateAfterFirst).some((activity) => JSON.stringify(activity.payload ?? {}).includes('file contents should stay out of agent text')), false);
440
+ assert.equal(firstActivities.some((activity) => activity.type === 'tool.call.failed'), false);
441
+ const secondCtx = await ingestEvent(makeSlackEvent({
442
+ channelId: 'D-anima',
443
+ teamId: 'T-demo',
444
+ text: 'What did I ask?',
445
+ userId: 'U1',
446
+ }), config);
447
+ assert.equal((await runtime.run(await runtimeInput(runtime, secondCtx, await loadState()))).text, 'second run');
448
+ const stateAfterSecond = await loadState();
449
+ const resumedProviderSession = await providerSessionStartedPayload(secondCtx.item.id);
450
+ assert.equal(resumedProviderSession?.['id'], 'claude-session-1');
451
+ assert.equal(resumedProviderSession?.['kind'], 'claude-code');
452
+ assert.equal(resumedProviderSession?.['resumed'], true);
453
+ const calls = (await readFile(callsPath, 'utf8')).trim().split('\n').map((line) => JSON.parse(line));
454
+ assert.equal(calls.length, 2);
455
+ assert.equal(calls[0]?.argv.includes('-p'), false);
456
+ assert.equal(calls[0]?.argv.includes('--append-system-prompt'), false);
457
+ assert.equal(calls[0]?.argv.includes('--system-prompt-file'), true);
458
+ assert.equal(calls[0]?.argv.includes('--resume'), false);
459
+ const compactStarted = allActivities(stateAfterSecond).find((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'claude.compact.started');
460
+ const compactCompleted = allActivities(stateAfterSecond).find((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'claude.compact.completed');
461
+ const stats = allActivities(stateAfterSecond).filter((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'claude.session.stats').at(-1);
462
+ const rateLimit = allActivities(stateAfterSecond).find((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'claude.rate_limit');
463
+ assert.ok(compactStarted);
464
+ assert.ok(compactCompleted);
465
+ for (const hiddenEventType of [
466
+ 'claude.context.stats',
467
+ 'claude.system.init',
468
+ 'claude.stream.message_start',
469
+ 'claude.stream.message_delta',
470
+ 'claude.thinking.delta',
471
+ 'provider.reasoning',
472
+ 'claude.tool_result',
473
+ ]) {
474
+ assert.equal(allActivities(stateAfterSecond).some((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === hiddenEventType), false);
475
+ }
476
+ assert.equal(rateLimit?.payload?.['rateLimitType'], 'seven_day');
477
+ assert.equal(rateLimit?.payload?.['utilization'], 0.26);
478
+ assert.equal(stats?.payload?.['model'], 'claude-opus-test');
479
+ assert.equal(stats?.payload?.['contextWindow'], 200000);
480
+ assert.equal(stats?.payload?.['durationMs'], 1200);
481
+ assert.equal(stats?.payload?.['durationApiMs'], 900);
482
+ assert.equal(stats?.payload?.['numTurns'], 1);
483
+ assert.equal(stats?.payload?.['webSearchRequests'], 1);
484
+ assert.equal(stats?.payload?.['webFetchRequests'], 2);
485
+ assert.equal(stats?.payload?.['maxOutputTokens'], 32000);
486
+ assert.equal(stats?.payload?.['permissionDenialCount'], 1);
487
+ await runtime.close?.();
488
+ });
489
+ }
490
+ finally {
491
+ if (previousClaudeProjectsDir === undefined)
492
+ delete process.env.CLAUDE_PROJECTS_DIR;
493
+ else
494
+ process.env.CLAUDE_PROJECTS_DIR = previousClaudeProjectsDir;
495
+ await rm(stateDir, { force: true, recursive: true });
496
+ }
497
+ });
498
+ test('claude-code runtime retries fresh when persisted session is missing', async () => {
499
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
500
+ try {
501
+ await withAnimaHome(stateDir, async () => {
502
+ const callsPath = join(stateDir, 'claude-stale-session-calls.jsonl');
503
+ const fakeClaude = join(stateDir, 'claude');
504
+ await writeFile(fakeClaude, [
505
+ '#!/usr/bin/env node',
506
+ "import { appendFileSync } from 'node:fs';",
507
+ "import readline from 'node:readline';",
508
+ "const argv = process.argv.slice(2);",
509
+ "const rl = readline.createInterface({ input: process.stdin });",
510
+ "rl.once('line', (line) => {",
511
+ " const prompt = JSON.parse(line).message.content[0].text;",
512
+ " appendFileSync(process.env.CALLS_PATH, JSON.stringify({ argv, prompt }) + '\\n');",
513
+ " if (argv.includes('--resume')) {",
514
+ " console.error('No conversation found with session ID: stale-claude-session');",
515
+ " process.exit(0);",
516
+ " }",
517
+ " console.log(JSON.stringify({ type: 'system', subtype: 'init', session_id: 'fresh-claude-session', cwd: process.cwd(), claude_code_version: 'test' }));",
518
+ " console.log(JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'fresh reply' }] }, session_id: 'fresh-claude-session' }));",
519
+ " console.log(JSON.stringify({ type: 'result', subtype: 'success', result: 'fresh run', session_id: 'fresh-claude-session' }));",
520
+ "});",
521
+ '',
522
+ ].join('\n'), 'utf8');
523
+ await chmod(fakeClaude, 0o755);
524
+ const ctx = await ingestEvent(makeSlackEvent({
525
+ channelId: 'D-anima',
526
+ teamId: 'T-demo',
527
+ text: 'recover after migration',
528
+ userId: 'U1',
529
+ }), { agentId: 'anima', stateDir });
530
+ const sessionPath = join(stateDir, 'agents/anima/sessions.json');
531
+ const session = JSON.parse(await readFile(sessionPath, 'utf8'));
532
+ await writeFile(sessionPath, `${JSON.stringify({
533
+ ...session,
534
+ current: {
535
+ id: 'stale-claude-session',
536
+ kind: 'claude-code',
537
+ updatedAt: '2026-05-19T00:00:00.000Z',
538
+ },
539
+ }, null, 2)}\n`, 'utf8');
540
+ const runtime = createAgentRuntime({
541
+ env: {
542
+ CALLS_PATH: callsPath,
543
+ ...runtimeTestEnv(stateDir),
544
+ },
545
+ kind: 'claude-code',
546
+ });
547
+ assert.equal((await runtime.run(await runtimeInput(runtime, ctx, await loadState()))).text, 'fresh run');
548
+ const calls = (await readFile(callsPath, 'utf8')).trim().split('\n').map((line) => JSON.parse(line));
549
+ assert.equal(calls.length, 2);
550
+ assert.deepEqual(calls[0]?.argv.slice(calls[0].argv.indexOf('--resume'), calls[0].argv.indexOf('--resume') + 2), ['--resume', 'stale-claude-session']);
551
+ assert.equal(calls[1]?.argv.includes('--resume'), false);
552
+ const state = await loadState();
553
+ assert.equal(state.sessions.anima?.current?.id, 'fresh-claude-session');
554
+ const activities = await activitiesForInboxItemWindow('anima', ctx.item.id);
555
+ assert.ok(activities.some((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'claude.session.resume_missing'));
556
+ assert.ok(activities.some((activity) => activity.type === 'runtime.completed'));
557
+ assert.equal(activities.some((activity) => activity.type === 'runtime.failed'), false);
558
+ await runtime.close?.();
559
+ });
560
+ }
561
+ finally {
562
+ await rm(stateDir, { force: true, recursive: true });
563
+ }
564
+ });
565
+ test('claude-code runtime retries transient provider protocol errors before tool use', async () => {
566
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
567
+ try {
568
+ await withAnimaHome(stateDir, async () => {
569
+ const callsPath = join(stateDir, 'claude-provider-retry-calls.jsonl');
570
+ const fakeClaude = join(stateDir, 'claude');
571
+ await writeFile(fakeClaude, [
572
+ '#!/usr/bin/env node',
573
+ "import { appendFileSync } from 'node:fs';",
574
+ "import readline from 'node:readline';",
575
+ "const rl = readline.createInterface({ input: process.stdin });",
576
+ "let count = 0;",
577
+ "rl.on('line', (line) => {",
578
+ " count += 1;",
579
+ " appendFileSync(process.env.CALLS_PATH, line + '\\n');",
580
+ " console.log(JSON.stringify({ type: 'system', subtype: 'init', session_id: 'claude-error-session', cwd: process.cwd(), claude_code_version: 'test' }));",
581
+ " if (count === 1) {",
582
+ " console.log(JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'API Error: The socket connection was closed unexpectedly' }] }, session_id: 'claude-error-session', error: 'socket_closed', request_id: 'req-test' }));",
583
+ " console.log(JSON.stringify({ type: 'result', subtype: 'success', is_error: true, api_error_status: 503, result: 'API Error: The socket connection was closed unexpectedly', session_id: 'claude-error-session', usage: { input_tokens: 0, output_tokens: 0 }, terminal_reason: 'completed' }));",
584
+ " return;",
585
+ " }",
586
+ " console.log(JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'recovered after retry' }] }, session_id: 'claude-error-session' }));",
587
+ " console.log(JSON.stringify({ type: 'result', subtype: 'success', result: 'recovered after retry', session_id: 'claude-error-session' }));",
588
+ "});",
589
+ '',
590
+ ].join('\n'), 'utf8');
591
+ await chmod(fakeClaude, 0o755);
592
+ const config = { agentId: 'anima', stateDir };
593
+ const ctx = await ingestEvent(makeSlackEvent({
594
+ channelId: 'D-anima',
595
+ teamId: 'T-demo',
596
+ text: 'trigger provider error',
597
+ userId: 'U1',
598
+ }), config);
599
+ const runtime = createAgentRuntime({
600
+ env: runtimeTestEnv(stateDir, { CALLS_PATH: callsPath }),
601
+ kind: 'claude-code',
602
+ });
603
+ assert.equal((await runtime.run(await runtimeInput(runtime, ctx, await loadState()))).text, 'recovered after retry');
604
+ const activities = await activitiesForInboxItemWindow('anima', ctx.item.id);
605
+ assert.equal(activities.some((activity) => activity.type === 'agent.text' && activity.payload?.['text'] === 'API Error: The socket connection was closed unexpectedly'), false);
606
+ assert.ok(activities.some((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'claude.provider.retry'));
607
+ assert.ok(activities.some((activity) => activity.type === 'runtime.completed'));
608
+ assert.equal(activities.some((activity) => activity.type === 'runtime.failed'), false);
609
+ assert.equal((await readFile(callsPath, 'utf8')).trim().split('\n').length, 2);
610
+ await runtime.close?.();
611
+ });
612
+ }
613
+ finally {
614
+ await rm(stateDir, { force: true, recursive: true });
615
+ }
616
+ });
617
+ test('claude-code runtime does not retry non-transient provider protocol errors', async () => {
618
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
619
+ try {
620
+ await withAnimaHome(stateDir, async () => {
621
+ const fakeClaude = join(stateDir, 'claude');
622
+ await writeFile(fakeClaude, [
623
+ '#!/usr/bin/env node',
624
+ "import readline from 'node:readline';",
625
+ "const rl = readline.createInterface({ input: process.stdin });",
626
+ "rl.once('line', () => {",
627
+ " console.log(JSON.stringify({ type: 'system', subtype: 'init', session_id: 'claude-error-session', cwd: process.cwd(), claude_code_version: 'test' }));",
628
+ " console.log(JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'Invalid API key' }] }, session_id: 'claude-error-session', error: 'authentication_failed', request_id: 'req-test' }));",
629
+ " console.log(JSON.stringify({ type: 'result', subtype: 'success', is_error: true, api_error_status: 401, result: 'Invalid API key', session_id: 'claude-error-session', usage: { input_tokens: 0, output_tokens: 0 }, terminal_reason: 'completed' }));",
630
+ "});",
631
+ '',
632
+ ].join('\n'), 'utf8');
633
+ await chmod(fakeClaude, 0o755);
634
+ const config = { agentId: 'anima', stateDir };
635
+ const ctx = await ingestEvent(makeSlackEvent({
636
+ channelId: 'D-anima',
637
+ teamId: 'T-demo',
638
+ text: 'trigger provider error',
639
+ userId: 'U1',
640
+ }), config);
641
+ const runtime = createAgentRuntime({
642
+ env: runtimeTestEnv(stateDir),
643
+ kind: 'claude-code',
644
+ });
645
+ await assert.rejects(runtime.run(await runtimeInput(runtime, ctx, await loadState())), /Invalid API key \(api status 401\)/);
646
+ const activities = await activitiesForInboxItemWindow('anima', ctx.item.id);
647
+ assert.equal(activities.some((activity) => activity.type === 'agent.text'), false);
648
+ const failed = activities.find((activity) => activity.type === 'runtime.failed');
649
+ assert.equal(failed?.payload?.['failureSource'], 'provider');
650
+ assert.equal(failed?.payload?.['providerReason'], 'api_status_401');
651
+ assert.equal(failed?.payload?.['retryable'], false);
652
+ assert.ok(activities.some((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'assistant'));
653
+ await runtime.close?.();
654
+ });
655
+ }
656
+ finally {
657
+ await rm(stateDir, { force: true, recursive: true });
658
+ }
659
+ });
660
+ test('claude-code runtime resumes after transient provider errors when tool use already started', async () => {
661
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
662
+ try {
663
+ await withAnimaHome(stateDir, async () => {
664
+ const callsPath = join(stateDir, 'claude-provider-tool-error-calls.jsonl');
665
+ const fakeClaude = join(stateDir, 'claude');
666
+ await writeFile(fakeClaude, [
667
+ '#!/usr/bin/env node',
668
+ "import { appendFileSync } from 'node:fs';",
669
+ "import readline from 'node:readline';",
670
+ "const rl = readline.createInterface({ input: process.stdin });",
671
+ "let count = 0;",
672
+ "rl.on('line', (line) => {",
673
+ " count += 1;",
674
+ " appendFileSync(process.env.CALLS_PATH, line + '\\n');",
675
+ " console.log(JSON.stringify({ type: 'system', subtype: 'init', session_id: 'claude-error-session', cwd: process.cwd(), claude_code_version: 'test' }));",
676
+ " if (count === 1) {",
677
+ " console.log(JSON.stringify({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'toolu_side_effect', name: 'Bash', input: { command: 'touch /tmp/anima-side-effect' } }] }, session_id: 'claude-error-session' }));",
678
+ " console.log(JSON.stringify({ type: 'result', subtype: 'success', is_error: true, api_error_status: 503, result: 'API Error: The socket connection was closed unexpectedly', session_id: 'claude-error-session', usage: { input_tokens: 0, output_tokens: 0 }, terminal_reason: 'completed' }));",
679
+ " return;",
680
+ " }",
681
+ " console.log(JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'continued safely after provider error' }] }, session_id: 'claude-error-session' }));",
682
+ " console.log(JSON.stringify({ type: 'result', subtype: 'success', result: 'continued safely after provider error', session_id: 'claude-error-session' }));",
683
+ "});",
684
+ '',
685
+ ].join('\n'), 'utf8');
686
+ await chmod(fakeClaude, 0o755);
687
+ const config = { agentId: 'anima', stateDir };
688
+ const ctx = await ingestEvent(makeSlackEvent({
689
+ channelId: 'D-anima',
690
+ teamId: 'T-demo',
691
+ text: 'trigger provider error',
692
+ userId: 'U1',
693
+ }), config);
694
+ const runtime = createAgentRuntime({
695
+ env: runtimeTestEnv(stateDir, { CALLS_PATH: callsPath }),
696
+ kind: 'claude-code',
697
+ });
698
+ assert.equal((await runtime.run(await runtimeInput(runtime, ctx, await loadState()))).text, 'continued safely after provider error');
699
+ const activities = await activitiesForInboxItemWindow('anima', ctx.item.id);
700
+ assert.ok(activities.some((activity) => activity.type === 'tool.call.started' && activity.payload?.['providerToolId'] === 'toolu_side_effect'));
701
+ assert.ok(activities.some((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'claude.provider.resume_retry'));
702
+ assert.ok(activities.some((activity) => activity.type === 'runtime.completed'));
703
+ assert.equal(activities.some((activity) => activity.type === 'runtime.failed'), false);
704
+ const calls = (await readFile(callsPath, 'utf8')).trim().split('\n').map((line) => JSON.parse(line));
705
+ assert.equal(calls.length, 2);
706
+ assert.match(calls[1]?.message.content[0]?.text ?? '', /transient API or transport error/);
707
+ assert.doesNotMatch(calls[1]?.message.content[0]?.text ?? '', /trigger provider error/);
708
+ await runtime.close?.();
709
+ });
710
+ }
711
+ finally {
712
+ await rm(stateDir, { force: true, recursive: true });
713
+ }
714
+ });
715
+ test('claude-code stream-json input keeps stdin open for active-run follow-up', async () => {
716
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
717
+ try {
718
+ await withAnimaHome(stateDir, async () => {
719
+ const callsPath = join(stateDir, 'claude-stream-input.jsonl');
720
+ const fakeClaude = join(stateDir, 'claude');
721
+ await writeFile(fakeClaude, [
722
+ '#!/usr/bin/env node',
723
+ "import { appendFileSync } from 'node:fs';",
724
+ "import readline from 'node:readline';",
725
+ "const argv = process.argv.slice(2);",
726
+ "if (argv[argv.indexOf('--input-format') + 1] !== 'stream-json') process.exit(50);",
727
+ "if (argv[argv.indexOf('--output-format') + 1] !== 'stream-json') process.exit(51);",
728
+ "const send = (message) => process.stdout.write(JSON.stringify(message) + '\\n');",
729
+ "let count = 0;",
730
+ "send({ type: 'system', subtype: 'init', session_id: 'claude-stream-session', cwd: process.cwd(), claude_code_version: 'test' });",
731
+ "const rl = readline.createInterface({ input: process.stdin });",
732
+ "rl.on('line', (line) => {",
733
+ " const msg = JSON.parse(line);",
734
+ " appendFileSync(process.env.CALLS_PATH, JSON.stringify(msg) + '\\n');",
735
+ " const text = msg.message.content[0].text;",
736
+ " count += 1;",
737
+ " if (count === 1 && !text.includes('first message')) process.exit(52);",
738
+ " if (count === 2) {",
739
+ " if (!text.includes('second message')) process.exit(53);",
740
+ " send({ type: 'assistant', message: { content: [{ type: 'text', text: 'handled both messages' }] }, session_id: 'claude-stream-session' });",
741
+ " send({ type: 'result', subtype: 'success', result: 'stream-json done', session_id: 'claude-stream-session' });",
742
+ " }",
743
+ "});",
744
+ '',
745
+ ].join('\n'), 'utf8');
746
+ await chmod(fakeClaude, 0o755);
747
+ const config = { agentId: 'anima', stateDir };
748
+ const firstCtx = await ingestEvent(makeSlackEvent({
749
+ channelId: 'D-anima',
750
+ teamId: 'T-demo',
751
+ text: 'first message',
752
+ userId: 'U1',
753
+ }), config);
754
+ const secondCtx = await ingestEvent(makeSlackEvent({
755
+ channelId: 'D-anima',
756
+ teamId: 'T-demo',
757
+ text: 'second message',
758
+ userId: 'U1',
759
+ }), config);
760
+ const runtime = createAgentRuntime({
761
+ env: runtimeTestEnv(stateDir, { CALLS_PATH: callsPath }),
762
+ kind: 'claude-code',
763
+ });
764
+ const runPromise = runtime.run(await runtimeInput(runtime, firstCtx, await loadState()));
765
+ await waitFor(async () => (await readFile(callsPath, 'utf8')).includes('first message'));
766
+ assert.deepEqual(await runtime.appendToActiveRun(await runtimeFollowupInput(runtime, firstCtx, secondCtx, await loadState())), { accepted: true, text: 'appended to Claude stream-json stdin' });
767
+ assert.equal((await runPromise).text, 'stream-json done');
768
+ const calls = (await readFile(callsPath, 'utf8')).trim().split('\n').map((line) => JSON.parse(line));
769
+ assert.equal(calls.length, 2);
770
+ assert.match(calls[1]?.message.content[0]?.text ?? '', /second message/);
771
+ await runtime.close?.();
772
+ });
773
+ }
774
+ finally {
775
+ await rm(stateDir, { force: true, recursive: true });
776
+ }
777
+ });
778
+ test('claude-code follow-up append waits for compact and tool gates before writing stdin', async () => {
779
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
780
+ let runtime;
781
+ try {
782
+ await withAnimaHome(stateDir, async () => {
783
+ const callsPath = join(stateDir, 'claude-gated-input.jsonl');
784
+ const releasePath = join(stateDir, 'claude-gated-release');
785
+ const fakeClaude = join(stateDir, 'claude');
786
+ await writeFile(fakeClaude, [
787
+ '#!/usr/bin/env node',
788
+ "import { appendFileSync, existsSync } from 'node:fs';",
789
+ "import readline from 'node:readline';",
790
+ "const send = (message) => process.stdout.write(JSON.stringify(message) + '\\n');",
791
+ "let count = 0;",
792
+ "send({ type: 'system', subtype: 'init', session_id: 'claude-gated-session', cwd: process.cwd(), claude_code_version: 'test' });",
793
+ "const rl = readline.createInterface({ input: process.stdin });",
794
+ "rl.on('line', (line) => {",
795
+ " const msg = JSON.parse(line);",
796
+ " appendFileSync(process.env.CALLS_PATH, JSON.stringify(msg) + '\\n');",
797
+ " const text = msg.message.content[0].text;",
798
+ " count += 1;",
799
+ " if (count === 1) {",
800
+ " if (!text.includes('first message')) process.exit(52);",
801
+ " send({ type: 'system', subtype: 'status', status: 'compacting', session_id: 'claude-gated-session' });",
802
+ " send({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'toolu_gate_1', name: 'Read', input: { file_path: '/tmp/gated.md' } }] }, session_id: 'claude-gated-session' });",
803
+ " setTimeout(() => send({ type: 'system', subtype: 'compact_boundary', session_id: 'claude-gated-session' }), 50);",
804
+ " const release = setInterval(() => {",
805
+ " if (!existsSync(process.env.RELEASE_PATH)) return;",
806
+ " clearInterval(release);",
807
+ " send({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'toolu_gate_1', content: 'done', is_error: false }] }, session_id: 'claude-gated-session' });",
808
+ " }, 10);",
809
+ " return;",
810
+ " }",
811
+ " if (count === 2) {",
812
+ " if (!text.includes('second message')) process.exit(53);",
813
+ " send({ type: 'assistant', message: { content: [{ type: 'text', text: 'handled gated follow-up' }] }, session_id: 'claude-gated-session' });",
814
+ " send({ type: 'result', subtype: 'success', result: 'gated done', session_id: 'claude-gated-session' });",
815
+ " }",
816
+ "});",
817
+ '',
818
+ ].join('\n'), 'utf8');
819
+ await chmod(fakeClaude, 0o755);
820
+ const config = { agentId: 'anima', stateDir };
821
+ const firstCtx = await ingestEvent(makeSlackEvent({
822
+ channelId: 'D-anima',
823
+ teamId: 'T-demo',
824
+ text: 'first message',
825
+ userId: 'U1',
826
+ }), config);
827
+ const secondCtx = await ingestEvent(makeSlackEvent({
828
+ channelId: 'D-anima',
829
+ teamId: 'T-demo',
830
+ text: 'second message',
831
+ userId: 'U1',
832
+ }), config);
833
+ runtime = createAgentRuntime({
834
+ env: runtimeTestEnv(stateDir, { CALLS_PATH: callsPath, RELEASE_PATH: releasePath }),
835
+ kind: 'claude-code',
836
+ });
837
+ const runPromise = runtime.run(await runtimeInput(runtime, firstCtx, await loadState()));
838
+ await waitFor(async () => (await readFile(callsPath, 'utf8')).includes('first message'));
839
+ await waitFor(async () => {
840
+ const activities = await activitiesForInboxItemWindow('anima', firstCtx.item.id);
841
+ return activities.some((activity) => activity.type === 'tool.call.started' && activity.payload?.['providerToolId'] === 'toolu_gate_1');
842
+ });
843
+ assert.deepEqual(await runtime.appendToActiveRun(await runtimeFollowupInput(runtime, firstCtx, secondCtx, await loadState())), { accepted: true, text: 'appended to Claude stream-json stdin' });
844
+ await new Promise((resolve) => setTimeout(resolve, 100));
845
+ assert.equal((await readFile(callsPath, 'utf8')).trim().split('\n').length, 1);
846
+ await writeFile(releasePath, '1', 'utf8');
847
+ assert.equal((await withTimeout(runPromise, 2_000)).text, 'gated done');
848
+ const calls = (await readFile(callsPath, 'utf8')).trim().split('\n').map((line) => JSON.parse(line));
849
+ assert.equal(calls.length, 2);
850
+ assert.match(calls[1]?.message.content[0]?.text ?? '', /second message/);
851
+ await runtime.close?.();
852
+ runtime = undefined;
853
+ });
854
+ }
855
+ finally {
856
+ await runtime?.close?.();
857
+ await rm(stateDir, { force: true, recursive: true });
858
+ }
859
+ });
860
+ test('claude-code stream-json input completes when process exits without a result event', async () => {
861
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
862
+ try {
863
+ await withAnimaHome(stateDir, async () => {
864
+ const fakeClaude = join(stateDir, 'claude');
865
+ await writeFile(fakeClaude, [
866
+ '#!/usr/bin/env node',
867
+ "import readline from 'node:readline';",
868
+ "const argv = process.argv.slice(2);",
869
+ "if (argv[argv.indexOf('--input-format') + 1] !== 'stream-json') process.exit(50);",
870
+ "if (argv[argv.indexOf('--output-format') + 1] !== 'stream-json') process.exit(51);",
871
+ "const send = (message) => process.stdout.write(JSON.stringify(message) + '\\n');",
872
+ "const rl = readline.createInterface({ input: process.stdin });",
873
+ "rl.once('line', () => {",
874
+ " send({ type: 'assistant', message: { content: [{ type: 'text', text: 'assistant fallback' }] }, session_id: 'claude-stream-session' });",
875
+ " process.exit(0);",
876
+ "});",
877
+ '',
878
+ ].join('\n'), 'utf8');
879
+ await chmod(fakeClaude, 0o755);
880
+ const ctx = await ingestEvent(makeSlackEvent({
881
+ channelId: 'D-anima',
882
+ teamId: 'T-demo',
883
+ text: 'first message',
884
+ userId: 'U1',
885
+ }), { agentId: 'anima', stateDir });
886
+ const runtime = createAgentRuntime({
887
+ env: runtimeTestEnv(stateDir),
888
+ kind: 'claude-code',
889
+ });
890
+ const result = await withTimeout(runtime.run(await runtimeInput(runtime, ctx, await loadState())), 1_000);
891
+ assert.equal(result.text, 'assistant fallback');
892
+ });
893
+ }
894
+ finally {
895
+ await rm(stateDir, { force: true, recursive: true });
896
+ }
897
+ });
898
+ test('claude-code runtime records failed Bash command details', async () => {
899
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
900
+ try {
901
+ await withAnimaHome(stateDir, async () => {
902
+ const fakeClaude = join(stateDir, 'claude');
903
+ await writeFile(fakeClaude, [
904
+ '#!/usr/bin/env node',
905
+ "import readline from 'node:readline';",
906
+ 'const rl = readline.createInterface({ input: process.stdin });',
907
+ "rl.once('line', () => {",
908
+ ' console.log(JSON.stringify({ type: "system", subtype: "init", session_id: "claude-bash-session", cwd: process.cwd(), claude_code_version: "test" }));',
909
+ ' console.log(JSON.stringify({ type: "assistant", message: { content: [{ type: "tool_use", id: "toolu_bash_1", name: "Bash", input: { command: "pnpm missing-script", description: "Run missing script" } }] }, session_id: "claude-bash-session" }));',
910
+ ' console.log(JSON.stringify({ type: "user", message: { content: [{ type: "tool_result", tool_use_id: "toolu_bash_1", content: "ERR_PNPM_NO_SCRIPT Missing script: missing-script", is_error: true }] }, session_id: "claude-bash-session" }));',
911
+ ' console.log(JSON.stringify({ type: "result", subtype: "success", result: "reported failure", session_id: "claude-bash-session" }));',
912
+ '});',
913
+ '',
914
+ ].join('\n'), 'utf8');
915
+ await chmod(fakeClaude, 0o755);
916
+ const ctx = await ingestEvent(makeSlackEvent({
917
+ channelId: 'D-anima',
918
+ teamId: 'T-demo',
919
+ text: 'Run the failing command.',
920
+ userId: 'U1',
921
+ }), { agentId: 'anima', stateDir });
922
+ const runtime = createAgentRuntime({
923
+ env: runtimeTestEnv(stateDir),
924
+ kind: 'claude-code',
925
+ });
926
+ const result = await runtime.run(await runtimeInput(runtime, ctx, await loadState()));
927
+ assert.equal(result.text, 'reported failure');
928
+ const activities = await activitiesForInboxItemWindow('anima', ctx.item.id);
929
+ const started = activities.find((activity) => activity.type === 'tool.call.started' && activity.payload?.['providerToolId'] === 'toolu_bash_1');
930
+ const failed = activities.find((activity) => activity.type === 'tool.call.failed' && activity.payload?.['providerToolId'] === 'toolu_bash_1');
931
+ assert.equal(started?.payload?.['tool'], 'claude.Bash');
932
+ assert.equal(started?.payload?.['command'], 'pnpm missing-script');
933
+ assert.equal(started?.payload?.['target'], 'Run missing script');
934
+ assert.equal(failed?.payload?.['tool'], 'claude.Bash');
935
+ assert.equal(failed?.payload?.['command'], 'pnpm missing-script');
936
+ assert.equal(failed?.payload?.['target'], 'Run missing script');
937
+ assert.match(String(failed?.payload?.['error']), /ERR_PNPM_NO_SCRIPT/);
938
+ await runtime.close?.();
939
+ });
940
+ }
941
+ finally {
942
+ await rm(stateDir, { force: true, recursive: true });
943
+ }
944
+ });
945
+ test('kimi-cli wire transport starts a turn and appends subscription follow-up input', async () => {
946
+ const stateDir = await mkdtemp(join(tmpdir(), 'anima-runtime-test-'));
947
+ let runtime;
948
+ try {
949
+ await withAnimaHome(stateDir, async () => {
950
+ const callsPath = join(stateDir, 'kimi-wire-calls.jsonl');
951
+ const fakeKimi = join(stateDir, 'kimi');
952
+ await writeFile(fakeKimi, [
953
+ '#!/usr/bin/env node',
954
+ "const fs = require('fs');",
955
+ `fs.appendFileSync(${JSON.stringify(callsPath)}, JSON.stringify({ argv: process.argv.slice(2) }) + '\\n');`,
956
+ "process.stdin.setEncoding('utf8');",
957
+ "let buffer = '';",
958
+ 'function send(message) { process.stdout.write(JSON.stringify(message) + "\\n"); }',
959
+ 'function event(type, payload = {}) { send({ jsonrpc: "2.0", method: "event", params: { type, payload } }); }',
960
+ 'process.stdin.on("data", (chunk) => {',
961
+ ' buffer += chunk;',
962
+ ' const lines = buffer.split(/\\r?\\n/);',
963
+ ' buffer = lines.pop() || "";',
964
+ ' for (const line of lines) {',
965
+ ' if (!line.trim()) continue;',
966
+ ' const msg = JSON.parse(line);',
967
+ ' fs.appendFileSync(process.env.CALLS_PATH, JSON.stringify(msg) + "\\n");',
968
+ ' if (msg.method === "initialize") {',
969
+ ' send({ jsonrpc: "2.0", id: msg.id, result: { protocol_version: "1.10", server: { name: "Kimi Code CLI", version: "1.44.0" }, slash_commands: [{ name: "init", description: "Init", aliases: [] }], capabilities: { supports_question: true }, hooks: { supported_events: ["PreToolUse", "Stop"], configured: { PreToolUse: 1 } } } });',
970
+ ' }',
971
+ ' if (msg.method === "prompt") {',
972
+ ' event("TurnBegin", { user_input: "Start Kimi." });',
973
+ ' event("StepBegin", { n: 1 });',
974
+ ' event("ContentPart", { type: "think", think: "thinking chunk", encrypted: null });',
975
+ ' event("StatusUpdate", { context_usage: 0.5, context_tokens: 13131, max_context_tokens: 262144, token_usage: { input_other: 100, output: 24, input_cache_read: 1024, input_cache_creation: 0 }, message_id: "chatcmpl-test", plan_mode: false });',
976
+ ' event("ToolCall", { id: "kimi-tool-1", function: { name: "Shell", arguments: "" } });',
977
+ ' event("ToolCallPart", { arguments_part: "{\\"command\\":\\"pw" });',
978
+ ' event("ToolCallPart", { arguments_part: "d\\"}" });',
979
+ ' event("ToolResult", { tool_call_id: "kimi-tool-1", return_value: { is_error: false, output: "/tmp", message: "ok" } });',
980
+ ' event("ToolCall", { id: "kimi-tool-read", function: { name: "ReadFile", arguments: "" } });',
981
+ ' event("ToolCallPart", { arguments_part: "{\\"path\\":\\"notes.md\\"}" });',
982
+ ' event("ToolResult", { tool_call_id: "kimi-tool-read", return_value: { is_error: false, output: "old", message: "ok" } });',
983
+ ' event("ToolCall", { id: "kimi-tool-2", function: { name: "StrReplaceFile", arguments: "" } });',
984
+ ' event("ToolCallPart", { arguments_part: "{\\"path\\":\\"notes.md\\",\\"edit\\":{\\"old\\":\\"old" });',
985
+ ' event("ToolCallPart", { arguments_part: "\\",\\"new\\":\\"new\\"}}" });',
986
+ ' event("ToolResult", { tool_call_id: "kimi-tool-2", return_value: { is_error: false, output: "", message: "ok" } });',
987
+ ' event("HookTriggered", { event: "PreToolUse", target: "Shell", hook_count: 1 });',
988
+ ' event("HookResolved", { event: "PreToolUse", target: "Shell", action: "allow", reason: "", duration_ms: 12 });',
989
+ ' event("PlanDisplay", { content: "plan text", file_path: "/tmp/plan.md" });',
990
+ ' event("ContentPart", { type: "text", text: "handled first" });',
991
+ ' }',
992
+ ' if (msg.method === "steer") {',
993
+ ' event("SteerInput", { user_input: "Steer Kimi." });',
994
+ ' event("ContentPart", { type: "text", text: " + appended" });',
995
+ ' event("TurnEnd");',
996
+ ' }',
997
+ ' }',
998
+ '});',
999
+ ].join('\n'), 'utf8');
1000
+ await chmod(fakeKimi, 0o755);
1001
+ const firstCtx = await ingestEvent(makeSlackEvent({
1002
+ channelId: 'D-kimi',
1003
+ teamId: 'T-demo',
1004
+ text: 'Start Kimi.',
1005
+ ts: '1770000600.000001',
1006
+ userId: 'U1',
1007
+ }), { agentId: 'anima', stateDir });
1008
+ const secondCtx = await ingestEvent(makeSlackEvent({
1009
+ channelId: 'D-kimi',
1010
+ teamId: 'T-demo',
1011
+ text: 'Steer Kimi.',
1012
+ ts: '1770000600.000002',
1013
+ userId: 'U1',
1014
+ }), { agentId: 'anima', stateDir });
1015
+ runtime = createAgentRuntime({
1016
+ env: runtimeTestEnv(stateDir, { CALLS_PATH: callsPath }),
1017
+ kind: 'kimi-cli',
1018
+ model: 'kimi-code/kimi-for-coding',
1019
+ });
1020
+ const runPromise = runtime.run(await runtimeInput(runtime, firstCtx, await loadState()));
1021
+ await waitFor(() => readFile(callsPath, 'utf8').then((text) => text.includes('"method":"prompt"')));
1022
+ assert.deepEqual(await runtime.appendToActiveRun(await runtimeFollowupInput(runtime, firstCtx, secondCtx, await loadState())), { accepted: true, text: 'appended to Kimi wire stdin' });
1023
+ assert.equal((await withTimeout(runPromise, 1_000)).text, 'handled first + appended');
1024
+ const state = await loadState();
1025
+ const sessionId = state.sessions.anima?.current?.id;
1026
+ assert.ok(sessionId);
1027
+ const args = JSON.parse((await readFile(callsPath, 'utf8')).split('\n')[0] ?? '{}');
1028
+ assert.ok(args.argv.includes('--wire'));
1029
+ assert.ok(args.argv.includes('--yolo'));
1030
+ assert.ok(args.argv.includes('--thinking'));
1031
+ assert.equal(args.argv[args.argv.indexOf('--model') + 1], 'kimi-code/kimi-for-coding');
1032
+ assert.equal(args.argv[args.argv.indexOf('--session') + 1], sessionId);
1033
+ assert.deepEqual(await providerSessionStartedPayload(firstCtx.item.id), { kind: 'kimi-cli', resumed: false });
1034
+ const kimiTool = allActivities(state).find((activity) => activity.type === 'tool.call.started' && activity.payload?.['providerToolId'] === 'kimi-tool-1');
1035
+ assert.equal(kimiTool?.payload?.['tool'], 'kimi.Shell');
1036
+ assert.equal(kimiTool?.payload?.['providerToolName'], 'Shell');
1037
+ assert.equal(kimiTool?.payload?.['command'], 'pwd');
1038
+ assert.equal(kimiTool?.payload?.['target'], 'pwd');
1039
+ const kimiEdit = allActivities(state).find((activity) => activity.type === 'tool.call.started' && activity.payload?.['providerToolId'] === 'kimi-tool-2');
1040
+ assert.equal(kimiEdit?.payload?.['tool'], 'kimi.StrReplaceFile');
1041
+ assert.equal(kimiEdit?.payload?.['target'], 'notes.md');
1042
+ assert.equal(kimiEdit?.payload?.['diff'], '--- old\nold\n+++ new\nnew');
1043
+ const kimiRead = allActivities(state).find((activity) => activity.type === 'tool.call.started' && activity.payload?.['providerToolId'] === 'kimi-tool-read');
1044
+ assert.equal(kimiRead?.payload?.['tool'], 'kimi.ReadFile');
1045
+ assert.equal(kimiRead?.payload?.['target'], 'notes.md');
1046
+ const activities = allActivities(state);
1047
+ assert.ok(activities.some((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === 'kimi.context.stats' && activity.payload?.['currentContextTokens'] === 13131 && activity.payload?.['contextWindow'] === 262144));
1048
+ for (const hiddenEventType of [
1049
+ 'kimi.system.init',
1050
+ 'kimi.turn.started',
1051
+ 'kimi.step.started',
1052
+ 'kimi.thinking.delta',
1053
+ 'provider.reasoning',
1054
+ 'kimi.tool_result',
1055
+ 'kimi.hook.resolved',
1056
+ 'kimi.plan.display',
1057
+ 'kimi.steer.consumed',
1058
+ 'kimi.turn.completed',
1059
+ ]) {
1060
+ assert.equal(activities.some((activity) => activity.type === 'runtime.event' && activity.payload?.['eventType'] === hiddenEventType), false);
1061
+ }
1062
+ });
1063
+ }
1064
+ finally {
1065
+ await runtime?.close?.();
1066
+ await rm(stateDir, { force: true, recursive: true });
1067
+ }
1068
+ });
1069
+ async function waitFor(predicate, timeoutMs = 1000) {
1070
+ const startedAt = Date.now();
1071
+ while (!(await waitForPredicate(predicate))) {
1072
+ if (Date.now() - startedAt > timeoutMs)
1073
+ throw new Error('Timed out waiting for condition');
1074
+ await new Promise((resolve) => setTimeout(resolve, 10));
1075
+ }
1076
+ }
1077
+ async function providerSessionStartedPayload(turnId) {
1078
+ const payload = (await activitiesForInboxItemWindow('anima', turnId)).find((activity) => activity.type === 'runtime.started')
1079
+ ?.payload?.['providerSession'];
1080
+ return payload && typeof payload === 'object' ? payload : undefined;
1081
+ }
1082
+ function runtimeTestEnv(binDir, env = {}) {
1083
+ return {
1084
+ ...env,
1085
+ PATH: [binDir, process.env.PATH ?? ''].filter(Boolean).join(':'),
1086
+ };
1087
+ }
1088
+ async function waitForPredicate(predicate) {
1089
+ try {
1090
+ return await predicate();
1091
+ }
1092
+ catch {
1093
+ return false;
1094
+ }
1095
+ }
1096
+ async function withTimeout(promise, timeoutMs) {
1097
+ let timeout;
1098
+ try {
1099
+ return await Promise.race([
1100
+ promise,
1101
+ new Promise((_, reject) => {
1102
+ timeout = setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
1103
+ }),
1104
+ ]);
1105
+ }
1106
+ finally {
1107
+ if (timeout)
1108
+ clearTimeout(timeout);
1109
+ }
1110
+ }