@qduc/term2 0.11.0 → 0.11.1

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 (146) hide show
  1. package/dist/agent.d.ts +11 -0
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +54 -18
  4. package/dist/agent.js.map +1 -1
  5. package/dist/components/layout/BackgroundTasksPanel.d.ts.map +1 -1
  6. package/dist/components/layout/BackgroundTasksPanel.js +35 -16
  7. package/dist/components/layout/BackgroundTasksPanel.js.map +1 -1
  8. package/dist/components/message/CommandMessage.js +4 -2
  9. package/dist/components/message/CommandMessage.js.map +1 -1
  10. package/dist/components/message/command-message-helpers.d.ts +4 -4
  11. package/dist/hooks/conversation-ui-reducer.d.ts +5 -0
  12. package/dist/hooks/conversation-ui-reducer.d.ts.map +1 -1
  13. package/dist/hooks/conversation-ui-reducer.js +8 -0
  14. package/dist/hooks/conversation-ui-reducer.js.map +1 -1
  15. package/dist/hooks/use-conversation.d.ts.map +1 -1
  16. package/dist/hooks/use-conversation.js +1 -0
  17. package/dist/hooks/use-conversation.js.map +1 -1
  18. package/dist/lib/agent-configuration.d.ts.map +1 -1
  19. package/dist/lib/agent-configuration.js +3 -0
  20. package/dist/lib/agent-configuration.js.map +1 -1
  21. package/dist/lib/agent-factory.d.ts +11 -0
  22. package/dist/lib/agent-factory.d.ts.map +1 -1
  23. package/dist/lib/agent-factory.js +3 -0
  24. package/dist/lib/agent-factory.js.map +1 -1
  25. package/dist/lib/chained-input-filter.d.ts +20 -0
  26. package/dist/lib/chained-input-filter.d.ts.map +1 -1
  27. package/dist/lib/chained-input-filter.js +75 -0
  28. package/dist/lib/chained-input-filter.js.map +1 -1
  29. package/dist/lib/subagent-bridge.d.ts +20 -1
  30. package/dist/lib/subagent-bridge.d.ts.map +1 -1
  31. package/dist/lib/subagent-bridge.js +26 -0
  32. package/dist/lib/subagent-bridge.js.map +1 -1
  33. package/dist/prompts/anthropic.md +6 -55
  34. package/dist/prompts/async-subagent-delegation.d.ts +2 -1
  35. package/dist/prompts/async-subagent-delegation.d.ts.map +1 -1
  36. package/dist/prompts/async-subagent-delegation.js +20 -6
  37. package/dist/prompts/async-subagent-delegation.js.map +1 -1
  38. package/dist/prompts/async-subagent-delegation.ts +26 -6
  39. package/dist/prompts/gpt-5.6.md +49 -0
  40. package/dist/prompts/kimi.md +25 -63
  41. package/dist/prompts/orchestrator-prompt.test.ts +77 -0
  42. package/dist/prompts/orchestrator.md +15 -1
  43. package/dist/prompts/prompt-constructor.d.ts +1 -0
  44. package/dist/prompts/prompt-constructor.d.ts.map +1 -1
  45. package/dist/prompts/prompt-constructor.js +6 -2
  46. package/dist/prompts/prompt-constructor.js.map +1 -1
  47. package/dist/prompts/prompt-constructor.test.ts +22 -1
  48. package/dist/prompts/prompt-constructor.ts +9 -1
  49. package/dist/prompts/prompt-profiles.d.ts.map +1 -1
  50. package/dist/prompts/prompt-profiles.js +5 -0
  51. package/dist/prompts/prompt-profiles.js.map +1 -1
  52. package/dist/prompts/prompt-profiles.test.ts +20 -0
  53. package/dist/prompts/prompt-profiles.ts +5 -0
  54. package/dist/prompts/search-via-shell.d.ts.map +1 -1
  55. package/dist/prompts/search-via-shell.js +8 -25
  56. package/dist/prompts/search-via-shell.js.map +1 -1
  57. package/dist/prompts/search-via-shell.test.ts +18 -0
  58. package/dist/prompts/search-via-shell.ts +8 -25
  59. package/dist/prompts/subagent-delegation.test.ts +15 -0
  60. package/dist/prompts/subagents/researcher.md +6 -0
  61. package/dist/prompts/subagents/worker-prompt.test.ts +35 -0
  62. package/dist/prompts/subagents/worker.md +8 -0
  63. package/dist/services/conversation/conversation-adapter.d.ts +11 -3
  64. package/dist/services/conversation/conversation-adapter.d.ts.map +1 -1
  65. package/dist/services/conversation/conversation-adapter.js +123 -31
  66. package/dist/services/conversation/conversation-adapter.js.map +1 -1
  67. package/dist/services/conversation/conversation-events.d.ts +25 -1
  68. package/dist/services/conversation/conversation-events.d.ts.map +1 -1
  69. package/dist/services/conversation/conversation-orchestrator.d.ts.map +1 -1
  70. package/dist/services/conversation/conversation-orchestrator.js +141 -26
  71. package/dist/services/conversation/conversation-orchestrator.js.map +1 -1
  72. package/dist/services/conversation/conversation-orchestrator.types.d.ts +2 -0
  73. package/dist/services/conversation/conversation-orchestrator.types.d.ts.map +1 -1
  74. package/dist/services/conversation/conversation-service.d.ts +3 -0
  75. package/dist/services/conversation/conversation-service.d.ts.map +1 -1
  76. package/dist/services/conversation/conversation-service.js +4 -0
  77. package/dist/services/conversation/conversation-service.js.map +1 -1
  78. package/dist/services/logging/conversation-log-events.d.ts +9 -1
  79. package/dist/services/logging/conversation-log-events.d.ts.map +1 -1
  80. package/dist/services/logging/conversation-log-events.js.map +1 -1
  81. package/dist/services/logging/conversation-log-writer.d.ts +1 -0
  82. package/dist/services/logging/conversation-log-writer.d.ts.map +1 -1
  83. package/dist/services/logging/conversation-log-writer.js +1 -1
  84. package/dist/services/logging/conversation-log-writer.js.map +1 -1
  85. package/dist/services/logging/conversation-logger.d.ts.map +1 -1
  86. package/dist/services/logging/conversation-logger.js +10 -0
  87. package/dist/services/logging/conversation-logger.js.map +1 -1
  88. package/dist/services/queue/queue-controller.d.ts +1 -0
  89. package/dist/services/queue/queue-controller.d.ts.map +1 -1
  90. package/dist/services/queue/queue-controller.js +7 -4
  91. package/dist/services/queue/queue-controller.js.map +1 -1
  92. package/dist/services/retry/retry-classifier.d.ts.map +1 -1
  93. package/dist/services/retry/retry-classifier.js +4 -2
  94. package/dist/services/retry/retry-classifier.js.map +1 -1
  95. package/dist/services/subagents/execution-runner.d.ts +3 -3
  96. package/dist/services/subagents/execution-runner.d.ts.map +1 -1
  97. package/dist/services/subagents/execution-runner.js +96 -7
  98. package/dist/services/subagents/execution-runner.js.map +1 -1
  99. package/dist/services/subagents/mentor-runner.d.ts.map +1 -1
  100. package/dist/services/subagents/mentor-runner.js +24 -5
  101. package/dist/services/subagents/mentor-runner.js.map +1 -1
  102. package/dist/services/subagents/runtime.d.ts.map +1 -1
  103. package/dist/services/subagents/runtime.js +17 -7
  104. package/dist/services/subagents/runtime.js.map +1 -1
  105. package/dist/services/subagents/subagent-async-registry.d.ts +30 -2
  106. package/dist/services/subagents/subagent-async-registry.d.ts.map +1 -1
  107. package/dist/services/subagents/subagent-async-registry.js +314 -41
  108. package/dist/services/subagents/subagent-async-registry.js.map +1 -1
  109. package/dist/services/subagents/subagent-manager.d.ts +15 -1
  110. package/dist/services/subagents/subagent-manager.d.ts.map +1 -1
  111. package/dist/services/subagents/subagent-manager.js +16 -0
  112. package/dist/services/subagents/subagent-manager.js.map +1 -1
  113. package/dist/services/subagents/subagent-notification-store.d.ts +40 -22
  114. package/dist/services/subagents/subagent-notification-store.d.ts.map +1 -1
  115. package/dist/services/subagents/subagent-notification-store.js +81 -39
  116. package/dist/services/subagents/subagent-notification-store.js.map +1 -1
  117. package/dist/services/subagents/subagent-run-control.d.ts +41 -0
  118. package/dist/services/subagents/subagent-run-control.d.ts.map +1 -0
  119. package/dist/services/subagents/subagent-run-control.js +166 -0
  120. package/dist/services/subagents/subagent-run-control.js.map +1 -0
  121. package/dist/services/subagents/tool-policy.d.ts +24 -4
  122. package/dist/services/subagents/tool-policy.d.ts.map +1 -1
  123. package/dist/services/subagents/tool-policy.js +134 -14
  124. package/dist/services/subagents/tool-policy.js.map +1 -1
  125. package/dist/services/subagents/types.d.ts +93 -0
  126. package/dist/services/subagents/types.d.ts.map +1 -1
  127. package/dist/tools/agent/ask-orchestrator.d.ts +11 -0
  128. package/dist/tools/agent/ask-orchestrator.d.ts.map +1 -0
  129. package/dist/tools/agent/ask-orchestrator.js +39 -0
  130. package/dist/tools/agent/ask-orchestrator.js.map +1 -0
  131. package/dist/tools/agent/run-subagent-async.d.ts +43 -1
  132. package/dist/tools/agent/run-subagent-async.d.ts.map +1 -1
  133. package/dist/tools/agent/run-subagent-async.js +230 -7
  134. package/dist/tools/agent/run-subagent-async.js.map +1 -1
  135. package/dist/tools/file/code-context.d.ts +1 -0
  136. package/dist/tools/file/code-context.d.ts.map +1 -1
  137. package/dist/tools/file/code-context.js +4 -4
  138. package/dist/tools/file/code-context.js.map +1 -1
  139. package/dist/tools/file/grep.d.ts +1 -0
  140. package/dist/tools/file/grep.d.ts.map +1 -1
  141. package/dist/tools/file/grep.js +16 -10
  142. package/dist/tools/file/grep.js.map +1 -1
  143. package/dist/utils/conversation/conversation-event-handler.d.ts.map +1 -1
  144. package/dist/utils/conversation/conversation-event-handler.js +6 -3
  145. package/dist/utils/conversation/conversation-event-handler.js.map +1 -1
  146. package/package.json +1 -1
@@ -31,6 +31,11 @@ export const PROMPT_PROFILES: PromptProfile[] = [
31
31
  basePromptFile: 'codex.md',
32
32
  matches: ({ normalizedModel }) => normalizedModel.includes('gpt-5') && normalizedModel.includes('codex'),
33
33
  },
34
+ {
35
+ id: 'gpt-5.6',
36
+ basePromptFile: 'gpt-5.6.md',
37
+ matches: ({ normalizedModel }) => normalizedModel.includes('gpt-5.6'),
38
+ },
34
39
  {
35
40
  id: 'gpt-5.5',
36
41
  basePromptFile: 'gpt-5.5.md',
@@ -1 +1 @@
1
- {"version":3,"file":"search-via-shell.d.ts","sourceRoot":"","sources":["../../source/prompts/search-via-shell.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAcpE,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACvC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAiDrF"}
1
+ {"version":3,"file":"search-via-shell.d.ts","sourceRoot":"","sources":["../../source/prompts/search-via-shell.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAcpE,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACvC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAED,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAgCrF"}
@@ -22,32 +22,15 @@ export function getSearchViaShellAddendum(options = {}) {
22
22
 
23
23
  Use the \`shell\` tool with the standard CLI binaries.`;
24
24
  const textSearch = hasRg
25
- ? `**For text search**, use \`rg\` (ripgrep). Examples:
26
- - \`rg "pattern" src/\` — basic search, respects \`.gitignore\` by default.
27
- - \`rg -i "pattern" .\` case-insensitive.
28
- - \`rg --no-ignore "pattern" .\` when you need to search \`node_modules\`, build output, or anything in \`.gitignore\`.
29
- - \`rg -uu "pattern" .\` — include hidden + gitignored.
30
- - \`rg -g '*.ts' "pattern" .\` — restrict by glob.
31
- - \`rg -t ts "pattern" .\` — restrict by language preset.
32
- - \`rg -n "pattern" .\` — show line numbers (useful for follow-up edits).
33
- - \`rg -l "pattern" .\` — list files only.
34
- - \`rg -C 3 "pattern" .\` — 3 lines of context.
35
- - **Always pass an explicit path** (e.g. \`.\` or \`src/\`) to \`rg\`. When no path is given and stdin is a pipe, ripgrep reads stdin instead of searching the filesystem, returning no results.`
36
- : `**For text search**, use \`grep\`. Examples:
37
- - \`grep -rn "pattern" src/\` — recursive search with line numbers.
38
- - \`grep -ri "pattern" src/\` — case-insensitive recursive search.
39
- - \`grep -rl "pattern" src/\` — list matching files only.
40
- - \`grep -C 3 "pattern" src/\` — show 3 lines of context.`;
25
+ ? `**For text search**, use \`rg\` (ripgrep). It respects \`.gitignore\` by default; \`--no-ignore\` or \`-uu\` when you need to search past it.
26
+
27
+ **Always pass an explicit path** (e.g. \`.\` or \`src/\`). With no path given and stdin attached to a pipe, ripgrep reads stdin instead of searching the filesystem and silently returns no results.`
28
+ : `**For text search**, use \`grep\`, with \`-rn\` for a recursive search reporting line numbers.`;
41
29
  const fileSearch = hasFd
42
- ? `**For file search**, use \`fd\`. Examples:
43
- - \`fd '\\.ts$'\` — regex over basenames.
44
- - \`fd -e ts\` — by extension.
45
- - \`fd -H -I\` include hidden + gitignored (\`-uu\` style).
46
- - \`fd 'pattern' path/\` — scoped to a directory.`
47
- : `**For file search**, use \`find\`. Examples:
48
- - \`find src/ -type f -name '*.ts'\` — search a subtree by basename glob.
49
- - **Always search from a specific path, not \`/\`.** Scanning the whole filesystem can exhaust resources on large trees.
50
- - When using \`find -regex\` with alternation, put the longest alternative first: \`'.*\\.(tsx|ts)'\` works; \`'.*\\.(ts|tsx)'\` silently skips \`.tsx\`.`;
30
+ ? `**For file search**, use \`fd\`. It also respects \`.gitignore\`; \`-H -I\` includes hidden and ignored files.`
31
+ : `**For file search**, use \`find\`, scoped to a specific path rather than \`/\` scanning the whole filesystem can exhaust resources on large trees.
32
+
33
+ With \`find -regex\` alternation, put the longest alternative first: \`'.*\\.(tsx|ts)'\` matches, while \`'.*\\.(ts|tsx)'\` silently skips \`.tsx\`.`;
51
34
  const hygiene = `**General shell hygiene:**
52
35
  - Quote paths that contain spaces.
53
36
  - Prefer absolute paths or paths relative to a known root; avoid \`cd\`.
@@ -1 +1 @@
1
- {"version":3,"file":"search-via-shell.js","sourceRoot":"","sources":["../../source/prompts/search-via-shell.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAG1C,SAAS,kBAAkB,CAAC,GAAW;IACrC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,EAAE;YAC3C,KAAK,EAAE,QAAQ;YACf,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAOD,MAAM,UAAU,yBAAyB,CAAC,OAAO,GAA0B,EAAE;IAC3E,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,kBAAkB,CAAC;IAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,EAAE,QAAQ,EAAE,IAAI,KAAK,CAAC;IAE/D,sEAAsE;IACtE,6EAA6E;IAC7E,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAEnD,MAAM,MAAM,GAAG;;uDAEsC,CAAC;IAEtD,MAAM,UAAU,GAAG,KAAK;QACtB,CAAC,CAAC;;;;;;;;;;iMAU2L;QAC7L,CAAC,CAAC;;;;0DAIoD,CAAC;IAEzD,MAAM,UAAU,GAAG,KAAK;QACtB,CAAC,CAAC;;;;kDAI4C;QAC9C,CAAC,CAAC;;;0JAGoJ,CAAC;IAEzJ,MAAM,OAAO,GAAG;;;;2GAIyF,CAAC;IAE1G,OAAO,GAAG,MAAM,OAAO,UAAU,OAAO,UAAU,OAAO,OAAO,EAAE,CAAC;AACrE,CAAC"}
1
+ {"version":3,"file":"search-via-shell.js","sourceRoot":"","sources":["../../source/prompts/search-via-shell.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAG1C,SAAS,kBAAkB,CAAC,GAAW;IACrC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,EAAE;YAC3C,KAAK,EAAE,QAAQ;YACf,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;QACH,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAOD,MAAM,UAAU,yBAAyB,CAAC,OAAO,GAA0B,EAAE;IAC3E,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,kBAAkB,CAAC;IAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,EAAE,QAAQ,EAAE,IAAI,KAAK,CAAC;IAE/D,sEAAsE;IACtE,6EAA6E;IAC7E,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IACnD,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAEnD,MAAM,MAAM,GAAG;;uDAEsC,CAAC;IAEtD,MAAM,UAAU,GAAG,KAAK;QACtB,CAAC,CAAC;;qMAE+L;QACjM,CAAC,CAAC,gGAAgG,CAAC;IAErG,MAAM,UAAU,GAAG,KAAK;QACtB,CAAC,CAAC,gHAAgH;QAClH,CAAC,CAAC;;qJAE+I,CAAC;IAEpJ,MAAM,OAAO,GAAG;;;;2GAIyF,CAAC;IAE1G,OAAO,GAAG,MAAM,OAAO,UAAU,OAAO,UAAU,OAAO,OAAO,EAAE,CAAC;AACrE,CAAC"}
@@ -49,3 +49,21 @@ it('getSearchViaShellAddendum falls back to grep and find when both missing', ()
49
49
  expect(includesWord(result, 'find')).toBe(true);
50
50
  expect(result.toLowerCase().includes('hygiene')).toBe(true);
51
51
  });
52
+
53
+ it('getSearchViaShellAddendum warns that pathless rg reads stdin', () => {
54
+ const result = getSearchViaShellAddendum({
55
+ checkBinary: () => true,
56
+ });
57
+
58
+ expect(result.toLowerCase()).toContain('explicit path');
59
+ expect(result.toLowerCase()).toContain('stdin');
60
+ });
61
+
62
+ it('getSearchViaShellAddendum warns about find -regex alternation ordering', () => {
63
+ const result = getSearchViaShellAddendum({
64
+ checkBinary: () => false,
65
+ });
66
+
67
+ expect(result).toContain('-regex');
68
+ expect(result.toLowerCase()).toContain('longest alternative first');
69
+ });
@@ -32,33 +32,16 @@ export function getSearchViaShellAddendum(options: SearchViaShellOptions = {}):
32
32
  Use the \`shell\` tool with the standard CLI binaries.`;
33
33
 
34
34
  const textSearch = hasRg
35
- ? `**For text search**, use \`rg\` (ripgrep). Examples:
36
- - \`rg "pattern" src/\` — basic search, respects \`.gitignore\` by default.
37
- - \`rg -i "pattern" .\` case-insensitive.
38
- - \`rg --no-ignore "pattern" .\` when you need to search \`node_modules\`, build output, or anything in \`.gitignore\`.
39
- - \`rg -uu "pattern" .\` — include hidden + gitignored.
40
- - \`rg -g '*.ts' "pattern" .\` — restrict by glob.
41
- - \`rg -t ts "pattern" .\` — restrict by language preset.
42
- - \`rg -n "pattern" .\` — show line numbers (useful for follow-up edits).
43
- - \`rg -l "pattern" .\` — list files only.
44
- - \`rg -C 3 "pattern" .\` — 3 lines of context.
45
- - **Always pass an explicit path** (e.g. \`.\` or \`src/\`) to \`rg\`. When no path is given and stdin is a pipe, ripgrep reads stdin instead of searching the filesystem, returning no results.`
46
- : `**For text search**, use \`grep\`. Examples:
47
- - \`grep -rn "pattern" src/\` — recursive search with line numbers.
48
- - \`grep -ri "pattern" src/\` — case-insensitive recursive search.
49
- - \`grep -rl "pattern" src/\` — list matching files only.
50
- - \`grep -C 3 "pattern" src/\` — show 3 lines of context.`;
35
+ ? `**For text search**, use \`rg\` (ripgrep). It respects \`.gitignore\` by default; \`--no-ignore\` or \`-uu\` when you need to search past it.
36
+
37
+ **Always pass an explicit path** (e.g. \`.\` or \`src/\`). With no path given and stdin attached to a pipe, ripgrep reads stdin instead of searching the filesystem and silently returns no results.`
38
+ : `**For text search**, use \`grep\`, with \`-rn\` for a recursive search reporting line numbers.`;
51
39
 
52
40
  const fileSearch = hasFd
53
- ? `**For file search**, use \`fd\`. Examples:
54
- - \`fd '\\.ts$'\` — regex over basenames.
55
- - \`fd -e ts\` — by extension.
56
- - \`fd -H -I\` include hidden + gitignored (\`-uu\` style).
57
- - \`fd 'pattern' path/\` — scoped to a directory.`
58
- : `**For file search**, use \`find\`. Examples:
59
- - \`find src/ -type f -name '*.ts'\` — search a subtree by basename glob.
60
- - **Always search from a specific path, not \`/\`.** Scanning the whole filesystem can exhaust resources on large trees.
61
- - When using \`find -regex\` with alternation, put the longest alternative first: \`'.*\\.(tsx|ts)'\` works; \`'.*\\.(ts|tsx)'\` silently skips \`.tsx\`.`;
41
+ ? `**For file search**, use \`fd\`. It also respects \`.gitignore\`; \`-H -I\` includes hidden and ignored files.`
42
+ : `**For file search**, use \`find\`, scoped to a specific path rather than \`/\` scanning the whole filesystem can exhaust resources on large trees.
43
+
44
+ With \`find -regex\` alternation, put the longest alternative first: \`'.*\\.(tsx|ts)'\` matches, while \`'.*\\.(ts|tsx)'\` silently skips \`.tsx\`.`;
62
45
 
63
46
  const hygiene = `**General shell hygiene:**
64
47
  - Quote paths that contain spaces.
@@ -1,5 +1,6 @@
1
1
  import { it, expect } from 'vitest';
2
2
  import { getSubagentDelegationAddendum } from './subagent-delegation.js';
3
+ import { getAsyncSubagentDelegationAddendum } from './async-subagent-delegation.js';
3
4
 
4
5
  it('getSubagentDelegationAddendum returns non-empty delegation guidance', () => {
5
6
  const result = getSubagentDelegationAddendum();
@@ -70,3 +71,17 @@ it('does not advertise the librarian when persistent memory is disabled', () =>
70
71
  const result = getSubagentDelegationAddendum({ memoryEnabled: false });
71
72
  expect(result.includes('librarian')).toBe(false);
72
73
  });
74
+
75
+ it('async control guidance distinguishes steering, cancellation, answers, and fresh-turn limits honestly', () => {
76
+ const result = getAsyncSubagentDelegationAddendum({ orchestratorMode: true, controlsEnabled: true });
77
+
78
+ expect(result).toContain('`send_message`');
79
+ expect(result).toContain('`cancel_run`');
80
+ expect(result).toContain('name or runId');
81
+ expect(result).toContain('`reply_to`');
82
+ expect(result).toContain('fresh session turn');
83
+ expect(result).toContain('maximum of three continuation segments');
84
+ expect(result).toContain('not SDK live injection');
85
+ expect(result).toContain('single point of contact');
86
+ expect(result).toContain('Do NOT call `get_subagent_result` immediately');
87
+ });
@@ -23,6 +23,12 @@ You have access to web search and web fetch tools, and read-only access to the w
23
23
  - Use only tools listed in the Available Tool Guidance section. If a tool is not listed there, it is not available.
24
24
  - Use available read, search, and code-context tools to inspect workspace files when relevant.
25
25
 
26
+ ## Asking the orchestrator
27
+
28
+ - Use `ask_orchestrator` only for a genuine blocker that cannot be resolved from the task, workspace, or available research tools. State the specific decision needed.
29
+ - The tool is available only when listed in your tools. It asks the owning orchestrator, never the user; do not contact the user directly.
30
+ - Continue after the answer and finish the research task rather than ending or relaunching it.
31
+
26
32
  ## Approach
27
33
 
28
34
  1. Identify what information is needed.
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { it, expect } from 'vitest';
4
+
5
+ const workerPrompt = fs.readFileSync(path.join(import.meta.dirname, 'worker.md'), 'utf-8');
6
+ const researcherPrompt = fs.readFileSync(path.join(import.meta.dirname, 'researcher.md'), 'utf-8');
7
+
8
+ it('worker prompt still requires running a validation command', () => {
9
+ const lower = workerPrompt.toLowerCase();
10
+ expect(lower).toContain('validation command');
11
+ });
12
+
13
+ it('worker prompt notes that validation and diff stat are auto-captured', () => {
14
+ const lower = workerPrompt.toLowerCase();
15
+ expect(lower).toContain('automatically captures');
16
+ expect(lower).toContain('diff stat');
17
+ expect(lower).toContain('do not need to paste the full output');
18
+ });
19
+
20
+ it('worker prompt mentions that shell-driven edits may not appear in diff stat', () => {
21
+ const lower = workerPrompt.toLowerCase();
22
+ expect(lower).toContain('shell-driven edits');
23
+ expect(lower).toContain('may not appear');
24
+ });
25
+
26
+ it('execution role prompts reserve ask_orchestrator for genuine blockers and never direct user contact', () => {
27
+ for (const prompt of [workerPrompt, researcherPrompt]) {
28
+ const lower = prompt.toLowerCase();
29
+ expect(lower).toContain('ask_orchestrator');
30
+ expect(lower).toContain('genuine blocker');
31
+ expect(lower).toContain('decision needed');
32
+ expect(lower).toContain('continue after');
33
+ expect(lower).toContain('do not contact the user');
34
+ }
35
+ });
@@ -24,6 +24,12 @@ You have read and write access to the workspace (within the assigned write bound
24
24
  - Read relevant files before editing them.
25
25
  - Keep edits limited to the assigned scope. Do not broaden the task.
26
26
 
27
+ ## Asking the orchestrator
28
+
29
+ - Use `ask_orchestrator` only for a genuine blocker that cannot be resolved from the task, workspace, or available tools. State the specific decision needed, not an open-ended status update.
30
+ - The tool is available only when listed in your tools. It asks the owning orchestrator, never the user; do not contact the user directly.
31
+ - After the answer arrives, continue after it and complete the assigned work. Do not terminate or relaunch the task merely because you asked.
32
+
27
33
  ## Scope discipline
28
34
 
29
35
  - If the parent names editable, read-only, or forbidden files in the task, obey those boundaries strictly.
@@ -45,5 +51,7 @@ After completing the task, return a concise report that includes:
45
51
  - Any issues encountered or assumptions made
46
52
  - Any scope conflicts: work you identified as needed but that fell outside your assigned scope
47
53
 
54
+ The harness automatically captures the last validation command you ran (test/lint/typecheck/tsc/build) with its exit status and output, and a per-file line-change diff stat from your write-tool edits. You still need to run a validation command and may state which command you ran, but you do not need to paste the full output — the structured evidence is captured for you. Shell-driven edits outside the write tools may not appear in the diff stat; mention them in your summary if they matter.
55
+
48
56
  Do not include implementation details that are already visible in the diff.
49
57
  Do not claim work you did not do.
@@ -16,6 +16,8 @@ export type SendMessageOptions = {
16
16
  bypassInputSurgeGuard?: boolean;
17
17
  replayFromHistory?: boolean;
18
18
  preferredMessageId?: string;
19
+ /** The turn is model input only and must not be projected as a user message in the UI. */
20
+ suppressUserMessageDisplay?: boolean;
19
21
  };
20
22
  export type HandleApprovalDecisionOptions = {
21
23
  onTextChunk?: (fullText: string, chunk: string) => void;
@@ -52,6 +54,7 @@ export type ConversationEventSink = (event: ConversationEvent) => void;
52
54
  export type QueuedTurnStartObserver = (execution: {
53
55
  readonly requestId: string;
54
56
  readonly input: string | UserTurn;
57
+ readonly suppressUserMessageDisplay?: boolean;
55
58
  }) => void;
56
59
  export declare class ConversationAdapter {
57
60
  #private;
@@ -68,13 +71,18 @@ export declare class ConversationAdapter {
68
71
  approval: SessionApprovalQuery;
69
72
  turnFlow: TurnFlow;
70
73
  queueForeground?: boolean;
74
+ queueCapacity?: number;
75
+ /** Test seam: bound how long cancel waits for a hung active turn. */
76
+ activeCancelTimeoutMs?: number;
71
77
  queuePersistence?: QueuePersistence<QueuedMessageSnapshot>;
72
78
  });
73
79
  setEventSink(sink: ConversationEventSink | null): void;
74
80
  setQueueStateObserver(observer: QueueStateObserver | null): void;
75
81
  setQueuedTurnStartObserver(observer: QueuedTurnStartObserver | null): void;
76
82
  isQueueActive(): boolean;
77
- sendMessage(input: string | UserTurn, { onTextChunk, onReasoningChunk, onCommandMessage, onEvent, hallucinationRetryCount, bypassInputSurgeGuard, replayFromHistory, preferredMessageId, }?: SendMessageOptions): Promise<ConversationTerminal>;
83
+ /** Whether the foreground queue owns a new submission's UI lifecycle. */
84
+ isQueueOwningSubmissions(): boolean;
85
+ sendMessage(input: string | UserTurn, { onTextChunk, onReasoningChunk, onCommandMessage, onEvent, hallucinationRetryCount, bypassInputSurgeGuard, replayFromHistory, preferredMessageId, suppressUserMessageDisplay, }?: SendMessageOptions): Promise<ConversationTerminal>;
78
86
  resumeQueue(): Promise<void>;
79
87
  discardQueue(): Promise<void>;
80
88
  /**
@@ -83,10 +91,10 @@ export declare class ConversationAdapter {
83
91
  * the queue is empty, the adapter has no queue (pass-through mode), or the
84
92
  * underlying controller rejects the removal.
85
93
  *
86
- * Also pops the matching pending message from the internal pending list so
87
- * the queue/pending state observers fire consistently.
94
+ * The item is correlated by its controller id, never its queue position.
88
95
  */
89
96
  removeLastQueuedItem(): Promise<{
97
+ id: string;
90
98
  text: string;
91
99
  } | null>;
92
100
  abort(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"conversation-adapter.d.ts","sourceRoot":"","sources":["../../../source/services/conversation/conversation-adapter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"conversation-adapter.d.ts","sourceRoot":"","sources":["../../../source/services/conversation/conversation-adapter.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,20 @@ import { collectTerminalResult } from '../session/terminal-result-collector.js';
2
2
  import { getCallIdFromObject } from '../interruption-info.js';
3
3
  import { normalizeUserTurn } from '../../types/user-turn.js';
4
4
  import { QueueController, } from '../queue/queue-controller.js';
5
+ const QUEUED_NON_TEXT_PLACEHOLDER = '[queued non-text user turn]';
6
+ const LEGACY_QUEUED_MESSAGE_PLACEHOLDER = '\u0000queued-message';
7
+ /** Upper bound for waiting on an active turn during cancel so the queue cannot stick in `cancelling`. */
8
+ const ACTIVE_CANCEL_TIMEOUT_MS = 10_000;
9
+ function queueCancellationError(message) {
10
+ const error = new Error(message);
11
+ error.name = 'AbortError';
12
+ return error;
13
+ }
14
+ function delay(ms) {
15
+ return new Promise((resolve) => {
16
+ setTimeout(resolve, ms);
17
+ });
18
+ }
5
19
  export class ConversationAdapter {
6
20
  #sessionId;
7
21
  #startedAt;
@@ -15,17 +29,18 @@ export class ConversationAdapter {
15
29
  #logs;
16
30
  #approval;
17
31
  #turnFlow;
18
- #pendingMessages = [];
19
32
  #messagesById = new Map();
20
33
  #queue;
21
34
  #nextQueuedMessageId = 1;
22
35
  #nextActionId = 1;
23
36
  #activeTurn = Promise.resolve();
37
+ #activeRequestId = null;
24
38
  #cancellation = Promise.resolve();
25
39
  #approvalExecutionId = null;
26
40
  #approvalActionId = null;
27
41
  #queueStateObserver = null;
28
42
  #queuedTurnStartObserver = null;
43
+ #activeCancelTimeoutMs;
29
44
  constructor(deps) {
30
45
  this.#sessionId = deps.sessionId;
31
46
  this.#startedAt = deps.startedAt;
@@ -38,22 +53,26 @@ export class ConversationAdapter {
38
53
  this.#logs = deps.logs;
39
54
  this.#approval = deps.approval;
40
55
  this.#turnFlow = deps.turnFlow;
56
+ this.#activeCancelTimeoutMs = deps.activeCancelTimeoutMs ?? ACTIVE_CANCEL_TIMEOUT_MS;
41
57
  if (deps.queueForeground) {
42
58
  const driver = {
43
59
  start: (execution) => this.#startQueuedTurn(execution),
44
60
  cancel: async () => {
45
- await this.#activeTurn;
61
+ // Prefer natural abort settlement, but never block queue cancel forever
62
+ // if the underlying turn ignores abort.
63
+ await Promise.race([
64
+ this.#activeTurn.then(() => undefined, () => undefined),
65
+ delay(this.#activeCancelTimeoutMs),
66
+ ]);
46
67
  },
47
68
  };
48
69
  this.#queue = new QueueController({
49
70
  driver,
50
71
  snapshotFactory: (item) => {
51
- if (Date.parse(item.submittedAt) < Date.parse(this.#startedAt)) {
52
- return { requestId: `recovered:${item.id}`, recovered: true };
53
- }
54
- const pending = this.#pendingMessages.shift();
55
- return pending ? { requestId: pending.requestId } : { requestId: `recovered:${item.id}`, recovered: true };
72
+ const message = this.#messagesById.get(item.id);
73
+ return message ? { requestId: item.id } : { requestId: item.id, recovered: true };
56
74
  },
75
+ capacity: deps.queueCapacity,
57
76
  persistence: deps.queuePersistence,
58
77
  });
59
78
  }
@@ -84,6 +103,13 @@ export class ConversationAdapter {
84
103
  state.kind === 'cancelling' ||
85
104
  state.kind === 'completing');
86
105
  }
106
+ /** Whether the foreground queue owns a new submission's UI lifecycle. */
107
+ isQueueOwningSubmissions() {
108
+ if (!this.#queue)
109
+ return false;
110
+ const state = this.#queue.state();
111
+ return state.kind !== 'idle' || state.queue.length > 0;
112
+ }
87
113
  #notifyQueueState() {
88
114
  if (!this.#queue || !this.#queueStateObserver)
89
115
  return;
@@ -120,7 +146,7 @@ export class ConversationAdapter {
120
146
  firstUserMessagePreview,
121
147
  }, fn);
122
148
  }
123
- async sendMessage(input, { onTextChunk, onReasoningChunk, onCommandMessage, onEvent, hallucinationRetryCount = 0, bypassInputSurgeGuard, replayFromHistory, preferredMessageId, } = {}) {
149
+ async sendMessage(input, { onTextChunk, onReasoningChunk, onCommandMessage, onEvent, hallucinationRetryCount = 0, bypassInputSurgeGuard, replayFromHistory, preferredMessageId, suppressUserMessageDisplay, } = {}) {
124
150
  const queue = this.#queue;
125
151
  if (!queue) {
126
152
  return this.#executeMessage(input, {
@@ -135,8 +161,12 @@ export class ConversationAdapter {
135
161
  }
136
162
  return new Promise((resolve, reject) => {
137
163
  const requestId = preferredMessageId ?? String(this.#nextQueuedMessageId++);
164
+ if (this.#messagesById.has(requestId)) {
165
+ reject(new Error(`A queued message already uses request id ${requestId}`));
166
+ return;
167
+ }
138
168
  const message = {
139
- input,
169
+ input: typeof input === 'string' ? input : structuredClone(input),
140
170
  options: {
141
171
  onTextChunk,
142
172
  onReasoningChunk,
@@ -145,15 +175,27 @@ export class ConversationAdapter {
145
175
  hallucinationRetryCount,
146
176
  bypassInputSurgeGuard,
147
177
  replayFromHistory,
178
+ suppressUserMessageDisplay,
148
179
  },
149
180
  resolve,
150
181
  reject,
151
182
  };
152
183
  this.#messagesById.set(requestId, message);
153
- this.#pendingMessages.push({ requestId, message });
184
+ const displayText = normalizeUserTurn(input).text;
185
+ const controllerText = displayText.trim() ? displayText : QUEUED_NON_TEXT_PLACEHOLDER;
154
186
  void queue
155
- .command({ kind: 'submit', text: normalizeUserTurn(input).text || '\u0000queued-message' })
156
- .then(() => this.#notifyQueueState());
187
+ .command({ kind: 'submit', id: requestId, text: controllerText })
188
+ .then((result) => {
189
+ if (result.kind !== 'accepted') {
190
+ const reason = result.kind === 'rejected' ? result.reason : result.kind;
191
+ this.#settleFailure(requestId, new Error(`Foreground queue rejected message: ${reason}`));
192
+ }
193
+ this.#notifyQueueState();
194
+ })
195
+ .catch((error) => {
196
+ this.#settleFailure(requestId, error);
197
+ this.#notifyQueueState();
198
+ });
157
199
  });
158
200
  }
159
201
  async resumeQueue() {
@@ -166,7 +208,13 @@ export class ConversationAdapter {
166
208
  async discardQueue() {
167
209
  if (!this.#queue)
168
210
  return;
169
- await this.#queue.command({ kind: 'discard_queue' });
211
+ const queuedIds = this.#queue.state().queue.map((item) => item.id);
212
+ const result = await this.#queue.command({ kind: 'discard_queue' });
213
+ if (result.kind === 'accepted') {
214
+ for (const requestId of queuedIds) {
215
+ this.#settleFailure(requestId, queueCancellationError('Queued message was discarded'));
216
+ }
217
+ }
170
218
  this.#notifyQueueState();
171
219
  }
172
220
  /**
@@ -175,8 +223,7 @@ export class ConversationAdapter {
175
223
  * the queue is empty, the adapter has no queue (pass-through mode), or the
176
224
  * underlying controller rejects the removal.
177
225
  *
178
- * Also pops the matching pending message from the internal pending list so
179
- * the queue/pending state observers fire consistently.
226
+ * The item is correlated by its controller id, never its queue position.
180
227
  */
181
228
  async removeLastQueuedItem() {
182
229
  const queue = this.#queue;
@@ -187,27 +234,39 @@ export class ConversationAdapter {
187
234
  return null;
188
235
  // The queue is FIFO. The "last" queued item is the one at the tail.
189
236
  const lastItem = state.queue[state.queue.length - 1];
237
+ // Prefer the in-memory turn text: controller text may be a non-text placeholder.
238
+ const pending = this.#messagesById.get(lastItem.id);
239
+ const restoredText = pending
240
+ ? normalizeUserTurn(pending.input).text
241
+ : lastItem.text === QUEUED_NON_TEXT_PLACEHOLDER || lastItem.text === LEGACY_QUEUED_MESSAGE_PLACEHOLDER
242
+ ? ''
243
+ : lastItem.text;
190
244
  const result = await queue.command({ kind: 'remove_queued', itemId: lastItem.id });
191
245
  if (result.kind !== 'accepted')
192
246
  return null;
193
- // The pending message list is in submission order. The corresponding
194
- // pending entry for this item is the last one we added. Removing it keeps
195
- // the adapter's internal bookkeeping consistent with the queue controller.
196
- this.#pendingMessages.pop();
197
- // Note: we don't need to touch #messagesById here because the queue head
198
- // has not yet started executing this item (it was still queued), so there
199
- // is no pending execution for it to resolve. The pending message entry
200
- // was only there to support the snapshot factory if the head was popped.
247
+ this.#settleFailure(lastItem.id, queueCancellationError('Queued message was removed'));
201
248
  this.#notifyQueueState();
202
- return { text: lastItem.text };
249
+ return { id: lastItem.id, text: restoredText };
203
250
  }
204
251
  abort() {
205
252
  if (!this.#queue) {
206
253
  this.#turnFlow.abort?.();
207
254
  return;
208
255
  }
256
+ // Abort the live model/tool turn, then ask the controller to leave running
257
+ // and pause with retained work. If the active turn fails to settle on its
258
+ // own (a hung generator, missing abort hook, etc.), force-settle the active
259
+ // request so orchestrator awaits cannot stick forever. Retained queued
260
+ // requests stay pending — pause is not a terminal fate.
209
261
  this.#turnFlow.abort?.();
262
+ const activeRequestId = this.#activeRequestId;
210
263
  this.#cancellation = this.#queue.command({ kind: 'cancel' }).then(() => {
264
+ if (activeRequestId && this.#messagesById.has(activeRequestId)) {
265
+ this.#settleFailure(activeRequestId, queueCancellationError('Active turn was cancelled'));
266
+ }
267
+ if (this.#activeRequestId === activeRequestId) {
268
+ this.#activeRequestId = null;
269
+ }
211
270
  this.#notifyQueueState();
212
271
  });
213
272
  }
@@ -220,6 +279,7 @@ export class ConversationAdapter {
220
279
  this.#queuedTurnStartObserver({
221
280
  requestId: execution.snapshot.requestId,
222
281
  input: message.input,
282
+ suppressUserMessageDisplay: message.options.suppressUserMessageDisplay,
223
283
  });
224
284
  }
225
285
  catch (error) {
@@ -228,14 +288,31 @@ export class ConversationAdapter {
228
288
  });
229
289
  }
230
290
  }
231
- this.#activeTurn = this.#runQueuedTurn(execution);
291
+ this.#activeRequestId = execution.snapshot.requestId;
292
+ this.#activeTurn = this.#runQueuedTurn(execution).finally(() => {
293
+ if (this.#activeRequestId === execution.snapshot.requestId) {
294
+ this.#activeRequestId = null;
295
+ }
296
+ });
232
297
  }
298
+ /**
299
+ * Run one foreground queue item.
300
+ *
301
+ * Failure policy: a failed active turn emits `failed` to the controller, which
302
+ * pauses the queue (`reason: 'failure'`) whenever retained work remains. The
303
+ * next item does not auto-start; the user must resume or discard. Cancellation
304
+ * of the active turn is the same shape with `reason: 'manual'`.
305
+ */
233
306
  async #runQueuedTurn(execution) {
234
307
  const message = this.#messagesById.get(execution.snapshot.requestId);
235
- if (message)
236
- this.#messagesById.delete(execution.snapshot.requestId);
237
308
  try {
238
- const result = await this.#executeMessage(message?.input ?? execution.item.text, message?.options ?? {});
309
+ const recoveredInput = execution.item.text === QUEUED_NON_TEXT_PLACEHOLDER || execution.item.text === LEGACY_QUEUED_MESSAGE_PLACEHOLDER
310
+ ? null
311
+ : execution.item.text;
312
+ if (!message && !recoveredInput) {
313
+ throw new Error('Recovered queued message has no executable text input');
314
+ }
315
+ const result = await this.#executeMessage(message?.input ?? recoveredInput, message?.options ?? {});
239
316
  if (result.type === 'approval_required') {
240
317
  this.#approvalExecutionId = execution.executionId;
241
318
  this.#approvalActionId = `adapter-action-${this.#nextActionId++}`;
@@ -246,19 +323,34 @@ export class ConversationAdapter {
246
323
  request: {}, // existing runtime doesn't expose typed tool request details
247
324
  });
248
325
  this.#notifyQueueState();
249
- message?.resolve(result);
326
+ this.#settleSuccess(execution.snapshot.requestId, result);
250
327
  return;
251
328
  }
252
329
  await this.#queue.event({ kind: 'completed', executionId: execution.executionId, terminal: result });
253
330
  this.#notifyQueueState();
254
- message?.resolve(result);
331
+ this.#settleSuccess(execution.snapshot.requestId, result);
255
332
  }
256
333
  catch (error) {
334
+ // Controller pauses with retained queue on failure when work remains.
257
335
  await this.#queue.event({ kind: 'failed', executionId: execution.executionId, failure: error });
258
336
  this.#notifyQueueState();
259
- message?.reject(error);
337
+ this.#settleFailure(execution.snapshot.requestId, error);
260
338
  }
261
339
  }
340
+ #settleSuccess(requestId, terminal) {
341
+ const message = this.#messagesById.get(requestId);
342
+ if (!message)
343
+ return;
344
+ this.#messagesById.delete(requestId);
345
+ message.resolve(terminal);
346
+ }
347
+ #settleFailure(requestId, error) {
348
+ const message = this.#messagesById.get(requestId);
349
+ if (!message)
350
+ return;
351
+ this.#messagesById.delete(requestId);
352
+ message.reject(error);
353
+ }
262
354
  async #executeMessage(input, { onTextChunk, onReasoningChunk, onCommandMessage, onEvent, hallucinationRetryCount = 0, bypassInputSurgeGuard, replayFromHistory, } = {}) {
263
355
  const turn = normalizeUserTurn(input);
264
356
  return this.#withTrafficContext(turn.text, async () => {