opencode-dux 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +452 -0
  3. package/dist/agents/descriptions.d.ts +6 -0
  4. package/dist/agents/designer.d.ts +2 -0
  5. package/dist/agents/explorer.d.ts +2 -0
  6. package/dist/agents/fixer.d.ts +2 -0
  7. package/dist/agents/index.d.ts +22 -0
  8. package/dist/agents/interpreter.d.ts +2 -0
  9. package/dist/agents/librarian.d.ts +2 -0
  10. package/dist/agents/oracle.d.ts +2 -0
  11. package/dist/agents/orchestrator.d.ts +27 -0
  12. package/dist/agents/overrides.d.ts +18 -0
  13. package/dist/agents/prompt-blocks.d.ts +97 -0
  14. package/dist/agents/steward.d.ts +3 -0
  15. package/dist/cli/config-io.d.ts +24 -0
  16. package/dist/cli/config-manager.d.ts +4 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +1006 -0
  19. package/dist/cli/install.d.ts +2 -0
  20. package/dist/cli/mcps.d.ts +13 -0
  21. package/dist/cli/model-key-normalization.d.ts +1 -0
  22. package/dist/cli/paths.d.ts +35 -0
  23. package/dist/cli/providers.d.ts +137 -0
  24. package/dist/cli/skills.d.ts +22 -0
  25. package/dist/cli/system.d.ts +5 -0
  26. package/dist/cli/types.d.ts +38 -0
  27. package/dist/config/constants.d.ts +12 -0
  28. package/dist/config/index.d.ts +4 -0
  29. package/dist/config/loader.d.ts +40 -0
  30. package/dist/config/runtime-preset.d.ts +12 -0
  31. package/dist/config/schema.d.ts +281 -0
  32. package/dist/config/utils.d.ts +10 -0
  33. package/dist/discovery/local/types.d.ts +79 -0
  34. package/dist/discovery/local.d.ts +73 -0
  35. package/dist/discovery/mcp-servers.d.ts +88 -0
  36. package/dist/discovery/skills.d.ts +94 -0
  37. package/dist/hooks/apply-patch/codec.d.ts +7 -0
  38. package/dist/hooks/apply-patch/errors.d.ts +25 -0
  39. package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
  40. package/dist/hooks/apply-patch/index.d.ts +15 -0
  41. package/dist/hooks/apply-patch/matching.d.ts +26 -0
  42. package/dist/hooks/apply-patch/operations.d.ts +3 -0
  43. package/dist/hooks/apply-patch/patch.d.ts +2 -0
  44. package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
  45. package/dist/hooks/apply-patch/resolution.d.ts +19 -0
  46. package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
  47. package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
  48. package/dist/hooks/apply-patch/types.d.ts +80 -0
  49. package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
  50. package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
  51. package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
  52. package/dist/hooks/auto-update-checker/index.d.ts +18 -0
  53. package/dist/hooks/auto-update-checker/types.d.ts +22 -0
  54. package/dist/hooks/chat-headers.d.ts +16 -0
  55. package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
  56. package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
  57. package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
  58. package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
  59. package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
  60. package/dist/hooks/filter-available-skills/index.d.ts +32 -0
  61. package/dist/hooks/foreground-fallback/index.d.ts +72 -0
  62. package/dist/hooks/image-hook.d.ts +5 -0
  63. package/dist/hooks/index.d.ts +14 -0
  64. package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
  65. package/dist/hooks/json-error-recovery/index.d.ts +1 -0
  66. package/dist/hooks/phase-reminder/index.d.ts +26 -0
  67. package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
  68. package/dist/hooks/task-session-manager/index.d.ts +52 -0
  69. package/dist/hooks/todo-continuation/index.d.ts +53 -0
  70. package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
  71. package/dist/index.d.ts +5 -0
  72. package/dist/index.js +31782 -0
  73. package/dist/mcp/context7.d.ts +6 -0
  74. package/dist/mcp/grep-app.d.ts +6 -0
  75. package/dist/mcp/index.d.ts +13 -0
  76. package/dist/mcp/types.d.ts +12 -0
  77. package/dist/mcp/websearch.d.ts +9 -0
  78. package/dist/skills/registry.d.ts +29 -0
  79. package/dist/subscriptions/accounts-store.d.ts +57 -0
  80. package/dist/subscriptions/index.d.ts +13 -0
  81. package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
  82. package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
  83. package/dist/subscriptions/types.d.ts +115 -0
  84. package/dist/subscriptions/usage-service.d.ts +74 -0
  85. package/dist/tools/ast-grep/cli.d.ts +15 -0
  86. package/dist/tools/ast-grep/constants.d.ts +25 -0
  87. package/dist/tools/ast-grep/downloader.d.ts +5 -0
  88. package/dist/tools/ast-grep/index.d.ts +10 -0
  89. package/dist/tools/ast-grep/tools.d.ts +3 -0
  90. package/dist/tools/ast-grep/types.d.ts +30 -0
  91. package/dist/tools/ast-grep/utils.d.ts +4 -0
  92. package/dist/tools/delegate.d.ts +14 -0
  93. package/dist/tools/index.d.ts +5 -0
  94. package/dist/tools/preset-manager.d.ts +27 -0
  95. package/dist/tools/smartfetch/binary.d.ts +3 -0
  96. package/dist/tools/smartfetch/cache.d.ts +6 -0
  97. package/dist/tools/smartfetch/constants.d.ts +12 -0
  98. package/dist/tools/smartfetch/index.d.ts +3 -0
  99. package/dist/tools/smartfetch/network.d.ts +38 -0
  100. package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
  101. package/dist/tools/smartfetch/tool.d.ts +3 -0
  102. package/dist/tools/smartfetch/types.d.ts +122 -0
  103. package/dist/tools/smartfetch/utils.d.ts +18 -0
  104. package/dist/tui-state.d.ts +168 -0
  105. package/dist/tui.d.ts +37 -0
  106. package/dist/tui.js +1896 -0
  107. package/dist/utils/agent-variant.d.ts +63 -0
  108. package/dist/utils/compat.d.ts +30 -0
  109. package/dist/utils/env.d.ts +1 -0
  110. package/dist/utils/index.d.ts +9 -0
  111. package/dist/utils/internal-initiator.d.ts +6 -0
  112. package/dist/utils/logger.d.ts +8 -0
  113. package/dist/utils/polling.d.ts +21 -0
  114. package/dist/utils/session-manager.d.ts +55 -0
  115. package/dist/utils/session.d.ts +90 -0
  116. package/dist/utils/subagent-depth.d.ts +35 -0
  117. package/dist/utils/system-collapse.d.ts +6 -0
  118. package/dist/utils/task.d.ts +4 -0
  119. package/dist/utils/zip-extractor.d.ts +1 -0
  120. package/index.ts +1 -0
  121. package/opencode-dux.schema.json +634 -0
  122. package/package.json +103 -0
  123. package/src/agents/descriptions.ts +55 -0
  124. package/src/agents/designer.test.ts +86 -0
  125. package/src/agents/designer.ts +154 -0
  126. package/src/agents/display-name.test.ts +186 -0
  127. package/src/agents/explorer.test.ts +79 -0
  128. package/src/agents/explorer.ts +144 -0
  129. package/src/agents/fixer.test.ts +79 -0
  130. package/src/agents/fixer.ts +145 -0
  131. package/src/agents/index.test.ts +472 -0
  132. package/src/agents/index.ts +248 -0
  133. package/src/agents/interpreter.ts +136 -0
  134. package/src/agents/librarian.test.ts +80 -0
  135. package/src/agents/librarian.ts +145 -0
  136. package/src/agents/oracle.test.ts +89 -0
  137. package/src/agents/oracle.ts +184 -0
  138. package/src/agents/orchestrator.test.ts +116 -0
  139. package/src/agents/orchestrator.ts +574 -0
  140. package/src/agents/overrides.ts +95 -0
  141. package/src/agents/prompt-blocks.test.ts +114 -0
  142. package/src/agents/prompt-blocks.ts +640 -0
  143. package/src/agents/steward.ts +146 -0
  144. package/src/cli/config-io.test.ts +536 -0
  145. package/src/cli/config-io.ts +473 -0
  146. package/src/cli/config-manager.test.ts +141 -0
  147. package/src/cli/config-manager.ts +4 -0
  148. package/src/cli/index.ts +88 -0
  149. package/src/cli/install.ts +282 -0
  150. package/src/cli/mcps.test.ts +62 -0
  151. package/src/cli/mcps.ts +39 -0
  152. package/src/cli/model-key-normalization.test.ts +21 -0
  153. package/src/cli/model-key-normalization.ts +60 -0
  154. package/src/cli/paths.test.ts +167 -0
  155. package/src/cli/paths.ts +144 -0
  156. package/src/cli/providers.test.ts +118 -0
  157. package/src/cli/providers.ts +141 -0
  158. package/src/cli/skills.test.ts +111 -0
  159. package/src/cli/skills.ts +103 -0
  160. package/src/cli/system.test.ts +91 -0
  161. package/src/cli/system.ts +180 -0
  162. package/src/cli/types.ts +43 -0
  163. package/src/config/constants.ts +58 -0
  164. package/src/config/index.ts +4 -0
  165. package/src/config/loader.test.ts +1194 -0
  166. package/src/config/loader.ts +269 -0
  167. package/src/config/model-resolution.test.ts +176 -0
  168. package/src/config/runtime-preset.test.ts +61 -0
  169. package/src/config/runtime-preset.ts +37 -0
  170. package/src/config/schema.ts +248 -0
  171. package/src/config/utils.test.ts +41 -0
  172. package/src/config/utils.ts +23 -0
  173. package/src/discovery/local/types.ts +85 -0
  174. package/src/discovery/local.ts +322 -0
  175. package/src/discovery/mcp-servers.ts +804 -0
  176. package/src/discovery/skills.ts +959 -0
  177. package/src/hooks/apply-patch/codec.test.ts +184 -0
  178. package/src/hooks/apply-patch/codec.ts +352 -0
  179. package/src/hooks/apply-patch/errors.ts +117 -0
  180. package/src/hooks/apply-patch/execution-context.ts +432 -0
  181. package/src/hooks/apply-patch/hook.test.ts +768 -0
  182. package/src/hooks/apply-patch/index.ts +126 -0
  183. package/src/hooks/apply-patch/matching.test.ts +215 -0
  184. package/src/hooks/apply-patch/matching.ts +586 -0
  185. package/src/hooks/apply-patch/operations.test.ts +1535 -0
  186. package/src/hooks/apply-patch/operations.ts +3 -0
  187. package/src/hooks/apply-patch/patch.ts +9 -0
  188. package/src/hooks/apply-patch/prepared-changes.ts +400 -0
  189. package/src/hooks/apply-patch/resolution.test.ts +420 -0
  190. package/src/hooks/apply-patch/resolution.ts +437 -0
  191. package/src/hooks/apply-patch/rewrite.ts +496 -0
  192. package/src/hooks/apply-patch/test-helpers.ts +52 -0
  193. package/src/hooks/apply-patch/types.ts +111 -0
  194. package/src/hooks/auto-update-checker/cache.test.ts +179 -0
  195. package/src/hooks/auto-update-checker/cache.ts +188 -0
  196. package/src/hooks/auto-update-checker/checker.test.ts +159 -0
  197. package/src/hooks/auto-update-checker/checker.ts +308 -0
  198. package/src/hooks/auto-update-checker/constants.ts +33 -0
  199. package/src/hooks/auto-update-checker/index.test.ts +282 -0
  200. package/src/hooks/auto-update-checker/index.ts +225 -0
  201. package/src/hooks/auto-update-checker/types.ts +26 -0
  202. package/src/hooks/chat-headers.test.ts +236 -0
  203. package/src/hooks/chat-headers.ts +97 -0
  204. package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
  205. package/src/hooks/context-pressure-reminder/index.ts +137 -0
  206. package/src/hooks/delegate-task-retry/guidance.ts +41 -0
  207. package/src/hooks/delegate-task-retry/hook.ts +23 -0
  208. package/src/hooks/delegate-task-retry/index.test.ts +38 -0
  209. package/src/hooks/delegate-task-retry/index.ts +7 -0
  210. package/src/hooks/delegate-task-retry/patterns.ts +79 -0
  211. package/src/hooks/filter-available-skills/index.test.ts +297 -0
  212. package/src/hooks/filter-available-skills/index.ts +160 -0
  213. package/src/hooks/foreground-fallback/index.test.ts +624 -0
  214. package/src/hooks/foreground-fallback/index.ts +374 -0
  215. package/src/hooks/image-hook.ts +6 -0
  216. package/src/hooks/index.ts +17 -0
  217. package/src/hooks/json-error-recovery/hook.ts +73 -0
  218. package/src/hooks/json-error-recovery/index.test.ts +111 -0
  219. package/src/hooks/json-error-recovery/index.ts +6 -0
  220. package/src/hooks/phase-reminder/index.test.ts +74 -0
  221. package/src/hooks/phase-reminder/index.ts +85 -0
  222. package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
  223. package/src/hooks/post-file-tool-nudge/index.ts +63 -0
  224. package/src/hooks/task-session-manager/index.test.ts +833 -0
  225. package/src/hooks/task-session-manager/index.ts +434 -0
  226. package/src/hooks/todo-continuation/index.test.ts +3026 -0
  227. package/src/hooks/todo-continuation/index.ts +878 -0
  228. package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
  229. package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
  230. package/src/index.ts +1672 -0
  231. package/src/mcp/context7.ts +14 -0
  232. package/src/mcp/grep-app.ts +11 -0
  233. package/src/mcp/index.test.ts +96 -0
  234. package/src/mcp/index.ts +66 -0
  235. package/src/mcp/types.ts +16 -0
  236. package/src/mcp/websearch.ts +47 -0
  237. package/src/skills/codemap/README.md +60 -0
  238. package/src/skills/codemap/SKILL.md +174 -0
  239. package/src/skills/codemap/scripts/codemap.mjs +483 -0
  240. package/src/skills/codemap/scripts/codemap.test.ts +129 -0
  241. package/src/skills/registry.ts +218 -0
  242. package/src/skills/simplify/README.md +19 -0
  243. package/src/skills/simplify/SKILL.md +138 -0
  244. package/src/subscriptions/accounts-store.test.ts +236 -0
  245. package/src/subscriptions/accounts-store.ts +184 -0
  246. package/src/subscriptions/index.ts +30 -0
  247. package/src/subscriptions/neuralwatt-scraper.ts +108 -0
  248. package/src/subscriptions/opencode-go-scraper.ts +301 -0
  249. package/src/subscriptions/types.ts +145 -0
  250. package/src/subscriptions/usage-service.test.ts +202 -0
  251. package/src/subscriptions/usage-service.ts +651 -0
  252. package/src/tools/ast-grep/cli.ts +257 -0
  253. package/src/tools/ast-grep/constants.ts +214 -0
  254. package/src/tools/ast-grep/downloader.ts +131 -0
  255. package/src/tools/ast-grep/index.ts +24 -0
  256. package/src/tools/ast-grep/tools.ts +117 -0
  257. package/src/tools/ast-grep/types.ts +51 -0
  258. package/src/tools/ast-grep/utils.ts +126 -0
  259. package/src/tools/delegate-handoff.test.ts +18 -0
  260. package/src/tools/delegate.ts +508 -0
  261. package/src/tools/index.ts +8 -0
  262. package/src/tools/preset-manager.test.ts +795 -0
  263. package/src/tools/preset-manager.ts +332 -0
  264. package/src/tools/smartfetch/binary.ts +58 -0
  265. package/src/tools/smartfetch/cache.test.ts +34 -0
  266. package/src/tools/smartfetch/cache.ts +112 -0
  267. package/src/tools/smartfetch/constants.ts +29 -0
  268. package/src/tools/smartfetch/index.ts +8 -0
  269. package/src/tools/smartfetch/network.test.ts +178 -0
  270. package/src/tools/smartfetch/network.ts +614 -0
  271. package/src/tools/smartfetch/secondary-model.test.ts +85 -0
  272. package/src/tools/smartfetch/secondary-model.ts +276 -0
  273. package/src/tools/smartfetch/tool.test.ts +60 -0
  274. package/src/tools/smartfetch/tool.ts +832 -0
  275. package/src/tools/smartfetch/types.ts +135 -0
  276. package/src/tools/smartfetch/utils.test.ts +24 -0
  277. package/src/tools/smartfetch/utils.ts +456 -0
  278. package/src/tui-state.test.ts +867 -0
  279. package/src/tui-state.ts +1255 -0
  280. package/src/tui.test.ts +336 -0
  281. package/src/tui.ts +1539 -0
  282. package/src/utils/agent-variant.test.ts +244 -0
  283. package/src/utils/agent-variant.ts +187 -0
  284. package/src/utils/compat.ts +91 -0
  285. package/src/utils/env.ts +12 -0
  286. package/src/utils/index.ts +9 -0
  287. package/src/utils/internal-initiator.ts +28 -0
  288. package/src/utils/logger.test.ts +220 -0
  289. package/src/utils/logger.ts +136 -0
  290. package/src/utils/polling.test.ts +191 -0
  291. package/src/utils/polling.ts +67 -0
  292. package/src/utils/session-manager.test.ts +173 -0
  293. package/src/utils/session-manager.ts +356 -0
  294. package/src/utils/session.test.ts +110 -0
  295. package/src/utils/session.ts +389 -0
  296. package/src/utils/subagent-depth.test.ts +170 -0
  297. package/src/utils/subagent-depth.ts +75 -0
  298. package/src/utils/system-collapse.test.ts +86 -0
  299. package/src/utils/system-collapse.ts +24 -0
  300. package/src/utils/task.test.ts +24 -0
  301. package/src/utils/task.ts +20 -0
  302. package/src/utils/zip-extractor.ts +102 -0
@@ -0,0 +1,833 @@
1
+ import { describe, expect, mock, test } from 'bun:test';
2
+ import { createTaskSessionManagerHook } from './index';
3
+
4
+ function createHook(options?: {
5
+ shouldManageSession?: (sessionID: string) => boolean;
6
+ readContextMinLines?: number;
7
+ readContextMaxFiles?: number;
8
+ }) {
9
+ const hook = createTaskSessionManagerHook(
10
+ {
11
+ client: { session: { status: mock(async () => ({ data: {} })) } },
12
+ directory: '/tmp',
13
+ worktree: '/tmp',
14
+ } as never,
15
+ {
16
+ maxSessionsPerAgent: 2,
17
+ readContextMinLines: options?.readContextMinLines,
18
+ readContextMaxFiles: options?.readContextMaxFiles,
19
+ shouldManageSession: options?.shouldManageSession ?? (() => true),
20
+ },
21
+ );
22
+
23
+ return { hook };
24
+ }
25
+
26
+ function createMessages(sessionID: string, text = 'user message') {
27
+ return {
28
+ messages: [
29
+ {
30
+ info: { role: 'user', agent: 'orchestrator', sessionID },
31
+ parts: [{ type: 'text', text }],
32
+ },
33
+ ],
34
+ };
35
+ }
36
+
37
+ describe('task-session-manager hook', () => {
38
+ test('stores task sessions and injects resumable-session block into user message', async () => {
39
+ const { hook } = createHook();
40
+
41
+ await hook['tool.execute.before'](
42
+ {
43
+ tool: 'task',
44
+ sessionID: 'parent-1',
45
+ callID: 'call-1',
46
+ },
47
+ {
48
+ args: {
49
+ subagent_type: 'explorer',
50
+ description: 'config schema',
51
+ prompt: 'inspect config schema',
52
+ },
53
+ },
54
+ );
55
+
56
+ await hook['tool.execute.after'](
57
+ {
58
+ tool: 'task',
59
+ sessionID: 'parent-1',
60
+ callID: 'call-1',
61
+ },
62
+ {
63
+ output:
64
+ 'task_id: child-1 (for resuming to continue this task if needed)',
65
+ },
66
+ );
67
+
68
+ const messages = createMessages('parent-1', 'do something');
69
+ await hook['experimental.chat.messages.transform']({}, messages);
70
+
71
+ const userMessage = messages.messages[0];
72
+ expect(userMessage.parts[0].text).toContain('<resumable_sessions>');
73
+ expect(userMessage.parts[0].text).toContain('### Resumable Sessions');
74
+ expect(userMessage.parts[0].text).toContain(
75
+ 'explorer: exp-1 config schema',
76
+ );
77
+ expect(userMessage.parts[0].text).toContain('</resumable_sessions>');
78
+ });
79
+
80
+ test('does not expose a system transform for resumable sessions', async () => {
81
+ const { hook } = createHook();
82
+ expect('experimental.chat.system.transform' in hook).toBe(false);
83
+ });
84
+
85
+ test('resolves remembered aliases to real task ids before execution', async () => {
86
+ const { hook } = createHook();
87
+
88
+ await hook['tool.execute.before'](
89
+ {
90
+ tool: 'task',
91
+ sessionID: 'parent-1',
92
+ callID: 'call-1',
93
+ },
94
+ {
95
+ args: {
96
+ subagent_type: 'explorer',
97
+ description: 'config schema',
98
+ prompt: 'inspect config schema',
99
+ },
100
+ },
101
+ );
102
+ await hook['tool.execute.after'](
103
+ {
104
+ tool: 'task',
105
+ sessionID: 'parent-1',
106
+ callID: 'call-1',
107
+ },
108
+ {
109
+ output:
110
+ 'task_id: child-1 (for resuming to continue this task if needed)',
111
+ },
112
+ );
113
+
114
+ const next = {
115
+ args: {
116
+ subagent_type: 'explorer',
117
+ description: 'continue schema work',
118
+ task_id: 'exp-1',
119
+ },
120
+ };
121
+ await hook['tool.execute.before'](
122
+ {
123
+ tool: 'task',
124
+ sessionID: 'parent-1',
125
+ callID: 'call-2',
126
+ },
127
+ next,
128
+ );
129
+
130
+ expect(next.args.task_id).toBe('child-1');
131
+ });
132
+
133
+ test('tracks files read by child sessions in resumable message context', async () => {
134
+ const { hook } = createHook();
135
+
136
+ await hook.event({
137
+ event: {
138
+ type: 'session.created',
139
+ properties: { info: { id: 'child-1', parentID: 'parent-1' } },
140
+ },
141
+ });
142
+
143
+ await hook['tool.execute.after'](
144
+ {
145
+ tool: 'read',
146
+ sessionID: 'child-1',
147
+ callID: 'read-1',
148
+ },
149
+ {
150
+ output: [
151
+ '<path>/tmp/src/index.ts</path>',
152
+ '<type>file</type>',
153
+ '<content>',
154
+ ...Array.from({ length: 12 }, (_, index) => `${index + 1}: line`),
155
+ '</content>',
156
+ ].join('\n'),
157
+ metadata: {
158
+ loaded: ['/tmp/AGENTS.md'],
159
+ },
160
+ },
161
+ );
162
+
163
+ await hook['tool.execute.before'](
164
+ {
165
+ tool: 'task',
166
+ sessionID: 'parent-1',
167
+ callID: 'call-1',
168
+ },
169
+ {
170
+ args: {
171
+ subagent_type: 'explorer',
172
+ description: 'session files',
173
+ },
174
+ },
175
+ );
176
+ await hook['tool.execute.after'](
177
+ {
178
+ tool: 'task',
179
+ sessionID: 'parent-1',
180
+ callID: 'call-1',
181
+ },
182
+ {
183
+ output:
184
+ 'task_id: child-1 (for resuming to continue this task if needed)',
185
+ },
186
+ );
187
+
188
+ const messages = createMessages('parent-1', 'do something');
189
+ await hook['experimental.chat.messages.transform']({}, messages);
190
+
191
+ const userMessage = messages.messages[0];
192
+ expect(userMessage.parts[0].text).toContain('exp-1 session files');
193
+ expect(userMessage.parts[0].text).toContain(
194
+ 'Context read by exp-1: src/index.ts (12 lines)',
195
+ );
196
+ });
197
+
198
+ test('accumulates multiple reads and hides tiny read context', async () => {
199
+ const { hook } = createHook();
200
+
201
+ await hook.event({
202
+ event: {
203
+ type: 'session.created',
204
+ properties: { info: { id: 'child-1', parentID: 'parent-1' } },
205
+ },
206
+ });
207
+
208
+ await hook['tool.execute.after'](
209
+ { tool: 'read', sessionID: 'child-1', callID: 'read-1' },
210
+ {
211
+ output: [
212
+ '<path>/tmp/src/small.ts</path>',
213
+ '<content>',
214
+ ...Array.from({ length: 4 }, (_, index) => `${index + 1}: line`),
215
+ '</content>',
216
+ ].join('\n'),
217
+ },
218
+ );
219
+ await hook['tool.execute.after'](
220
+ { tool: 'read', sessionID: 'child-1', callID: 'read-2' },
221
+ {
222
+ output: [
223
+ '<path>/tmp/src/large.ts</path>',
224
+ '<content>',
225
+ ...Array.from({ length: 7 }, (_, index) => `${index + 1}: line`),
226
+ '</content>',
227
+ ].join('\n'),
228
+ },
229
+ );
230
+ await hook['tool.execute.after'](
231
+ { tool: 'read', sessionID: 'child-1', callID: 'read-3' },
232
+ {
233
+ output: [
234
+ '<path>/tmp/src/large.ts</path>',
235
+ '<content>',
236
+ ...Array.from({ length: 5 }, (_, index) => `${index + 8}: line`),
237
+ '</content>',
238
+ ].join('\n'),
239
+ },
240
+ );
241
+
242
+ await hook['tool.execute.before'](
243
+ { tool: 'task', sessionID: 'parent-1', callID: 'call-1' },
244
+ { args: { subagent_type: 'explorer', description: 'line counts' } },
245
+ );
246
+ await hook['tool.execute.after'](
247
+ { tool: 'task', sessionID: 'parent-1', callID: 'call-1' },
248
+ {
249
+ output:
250
+ 'task_id: child-1 (for resuming to continue this task if needed)',
251
+ },
252
+ );
253
+
254
+ const messages = createMessages('parent-1', 'do something');
255
+ await hook['experimental.chat.messages.transform']({}, messages);
256
+
257
+ const prompt = messages.messages[0].parts[0].text;
258
+ expect(prompt).not.toContain('small.ts');
259
+ expect(prompt).toContain('src/large.ts (12 lines)');
260
+ });
261
+
262
+ test('counts overlapping repeated reads once per unique line', async () => {
263
+ const { hook } = createHook();
264
+
265
+ await hook.event({
266
+ event: {
267
+ type: 'session.created',
268
+ properties: { info: { id: 'child-1', parentID: 'parent-1' } },
269
+ },
270
+ });
271
+ for (const call of ['read-1', 'read-2']) {
272
+ await hook['tool.execute.after'](
273
+ { tool: 'read', sessionID: 'child-1', callID: call },
274
+ {
275
+ output: [
276
+ '<path>/tmp/src/repeat.ts</path>',
277
+ '<content>',
278
+ ...Array.from({ length: 12 }, (_, index) => `${index + 1}: line`),
279
+ '</content>',
280
+ ].join('\n'),
281
+ },
282
+ );
283
+ }
284
+
285
+ await hook['tool.execute.before'](
286
+ { tool: 'task', sessionID: 'parent-1', callID: 'call-1' },
287
+ { args: { subagent_type: 'explorer', description: 'repeat reads' } },
288
+ );
289
+ await hook['tool.execute.after'](
290
+ { tool: 'task', sessionID: 'parent-1', callID: 'call-1' },
291
+ {
292
+ output:
293
+ 'task_id: child-1 (for resuming to continue this task if needed)',
294
+ },
295
+ );
296
+
297
+ const messages = createMessages('parent-1', 'do something');
298
+ await hook['experimental.chat.messages.transform']({}, messages);
299
+
300
+ const prompt = messages.messages[0].parts[0].text;
301
+ expect(prompt).toContain('src/repeat.ts (12 lines)');
302
+ expect(prompt).not.toContain('src/repeat.ts (24 lines)');
303
+ });
304
+
305
+ test('uses configured read context thresholds', async () => {
306
+ const { hook } = createHook({
307
+ readContextMinLines: 5,
308
+ readContextMaxFiles: 1,
309
+ });
310
+
311
+ await hook.event({
312
+ event: {
313
+ type: 'session.created',
314
+ properties: { info: { id: 'child-1', parentID: 'parent-1' } },
315
+ },
316
+ });
317
+ for (const [file, lines] of [
318
+ ['small.ts', 4],
319
+ ['medium.ts', 5],
320
+ ['large.ts', 12],
321
+ ] as const) {
322
+ await hook['tool.execute.after'](
323
+ { tool: 'read', sessionID: 'child-1', callID: `read-${file}` },
324
+ {
325
+ output: [
326
+ `<path>/tmp/src/${file}</path>`,
327
+ '<content>',
328
+ ...Array.from({ length: lines }, (_, line) => `${line + 1}: line`),
329
+ '</content>',
330
+ ].join('\n'),
331
+ },
332
+ );
333
+ }
334
+
335
+ await hook['tool.execute.before'](
336
+ { tool: 'task', sessionID: 'parent-1', callID: 'call-1' },
337
+ { args: { subagent_type: 'explorer', description: 'configured caps' } },
338
+ );
339
+ await hook['tool.execute.after'](
340
+ { tool: 'task', sessionID: 'parent-1', callID: 'call-1' },
341
+ {
342
+ output:
343
+ 'task_id: child-1 (for resuming to continue this task if needed)',
344
+ },
345
+ );
346
+
347
+ const messages = createMessages('parent-1', 'do something');
348
+ await hook['experimental.chat.messages.transform']({}, messages);
349
+
350
+ const prompt = messages.messages[0].parts[0].text;
351
+ expect(prompt).not.toContain('small.ts');
352
+ expect(prompt).toContain('Context read by exp-1:');
353
+ expect(prompt).toContain('(+1 more)');
354
+ });
355
+
356
+ test('ignores reads from unmanaged child sessions', async () => {
357
+ const { hook } = createHook({
358
+ shouldManageSession: (sessionID) => sessionID === 'parent-1',
359
+ });
360
+
361
+ await hook.event({
362
+ event: {
363
+ type: 'session.created',
364
+ properties: { info: { id: 'child-1', parentID: 'other-parent' } },
365
+ },
366
+ });
367
+ await hook['tool.execute.after'](
368
+ { tool: 'read', sessionID: 'child-1', callID: 'read-1' },
369
+ {
370
+ output: [
371
+ '<path>/tmp/src/index.ts</path>',
372
+ '<content>',
373
+ ...Array.from({ length: 12 }, (_, index) => `${index + 1}: line`),
374
+ '</content>',
375
+ ].join('\n'),
376
+ },
377
+ );
378
+
379
+ await hook['tool.execute.before'](
380
+ { tool: 'task', sessionID: 'parent-1', callID: 'call-1' },
381
+ { args: { subagent_type: 'explorer', description: 'unmanaged read' } },
382
+ );
383
+ await hook['tool.execute.after'](
384
+ { tool: 'task', sessionID: 'parent-1', callID: 'call-1' },
385
+ {
386
+ output:
387
+ 'task_id: child-1 (for resuming to continue this task if needed)',
388
+ },
389
+ );
390
+
391
+ const messages = createMessages('parent-1', 'do something');
392
+ await hook['experimental.chat.messages.transform']({}, messages);
393
+
394
+ const prompt = messages.messages[0].parts[0].text;
395
+ expect(prompt).toContain('exp-1 unmanaged read');
396
+ expect(prompt).not.toContain('Context read by exp-1');
397
+ });
398
+
399
+ test('prunes read context when remembered sessions are evicted', async () => {
400
+ const { hook } = createHook();
401
+
402
+ for (const index of [1, 2, 3]) {
403
+ await hook.event({
404
+ event: {
405
+ type: 'session.created',
406
+ properties: {
407
+ info: { id: `child-${index}`, parentID: 'parent-1' },
408
+ },
409
+ },
410
+ });
411
+ await hook['tool.execute.after'](
412
+ { tool: 'read', sessionID: `child-${index}`, callID: `read-${index}` },
413
+ {
414
+ output: [
415
+ `<path>/tmp/src/file-${index}.ts</path>`,
416
+ '<content>',
417
+ ...Array.from({ length: 12 }, (_, line) => `${line + 1}: line`),
418
+ '</content>',
419
+ ].join('\n'),
420
+ },
421
+ );
422
+ await hook['tool.execute.before'](
423
+ { tool: 'task', sessionID: 'parent-1', callID: `call-${index}` },
424
+ { args: { subagent_type: 'explorer', description: `thread ${index}` } },
425
+ );
426
+ await hook['tool.execute.after'](
427
+ { tool: 'task', sessionID: 'parent-1', callID: `call-${index}` },
428
+ {
429
+ output: `task_id: child-${index} (for resuming to continue this task if needed)`,
430
+ },
431
+ );
432
+ }
433
+
434
+ const messages = createMessages('parent-1', 'do something');
435
+ await hook['experimental.chat.messages.transform']({}, messages);
436
+
437
+ const prompt = messages.messages[0].parts[0].text;
438
+ expect(prompt).not.toContain('exp-1 thread 1');
439
+ expect(prompt).not.toContain('file-1.ts');
440
+ expect(prompt).toContain('exp-2 thread 2');
441
+ expect(prompt).toContain('file-2.ts (12 lines)');
442
+ expect(prompt).toContain('exp-3 thread 3');
443
+ expect(prompt).toContain('file-3.ts (12 lines)');
444
+ });
445
+
446
+ test('drops stale remembered sessions and falls back to fresh', async () => {
447
+ const { hook } = createHook();
448
+
449
+ await hook['tool.execute.before'](
450
+ {
451
+ tool: 'task',
452
+ sessionID: 'parent-1',
453
+ callID: 'call-1',
454
+ },
455
+ {
456
+ args: {
457
+ subagent_type: 'explorer',
458
+ description: 'config schema',
459
+ },
460
+ },
461
+ );
462
+ await hook['tool.execute.after'](
463
+ {
464
+ tool: 'task',
465
+ sessionID: 'parent-1',
466
+ callID: 'call-1',
467
+ },
468
+ {
469
+ output:
470
+ 'task_id: child-1 (for resuming to continue this task if needed)',
471
+ },
472
+ );
473
+
474
+ const next = {
475
+ args: {
476
+ subagent_type: 'explorer',
477
+ description: 'continue schema work',
478
+ task_id: 'exp-1',
479
+ },
480
+ };
481
+ await hook['tool.execute.before'](
482
+ {
483
+ tool: 'task',
484
+ sessionID: 'parent-1',
485
+ callID: 'call-2',
486
+ },
487
+ next,
488
+ );
489
+
490
+ expect(next.args.task_id).toBe('child-1');
491
+
492
+ await hook['tool.execute.after'](
493
+ {
494
+ tool: 'task',
495
+ sessionID: 'parent-1',
496
+ callID: 'call-2',
497
+ },
498
+ {
499
+ output: '[ERROR] Session not found',
500
+ },
501
+ );
502
+
503
+ const messages = createMessages('parent-1', 'do something');
504
+ await hook['experimental.chat.messages.transform']({}, messages);
505
+ expect(messages.messages[0].parts[0].text).not.toContain('exp-1');
506
+ });
507
+
508
+ test('drops resumed predecessor when success returns a new task id', async () => {
509
+ const { hook } = createHook();
510
+
511
+ await hook['tool.execute.before'](
512
+ {
513
+ tool: 'task',
514
+ sessionID: 'parent-1',
515
+ callID: 'call-1',
516
+ },
517
+ {
518
+ args: {
519
+ subagent_type: 'explorer',
520
+ description: 'config schema',
521
+ },
522
+ },
523
+ );
524
+ await hook['tool.execute.after'](
525
+ {
526
+ tool: 'task',
527
+ sessionID: 'parent-1',
528
+ callID: 'call-1',
529
+ },
530
+ {
531
+ output:
532
+ 'task_id: child-1 (for resuming to continue this task if needed)',
533
+ },
534
+ );
535
+
536
+ await hook['tool.execute.before'](
537
+ {
538
+ tool: 'task',
539
+ sessionID: 'parent-1',
540
+ callID: 'call-2',
541
+ },
542
+ {
543
+ args: {
544
+ subagent_type: 'explorer',
545
+ description: 'continue schema work',
546
+ task_id: 'exp-1',
547
+ },
548
+ },
549
+ );
550
+ await hook['tool.execute.after'](
551
+ {
552
+ tool: 'task',
553
+ sessionID: 'parent-1',
554
+ callID: 'call-2',
555
+ },
556
+ {
557
+ output:
558
+ 'task_id: child-2 (for resuming to continue this task if needed)',
559
+ },
560
+ );
561
+
562
+ const messages = createMessages('parent-1', 'do something');
563
+ await hook['experimental.chat.messages.transform']({}, messages);
564
+
565
+ const prompt = messages.messages[0].parts[0].text;
566
+ expect(prompt).toContain('continue schema work');
567
+ expect(prompt).not.toContain('config schema');
568
+ });
569
+
570
+ test('does not drop remembered session on non-runtime session text', async () => {
571
+ const { hook } = createHook();
572
+
573
+ await hook['tool.execute.before'](
574
+ {
575
+ tool: 'task',
576
+ sessionID: 'parent-1',
577
+ callID: 'call-1',
578
+ },
579
+ {
580
+ args: {
581
+ subagent_type: 'explorer',
582
+ description: 'config schema',
583
+ },
584
+ },
585
+ );
586
+ await hook['tool.execute.after'](
587
+ {
588
+ tool: 'task',
589
+ sessionID: 'parent-1',
590
+ callID: 'call-1',
591
+ },
592
+ {
593
+ output:
594
+ 'task_id: child-1 (for resuming to continue this task if needed)',
595
+ },
596
+ );
597
+
598
+ await hook['tool.execute.before'](
599
+ {
600
+ tool: 'task',
601
+ sessionID: 'parent-1',
602
+ callID: 'call-2',
603
+ },
604
+ {
605
+ args: {
606
+ subagent_type: 'explorer',
607
+ description: 'continue schema work',
608
+ task_id: 'exp-1',
609
+ },
610
+ },
611
+ );
612
+ await hook['tool.execute.after'](
613
+ {
614
+ tool: 'task',
615
+ sessionID: 'parent-1',
616
+ callID: 'call-2',
617
+ },
618
+ {
619
+ output: 'Found no session cookies in fixtures, continuing analysis.',
620
+ },
621
+ );
622
+
623
+ const messages = createMessages('parent-1', 'do something');
624
+ await hook['experimental.chat.messages.transform']({}, messages);
625
+
626
+ expect(messages.messages[0].parts[0].text).toContain('exp-1 config schema');
627
+ });
628
+
629
+ test('ignores sessions that are not orchestrator-managed', async () => {
630
+ const { hook } = createHook({ shouldManageSession: () => false });
631
+
632
+ await hook['tool.execute.before'](
633
+ {
634
+ tool: 'task',
635
+ sessionID: 'manual-1',
636
+ callID: 'call-1',
637
+ },
638
+ {
639
+ args: {
640
+ subagent_type: 'explorer',
641
+ description: 'config schema',
642
+ },
643
+ },
644
+ );
645
+ await hook['tool.execute.after'](
646
+ {
647
+ tool: 'task',
648
+ sessionID: 'manual-1',
649
+ callID: 'call-1',
650
+ },
651
+ {
652
+ output:
653
+ 'task_id: child-1 (for resuming to continue this task if needed)',
654
+ },
655
+ );
656
+
657
+ const messages = createMessages('manual-1', 'do something');
658
+ await hook['experimental.chat.messages.transform']({}, messages);
659
+
660
+ // Message should remain unchanged
661
+ expect(messages.messages[0].parts[0].text).toBe('do something');
662
+ });
663
+
664
+ test('cleans up remembered sessions when parent or child is deleted', async () => {
665
+ const { hook } = createHook();
666
+
667
+ await hook['tool.execute.before'](
668
+ {
669
+ tool: 'task',
670
+ sessionID: 'parent-1',
671
+ callID: 'call-1',
672
+ },
673
+ {
674
+ args: {
675
+ subagent_type: 'oracle',
676
+ description: 'architecture review',
677
+ },
678
+ },
679
+ );
680
+ await hook['tool.execute.after'](
681
+ {
682
+ tool: 'task',
683
+ sessionID: 'parent-1',
684
+ callID: 'call-1',
685
+ },
686
+ {
687
+ output:
688
+ 'task_id: child-1 (for resuming to continue this task if needed)',
689
+ },
690
+ );
691
+
692
+ await hook.event({
693
+ event: {
694
+ type: 'session.deleted',
695
+ properties: { sessionID: 'child-1' },
696
+ },
697
+ });
698
+
699
+ const messages = createMessages('parent-1', 'do something');
700
+ await hook['experimental.chat.messages.transform']({}, messages);
701
+ // Message should remain unchanged since session was deleted
702
+ expect(messages.messages[0].parts[0].text).toBe('do something');
703
+ });
704
+
705
+ test('cleans pending calls when parent session is deleted', async () => {
706
+ const { hook } = createHook();
707
+
708
+ await hook['tool.execute.before'](
709
+ {
710
+ tool: 'task',
711
+ sessionID: 'parent-1',
712
+ callID: 'call-1',
713
+ },
714
+ {
715
+ args: {
716
+ subagent_type: 'oracle',
717
+ description: 'architecture review',
718
+ },
719
+ },
720
+ );
721
+
722
+ await hook.event({
723
+ event: {
724
+ type: 'session.deleted',
725
+ properties: { sessionID: 'parent-1' },
726
+ },
727
+ });
728
+
729
+ await hook['tool.execute.after'](
730
+ {
731
+ tool: 'task',
732
+ sessionID: 'parent-1',
733
+ callID: 'call-1',
734
+ },
735
+ {
736
+ output:
737
+ 'task_id: child-1 (for resuming to continue this task if needed)',
738
+ },
739
+ );
740
+
741
+ const messages = createMessages('parent-1', 'do something');
742
+ await hook['experimental.chat.messages.transform']({}, messages);
743
+
744
+ // Message should remain unchanged since session was deleted
745
+ expect(messages.messages[0].parts[0].text).toBe('do something');
746
+ });
747
+
748
+ test('deduplicates pending call order when a resume call is recorded twice', async () => {
749
+ const { hook } = createHook();
750
+
751
+ await hook['tool.execute.before'](
752
+ {
753
+ tool: 'task',
754
+ sessionID: 'parent-1',
755
+ callID: 'call-1',
756
+ },
757
+ {
758
+ args: {
759
+ subagent_type: 'explorer',
760
+ description: 'config schema',
761
+ },
762
+ },
763
+ );
764
+ await hook['tool.execute.after'](
765
+ {
766
+ tool: 'task',
767
+ sessionID: 'parent-1',
768
+ callID: 'call-1',
769
+ },
770
+ {
771
+ output:
772
+ 'task_id: child-1 (for resuming to continue this task if needed)',
773
+ },
774
+ );
775
+
776
+ await hook['tool.execute.before'](
777
+ {
778
+ tool: 'task',
779
+ sessionID: 'parent-1',
780
+ callID: 'call-2',
781
+ },
782
+ {
783
+ args: {
784
+ subagent_type: 'explorer',
785
+ description: 'continue schema work',
786
+ task_id: 'exp-1',
787
+ },
788
+ },
789
+ );
790
+ await hook['tool.execute.after'](
791
+ {
792
+ tool: 'task',
793
+ sessionID: 'parent-1',
794
+ callID: 'call-2',
795
+ },
796
+ {
797
+ output: '[ERROR] Session not found',
798
+ },
799
+ );
800
+
801
+ await hook['tool.execute.before'](
802
+ {
803
+ tool: 'task',
804
+ sessionID: 'parent-1',
805
+ callID: 'call-3',
806
+ },
807
+ {
808
+ args: {
809
+ subagent_type: 'oracle',
810
+ description: 'architecture review',
811
+ },
812
+ },
813
+ );
814
+ await hook['tool.execute.after'](
815
+ {
816
+ tool: 'task',
817
+ sessionID: 'parent-1',
818
+ callID: 'call-3',
819
+ },
820
+ {
821
+ output:
822
+ 'task_id: child-3 (for resuming to continue this task if needed)',
823
+ },
824
+ );
825
+
826
+ const messages = createMessages('parent-1', 'do something');
827
+ await hook['experimental.chat.messages.transform']({}, messages);
828
+
829
+ expect(messages.messages[0].parts[0].text).toContain(
830
+ 'oracle: ora-1 architecture review',
831
+ );
832
+ });
833
+ });