@dyyz1993/pi-coding-agent 0.74.24 → 0.74.25

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 (145) hide show
  1. package/dist/core/agent-session.d.ts.map +1 -1
  2. package/dist/core/agent-session.js +3 -0
  3. package/dist/core/agent-session.js.map +1 -1
  4. package/dist/extensions/agent-permissions/index.ts +235 -0
  5. package/dist/extensions/ask-tools/index.ts +115 -0
  6. package/dist/extensions/auto-memory/contract.d.ts +51 -0
  7. package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
  8. package/dist/extensions/auto-memory/contract.js +2 -0
  9. package/dist/extensions/auto-memory/contract.js.map +1 -0
  10. package/dist/extensions/auto-memory/contract.ts +56 -0
  11. package/dist/extensions/auto-memory/index.ts +969 -0
  12. package/dist/extensions/auto-memory/prompts.ts +202 -0
  13. package/dist/extensions/auto-memory/skip-rules.ts +297 -0
  14. package/dist/extensions/auto-memory/utils.ts +208 -0
  15. package/dist/extensions/auto-session-title/index.ts +83 -0
  16. package/dist/extensions/bash-ext/contract.d.ts +79 -0
  17. package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
  18. package/dist/extensions/bash-ext/contract.js +2 -0
  19. package/dist/extensions/bash-ext/contract.js.map +1 -0
  20. package/dist/extensions/bash-ext/contract.ts +69 -0
  21. package/dist/extensions/bash-ext/index.ts +858 -0
  22. package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
  23. package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
  24. package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
  25. package/dist/extensions/claude-hooks-compat/index.ts +178 -0
  26. package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
  27. package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
  28. package/dist/extensions/claude-hooks-compat/types.ts +77 -0
  29. package/dist/extensions/compaction-manager/config.ts +47 -0
  30. package/dist/extensions/compaction-manager/context-fold.ts +63 -0
  31. package/dist/extensions/compaction-manager/index.ts +151 -0
  32. package/dist/extensions/compaction-manager/microcompact.ts +49 -0
  33. package/dist/extensions/compaction-manager/reactive.ts +9 -0
  34. package/dist/extensions/compaction-manager/session-memory.ts +48 -0
  35. package/dist/extensions/coordinator/INTEGRATION.md +376 -0
  36. package/dist/extensions/coordinator/handler.test.ts +277 -0
  37. package/dist/extensions/coordinator/handler.ts +189 -0
  38. package/dist/extensions/coordinator/index.ts +261 -0
  39. package/dist/extensions/coordinator/types.d.ts +100 -0
  40. package/dist/extensions/coordinator/types.d.ts.map +1 -0
  41. package/dist/extensions/coordinator/types.js +2 -0
  42. package/dist/extensions/coordinator/types.js.map +1 -0
  43. package/dist/extensions/coordinator/types.ts +72 -0
  44. package/dist/extensions/file-snapshot/index.ts +131 -0
  45. package/dist/extensions/file-time-guard/README.md +133 -0
  46. package/dist/extensions/file-time-guard/config.ts +13 -0
  47. package/dist/extensions/file-time-guard/index.ts +171 -0
  48. package/dist/extensions/hooks-engine/index.ts +117 -0
  49. package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
  50. package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
  51. package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
  52. package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
  53. package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
  54. package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
  55. package/dist/extensions/lsp/lsp/contract.js +2 -0
  56. package/dist/extensions/lsp/lsp/contract.js.map +1 -0
  57. package/dist/extensions/lsp/lsp/contract.ts +103 -0
  58. package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
  59. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
  60. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
  61. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
  62. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
  63. package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
  64. package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
  65. package/dist/extensions/lsp/lsp/index.ts +307 -0
  66. package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
  67. package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
  68. package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
  69. package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
  70. package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
  71. package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
  72. package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
  73. package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
  74. package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
  75. package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
  76. package/dist/extensions/message-bridge/GUIDE.md +210 -0
  77. package/dist/extensions/message-bridge/index.ts +222 -0
  78. package/dist/extensions/output-guard/index.ts +384 -0
  79. package/dist/extensions/preview/index.ts +278 -0
  80. package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
  81. package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
  82. package/dist/extensions/rules-engine/cache.js +232 -0
  83. package/dist/extensions/rules-engine/cache.ts +38 -0
  84. package/dist/extensions/rules-engine/config.js +63 -0
  85. package/dist/extensions/rules-engine/config.ts +70 -0
  86. package/dist/extensions/rules-engine/index.js +1530 -0
  87. package/dist/extensions/rules-engine/index.ts +552 -0
  88. package/dist/extensions/rules-engine/injector.js +68 -0
  89. package/dist/extensions/rules-engine/injector.ts +74 -0
  90. package/dist/extensions/rules-engine/loader.js +179 -0
  91. package/dist/extensions/rules-engine/loader.ts +205 -0
  92. package/dist/extensions/rules-engine/matcher.js +534 -0
  93. package/dist/extensions/rules-engine/matcher.ts +52 -0
  94. package/dist/extensions/rules-engine/types.d.ts +156 -0
  95. package/dist/extensions/rules-engine/types.d.ts.map +1 -0
  96. package/dist/extensions/rules-engine/types.js +2 -0
  97. package/dist/extensions/rules-engine/types.js.map +1 -0
  98. package/dist/extensions/rules-engine/types.ts +169 -0
  99. package/dist/extensions/session-supervisor/checker.ts +116 -0
  100. package/dist/extensions/session-supervisor/config.ts +45 -0
  101. package/dist/extensions/session-supervisor/index.ts +726 -0
  102. package/dist/extensions/session-supervisor/prompts.ts +132 -0
  103. package/dist/extensions/session-supervisor/scheduler.ts +69 -0
  104. package/dist/extensions/session-supervisor/types.ts +215 -0
  105. package/dist/extensions/subagent/README.md +172 -0
  106. package/dist/extensions/subagent/agents/explorer.md +25 -0
  107. package/dist/extensions/subagent/agents/guide.md +27 -0
  108. package/dist/extensions/subagent/agents/planner.md +37 -0
  109. package/dist/extensions/subagent/agents/reviewer.md +35 -0
  110. package/dist/extensions/subagent/agents/scout.md +50 -0
  111. package/dist/extensions/subagent/agents/verification.md +35 -0
  112. package/dist/extensions/subagent/agents/worker.md +24 -0
  113. package/dist/extensions/subagent/agents.ts +25 -0
  114. package/dist/extensions/subagent/index.ts +987 -0
  115. package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
  116. package/dist/extensions/subagent/prompts/implement.md +10 -0
  117. package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
  118. package/dist/extensions/subagent-ext/contract.d.ts +2 -0
  119. package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
  120. package/dist/extensions/subagent-ext/contract.js +2 -0
  121. package/dist/extensions/subagent-ext/contract.js.map +1 -0
  122. package/dist/extensions/subagent-ext/contract.ts +1 -0
  123. package/dist/extensions/subagent-ext/index.ts +347 -0
  124. package/dist/extensions/subagent-shared/contract.d.ts +25 -0
  125. package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
  126. package/dist/extensions/subagent-shared/contract.js +2 -0
  127. package/dist/extensions/subagent-shared/contract.js.map +1 -0
  128. package/dist/extensions/subagent-shared/contract.ts +28 -0
  129. package/dist/extensions/subagent-shared/index.ts +4 -0
  130. package/dist/extensions/subagent-shared/render.ts +166 -0
  131. package/dist/extensions/subagent-shared/types.ts +35 -0
  132. package/dist/extensions/subagent-shared/utils.ts +112 -0
  133. package/dist/extensions/subagent-v2/contract.d.ts +2 -0
  134. package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
  135. package/dist/extensions/subagent-v2/contract.js +2 -0
  136. package/dist/extensions/subagent-v2/contract.js.map +1 -0
  137. package/dist/extensions/subagent-v2/contract.ts +1 -0
  138. package/dist/extensions/subagent-v2/index.ts +599 -0
  139. package/dist/extensions/todo-ext/contract.d.ts +27 -0
  140. package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
  141. package/dist/extensions/todo-ext/contract.js +2 -0
  142. package/dist/extensions/todo-ext/contract.js.map +1 -0
  143. package/dist/extensions/todo-ext/contract.ts +30 -0
  144. package/dist/extensions/todo-ext/index.ts +419 -0
  145. package/package.json +6 -5
@@ -0,0 +1,278 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { extname, isAbsolute, resolve } from "node:path";
3
+ import { createConnection } from "node:net";
4
+ import type { AgentToolResult } from "@dyyz1993/pi-agent-core";
5
+ import { Text } from "@dyyz1993/pi-tui";
6
+ import { Type } from "typebox";
7
+ import type { ExtensionAPI, ExtensionContext } from "@dyyz1993/pi-coding-agent";
8
+
9
+ export type ResourceType = "image" | "url" | "html" | "pdf" | "video" | "audio" | "markdown" | "text";
10
+
11
+ export interface PreviewDetails {
12
+ source: string;
13
+ absolutePath?: string;
14
+ resourceType: ResourceType;
15
+ mimeType?: string;
16
+ status: "ok" | "not_found" | "error";
17
+ size?: number;
18
+ title?: string;
19
+ error?: string;
20
+ }
21
+
22
+ const EXT_TO_RESOURCE: Record<string, { resourceType: ResourceType; mimeType: string }> = {
23
+ ".png": { resourceType: "image", mimeType: "image/png" },
24
+ ".jpg": { resourceType: "image", mimeType: "image/jpeg" },
25
+ ".jpeg": { resourceType: "image", mimeType: "image/jpeg" },
26
+ ".gif": { resourceType: "image", mimeType: "image/gif" },
27
+ ".webp": { resourceType: "image", mimeType: "image/webp" },
28
+ ".svg": { resourceType: "image", mimeType: "image/svg+xml" },
29
+ ".bmp": { resourceType: "image", mimeType: "image/bmp" },
30
+ ".ico": { resourceType: "image", mimeType: "image/x-icon" },
31
+ ".html": { resourceType: "html", mimeType: "text/html" },
32
+ ".htm": { resourceType: "html", mimeType: "text/html" },
33
+ ".pdf": { resourceType: "pdf", mimeType: "application/pdf" },
34
+ ".mp4": { resourceType: "video", mimeType: "video/mp4" },
35
+ ".webm": { resourceType: "video", mimeType: "video/webm" },
36
+ ".ogg": { resourceType: "video", mimeType: "video/ogg" },
37
+ ".mp3": { resourceType: "audio", mimeType: "audio/mpeg" },
38
+ ".wav": { resourceType: "audio", mimeType: "audio/wav" },
39
+ ".flac": { resourceType: "audio", mimeType: "audio/flac" },
40
+ ".md": { resourceType: "markdown", mimeType: "text/markdown" },
41
+ ".mdx": { resourceType: "markdown", mimeType: "text/mdx" },
42
+ };
43
+
44
+ const URL_PATTERN = /^https?:\/\//i;
45
+
46
+ function isLocalAddress(host: string): boolean {
47
+ if (!host) return false;
48
+ const lower = host.toLowerCase();
49
+ if (lower === "localhost" || lower === "127.0.0.1" || lower === "::1") return true;
50
+ if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(host)) return true;
51
+ if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host)) return true;
52
+ if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(host)) return true;
53
+ return false;
54
+ }
55
+
56
+ function checkReachable(host: string, port: number, timeoutMs = 2000): Promise<boolean> {
57
+ return new Promise((resolve) => {
58
+ const socket = createConnection({ host, port }, () => {
59
+ socket.destroy();
60
+ resolve(true);
61
+ });
62
+ socket.setTimeout(timeoutMs);
63
+ socket.on("timeout", () => {
64
+ socket.destroy();
65
+ resolve(false);
66
+ });
67
+ socket.on("error", () => {
68
+ socket.destroy();
69
+ resolve(false);
70
+ });
71
+ });
72
+ }
73
+
74
+ function isUrl(source: string): boolean {
75
+ return URL_PATTERN.test(source);
76
+ }
77
+
78
+ function detectResource(
79
+ source: string,
80
+ cwd: string,
81
+ ): {
82
+ resourceType: ResourceType;
83
+ mimeType?: string;
84
+ absolutePath?: string;
85
+ } {
86
+ if (isUrl(source)) {
87
+ return { resourceType: "url", absolutePath: source };
88
+ }
89
+
90
+ const absolutePath = isAbsolute(source) ? source : resolve(cwd, source);
91
+ const ext = extname(source).toLowerCase();
92
+ const mapped = EXT_TO_RESOURCE[ext];
93
+ if (mapped) {
94
+ return { resourceType: mapped.resourceType, mimeType: mapped.mimeType, absolutePath };
95
+ }
96
+ return { resourceType: "text", mimeType: "text/plain", absolutePath };
97
+ }
98
+
99
+ const PreviewParams = Type.Object({
100
+ source: Type.String({ description: "File path or URL to preview" }),
101
+ title: Type.Optional(Type.String({ description: "Optional display title for the card" })),
102
+ });
103
+
104
+ export default function (pi: ExtensionAPI) {
105
+ pi.registerTool({
106
+ name: "preview",
107
+ label: "Preview",
108
+ description:
109
+ "Preview a resource (image, URL, PDF, video, audio, markdown, etc.) as a card in the UI. Does not send file content to the LLM.",
110
+ promptSnippet: "Preview a file or URL as a card",
111
+ parameters: PreviewParams,
112
+
113
+ async execute(
114
+ _toolCallId: string,
115
+ params: { source: string; title?: string },
116
+ _signal?: AbortSignal,
117
+ _onUpdate?: unknown,
118
+ ctx?: ExtensionContext,
119
+ ): Promise<AgentToolResult<PreviewDetails>> {
120
+ const cwd = ctx?.cwd ?? process.cwd();
121
+
122
+ if (!params.source?.trim()) {
123
+ return {
124
+ content: [{ type: "text", text: "Error: source is required" }],
125
+ details: { source: "", resourceType: "text", status: "error", error: "source required" },
126
+ };
127
+ }
128
+
129
+ const { resourceType, mimeType, absolutePath } = detectResource(params.source, cwd);
130
+
131
+ if (resourceType === "url") {
132
+ // 对本地/LAN 地址做 TCP 可达性检测
133
+ if (params.source.startsWith("http://")) {
134
+ try {
135
+ const parsed = new URL(params.source);
136
+ if (isLocalAddress(parsed.hostname)) {
137
+ const port = parseInt(parsed.port || "80", 10);
138
+ const reachable = await checkReachable(parsed.hostname, port);
139
+ if (!reachable) {
140
+ const msg = `Preview 失败:${parsed.host} 未在局域网开放,服务可能只监听 127.0.0.1。请将服务绑定到 0.0.0.0 后重试。`;
141
+ return {
142
+ content: [{ type: "text", text: msg }],
143
+ details: {
144
+ source: params.source,
145
+ absolutePath: params.source,
146
+ resourceType: "url",
147
+ status: "error",
148
+ title: params.title,
149
+ error: `${parsed.host} 未在局域网开放,可能只监听 127.0.0.1`,
150
+ },
151
+ };
152
+ }
153
+ }
154
+ } catch {
155
+ // URL parse 失败,继续正常流程
156
+ }
157
+ }
158
+
159
+ return {
160
+ content: [{ type: "text", text: `Preview: ${params.source} (url)` }],
161
+ details: {
162
+ source: params.source,
163
+ absolutePath: params.source,
164
+ resourceType: "url",
165
+ status: "ok",
166
+ title: params.title,
167
+ },
168
+ };
169
+ }
170
+
171
+ if (!absolutePath || !existsSync(absolutePath)) {
172
+ return {
173
+ content: [{ type: "text", text: `Preview: ${params.source} not found` }],
174
+ details: {
175
+ source: params.source,
176
+ absolutePath,
177
+ resourceType,
178
+ status: "not_found",
179
+ title: params.title,
180
+ error: "file not found",
181
+ },
182
+ };
183
+ }
184
+
185
+ const stat = statSync(absolutePath);
186
+ if (stat.isDirectory()) {
187
+ return {
188
+ content: [{ type: "text", text: `Preview: ${params.source} is a directory` }],
189
+ details: {
190
+ source: params.source,
191
+ absolutePath,
192
+ resourceType,
193
+ status: "error",
194
+ title: params.title,
195
+ error: "is a directory",
196
+ },
197
+ };
198
+ }
199
+
200
+ const sizeStr =
201
+ stat.size > 1024 * 1024
202
+ ? `${(stat.size / (1024 * 1024)).toFixed(1)}MB`
203
+ : stat.size > 1024
204
+ ? `${(stat.size / 1024).toFixed(1)}KB`
205
+ : `${stat.size}B`;
206
+
207
+ return {
208
+ content: [
209
+ {
210
+ type: "text",
211
+ text: `Preview: ${params.source} (${resourceType}${mimeType ? `, ${mimeType}` : ""}, ${sizeStr})`,
212
+ },
213
+ ],
214
+ details: {
215
+ source: params.source,
216
+ absolutePath,
217
+ resourceType,
218
+ mimeType,
219
+ status: "ok",
220
+ size: stat.size,
221
+ title: params.title,
222
+ },
223
+ };
224
+ },
225
+
226
+ renderCall(args, theme) {
227
+ let text = theme.fg("toolTitle", theme.bold("preview "));
228
+ if (args.title) text += theme.fg("dim", `"${args.title}" `);
229
+ text += theme.fg("muted", args.source ?? "");
230
+ return new Text(text, 0, 0);
231
+ },
232
+
233
+ renderResult(result, { expanded: _expanded }, theme) {
234
+ const details = result.details as PreviewDetails | undefined;
235
+ if (!details) {
236
+ const text = result.content[0];
237
+ return new Text(text?.type === "text" ? text.text : "", 0, 0);
238
+ }
239
+
240
+ if (details.error || details.status === "not_found") {
241
+ return new Text(theme.fg("error", `Preview: ${details.error ?? "error"}`), 0, 0);
242
+ }
243
+
244
+ const icon = resourceIcon(details.resourceType);
245
+ const label = details.title ?? details.source;
246
+ const sizeStr = details.size ? ` (${formatSize(details.size)})` : "";
247
+
248
+ return new Text(`${theme.fg("success", icon)} ${theme.fg("muted", `${label}${sizeStr}`)}`, 0, 0);
249
+ },
250
+ });
251
+ }
252
+
253
+ function resourceIcon(type: ResourceType): string {
254
+ switch (type) {
255
+ case "image":
256
+ return "🖼";
257
+ case "url":
258
+ return "🔗";
259
+ case "video":
260
+ return "🎬";
261
+ case "audio":
262
+ return "🎵";
263
+ case "pdf":
264
+ return "📄";
265
+ case "html":
266
+ return "🌐";
267
+ case "markdown":
268
+ return "📝";
269
+ default:
270
+ return "📋";
271
+ }
272
+ }
273
+
274
+ function formatSize(bytes: number): string {
275
+ if (bytes > 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
276
+ if (bytes > 1024) return `${(bytes / 1024).toFixed(1)}KB`;
277
+ return `${bytes}B`;
278
+ }
@@ -0,0 +1,111 @@
1
+ # Rules Engine: MatchHistory Staleness Scenarios & Reconciliation
2
+
3
+ ## Problem
4
+
5
+ Conditional rule matches are tracked in `matchHistory` (an array of `MatchRecord`).
6
+ The history can become **stale** when messages that triggered matches are removed or modified
7
+ without an explicit notification to the rules-engine plugin.
8
+
9
+ ## Scenarios
10
+
11
+ ### 1. Context Compaction
12
+
13
+ **What happens**: pi compacts old messages into a summary. ToolResult messages
14
+ that contained `details.rulesMatched` are replaced by a compact summary entry.
15
+
16
+ **Signal**: `session_compact` event fires. Plugin resets `cachedMatchHash` and clears `lastMessages`.
17
+
18
+ **Reconciliation**:
19
+ - `context` event fires on the next agent turn with the **post-compaction** message list
20
+ - `rebuildMatchHistory()` scans remaining messages — compacted-away matches disappear
21
+ - `turn_end` also checks for drift and sends corrected snapshot if needed
22
+
23
+ **Result**: Matched rules from compacted-away messages are automatically cleared.
24
+
25
+ ### 2. Rollback / Abort Retry
26
+
27
+ **What happens**: User triggers abort_retry, which removes the last assistant turn
28
+ including its tool results. Previously matched rules are no longer in context.
29
+
30
+ **Signal**: No explicit event. The rolled-back messages are removed from the message list.
31
+
32
+ **Reconciliation**:
33
+ - `context` event fires on the next agent turn — rolled-back messages are absent
34
+ - `rebuildMatchHistory()` only finds matches in remaining messages
35
+ - `turn_end` catches this too
36
+
37
+ **Result**: Stale. Panel may show old matches until next agent turn or manual `requestSnapshot`.
38
+
39
+ ### 3. Manual Message Deletion
40
+
41
+ **What happens**: User manually deletes a message (e.g., via UI). If the deleted message
42
+ was a toolResult with `details.rulesMatched`, the match is no longer valid.
43
+
44
+ **Signal**: No event at all.
45
+
46
+ **Reconciliation**:
47
+ - Same as rollback: `context` and `turn_end` eventually reconcile
48
+ - Frontend `requestSnapshot` (on panel open/refresh) triggers `rebuildMatchHistory(lastMessages)`
49
+ - But `lastMessages` may be stale if no `context` event fired since deletion
50
+
51
+ **Result**: Stale until next agent turn or `requestSnapshot` with fresh context.
52
+
53
+ ### 4. Other Plugin Modifies/Hides Tool Results
54
+
55
+ **What happens**: Another plugin (e.g., todo-cleanup) hides or modifies a toolResult entry,
56
+ removing its `details.rulesMatched` field or replacing the message entirely.
57
+
58
+ **Signal**: No cross-plugin notification.
59
+
60
+ **Reconciliation**:
61
+ - `context` event sees the modified messages and rebuilds accordingly
62
+ - If the other plugin modifies messages between `context` events, stale data persists
63
+
64
+ **Result**: Same as deletion — eventually consistent after next `context`.
65
+
66
+ ### 5. Session Fork / Switch
67
+
68
+ **What happens**: Session is forked or user switches to a different session. The forked
69
+ session has a copy of messages, but the rules-engine state is per-process.
70
+
71
+ **Signal**: `session_start` fires for the new session. Plugin sends fresh snapshot.
72
+
73
+ **Reconciliation**:
74
+ - New pi process → fresh state → `session_start` sends snapshot with empty history
75
+ - `context` event rebuilds from the forked messages
76
+ - Any matches present in the forked messages will be picked up
77
+
78
+ **Result**: Correct — matches from the fork are preserved, new matches start fresh.
79
+
80
+ ## Reconciliation Mechanism Summary
81
+
82
+ | Mechanism | Trigger | Uses Fresh Data? | Cost |
83
+ |-----------|---------|------------------|------|
84
+ | `context` event | Every agent turn (before LLM call) | Yes (current messages) | Medium — scans all messages |
85
+ | `turn_end` event | End of every turn | Yes (cached `lastMessages`) | Low — hash comparison + conditional snapshot |
86
+ | `requestSnapshot` RPC | Frontend panel open/refresh | Yes (cached `lastMessages`) | Low — only when user requests |
87
+ | `session_compact` | After compaction | Clears cache → forces rebuild | Negligible |
88
+
89
+ ## Latency Analysis
90
+
91
+ The `context` event fires via `transformContext` in the agent loop (`packages/agent/agent-loop.ts:145-146`),
92
+ called **before every LLM call**. This is the primary reconciliation point.
93
+
94
+ **Latency gap**: Between a message mutation (rollback, deletion, compaction) and the next LLM call,
95
+ the panel may show stale data. This is acceptable because:
96
+
97
+ 1. Users rarely watch the rules panel during message mutations
98
+ 2. Next agent turn reconciles immediately (sub-second)
99
+ 3. `requestSnapshot` (panel open/refresh) uses `lastMessages` which is at most one turn stale
100
+ 4. No event exists for message deletion in the pi extension API — polling would be the only alternative
101
+
102
+ **Why not poll?** Periodic `requestSnapshot` would require fresh messages (not cached), which means
103
+ calling into the agent state on a timer. The `context` event already provides this for free on every turn.
104
+
105
+ ## Implementation Notes
106
+
107
+ - `rebuildMatchHistory()` is the single source of truth — shared by `context`, `turn_end`, and `requestSnapshot`
108
+ - `lastMessages` is cached from the most recent `context` event
109
+ - `cachedMatchHash` is used to avoid sending duplicate snapshots when nothing changed
110
+ - Frontend merges `matchHistory` on snapshot (doesn't wipe on empty) to survive between reconciliations
111
+ - All 5 scenarios are **eventually consistent** with at most one-turn latency — no additional mechanism needed