@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.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 (285) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4659 -4261
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +316 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  25. package/dist/types/live/controller.d.ts +40 -0
  26. package/dist/types/live/protocol.d.ts +85 -0
  27. package/dist/types/live/protocol.test.d.ts +1 -0
  28. package/dist/types/live/transport.d.ts +48 -0
  29. package/dist/types/live/visualizer.d.ts +33 -0
  30. package/dist/types/lsp/client.d.ts +16 -1
  31. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  32. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  33. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  34. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  35. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  36. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  37. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  38. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  39. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  41. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  43. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  44. package/dist/types/modes/interactive-mode.d.ts +5 -0
  45. package/dist/types/modes/theme/theme.d.ts +1 -1
  46. package/dist/types/modes/types.d.ts +3 -1
  47. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  48. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  49. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  50. package/dist/types/registry/agent-registry.d.ts +11 -4
  51. package/dist/types/sdk.d.ts +15 -1
  52. package/dist/types/secrets/index.d.ts +15 -2
  53. package/dist/types/secrets/obfuscator.d.ts +113 -15
  54. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  55. package/dist/types/session/agent-session-events.d.ts +84 -0
  56. package/dist/types/session/agent-session-types.d.ts +314 -0
  57. package/dist/types/session/agent-session.d.ts +118 -755
  58. package/dist/types/session/async-job-delivery.d.ts +28 -0
  59. package/dist/types/session/bash-runner.d.ts +75 -0
  60. package/dist/types/session/blob-store.d.ts +11 -1
  61. package/dist/types/session/blob-store.test.d.ts +1 -0
  62. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  63. package/dist/types/session/eval-runner.d.ts +49 -0
  64. package/dist/types/session/irc-bridge.d.ts +46 -0
  65. package/dist/types/session/messages.d.ts +22 -0
  66. package/dist/types/session/model-controls.d.ts +180 -0
  67. package/dist/types/session/prewalk.d.ts +46 -0
  68. package/dist/types/session/queued-messages.d.ts +22 -0
  69. package/dist/types/session/redis-session-storage.d.ts +1 -0
  70. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  71. package/dist/types/session/role-models.d.ts +15 -0
  72. package/dist/types/session/session-advisors.d.ts +228 -0
  73. package/dist/types/session/session-entries.d.ts +8 -0
  74. package/dist/types/session/session-handoff.d.ts +70 -0
  75. package/dist/types/session/session-history-format.d.ts +6 -1
  76. package/dist/types/session/session-maintenance.d.ts +246 -0
  77. package/dist/types/session/session-manager.d.ts +41 -0
  78. package/dist/types/session/session-memory.d.ts +56 -0
  79. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  80. package/dist/types/session/session-stats.d.ts +48 -0
  81. package/dist/types/session/session-tools.d.ts +143 -0
  82. package/dist/types/session/session-workspace.d.ts +32 -0
  83. package/dist/types/session/stream-guards.d.ts +44 -0
  84. package/dist/types/session/todo-tracker.d.ts +56 -0
  85. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  86. package/dist/types/session/turn-recovery.d.ts +225 -0
  87. package/dist/types/system-prompt.d.ts +2 -0
  88. package/dist/types/task/executor.d.ts +4 -1
  89. package/dist/types/task/output-manager.d.ts +3 -1
  90. package/dist/types/tools/approval.d.ts +1 -0
  91. package/dist/types/tools/browser/launch.d.ts +5 -0
  92. package/dist/types/tools/image-gen.d.ts +4 -4
  93. package/dist/types/tools/image-providers.d.ts +38 -0
  94. package/dist/types/tools/index.d.ts +5 -0
  95. package/dist/types/tools/path-utils.d.ts +8 -0
  96. package/dist/types/tools/todo.d.ts +10 -4
  97. package/dist/types/tools/xdev.d.ts +5 -1
  98. package/dist/types/tts/streaming-player.d.ts +14 -0
  99. package/dist/types/tts/vocalizer.d.ts +5 -0
  100. package/dist/types/utils/jj.d.ts +29 -0
  101. package/dist/types/utils/lang-from-path.d.ts +1 -0
  102. package/dist/types/utils/title-generator.d.ts +30 -0
  103. package/dist/types/vibe/runtime.d.ts +36 -8
  104. package/dist/types/web/search/index.d.ts +1 -1
  105. package/dist/types/web/search/provider.d.ts +15 -5
  106. package/dist/types/web/search/types.d.ts +94 -0
  107. package/package.json +12 -12
  108. package/scripts/generate-share-viewer.ts +4 -6
  109. package/src/advisor/__tests__/advisor.test.ts +643 -0
  110. package/src/advisor/runtime.ts +144 -43
  111. package/src/async/job-manager.ts +90 -2
  112. package/src/cli/args.ts +2 -0
  113. package/src/cli/auth-gateway-cli.ts +18 -3
  114. package/src/cli/bench-cli.ts +395 -29
  115. package/src/cli/flag-tables.ts +3 -0
  116. package/src/cli/gc-cli.ts +2 -2
  117. package/src/cli/usage-cli.ts +10 -1
  118. package/src/collab/host.ts +3 -2
  119. package/src/commands/bench.ts +18 -4
  120. package/src/commands/launch.ts +4 -0
  121. package/src/config/model-registry.ts +10 -2
  122. package/src/config/models-config-schema.ts +15 -4
  123. package/src/config/provider-globals.ts +9 -9
  124. package/src/config/service-tier.ts +26 -1
  125. package/src/config/settings-schema.ts +209 -51
  126. package/src/config/settings.ts +81 -2
  127. package/src/dap/client.ts +37 -4
  128. package/src/edit/index.ts +49 -12
  129. package/src/eval/py/executor.ts +103 -33
  130. package/src/exec/bash-executor.ts +89 -6
  131. package/src/exec/direnv.ts +145 -0
  132. package/src/export/html/args.ts +20 -0
  133. package/src/export/html/index.ts +45 -47
  134. package/src/export/html/template.css +19 -1
  135. package/src/export/html/template.html +6 -0
  136. package/src/export/html/template.js +21 -0
  137. package/src/export/html/tool-views.generated.js +31 -31
  138. package/src/export/html/web-palette.ts +116 -132
  139. package/src/export/share.ts +249 -49
  140. package/src/extensibility/extensions/loader.ts +29 -1
  141. package/src/extensibility/extensions/runner.ts +6 -0
  142. package/src/extensibility/extensions/types.ts +33 -2
  143. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  144. package/src/hindsight/state.ts +64 -6
  145. package/src/internal-urls/local-protocol.ts +2 -1
  146. package/src/internal-urls/memory-protocol.ts +2 -2
  147. package/src/internal-urls/skill-protocol.ts +2 -2
  148. package/src/internal-urls/ssh-protocol.ts +2 -1
  149. package/src/internal-urls/vault-protocol.ts +2 -1
  150. package/src/live/audio-worklet.txt +59 -0
  151. package/src/live/browser-runtime.txt +221 -0
  152. package/src/live/controller.ts +464 -0
  153. package/src/live/prompts/agent-final-message.md +3 -0
  154. package/src/live/prompts/live-instructions.md +23 -0
  155. package/src/live/protocol.test.ts +140 -0
  156. package/src/live/protocol.ts +233 -0
  157. package/src/live/transport.ts +490 -0
  158. package/src/live/visualizer.ts +234 -0
  159. package/src/lsp/client.ts +29 -9
  160. package/src/lsp/index.ts +19 -4
  161. package/src/main.ts +8 -1
  162. package/src/mcp/config-writer.test.ts +43 -0
  163. package/src/mcp/config-writer.ts +109 -82
  164. package/src/mcp/oauth-discovery.ts +10 -2
  165. package/src/mcp/smithery-auth.test.ts +29 -0
  166. package/src/mcp/smithery-auth.ts +3 -2
  167. package/src/memories/index.ts +40 -20
  168. package/src/memory-backend/tool-names.ts +2 -0
  169. package/src/mnemopi/backend.ts +24 -7
  170. package/src/modes/acp/acp-agent.ts +56 -10
  171. package/src/modes/acp/acp-event-mapper.ts +8 -1
  172. package/src/modes/components/agent-hub.ts +1 -1
  173. package/src/modes/components/login-dialog.ts +14 -4
  174. package/src/modes/components/plan-review-overlay.ts +350 -35
  175. package/src/modes/components/settings-defs.ts +23 -1
  176. package/src/modes/components/settings-selector.ts +169 -1
  177. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  178. package/src/modes/components/status-line/component.ts +123 -2
  179. package/src/modes/components/tool-execution.ts +11 -2
  180. package/src/modes/controllers/command-controller.ts +56 -15
  181. package/src/modes/controllers/event-controller.ts +111 -16
  182. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  183. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  184. package/src/modes/controllers/live-command-controller.ts +178 -0
  185. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  186. package/src/modes/controllers/omfg-controller.ts +44 -40
  187. package/src/modes/controllers/selector-controller.ts +18 -10
  188. package/src/modes/controllers/session-focus-controller.ts +6 -1
  189. package/src/modes/interactive-mode.ts +157 -38
  190. package/src/modes/runtime-init.ts +2 -0
  191. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  192. package/src/modes/theme/theme.ts +1 -1
  193. package/src/modes/types.ts +3 -1
  194. package/src/modes/utils/context-usage.ts +23 -7
  195. package/src/modes/utils/ui-helpers.ts +10 -5
  196. package/src/plan-mode/plan-files.ts +40 -0
  197. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  198. package/src/prompts/bench/cache-prefix.md +3 -0
  199. package/src/prompts/bench/cache-suffix.md +1 -0
  200. package/src/prompts/system/custom-system-prompt.md +1 -1
  201. package/src/prompts/system/project-prompt.md +8 -1
  202. package/src/prompts/system/subagent-async-pending.md +6 -0
  203. package/src/prompts/system/system-prompt.md +1 -1
  204. package/src/prompts/system/workflow-notice.md +48 -40
  205. package/src/prompts/system/xdev-mount-notice.md +4 -0
  206. package/src/prompts/tools/todo.md +3 -1
  207. package/src/registry/agent-lifecycle.ts +116 -46
  208. package/src/registry/agent-registry.ts +35 -8
  209. package/src/registry/persisted-agents.ts +26 -2
  210. package/src/sdk.ts +210 -136
  211. package/src/secrets/index.ts +127 -4
  212. package/src/secrets/obfuscator.ts +2328 -105
  213. package/src/session/acp-permission-gate.ts +165 -0
  214. package/src/session/agent-session-error-log.test.ts +1 -1
  215. package/src/session/agent-session-events.ts +66 -0
  216. package/src/session/agent-session-types.ts +334 -0
  217. package/src/session/agent-session.ts +1892 -12226
  218. package/src/session/artifacts.ts +6 -5
  219. package/src/session/async-job-delivery.ts +74 -0
  220. package/src/session/bash-runner.ts +326 -0
  221. package/src/session/blob-store.test.ts +56 -0
  222. package/src/session/blob-store.ts +18 -2
  223. package/src/session/checkpoint-entries.ts +81 -0
  224. package/src/session/eval-runner.ts +212 -0
  225. package/src/session/indexed-session-storage.ts +9 -2
  226. package/src/session/irc-bridge.ts +203 -0
  227. package/src/session/messages.ts +263 -1
  228. package/src/session/model-controls.ts +714 -0
  229. package/src/session/prewalk.ts +252 -0
  230. package/src/session/queued-messages.ts +93 -0
  231. package/src/session/redis-session-storage.ts +51 -11
  232. package/src/session/retry-fallback-chains.ts +455 -0
  233. package/src/session/role-models.ts +85 -0
  234. package/src/session/session-advisors.ts +1679 -0
  235. package/src/session/session-context.ts +6 -3
  236. package/src/session/session-entries.ts +8 -0
  237. package/src/session/session-handoff.ts +308 -0
  238. package/src/session/session-history-format.ts +20 -9
  239. package/src/session/session-loader.ts +0 -46
  240. package/src/session/session-maintenance.ts +2983 -0
  241. package/src/session/session-manager.ts +390 -30
  242. package/src/session/session-memory.ts +222 -0
  243. package/src/session/session-provider-boundary.ts +307 -0
  244. package/src/session/session-stats.ts +293 -0
  245. package/src/session/session-tools.ts +943 -0
  246. package/src/session/session-workspace.ts +53 -0
  247. package/src/session/stream-guards.ts +417 -0
  248. package/src/session/todo-tracker.ts +380 -0
  249. package/src/session/ttsr-coordinator.ts +496 -0
  250. package/src/session/turn-recovery.ts +1629 -0
  251. package/src/slash-commands/builtin-registry.ts +92 -11
  252. package/src/slash-commands/helpers/usage-report.ts +22 -3
  253. package/src/system-prompt.ts +41 -17
  254. package/src/task/executor.ts +285 -70
  255. package/src/task/index.ts +98 -105
  256. package/src/task/output-manager.ts +25 -3
  257. package/src/task/persisted-revive.ts +4 -3
  258. package/src/task/structured-subagent.ts +1 -0
  259. package/src/tools/approval.ts +55 -10
  260. package/src/tools/bash-interactive.ts +26 -0
  261. package/src/tools/bash-skill-urls.ts +28 -2
  262. package/src/tools/bash.ts +339 -123
  263. package/src/tools/browser/launch.ts +9 -1
  264. package/src/tools/browser/tab-supervisor.ts +8 -5
  265. package/src/tools/browser.ts +94 -47
  266. package/src/tools/image-gen.ts +20 -28
  267. package/src/tools/image-providers.ts +50 -0
  268. package/src/tools/index.ts +9 -0
  269. package/src/tools/path-utils.ts +49 -2
  270. package/src/tools/read.ts +156 -88
  271. package/src/tools/renderers.ts +7 -1
  272. package/src/tools/todo.ts +101 -11
  273. package/src/tools/vibe.ts +1 -2
  274. package/src/tools/write.ts +51 -1
  275. package/src/tools/xdev.ts +67 -3
  276. package/src/tts/streaming-player.ts +62 -4
  277. package/src/tts/vocalizer.ts +18 -1
  278. package/src/utils/jj.ts +125 -4
  279. package/src/utils/lang-from-path.ts +7 -0
  280. package/src/utils/title-generator.ts +137 -1
  281. package/src/vibe/runtime.ts +852 -72
  282. package/src/web/search/index.ts +3 -6
  283. package/src/web/search/provider.ts +31 -16
  284. package/src/web/search/providers/xai.ts +7 -1
  285. package/src/web/search/types.ts +3 -0
@@ -0,0 +1,496 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import {
4
+ type AfterToolCallContext,
5
+ type AfterToolCallResult,
6
+ type Agent,
7
+ type AgentEvent,
8
+ type AgentMessage,
9
+ createToolScopedAbortReason,
10
+ } from "@oh-my-pi/pi-agent-core";
11
+ import type { AssistantMessage, ToolCall } from "@oh-my-pi/pi-ai";
12
+ import { isRecord, prompt, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
13
+ import type { Rule } from "../capability/rule";
14
+ import type { Settings } from "../config/settings";
15
+ import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
16
+ import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
17
+ import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
18
+ import type { AgentSessionEvent } from "./agent-session-events";
19
+ import type { SessionManager } from "./session-manager";
20
+
21
+ interface TtsrContinueOptions {
22
+ delayMs?: number;
23
+ generation?: number;
24
+ shouldContinue?: () => boolean;
25
+ onSkip?: () => void;
26
+ onError?: () => void;
27
+ }
28
+
29
+ /** Capabilities the TTSR coordinator borrows from its owning session. */
30
+ export interface TtsrCoordinatorHost {
31
+ agent: Agent;
32
+ sessionManager: SessionManager;
33
+ settings: Settings;
34
+ emitSessionEvent(event: AgentSessionEvent): Promise<void>;
35
+ schedulePostPromptTask(task: (signal: AbortSignal) => Promise<void>, options?: { delayMs?: number }): void;
36
+ scheduleAgentContinue(options: TtsrContinueOptions): void;
37
+ promptGeneration(): number;
38
+ }
39
+
40
+ /** Coordinates TTSR stream matching, interruption, injection, and resume gates. */
41
+ export class TtsrCoordinator {
42
+ readonly #host: TtsrCoordinatorHost;
43
+ readonly #manager: TtsrManager | undefined;
44
+ #pendingInjections: Rule[] = [];
45
+ #perToolInjections = new Map<string, Rule[]>();
46
+ #abortPending = false;
47
+ #retryToken = 0;
48
+ #resumePromise: Promise<void> | undefined;
49
+ #resumeResolve: (() => void) | undefined;
50
+
51
+ constructor(host: TtsrCoordinatorHost, manager: TtsrManager | undefined) {
52
+ this.#host = host;
53
+ this.#manager = manager;
54
+ }
55
+
56
+ /** Configured TTSR manager, when stream rules are enabled. */
57
+ get manager(): TtsrManager | undefined {
58
+ return this.#manager;
59
+ }
60
+
61
+ /** Whether a TTSR-triggered stream abort is awaiting its continuation. */
62
+ get abortPending(): boolean {
63
+ return this.#abortPending;
64
+ }
65
+
66
+ /** Current resume gate awaited by post-prompt recovery. */
67
+ get resumeGate(): Promise<void> | undefined {
68
+ return this.#resumePromise;
69
+ }
70
+
71
+ /** Resets stream buffers at turn start. */
72
+ onTurnStart(): void {
73
+ this.#manager?.resetBuffer();
74
+ }
75
+
76
+ /** Advances repeat-after-gap tracking at turn end. */
77
+ onTurnEnd(): void {
78
+ this.#manager?.incrementMessageCount();
79
+ }
80
+
81
+ /** Checks one streamed message update and reports whether TTSR consumed it by aborting. */
82
+ async checkMessageUpdate(event: AgentEvent): Promise<boolean> {
83
+ if (event.type !== "message_update" || !this.#manager?.hasRules()) return false;
84
+ const assistantEvent = event.assistantMessageEvent;
85
+ let matchContext: TtsrMatchContext | undefined;
86
+ let streamingToolCall: ToolCall | undefined;
87
+ if (assistantEvent.type === "text_delta") {
88
+ matchContext = { source: "text" };
89
+ } else if (assistantEvent.type === "thinking_delta") {
90
+ matchContext = { source: "thinking" };
91
+ } else if (assistantEvent.type === "toolcall_delta") {
92
+ streamingToolCall = this.#getStreamingToolCallBlock(event.message, assistantEvent.contentIndex);
93
+ matchContext = this.#getToolMatchContext(streamingToolCall, assistantEvent.contentIndex);
94
+ }
95
+ if (!matchContext || !("delta" in assistantEvent)) return false;
96
+ const targetMessageTimestamp = event.message.role === "assistant" ? event.message.timestamp : undefined;
97
+ const matches = this.#checkStream(assistantEvent.delta, matchContext, streamingToolCall);
98
+ if (matches.length > 0 && this.#handleMatches(matches, matchContext, targetMessageTimestamp)) return true;
99
+ // AST rules use the reconstructed edit/write snapshot and are awaited so
100
+ // the manager self-throttles native matching.
101
+ if (matchContext.source === "tool" && this.#manager.hasAstRules()) {
102
+ const astMatches = await this.#checkAstStream(matchContext, streamingToolCall);
103
+ if (astMatches.length > 0 && this.#handleMatches(astMatches, matchContext, targetMessageTimestamp))
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+
109
+ /** Settles the previous resume gate and queues any deferred injection. */
110
+ onAssistantMessageEnd(message: AssistantMessage): void {
111
+ // Gate on abortPending, not stopReason: unrelated aborts have no TTSR continuation.
112
+ if (!this.#abortPending) this.resolveResume();
113
+ this.#queueDeferredInjectionIfNeeded(message);
114
+ }
115
+
116
+ /** Marks names persisted with a delivered TTSR injection as injected. */
117
+ markInjectedFromDetails(details: unknown): void {
118
+ if (!details || typeof details !== "object" || Array.isArray(details)) return;
119
+ const rules = "rules" in details ? details.rules : undefined;
120
+ if (!Array.isArray(rules)) return;
121
+ this.#markInjected(rules.filter((ruleName): ruleName is string => typeof ruleName === "string"));
122
+ }
123
+
124
+ /** Folds per-tool reminders into the matched tool's result. */
125
+ afterToolCall(ctx: AfterToolCallContext): AfterToolCallResult | undefined {
126
+ const rules = this.#perToolInjections.get(ctx.toolCall.id);
127
+ if (!rules || rules.length === 0) return undefined;
128
+ this.#perToolInjections.delete(ctx.toolCall.id);
129
+ const reminder = rules
130
+ .map(rule =>
131
+ prompt.render(ttsrToolReminderTemplate, {
132
+ name: rule.name,
133
+ path: this.#displayRulePath(rule.path),
134
+ content: rule.content,
135
+ }),
136
+ )
137
+ .join("\n\n");
138
+ const ruleNames = rules.map(rule => rule.name.trim()).filter(name => name.length > 0);
139
+ if (ruleNames.length > 0) this.#host.sessionManager.appendTtsrInjection(ruleNames);
140
+ return { content: [{ type: "text", text: reminder }, ...ctx.result.content] };
141
+ }
142
+
143
+ /** Resolves and clears the current resume gate. */
144
+ resolveResume(): void {
145
+ if (!this.#resumeResolve) return;
146
+ this.#resumeResolve();
147
+ this.#resumeResolve = undefined;
148
+ this.#resumePromise = undefined;
149
+ }
150
+
151
+ #ensureResumePromise(): void {
152
+ if (this.#resumePromise) return;
153
+ const { promise, resolve } = Promise.withResolvers<void>();
154
+ this.#resumePromise = promise;
155
+ this.#resumeResolve = resolve;
156
+ }
157
+
158
+ #formatAbortReason(rules: Rule[]): string {
159
+ const label = rules.length === 1 ? "rule" : "rules";
160
+ return `TTSR matched ${label}: ${rules.map(rule => rule.name).join(", ")}`;
161
+ }
162
+
163
+ #getInjectionContent(): { content: string; rules: Rule[] } | undefined {
164
+ if (this.#pendingInjections.length === 0) return undefined;
165
+ const rules = this.#pendingInjections;
166
+ const content = rules
167
+ .map(rule =>
168
+ prompt.render(ttsrInterruptTemplate, {
169
+ name: rule.name,
170
+ path: this.#displayRulePath(rule.path),
171
+ content: rule.content,
172
+ }),
173
+ )
174
+ .join("\n\n");
175
+ this.#pendingInjections = [];
176
+ return { content, rules };
177
+ }
178
+
179
+ #displayRulePath(rulePath: string): string {
180
+ const cwd = this.#host.sessionManager.getCwd();
181
+ const cwdRelative = relativePathWithinRoot(cwd, rulePath) ?? this.#displayPathWithinRoot(cwd, rulePath);
182
+ if (cwdRelative) return cwdRelative;
183
+ const homeRelative = relativePathWithinRoot(os.homedir(), rulePath);
184
+ if (homeRelative) return `~/${homeRelative}`;
185
+ return rulePath;
186
+ }
187
+
188
+ #displayPathWithinRoot(root: string, candidate: string): string | null {
189
+ const relative = path.relative(path.resolve(root), path.resolve(candidate));
190
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative) ? relative : null;
191
+ }
192
+
193
+ #addPendingInjections(rules: Rule[]): void {
194
+ const seen = new Set(this.#pendingInjections.map(rule => rule.name));
195
+ for (const rule of rules) {
196
+ if (seen.has(rule.name)) continue;
197
+ this.#pendingInjections.push(rule);
198
+ seen.add(rule.name);
199
+ }
200
+ }
201
+
202
+ #extractToolCallId(matchContext: TtsrMatchContext): string | undefined {
203
+ if (matchContext.source !== "tool") return undefined;
204
+ const key = matchContext.streamKey;
205
+ if (typeof key !== "string" || !key.startsWith("toolcall:")) return undefined;
206
+ const id = key.slice("toolcall:".length);
207
+ return id.length > 0 ? id : undefined;
208
+ }
209
+
210
+ #addPerToolInjections(toolCallId: string, rules: Rule[]): void {
211
+ const bucket = this.#perToolInjections.get(toolCallId) ?? [];
212
+ const seen = new Set(bucket.map(rule => rule.name));
213
+ const claimedElsewhere = new Set<string>();
214
+ for (const [otherId, otherBucket] of this.#perToolInjections) {
215
+ if (otherId === toolCallId) continue;
216
+ for (const rule of otherBucket) claimedElsewhere.add(rule.name);
217
+ }
218
+ const newlyAdded: string[] = [];
219
+ for (const rule of rules) {
220
+ if (seen.has(rule.name) || claimedElsewhere.has(rule.name)) continue;
221
+ bucket.push(rule);
222
+ seen.add(rule.name);
223
+ newlyAdded.push(rule.name);
224
+ }
225
+ if (bucket.length === 0) return;
226
+ this.#perToolInjections.set(toolCallId, bucket);
227
+ if (newlyAdded.length > 0) this.#manager?.markInjectedByNames(newlyAdded);
228
+ }
229
+
230
+ #markInjected(ruleNames: string[]): void {
231
+ const uniqueRuleNames = Array.from(
232
+ new Set(ruleNames.map(ruleName => ruleName.trim()).filter(ruleName => ruleName.length > 0)),
233
+ );
234
+ if (uniqueRuleNames.length === 0) return;
235
+ this.#manager?.markInjectedByNames(uniqueRuleNames);
236
+ this.#host.sessionManager.appendTtsrInjection(uniqueRuleNames);
237
+ }
238
+
239
+ #findAssistantIndex(targetTimestamp: number | undefined): number {
240
+ const messages = this.#host.agent.state.messages;
241
+ for (let index = messages.length - 1; index >= 0; index--) {
242
+ const message = messages[index];
243
+ if (message.role === "assistant" && (targetTimestamp === undefined || message.timestamp === targetTimestamp)) {
244
+ return index;
245
+ }
246
+ }
247
+ return -1;
248
+ }
249
+
250
+ #shouldInterrupt(matches: Rule[], matchContext: TtsrMatchContext): boolean {
251
+ const globalMode = this.#manager?.getSettings().interruptMode ?? "always";
252
+ for (const rule of matches) {
253
+ const mode = rule.interruptMode ?? globalMode;
254
+ if (mode === "never") continue;
255
+ if (mode === "prose-only" && (matchContext.source === "text" || matchContext.source === "thinking")) {
256
+ return true;
257
+ }
258
+ if (mode === "tool-only" && matchContext.source === "tool") return true;
259
+ if (mode === "always") return true;
260
+ }
261
+ return false;
262
+ }
263
+
264
+ #queueDeferredInjectionIfNeeded(message: AssistantMessage): void {
265
+ if (message.stopReason === "aborted" || message.stopReason === "error") this.#perToolInjections.clear();
266
+ if (this.#abortPending || this.#pendingInjections.length === 0) return;
267
+ if (message.stopReason === "aborted" || message.stopReason === "error") {
268
+ this.#pendingInjections = [];
269
+ return;
270
+ }
271
+ const injection = this.#getInjectionContent();
272
+ if (!injection) return;
273
+ this.#host.agent.followUp({
274
+ role: "custom",
275
+ customType: "ttsr-injection",
276
+ content: injection.content,
277
+ display: false,
278
+ details: { rules: injection.rules.map(rule => rule.name) },
279
+ attribution: "agent",
280
+ timestamp: Date.now(),
281
+ });
282
+ this.#ensureResumePromise();
283
+ this.#host.scheduleAgentContinue({
284
+ delayMs: 1,
285
+ generation: this.#host.promptGeneration(),
286
+ onSkip: () => this.resolveResume(),
287
+ shouldContinue: () => {
288
+ if (this.#host.agent.state.isStreaming || !this.#host.agent.hasQueuedMessages()) {
289
+ this.resolveResume();
290
+ return false;
291
+ }
292
+ return true;
293
+ },
294
+ onError: () => this.resolveResume(),
295
+ });
296
+ }
297
+
298
+ #getStreamingToolCallBlock(message: AgentMessage, contentIndex: number): ToolCall | undefined {
299
+ if (message.role !== "assistant") return undefined;
300
+ const content = message.content;
301
+ if (!Array.isArray(content) || contentIndex < 0 || contentIndex >= content.length) return undefined;
302
+ const block = content[contentIndex];
303
+ return block && typeof block === "object" && block.type === "toolCall" ? (block as ToolCall) : undefined;
304
+ }
305
+
306
+ #getToolMatchContext(toolCall: ToolCall | undefined, contentIndex: number): TtsrMatchContext {
307
+ const context: TtsrMatchContext = { source: "tool" };
308
+ if (!toolCall) return context;
309
+ context.toolName = toolCall.name;
310
+ context.streamKey = toolCall.id ? `toolcall:${toolCall.id}` : `tool:${toolCall.name}:${contentIndex}`;
311
+ context.filePaths = this.#extractToolFilePaths(toolCall);
312
+ return context;
313
+ }
314
+
315
+ #extractToolFilePaths(toolCall: ToolCall): string[] | undefined {
316
+ const args = toolCall.arguments ?? {};
317
+ const tool = this.#resolveTool(toolCall);
318
+ const toolPaths = tool?.matcherPaths?.(args);
319
+ if (toolPaths && toolPaths.length > 0) {
320
+ const normalized = toolPaths.flatMap(filePath => this.#normalizePathCandidates(filePath));
321
+ if (normalized.length > 0) return Array.from(new Set(normalized));
322
+ }
323
+ return this.#extractFilePathsFromArgs(args);
324
+ }
325
+
326
+ #checkStream(delta: string, matchContext: TtsrMatchContext, toolCall: ToolCall | undefined): Rule[] {
327
+ if (!this.#manager) return [];
328
+ const entries = this.#resolveMatcherEntries(toolCall);
329
+ if (entries) {
330
+ const matches: Rule[] = [];
331
+ for (const entry of entries) {
332
+ matches.push(...this.#manager.checkSnapshot(entry.digest, this.#perFileContext(matchContext, entry.path)));
333
+ }
334
+ return matches;
335
+ }
336
+ const digest = this.#resolveMatcherDigest(toolCall);
337
+ return digest !== undefined
338
+ ? this.#manager.checkSnapshot(digest, matchContext)
339
+ : this.#manager.checkDelta(delta, matchContext);
340
+ }
341
+
342
+ #resolveMatcherDigest(toolCall: ToolCall | undefined): string | undefined {
343
+ const tool = this.#resolveTool(toolCall);
344
+ return tool?.matcherDigest?.(toolCall?.arguments ?? {});
345
+ }
346
+
347
+ #resolveMatcherEntries(toolCall: ToolCall | undefined): readonly { path: string; digest: string }[] | undefined {
348
+ const tool = this.#resolveTool(toolCall);
349
+ const entries = tool?.matcherEntries?.(toolCall?.arguments ?? {});
350
+ return entries && entries.length > 0 ? entries : undefined;
351
+ }
352
+
353
+ #resolveTool(toolCall: ToolCall | undefined) {
354
+ if (!toolCall) return undefined;
355
+ const tools = this.#host.agent.state.tools;
356
+ return (
357
+ tools.find(tool => tool.name === toolCall.name) ??
358
+ tools.find(tool => tool.customWireName !== undefined && tool.customWireName === toolCall.name)
359
+ );
360
+ }
361
+
362
+ #perFileContext(base: TtsrMatchContext, filePath: string): TtsrMatchContext {
363
+ const filePaths = this.#normalizePathCandidates(filePath);
364
+ return {
365
+ ...base,
366
+ filePaths: filePaths.length > 0 ? filePaths : [filePath],
367
+ streamKey: base.streamKey ? `${base.streamKey}#${filePath}` : undefined,
368
+ };
369
+ }
370
+
371
+ async #checkAstStream(matchContext: TtsrMatchContext, toolCall: ToolCall | undefined): Promise<Rule[]> {
372
+ if (!this.#manager) return [];
373
+ const entries = this.#resolveMatcherEntries(toolCall);
374
+ if (entries) {
375
+ const matches: Rule[] = [];
376
+ for (const entry of entries) {
377
+ matches.push(
378
+ ...(await this.#manager.checkAstSnapshot(entry.digest, this.#perFileContext(matchContext, entry.path))),
379
+ );
380
+ }
381
+ return matches;
382
+ }
383
+ const digest = this.#resolveMatcherDigest(toolCall);
384
+ return digest === undefined ? [] : this.#manager.checkAstSnapshot(digest, matchContext);
385
+ }
386
+
387
+ #handleMatches(matches: Rule[], matchContext: TtsrMatchContext, targetTimestamp: number | undefined): boolean {
388
+ const shouldInterrupt = this.#shouldInterrupt(matches, matchContext);
389
+ const matchedToolId = this.#extractToolCallId(matchContext);
390
+ const perToolId = shouldInterrupt ? undefined : matchedToolId;
391
+ if (perToolId) {
392
+ this.#addPerToolInjections(perToolId, matches);
393
+ this.#host.emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
394
+ return false;
395
+ }
396
+ this.#addPendingInjections(matches);
397
+ if (!shouldInterrupt) return false;
398
+
399
+ this.#abortPending = true;
400
+ this.#ensureResumePromise();
401
+ const abortReason = this.#formatAbortReason(matches);
402
+ this.#host.agent.abort(
403
+ matchedToolId
404
+ ? createToolScopedAbortReason(
405
+ abortReason,
406
+ { [matchedToolId]: abortReason },
407
+ "TTSR interrupt on another tool call",
408
+ )
409
+ : abortReason,
410
+ );
411
+ this.#host.emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
412
+ const retryToken = ++this.#retryToken;
413
+ const generation = this.#host.promptGeneration();
414
+ this.#host.schedulePostPromptTask(
415
+ async () => {
416
+ if (this.#retryToken !== retryToken) {
417
+ this.resolveResume();
418
+ return;
419
+ }
420
+ const targetAssistantIndex = this.#findAssistantIndex(targetTimestamp);
421
+ if (!this.#abortPending || this.#host.promptGeneration() !== generation || targetAssistantIndex === -1) {
422
+ this.#abortPending = false;
423
+ this.#pendingInjections = [];
424
+ this.#perToolInjections.clear();
425
+ this.resolveResume();
426
+ return;
427
+ }
428
+ this.#abortPending = false;
429
+ this.#perToolInjections.clear();
430
+ if (this.#manager?.getSettings().contextMode === "discard") {
431
+ this.#host.agent.replaceMessages(this.#host.agent.state.messages.slice(0, targetAssistantIndex));
432
+ }
433
+ const injection = this.#getInjectionContent();
434
+ if (injection) {
435
+ const details = { rules: injection.rules.map(rule => rule.name) };
436
+ this.#host.agent.appendMessage({
437
+ role: "custom",
438
+ customType: "ttsr-injection",
439
+ content: injection.content,
440
+ display: false,
441
+ details,
442
+ attribution: "agent",
443
+ timestamp: Date.now(),
444
+ });
445
+ this.#host.sessionManager.appendCustomMessageEntry(
446
+ "ttsr-injection",
447
+ injection.content,
448
+ false,
449
+ details,
450
+ "agent",
451
+ );
452
+ this.#markInjected(details.rules);
453
+ }
454
+ try {
455
+ await this.#host.agent.continue();
456
+ } catch {
457
+ this.resolveResume();
458
+ }
459
+ },
460
+ { delayMs: 50 },
461
+ );
462
+ return true;
463
+ }
464
+
465
+ #extractFilePathsFromArgs(args: unknown): string[] | undefined {
466
+ if (!isRecord(args)) return undefined;
467
+ const rawPaths: string[] = [];
468
+ for (const key in args) {
469
+ const value = args[key];
470
+ const normalizedKey = key.toLowerCase();
471
+ if (typeof value === "string" && (normalizedKey === "path" || normalizedKey.endsWith("path"))) {
472
+ rawPaths.push(value);
473
+ continue;
474
+ }
475
+ if (Array.isArray(value) && (normalizedKey === "paths" || normalizedKey.endsWith("paths"))) {
476
+ for (const candidate of value) if (typeof candidate === "string") rawPaths.push(candidate);
477
+ }
478
+ }
479
+ const normalizedPaths = rawPaths.flatMap(filePath => this.#normalizePathCandidates(filePath));
480
+ return normalizedPaths.length === 0 ? undefined : Array.from(new Set(normalizedPaths));
481
+ }
482
+
483
+ #normalizePathCandidates(rawPath: string): string[] {
484
+ const trimmed = rawPath.trim();
485
+ if (trimmed.length === 0) return [];
486
+ const normalizedInput = trimmed.replaceAll("\\", "/");
487
+ const candidates = new Set<string>([normalizedInput]);
488
+ if (normalizedInput.startsWith("./")) candidates.add(normalizedInput.slice(2));
489
+ const cwd = this.#host.sessionManager.getCwd();
490
+ const absolutePath = path.isAbsolute(trimmed) ? path.normalize(trimmed) : path.resolve(cwd, trimmed);
491
+ candidates.add(absolutePath.replaceAll("\\", "/"));
492
+ const relative = path.relative(cwd, absolutePath).replaceAll("\\", "/");
493
+ if (relative && relative !== "." && !relative.startsWith("../") && relative !== "..") candidates.add(relative);
494
+ return Array.from(candidates);
495
+ }
496
+ }