@agentmemory/agentmemory 0.9.28 → 0.9.30

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 (158) hide show
  1. package/.env.example +98 -28
  2. package/AGENTS.md +19 -26
  3. package/README.md +619 -274
  4. package/dist/.env.example +98 -28
  5. package/dist/cli.d.mts.map +1 -1
  6. package/dist/cli.mjs +1328 -244
  7. package/dist/cli.mjs.map +1 -1
  8. package/dist/{connect-JnsJXc68.mjs → connect-B4_ca6BD.mjs} +619 -70
  9. package/dist/connect-B4_ca6BD.mjs.map +1 -0
  10. package/dist/docker-compose.yml +42 -16
  11. package/dist/hooks/_capture.d.mts +22 -0
  12. package/dist/hooks/_capture.d.mts.map +1 -0
  13. package/dist/hooks/_capture.mjs +908 -0
  14. package/dist/hooks/_capture.mjs.map +1 -0
  15. package/dist/hooks/antigravity-bridge.d.mts +8 -0
  16. package/dist/hooks/antigravity-bridge.d.mts.map +1 -0
  17. package/dist/hooks/antigravity-bridge.mjs +123 -0
  18. package/dist/hooks/antigravity-bridge.mjs.map +1 -0
  19. package/dist/hooks/notification.mjs +34 -27
  20. package/dist/hooks/notification.mjs.map +1 -1
  21. package/dist/hooks/post-commit.mjs +84 -2
  22. package/dist/hooks/post-commit.mjs.map +1 -1
  23. package/dist/hooks/post-tool-failure.mjs +71 -26
  24. package/dist/hooks/post-tool-failure.mjs.map +1 -1
  25. package/dist/hooks/post-tool-use.mjs +99 -36
  26. package/dist/hooks/post-tool-use.mjs.map +1 -1
  27. package/dist/hooks/pre-compact.mjs +95 -5
  28. package/dist/hooks/pre-compact.mjs.map +1 -1
  29. package/dist/hooks/pre-tool-use.mjs +72 -2
  30. package/dist/hooks/pre-tool-use.mjs.map +1 -1
  31. package/dist/hooks/prompt-submit.mjs +25 -22
  32. package/dist/hooks/prompt-submit.mjs.map +1 -1
  33. package/dist/hooks/session-end.mjs +93 -24
  34. package/dist/hooks/session-end.mjs.map +1 -1
  35. package/dist/hooks/session-start.mjs +101 -6
  36. package/dist/hooks/session-start.mjs.map +1 -1
  37. package/dist/hooks/stop.mjs +72 -8
  38. package/dist/hooks/stop.mjs.map +1 -1
  39. package/dist/hooks/subagent-start.mjs +28 -26
  40. package/dist/hooks/subagent-start.mjs.map +1 -1
  41. package/dist/hooks/subagent-stop.mjs +29 -26
  42. package/dist/hooks/subagent-stop.mjs.map +1 -1
  43. package/dist/hooks/task-completed.mjs +31 -28
  44. package/dist/hooks/task-completed.mjs.map +1 -1
  45. package/dist/iii-config.docker.yaml +10 -10
  46. package/dist/iii-config.yaml +3 -1
  47. package/dist/{image-refs-BKLmp_sD.mjs → image-refs-BaT-9oMU.mjs} +2 -2
  48. package/dist/image-refs-BaT-9oMU.mjs.map +1 -0
  49. package/dist/{image-refs-C2YU7BIm.mjs → image-refs-T_urbwHM.mjs} +3 -3
  50. package/dist/image-refs-T_urbwHM.mjs.map +1 -0
  51. package/dist/{keyed-mutex-DTHtcZrt.mjs → image-store-BsN4vyjr.mjs} +2 -14
  52. package/dist/image-store-BsN4vyjr.mjs.map +1 -0
  53. package/dist/index.mjs +14021 -6494
  54. package/dist/index.mjs.map +1 -1
  55. package/dist/keyed-mutex-DchXsyfP.mjs +221 -0
  56. package/dist/keyed-mutex-DchXsyfP.mjs.map +1 -0
  57. package/dist/secret-store-BnY3A50C.mjs +2348 -0
  58. package/dist/secret-store-BnY3A50C.mjs.map +1 -0
  59. package/dist/{src-CzgoepGU.mjs → src-Jsq9LCeH.mjs} +7349 -2940
  60. package/dist/src-Jsq9LCeH.mjs.map +1 -0
  61. package/dist/{standalone-YsZFFQBM.mjs → standalone-DKCdrf1d.mjs} +62 -24
  62. package/dist/standalone-DKCdrf1d.mjs.map +1 -0
  63. package/dist/standalone.d.mts.map +1 -1
  64. package/dist/standalone.mjs +298 -27
  65. package/dist/standalone.mjs.map +1 -1
  66. package/dist/status-IRz7ta8-.mjs +2344 -0
  67. package/dist/status-IRz7ta8-.mjs.map +1 -0
  68. package/dist/viewer/favicon.svg +35 -1
  69. package/dist/viewer/index.html +7428 -2608
  70. package/docker-compose.yml +42 -16
  71. package/iii-config.docker.yaml +10 -10
  72. package/iii-config.yaml +3 -1
  73. package/integrations/pi/README.md +77 -0
  74. package/integrations/pi/index.ts +402 -0
  75. package/integrations/pi/package.json +32 -0
  76. package/integrations/pi/security.ts +87 -0
  77. package/package.json +15 -7
  78. package/plugin/.claude-plugin/plugin.json +2 -2
  79. package/plugin/.codex-plugin/plugin.json +26 -4
  80. package/plugin/.devin-plugin/plugin.json +32 -0
  81. package/plugin/.mcp.codex.json +12 -0
  82. package/plugin/.mcp.copilot.json +2 -7
  83. package/plugin/.mcp.json +2 -7
  84. package/plugin/assets/icon.svg +35 -0
  85. package/plugin/cursor/hooks.json +41 -0
  86. package/plugin/cursor/mcp.json +12 -0
  87. package/plugin/hooks/hooks.antigravity.json +43 -0
  88. package/plugin/hooks/hooks.copilot.json +11 -11
  89. package/plugin/hooks/hooks.devin.json +67 -0
  90. package/plugin/hooks/hooks.droid.json +55 -0
  91. package/plugin/opencode/README.md +267 -31
  92. package/plugin/opencode/agentmemory-capture.ts +644 -15
  93. package/plugin/opencode/plugin.json +1 -1
  94. package/plugin/plugin.json +2 -2
  95. package/plugin/scripts/_capture.d.mts +22 -0
  96. package/plugin/scripts/_capture.d.mts.map +1 -0
  97. package/plugin/scripts/_capture.mjs +908 -0
  98. package/plugin/scripts/_capture.mjs.map +1 -0
  99. package/plugin/scripts/antigravity-bridge.d.mts +8 -0
  100. package/plugin/scripts/antigravity-bridge.d.mts.map +1 -0
  101. package/plugin/scripts/antigravity-bridge.mjs +123 -0
  102. package/plugin/scripts/antigravity-bridge.mjs.map +1 -0
  103. package/plugin/scripts/notification.mjs +34 -27
  104. package/plugin/scripts/notification.mjs.map +1 -1
  105. package/plugin/scripts/plugin-bridge.mjs +378 -0
  106. package/plugin/scripts/post-commit.mjs +84 -2
  107. package/plugin/scripts/post-commit.mjs.map +1 -1
  108. package/plugin/scripts/post-tool-failure.mjs +71 -26
  109. package/plugin/scripts/post-tool-failure.mjs.map +1 -1
  110. package/plugin/scripts/post-tool-use.mjs +99 -36
  111. package/plugin/scripts/post-tool-use.mjs.map +1 -1
  112. package/plugin/scripts/pre-compact.mjs +95 -5
  113. package/plugin/scripts/pre-compact.mjs.map +1 -1
  114. package/plugin/scripts/pre-tool-use.mjs +72 -2
  115. package/plugin/scripts/pre-tool-use.mjs.map +1 -1
  116. package/plugin/scripts/prompt-submit.mjs +25 -22
  117. package/plugin/scripts/prompt-submit.mjs.map +1 -1
  118. package/plugin/scripts/session-end.mjs +93 -24
  119. package/plugin/scripts/session-end.mjs.map +1 -1
  120. package/plugin/scripts/session-start.mjs +101 -6
  121. package/plugin/scripts/session-start.mjs.map +1 -1
  122. package/plugin/scripts/stop.mjs +72 -8
  123. package/plugin/scripts/stop.mjs.map +1 -1
  124. package/plugin/scripts/subagent-start.mjs +28 -26
  125. package/plugin/scripts/subagent-start.mjs.map +1 -1
  126. package/plugin/scripts/subagent-stop.mjs +29 -26
  127. package/plugin/scripts/subagent-stop.mjs.map +1 -1
  128. package/plugin/scripts/task-completed.mjs +31 -28
  129. package/plugin/scripts/task-completed.mjs.map +1 -1
  130. package/plugin/skills/_shared/TROUBLESHOOTING.md +22 -6
  131. package/plugin/skills/agentmemory-agents/REFERENCE.md +8 -5
  132. package/plugin/skills/agentmemory-config/REFERENCE.md +52 -1
  133. package/plugin/skills/agentmemory-config/SKILL.md +44 -7
  134. package/plugin/skills/agentmemory-mcp-tools/REFERENCE.md +4 -3
  135. package/plugin/skills/agentmemory-rest-api/REFERENCE.md +23 -2
  136. package/plugin/skills/forget/SKILL.md +4 -1
  137. package/plugin/skills/handoff/SKILL.md +4 -2
  138. package/plugin/skills/lesson/SKILL.md +64 -0
  139. package/plugin/skills/memory-discipline/SKILL.md +65 -0
  140. package/plugin/skills/recall/SKILL.md +4 -1
  141. package/plugin/skills/remember/SKILL.md +6 -1
  142. package/dist/config-ChC9EtgV.mjs +0 -259
  143. package/dist/config-ChC9EtgV.mjs.map +0 -1
  144. package/dist/connect-JnsJXc68.mjs.map +0 -1
  145. package/dist/image-refs-BKLmp_sD.mjs.map +0 -1
  146. package/dist/image-refs-C2YU7BIm.mjs.map +0 -1
  147. package/dist/keyed-mutex-DTHtcZrt.mjs.map +0 -1
  148. package/dist/logger-BEB7pCI9.mjs +0 -43
  149. package/dist/logger-BEB7pCI9.mjs.map +0 -1
  150. package/dist/schema-CNiCWzV9.mjs +0 -78
  151. package/dist/schema-CNiCWzV9.mjs.map +0 -1
  152. package/dist/src-CzgoepGU.mjs.map +0 -1
  153. package/dist/standalone-YsZFFQBM.mjs.map +0 -1
  154. package/dist/tools-registry-D3NEoe5m.mjs +0 -1115
  155. package/dist/tools-registry-D3NEoe5m.mjs.map +0 -1
  156. package/plugin/scripts/diagnostics.d.mts +0 -17
  157. package/plugin/scripts/diagnostics.d.mts.map +0 -1
  158. package/plugin/scripts/diagnostics.mjs.map +0 -1
@@ -1,12 +1,67 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
+ import { execFileSync } from "node:child_process";
3
+ import { readFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { basename, join } from "node:path";
2
6
 
3
7
  const API = process.env.AGENTMEMORY_URL || "http://localhost:3111";
4
- const FILE_TOOLS = new Set(["Read", "Write", "Edit", "Glob", "Grep"]);
8
+ // OpenCode reports tool names in lowercase ("read", "edit", ...); matching is
9
+ // case-insensitive at the call site so a future casing change cannot silently
10
+ // kill file enrichment again.
11
+ const FILE_TOOLS = new Set(["read", "write", "edit", "glob", "grep"]);
5
12
  const FILE_KEYS = ["filePath", "file_path", "path", "file", "pattern"];
6
13
  const MAX_STASHED_FILES = 20;
7
14
 
8
15
  const DEBUG = process.env.OPENCODE_AGENTMEMORY_DEBUG === "1";
9
- const SECRET = process.env.AGENTMEMORY_SECRET || "";
16
+ function usableSecret(value: unknown): string {
17
+ const trimmed = typeof value === "string" ? value.trim() : "";
18
+ if (!trimmed || (trimmed.startsWith("${") && trimmed.endsWith("}"))) return "";
19
+ return trimmed;
20
+ }
21
+
22
+ function readAgentmemoryFile(name: string): string {
23
+ try {
24
+ return readFileSync(join(homedir(), ".agentmemory", name), "utf-8");
25
+ } catch {
26
+ return "";
27
+ }
28
+ }
29
+
30
+ function envFileSecret(): string {
31
+ let found = "";
32
+ for (const line of readAgentmemoryFile(".env").split("\n")) {
33
+ const trimmed = line.trim();
34
+ if (trimmed.startsWith("#")) continue;
35
+ const eq = trimmed.indexOf("=");
36
+ if (eq === -1) continue;
37
+ if (trimmed.slice(0, eq).replace(/^export\s+/, "").trim() !== "AGENTMEMORY_SECRET") continue;
38
+ let value = trimmed.slice(eq + 1).trim();
39
+ const quote = value[0];
40
+ const close = quote === '"' || quote === "'" ? value.indexOf(quote, 1) : -1;
41
+ if (close > 0) value = value.slice(1, close);
42
+ else if (value.includes(" #")) value = value.slice(0, value.indexOf(" #"));
43
+ found = usableSecret(value);
44
+ }
45
+ return found;
46
+ }
47
+
48
+ function isLoopbackUrl(url: string): boolean {
49
+ try {
50
+ const host = new URL(url).hostname.replace(/^\[|\]$/g, "").toLowerCase();
51
+ return host === "localhost" || host === "::1" || /^127(?:\.\d{1,3}){3}$/.test(host);
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function resolveSecret(url: string, explicit: string | undefined): string {
58
+ const configured = usableSecret(explicit);
59
+ if (configured) return configured;
60
+ if (!isLoopbackUrl(url)) return "";
61
+ return envFileSecret() || usableSecret(readAgentmemoryFile("secret"));
62
+ }
63
+
64
+ const SECRET = resolveSecret(API, process.env.AGENTMEMORY_SECRET);
10
65
 
11
66
  function authHeaders(): Record<string, string> {
12
67
  const headers: Record<string, string> = { "Content-Type": "application/json" };
@@ -47,11 +102,12 @@ async function observe(
47
102
  hookType: string,
48
103
  data: Record<string, unknown>,
49
104
  ): Promise<void> {
105
+ const proj = projectFor(sessionId);
50
106
  await post("/observe", {
51
107
  hookType,
52
108
  sessionId,
53
- project: projectPath,
54
- cwd: projectPath,
109
+ project: proj.name,
110
+ cwd: proj.cwd,
55
111
  timestamp: new Date().toISOString(),
56
112
  data,
57
113
  });
@@ -59,7 +115,46 @@ async function observe(
59
115
 
60
116
  let activeSessionId: string | null = null;
61
117
  let pendingConfig: Record<string, unknown> | null = null;
62
- let projectPath: string | null = null;
118
+ // Default scope resolved at plugin init (same resolution order as the hooks'
119
+ // resolveProject: env override, git toplevel basename, cwd basename). In a
120
+ // long-lived OpenCode process serving multiple directories these defaults are
121
+ // only a fallback — attribution is per-session via sessionProjects, resolved
122
+ // from each session's own directory at session.created. Module-level-only
123
+ // state recorded home-directory sessions under whatever repo loaded first.
124
+ let defaultProjectName: string | null = null;
125
+ let defaultProjectCwd: string | null = null;
126
+ const sessionProjects = new Map<string, { name: string; cwd: string }>();
127
+
128
+ function projectFor(sessionId: string): { name: string | null; cwd: string | null } {
129
+ const p = sessionProjects.get(sessionId);
130
+ return p ?? { name: defaultProjectName, cwd: defaultProjectCwd };
131
+ }
132
+
133
+ const projectNameCache = new Map<string, string>();
134
+
135
+ function resolveProjectName(dir: string): string {
136
+ const explicit = process.env.AGENTMEMORY_PROJECT_NAME?.trim();
137
+ if (explicit) return explicit;
138
+ const cached = projectNameCache.get(dir);
139
+ if (cached !== undefined) return cached;
140
+ try {
141
+ const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
142
+ cwd: dir,
143
+ stdio: ["ignore", "pipe", "ignore"],
144
+ encoding: "utf8",
145
+ }).trim();
146
+ if (top) {
147
+ const name = basename(top);
148
+ projectNameCache.set(dir, name);
149
+ return name;
150
+ }
151
+ } catch {
152
+ // not a git repo, fall through
153
+ }
154
+ const fallback = basename(dir) || dir;
155
+ projectNameCache.set(dir, fallback);
156
+ return fallback;
157
+ }
63
158
  const stashedFiles = new Map<string, Set<string>>();
64
159
  const seenSubtaskIds = new Map<string, Set<string>>();
65
160
  const seenToolCallIds = new Map<string, Set<string>>();
@@ -93,6 +188,7 @@ function pruneSessionMaps(sid: string): void {
93
188
  stashedFiles.delete(sid);
94
189
  seenSubtaskIds.delete(sid);
95
190
  seenToolCallIds.delete(sid);
191
+ sessionProjects.delete(sid);
96
192
  }
97
193
 
98
194
  function safeSlice(v: unknown, max: number): string {
@@ -167,8 +263,9 @@ function extractErrorMessage(err: unknown): string {
167
263
  return String(err ?? "");
168
264
  }
169
265
 
170
- export const AgentmemoryCapturePlugin: Plugin = async (ctx) => {
171
- projectPath = ctx.worktree || ctx.project?.id || process.cwd();
266
+ const v1Hooks: Plugin = async (ctx) => {
267
+ defaultProjectCwd = ctx.worktree || ctx.project?.id || process.cwd();
268
+ defaultProjectName = resolveProjectName(defaultProjectCwd);
172
269
 
173
270
  return {
174
271
  event: async ({ event }) => {
@@ -188,13 +285,28 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => {
188
285
  // and another `session.created` event during the await could
189
286
  // rebind it, causing context to be cached against the wrong key.
190
287
  const sessionId = activeSessionId;
288
+ // Attribute this session to its own directory when the event
289
+ // carries one; a multi-directory OpenCode process otherwise
290
+ // records every session under whichever repo loaded the plugin.
291
+ const sessionDir =
292
+ typeof info?.directory === "string" && info.directory
293
+ ? info.directory
294
+ : defaultProjectCwd;
295
+ let proj: { name: string | null; cwd: string | null };
296
+ if (sessionDir) {
297
+ const entry = { cwd: sessionDir, name: resolveProjectName(sessionDir) };
298
+ sessionProjects.set(sessionId, entry);
299
+ proj = entry;
300
+ } else {
301
+ proj = projectFor(sessionId);
302
+ }
191
303
  const startResult = await postJson("/session/start", {
192
304
  sessionId,
193
305
  title: info?.title ?? null,
194
306
  parentID: info?.parentID ?? null,
195
307
  version: info?.version ?? null,
196
- project: projectPath,
197
- cwd: projectPath,
308
+ project: proj.name,
309
+ cwd: proj.cwd,
198
310
  });
199
311
  // cache the context returned at session/start so the
200
312
  // chat.system.transform hook injects it without a second fetch.
@@ -272,10 +384,8 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => {
272
384
  post("/crystals/auto", { olderThanDays: 7 }, 30000);
273
385
  post("/consolidate-pipeline", { tier: "all", force: true }, 30000);
274
386
  if (sid === activeSessionId) activeSessionId = null;
275
- stashedFiles.delete(sid);
387
+ pruneSessionMaps(sid);
276
388
  startContextCache.delete(sid);
277
- seenSubtaskIds.delete(sid);
278
- seenToolCallIds.delete(sid);
279
389
  contextInjectedSessions.delete(sid);
280
390
  }
281
391
 
@@ -581,7 +691,7 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => {
581
691
 
582
692
  // ── tool.execute.before ──
583
693
  "tool.execute.before": async (input, output) => {
584
- if (!FILE_TOOLS.has(input.tool)) return;
694
+ if (!FILE_TOOLS.has(String(input.tool ?? "").toLowerCase())) return;
585
695
  const sid = input.sessionID || activeSessionId;
586
696
  if (!sid) return;
587
697
  const args = output.args as Record<string, unknown> | undefined;
@@ -612,7 +722,7 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => {
612
722
  if (typeof ctx !== "string" || ctx.length === 0) {
613
723
  const result = await postJson("/context", {
614
724
  sessionId: sid,
615
- project: projectPath,
725
+ project: projectFor(sid).name,
616
726
  });
617
727
  ctx = (result as any)?.context;
618
728
  } else {
@@ -650,7 +760,7 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => {
650
760
 
651
761
  const result = await postJson("/context", {
652
762
  sessionId: sid,
653
- project: projectPath,
763
+ project: projectFor(sid).name,
654
764
  });
655
765
  const ctx = (result as any)?.context;
656
766
  if (typeof ctx === "string" && ctx.length > 0) {
@@ -685,3 +795,522 @@ export const AgentmemoryCapturePlugin: Plugin = async (ctx) => {
685
795
  },
686
796
  };
687
797
  };
798
+
799
+ async function v2Setup(ctx: any) {
800
+ if (typeof ctx?.tool?.hook !== "function" || typeof ctx?.session?.hook !== "function" || typeof ctx?.event?.subscribe !== "function") {
801
+ return;
802
+ }
803
+ const location = ctx.location;
804
+ defaultProjectCwd = location?.directory ?? location?.project?.directory ?? process.cwd();
805
+ defaultProjectName = resolveProjectName(defaultProjectCwd);
806
+
807
+ async function observeV2(sessionId: string, hookType: string, data: Record<string, unknown>): Promise<void> {
808
+ await observe(sessionId, hookType, data);
809
+ }
810
+
811
+ const shellSessions = new Map<string, string>();
812
+ const turnContextCache = new Map<string, string>();
813
+
814
+ function stashAdd(sid: string, paths: Iterable<string>): void {
815
+ const stash = stashFor(sid);
816
+ for (const p of paths) stash.add(p);
817
+ if (stash.size > MAX_STASHED_FILES) {
818
+ const keep = [...stash].slice(-MAX_STASHED_FILES);
819
+ stash.clear();
820
+ for (const k of keep) stash.add(k);
821
+ }
822
+ }
823
+
824
+ const toolNames = new Map<string, string>();
825
+ const toolCallInputs = new Map<string, Record<string, unknown>>();
826
+ const reportedToolCalls = new Set<string>();
827
+ const registeredSessions = new Set<string>();
828
+
829
+ function namesFrom(result: unknown): string[] {
830
+ const arr = Array.isArray(result) ? result : (result as any)?.data;
831
+ if (!Array.isArray(arr)) return [];
832
+ return arr.map((x: any) => (typeof x === "string" ? x : x?.id ?? x?.name)).filter(Boolean);
833
+ }
834
+
835
+ let lastConfigSnapshot: string | null = null;
836
+
837
+ async function snapshotConfig(): Promise<void> {
838
+ const [agents, providers, mcp, modelDefault] = await Promise.all([
839
+ ctx.agent?.list?.().catch(() => null),
840
+ ctx.provider?.list?.().catch(() => null),
841
+ ctx.mcp?.list?.().catch(() => null),
842
+ Promise.resolve(ctx.model?.default?.()).catch(() => null),
843
+ ]);
844
+ const model = Array.isArray(modelDefault) ? modelDefault[0] : (modelDefault as any)?.data;
845
+ const payload = {
846
+ agents: namesFrom(agents),
847
+ providers: namesFrom(providers),
848
+ mcp_servers: namesFrom(mcp),
849
+ model: model ? `${model.providerID ?? ""}/${model.id ?? model.modelID ?? ""}` : null,
850
+ model_limits: model?.limit ?? null,
851
+ location: defaultProjectCwd,
852
+ };
853
+ const snapshot = JSON.stringify(payload);
854
+ if (snapshot === lastConfigSnapshot) return;
855
+ lastConfigSnapshot = snapshot;
856
+ if (activeSessionId) await observeV2(activeSessionId, "config_loaded", payload);
857
+ else pendingConfig = payload;
858
+ }
859
+
860
+ void snapshotConfig().catch((e) => {
861
+ if (DEBUG) console.error("[agentmemory] config snapshot failed:", (e as Error).message);
862
+ });
863
+
864
+ await ctx.tool.hook("execute.before", (event: any) => {
865
+ if (!FILE_TOOLS.has(String(event?.tool ?? "").toLowerCase())) return;
866
+ const sid = event?.sessionID || activeSessionId;
867
+ if (!sid) return;
868
+ const args = event?.input as Record<string, unknown> | undefined;
869
+ if (!args) return;
870
+ stashAdd(sid, extractFilePaths(args));
871
+ });
872
+
873
+ await ctx.tool.hook("execute.after", async (event: any) => {
874
+ const sid = event?.sessionID || activeSessionId;
875
+ if (!sid) return;
876
+ const tool = String(event?.tool ?? "");
877
+ const callId = (event?.id as string) || (event?.messageID as string) || null;
878
+ const status = String(event?.status ?? "");
879
+ const result = event?.result ?? {};
880
+ const metadata = (result?.metadata ?? {}) as Record<string, unknown>;
881
+ const output = result?.output as Record<string, unknown> | undefined;
882
+ const raw = output?.output ?? result?.content;
883
+ const text = Array.isArray(raw)
884
+ ? raw.map((p: any) => (typeof p === "string" ? p : (p?.text ?? ""))).join("\n")
885
+ : typeof raw === "string"
886
+ ? raw
887
+ : "";
888
+ const startMs = typeof metadata?.started === "number" ? metadata.started : null;
889
+ const endMs = typeof metadata?.ended === "number" ? metadata.ended : null;
890
+ const duration = startMs != null && endMs != null ? endMs - startMs : null;
891
+
892
+ if (status === "error") {
893
+ await observeV2(sid, "post_tool_failure", {
894
+ tool_name: tool,
895
+ call_id: callId,
896
+ tool_input: safeSlice(event?.input, 4000),
897
+ tool_output: safeSlice(text || extractErrorMessage(event?.error) || extractErrorMessage(metadata?.error), 8000),
898
+ duration_ms: duration,
899
+ });
900
+ if (callId) reportedToolCalls.add(`${sid}:${callId}`);
901
+ return;
902
+ }
903
+
904
+ await observeV2(sid, "post_tool_use", {
905
+ tool_name: tool,
906
+ call_id: callId,
907
+ tool_input: safeSlice(event?.input, 4000),
908
+ tool_output: safeSlice(text, 8000),
909
+ title: (output?.title as string) ?? null,
910
+ metadata: metadata,
911
+ duration_ms: duration,
912
+ attachments: Array.isArray(output?.attachments)
913
+ ? (output?.attachments as Array<Record<string, unknown>>).map((a) => a.filename || a.url)
914
+ : [],
915
+ });
916
+ if (callId) reportedToolCalls.add(`${sid}:${callId}`);
917
+ });
918
+
919
+ await ctx.session.hook("prompt", async (event: any) => {
920
+ const sid = event?.sessionID || activeSessionId;
921
+ if (!sid) return;
922
+ const files = (event?.prompt?.files ?? [])
923
+ .map((f: any) => (typeof f === "string" ? f : f?.uri ?? f?.filename ?? f?.url))
924
+ .filter(Boolean) as string[];
925
+ stashAdd(sid, files);
926
+ turnContextCache.delete(sid);
927
+ await observeV2(sid, "prompt_submit", {
928
+ prompt: (event?.prompt?.text ?? "").slice(0, 8000),
929
+ files: files.slice(0, 20),
930
+ agents: event?.prompt?.agents ?? [],
931
+ skills: event?.prompt?.skills ?? [],
932
+ delivery: event?.delivery ?? null,
933
+ });
934
+ });
935
+
936
+ await ctx.session.hook("context", async (event: any) => {
937
+ const sid = event?.sessionID || activeSessionId;
938
+ if (!sid) return;
939
+ if (!Array.isArray(event.system)) return;
940
+
941
+ if (!contextInjectedSessions.has(sid)) {
942
+ event.system.push({ type: "text", text: AGENTMEMORY_INSTRUCTIONS });
943
+ contextInjectedSessions.add(sid);
944
+ }
945
+
946
+ let ctxText = startContextCache.get(sid) ?? turnContextCache.get(sid);
947
+ if (typeof ctxText !== "string" || ctxText.length === 0) {
948
+ const result = await postJson("/context", { sessionId: sid, project: projectFor(sid).name });
949
+ ctxText = (result as any)?.context;
950
+ }
951
+ if (typeof ctxText === "string" && ctxText.length > 0) {
952
+ turnContextCache.set(sid, ctxText);
953
+ startContextCache.delete(sid);
954
+ event.system.push({ type: "text", text: ctxText });
955
+ }
956
+
957
+ const stash = stashFor(sid);
958
+ if (stash.size === 0) return;
959
+ const files = [...stash].slice(0, 10);
960
+ const enrichResult = await postJson("/enrich", { sessionId: sid, files, toolName: "enrich_inject" });
961
+ const enrichCtx = (enrichResult as any)?.context;
962
+ if (typeof enrichCtx === "string" && enrichCtx.length > 0) {
963
+ event.system.push({ type: "text", text: enrichCtx });
964
+ for (const f of files) stash.delete(f);
965
+ }
966
+ });
967
+
968
+ await ctx.session.hook("compaction", async (event: any) => {
969
+ const sid = event?.sessionID || activeSessionId;
970
+ if (!sid) return;
971
+ if (!Array.isArray(event?.system)) return;
972
+ let ctxText = turnContextCache.get(sid);
973
+ if (typeof ctxText !== "string" || ctxText.length === 0) {
974
+ const result = await postJson("/context", { sessionId: sid, project: projectFor(sid).name });
975
+ ctxText = (result as any)?.context;
976
+ if (typeof ctxText === "string" && ctxText.length > 0) turnContextCache.set(sid, ctxText);
977
+ }
978
+ if (typeof ctxText === "string" && ctxText.length > 0) {
979
+ event.system.push({ type: "text", text: ctxText });
980
+ }
981
+ });
982
+
983
+ const controller = new AbortController();
984
+
985
+ const handleEvent = async (event: any): Promise<void> => {
986
+ const type = String(event?.type ?? "");
987
+ const data: any = event?.data ?? {};
988
+ const eventSid = typeof data.sessionID === "string" && data.sessionID ? data.sessionID : null;
989
+ const sid0 = eventSid || activeSessionId;
990
+
991
+ if (eventSid && !registeredSessions.has(eventSid)) {
992
+ if (!activeSessionId) activeSessionId = eventSid;
993
+ stashedFiles.set(sid0, new Set());
994
+ seenSubtaskIds.delete(sid0);
995
+ seenToolCallIds.delete(sid0);
996
+ contextInjectedSessions.delete(sid0);
997
+ const dir = (data.location?.directory as string) || (event?.location?.directory as string) || null;
998
+ if (dir) {
999
+ sessionProjects.set(sid0, { cwd: dir, name: resolveProjectName(dir) });
1000
+ }
1001
+ const proj = projectFor(sid0);
1002
+ const startResult = await postJson("/session/start", {
1003
+ sessionId: sid0,
1004
+ title: (data.title as string) ?? null,
1005
+ parentID: (data.parentID as string) ?? null,
1006
+ project: proj.name,
1007
+ cwd: proj.cwd,
1008
+ });
1009
+
1010
+ if (startResult === null) {
1011
+ stashedFiles.delete(sid0);
1012
+ contextInjectedSessions.delete(sid0);
1013
+ if (activeSessionId === eventSid) activeSessionId = null;
1014
+ if (DEBUG) {
1015
+ console.error("[agentmemory] /session/start failed, will retry on the next event for", eventSid);
1016
+ }
1017
+ return;
1018
+ }
1019
+ registeredSessions.add(eventSid);
1020
+
1021
+ const startCtx = (startResult as any)?.context;
1022
+ if (typeof startCtx === "string" && startCtx.length > 0) startContextCache.set(sid0, startCtx);
1023
+ await observeV2(sid0, "session_started", {});
1024
+ if (pendingConfig) {
1025
+ await observeV2(sid0, "config_loaded", pendingConfig);
1026
+ pendingConfig = null;
1027
+ }
1028
+ }
1029
+
1030
+ switch (type) {
1031
+
1032
+ case "session.execution.failed": {
1033
+ if (!sid0) return;
1034
+ await observeV2(sid0, "post_tool_failure", {
1035
+ tool_name: "session.execution",
1036
+ tool_input: "",
1037
+ tool_output: safeSlice(extractErrorMessage(data.error ?? data), 8000),
1038
+ });
1039
+ return;
1040
+ }
1041
+
1042
+ case "session.compaction.started": {
1043
+ if (!sid0) return;
1044
+ await observeV2(sid0, "compaction_event", {
1045
+ state: "started",
1046
+ reason: (data.reason as string) ?? null,
1047
+ inputID: (data.inputID as string) ?? null,
1048
+ recent: safeSlice(data.recent, 8000),
1049
+ });
1050
+ return;
1051
+ }
1052
+
1053
+ case "session.compaction.failed": {
1054
+ if (!sid0) return;
1055
+ await observeV2(sid0, "compaction_event", {
1056
+ state: "failed",
1057
+ reason: (data.reason as string) ?? null,
1058
+ inputID: (data.inputID as string) ?? null,
1059
+ tool_output: safeSlice(extractErrorMessage(data.error ?? data.reason), 8000),
1060
+ });
1061
+ return;
1062
+ }
1063
+
1064
+ case "session.step.started": {
1065
+ if (!sid0) return;
1066
+ await observeV2(sid0, "step_start", {
1067
+ messageID: (data.assistantMessageID as string) ?? null,
1068
+ agent: (data.agent as string) ?? null,
1069
+ model: data.model ? `${data.model.providerID ?? ""}/${data.model.id ?? ""}` : null,
1070
+ });
1071
+ return;
1072
+ }
1073
+
1074
+ case "session.step.ended": {
1075
+ if (!sid0) return;
1076
+ const tokens = (data.tokens ?? {}) as Record<string, any>;
1077
+ await observeV2(sid0, "step_finish", {
1078
+ messageID: (data.assistantMessageID as string) ?? null,
1079
+ reason: (data.rawFinish as string) ?? (data.finish as string) ?? null,
1080
+ cost: data.cost ?? 0,
1081
+ input_tokens: tokens.input ?? 0,
1082
+ output_tokens: tokens.output ?? 0,
1083
+ reasoning_tokens: tokens.reasoning ?? 0,
1084
+ cache_read: tokens.cache?.read ?? 0,
1085
+ cache_write: tokens.cache?.write ?? 0,
1086
+ });
1087
+ return;
1088
+ }
1089
+
1090
+ case "session.agent.selected": {
1091
+ if (!sid0) return;
1092
+ await observeV2(sid0, "agent_selected", {
1093
+ name: (data.agent as string) ?? null,
1094
+ previous: (data.previous as string) ?? null,
1095
+ });
1096
+ return;
1097
+ }
1098
+
1099
+ case "session.reasoning.ended": {
1100
+ if (!sid0) return;
1101
+ await observeV2(sid0, "reasoning", {
1102
+ messageID: (data.assistantMessageID as string) ?? null,
1103
+ text: safeSlice(data.text, 4000),
1104
+ });
1105
+ return;
1106
+ }
1107
+
1108
+ case "session.instructions.updated": {
1109
+ if (!sid0) return;
1110
+ const sources = data.delta && typeof data.delta === "object" ? Object.keys(data.delta) : [];
1111
+ if (sources.length === 0) return;
1112
+ await observeV2(sid0, "notification", {
1113
+ notification_type: "instructions_updated",
1114
+ sources: sources.slice(0, 50),
1115
+ });
1116
+ return;
1117
+ }
1118
+
1119
+ case "session.text.ended": {
1120
+ if (!sid0) return;
1121
+ const text = typeof data.text === "string" ? data.text : "";
1122
+ if (!text) return;
1123
+ await observeV2(sid0, "assistant_message", {
1124
+ messageID: (data.assistantMessageID as string) ?? null,
1125
+ text: text.slice(0, 8000),
1126
+ });
1127
+ return;
1128
+ }
1129
+
1130
+ case "session.execution.succeeded": {
1131
+ if (!sid0) return;
1132
+ await post("/summarize", { sessionId: sid0 });
1133
+ return;
1134
+ }
1135
+
1136
+ case "shell.created": {
1137
+ const info = (data.info ?? {}) as Record<string, any>;
1138
+ const shellSid = (info.metadata?.sessionID as string) || sid0;
1139
+ if (!shellSid) return;
1140
+ if (info.id) shellSessions.set(String(info.id), shellSid);
1141
+ return;
1142
+ }
1143
+
1144
+ case "shell.exited": {
1145
+ const shellId = String(data.id ?? "");
1146
+ const shellSid = shellSessions.get(shellId) || activeSessionId;
1147
+ if (!shellSid) return;
1148
+ const exit = Number(data.exit ?? 0);
1149
+ if (exit !== 0) {
1150
+ await observeV2(shellSid, "post_tool_failure", {
1151
+ tool_name: "shell",
1152
+ call_id: shellId,
1153
+ tool_input: null,
1154
+ tool_output: safeSlice(data.status, 4000),
1155
+ duration_ms: null,
1156
+ });
1157
+ }
1158
+ return;
1159
+ }
1160
+
1161
+ case "shell.deleted": {
1162
+ const shellId = String(data.id ?? "");
1163
+ shellSessions.delete(shellId);
1164
+ return;
1165
+ }
1166
+
1167
+ case "file.watcher.updated":
1168
+ case "filesystem.changed":
1169
+ case "vcs.branch.updated": {
1170
+ const sid = sid0 || activeSessionId;
1171
+ if (!sid) return;
1172
+ const file = (data.file as string) ?? (data.path as string) ?? null;
1173
+ if (file) stashAdd(sid, [file]);
1174
+ return;
1175
+ }
1176
+
1177
+ case "session.tool.input.started": {
1178
+ if (!sid0) return;
1179
+ if (data.id) toolNames.set(String(data.id), String(data.name ?? ""));
1180
+ return;
1181
+ }
1182
+
1183
+ case "session.tool.input.ended": {
1184
+ if (!sid0) return;
1185
+ if (data.id && typeof data.text === "string" && !toolNames.has(String(data.id))) {
1186
+ try {
1187
+ const parsed = JSON.parse(data.text);
1188
+ if (typeof parsed?.name === "string") toolNames.set(String(data.id), parsed.name);
1189
+ } catch {
1190
+ }
1191
+ }
1192
+ return;
1193
+ }
1194
+
1195
+ case "session.tool.called": {
1196
+ if (!sid0) return;
1197
+ if (data.id) toolCallInputs.set(String(data.id), (data.input ?? {}) as Record<string, unknown>);
1198
+ return;
1199
+ }
1200
+
1201
+ case "session.tool.progress": {
1202
+ if (!sid0) return;
1203
+ const callId = String(data.id ?? "");
1204
+ if (!callId || !FILE_TOOLS.has((toolNames.get(callId) ?? "").toLowerCase())) return;
1205
+ const input = toolCallInputs.get(callId);
1206
+ if (!input) return;
1207
+ stashAdd(sid0, extractFilePaths(input));
1208
+ return;
1209
+ }
1210
+
1211
+ case "session.tool.success": {
1212
+ const callId = String(data.id ?? "");
1213
+ toolNames.delete(callId);
1214
+ toolCallInputs.delete(callId);
1215
+ if (sid0) reportedToolCalls.delete(`${sid0}:${callId}`);
1216
+ return;
1217
+ }
1218
+
1219
+ case "session.tool.failed": {
1220
+ if (!sid0) return;
1221
+ const callId = String(data.id ?? "");
1222
+ if (callId && reportedToolCalls.has(`${sid0}:${callId}`)) return;
1223
+ await observeV2(sid0, "post_tool_failure", {
1224
+ tool_name: toolNames.get(callId) || null,
1225
+ call_id: callId || null,
1226
+ tool_input: safeSlice(toolCallInputs.get(callId), 4000),
1227
+ tool_output: safeSlice(extractErrorMessage(data.error), 8000),
1228
+ duration_ms: null,
1229
+ });
1230
+ if (callId) {
1231
+ toolNames.delete(callId);
1232
+ toolCallInputs.delete(callId);
1233
+ }
1234
+ return;
1235
+ }
1236
+
1237
+ case "permission.asked": {
1238
+ if (!sid0) return;
1239
+ const resources = (data.resources ?? data.patterns ?? []) as unknown;
1240
+ await observeV2(sid0, "notification", {
1241
+ notification_type: "permission_prompt",
1242
+ permission: (data.action as string) ?? (data.permission as string) ?? "unknown",
1243
+ pattern: Array.isArray(resources) ? resources.join(", ") : String(resources ?? ""),
1244
+ tool_call_id: (data.tool?.callID as string) ?? (data.callID as string) ?? null,
1245
+ title: (data.action as string) ?? (data.permission as string) ?? "",
1246
+ metadata: data.metadata ?? {},
1247
+ });
1248
+ return;
1249
+ }
1250
+
1251
+ case "permission.replied": {
1252
+ if (!sid0) return;
1253
+ await observeV2(sid0, "permission_replied", {
1254
+ permission_id: (data.requestID as string) ?? (data.permissionID as string) ?? "",
1255
+ response: (data.reply as string) ?? (data.response as string) ?? "",
1256
+ });
1257
+ return;
1258
+ }
1259
+
1260
+ case "session.deleted": {
1261
+ const sid = (data.sessionID as string) || activeSessionId;
1262
+ if (!sid) return;
1263
+ await post("/session/end", { sessionId: sid });
1264
+ void post("/crystals/auto", { olderThanDays: 7 }, 30000);
1265
+ void post("/consolidate-pipeline", { tier: "all", force: true }, 30000);
1266
+ if (sid === activeSessionId) activeSessionId = null;
1267
+ registeredSessions.delete(sid);
1268
+ pruneSessionMaps(sid);
1269
+ startContextCache.delete(sid);
1270
+ contextInjectedSessions.delete(sid);
1271
+ for (const key of reportedToolCalls) {
1272
+ if (key.startsWith(`${sid}:`)) reportedToolCalls.delete(key);
1273
+ }
1274
+ return;
1275
+ }
1276
+
1277
+ case "config.updated":
1278
+ case "agent.updated":
1279
+ case "provider.updated":
1280
+ case "model.updated":
1281
+ case "mcp.status.changed": {
1282
+ if (DEBUG) console.error("[agentmemory] config changed, re-snapshotting");
1283
+ void snapshotConfig().catch(() => {});
1284
+ return;
1285
+ }
1286
+ }
1287
+ };
1288
+
1289
+ void (async () => {
1290
+ try {
1291
+ for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
1292
+ try {
1293
+ await handleEvent(event);
1294
+ } catch (e) {
1295
+ if (DEBUG) console.error("[agentmemory] event handler failed:", (e as Error).message);
1296
+ }
1297
+ }
1298
+ } catch (e) {
1299
+ if (DEBUG) console.error("[agentmemory] event stream failed:", (e as Error).message);
1300
+ }
1301
+ })();
1302
+
1303
+ return () => {
1304
+ controller.abort();
1305
+ startContextCache.clear();
1306
+ contextInjectedSessions.clear();
1307
+ };
1308
+ }
1309
+
1310
+ export default {
1311
+ id: "agentmemory-capture",
1312
+ setup: v2Setup,
1313
+ server: v1Hooks,
1314
+ };
1315
+
1316
+ export const AgentmemoryCapturePlugin: Plugin = v1Hooks;