@memorax/memorax-code 0.1.8 → 0.1.10

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 (102) hide show
  1. package/README.md +10 -0
  2. package/bin/memorax-code-codebuddy.mjs +4 -0
  3. package/bin/memorax-code-setup.mjs +182 -65
  4. package/bin/memorax-code.mjs +74 -45
  5. package/docs/configuration.md +82 -22
  6. package/docs/troubleshooting.md +69 -5
  7. package/lib/automatic-update.mjs +233 -0
  8. package/lib/memorax-code-adapter-common/src/automatic-update-state.d.mts +24 -0
  9. package/lib/memorax-code-adapter-common/src/automatic-update-state.mjs +73 -0
  10. package/lib/memorax-code-adapter-common/src/clients/codebuddy-command.mjs +82 -0
  11. package/lib/memorax-code-adapter-common/src/config-utils.d.mts +2 -0
  12. package/lib/memorax-code-adapter-common/src/config-utils.mjs +11 -0
  13. package/lib/memorax-code-adapter-common/src/hooks/client-hook-launcher.mjs +3 -0
  14. package/lib/memorax-code-adapter-common/src/hooks/ensure-backend-runner.mjs +45 -10
  15. package/lib/memorax-code-adapter-common/src/repo-memory/repo-memory-job-worker.mjs +88 -3
  16. package/lib/memorax-code-adapter-common/src/windows-cli-invocation.mjs +11 -1
  17. package/lib/memorax-code-backend/dist/app/backend-server.js +2 -2
  18. package/lib/memorax-code-backend/dist/app/memory-observability.js +2 -2
  19. package/lib/memorax-code-backend/dist/clients/codebuddy/jsonl-history.js +208 -0
  20. package/lib/memorax-code-backend/dist/clients/codebuddy/lifecycle.js +44 -0
  21. package/lib/memorax-code-backend/dist/clients/codebuddy/memory-hook-runtime.js +250 -0
  22. package/lib/memorax-code-backend/dist/clients/codebuddy/turn-id.js +16 -0
  23. package/lib/memorax-code-backend/dist/clients/codex/plugin-install.js +129 -7
  24. package/lib/memorax-code-backend/dist/clients/codex/rollout-turn.js +52 -2
  25. package/lib/memorax-code-backend/dist/config/memorax-code.js +14 -1
  26. package/lib/memorax-code-backend/dist/entrypoints/backend-cli.js +43 -6
  27. package/lib/memorax-code-backend/dist/lifecycle/active-clients.js +3 -0
  28. package/lib/memorax-code-backend/dist/lifecycle/automatic-update-scheduler.js +133 -0
  29. package/lib/memorax-code-backend/dist/lifecycle/client-plugin-removal.js +14 -2
  30. package/lib/memorax-code-backend/dist/lifecycle/client-selection.js +6 -4
  31. package/lib/memorax-code-backend/dist/lifecycle/orchestrator.js +48 -9
  32. package/lib/memorax-code-backend/dist/memory/cli.js +4 -2
  33. package/lib/memorax-code-backend/dist/memory/hook-command.js +31 -1
  34. package/lib/memorax-code-backend/dist/memory/reminder-trace-recorder.js +5 -1
  35. package/lib/memorax-code-backend/dist/memory/service.js +12 -0
  36. package/lib/memorax-code-backend/dist/shared/windows-cli-invocation.js +10 -1
  37. package/lib/memorax-code-backend/dist/trace/config.js +12 -0
  38. package/lib/memorax-code-backend/dist/trace/context.js +21 -1
  39. package/lib/memorax-code-backend/dist/trace/store.js +12 -1
  40. package/lib/memorax-code-backend/package.json +1 -1
  41. package/lib/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  42. package/lib/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  43. package/lib/memorax-code-claude-adapter/package.json +1 -1
  44. package/lib/memorax-code-claude-adapter/runtime-hooks/ensure-backend.mjs +2 -0
  45. package/lib/memorax-code-claude-adapter/src/plugin-install.mjs +3 -0
  46. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/.claude-plugin/plugin.json +1 -1
  47. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/hooks/runtime-shell.json +1 -1
  48. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/automatic-update-state.d.mts +24 -0
  49. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/automatic-update-state.mjs +73 -0
  50. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/clients/codebuddy-command.mjs +82 -0
  51. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.d.mts +2 -0
  52. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/config-utils.mjs +11 -0
  53. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/client-hook-launcher.mjs +3 -0
  54. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/hooks/ensure-backend-runner.mjs +45 -10
  55. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/repo-memory/repo-memory-job-worker.mjs +88 -3
  56. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/memorax-code-adapter-common/src/windows-cli-invocation.mjs +11 -1
  57. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/package.json +1 -1
  58. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/runtime-hooks/ensure-backend.mjs +2 -0
  59. package/lib/memorax-code-claude-marketplace/plugins/memorax-code-claude-adapter/src/plugin-install.mjs +3 -0
  60. package/lib/memorax-code-codebuddy-adapter/.codebuddy-plugin/plugin.json +7 -0
  61. package/lib/memorax-code-codebuddy-adapter/hooks/common-runtime.mjs +13 -0
  62. package/lib/memorax-code-codebuddy-adapter/hooks/hooks.json +38 -0
  63. package/lib/memorax-code-codebuddy-adapter/hooks/repo-memory-job.mjs +40 -0
  64. package/lib/memorax-code-codebuddy-adapter/hooks/runtime-hook.mjs +279 -0
  65. package/lib/memorax-code-codebuddy-adapter/package.json +9 -0
  66. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/SKILL.md +85 -0
  67. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/agents/claude.yaml +10 -0
  68. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/agents/openai.yaml +7 -0
  69. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/defaults.json +12 -0
  70. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/memorax-add.md +88 -0
  71. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/memorax-search.md +93 -0
  72. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/personal-read.md +46 -0
  73. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/personal-write.md +120 -0
  74. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-build.md +319 -0
  75. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-read.md +103 -0
  76. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-templates.md +390 -0
  77. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/references/repo-update.md +127 -0
  78. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/collect_all.py +579 -0
  79. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/detect_updates.py +919 -0
  80. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/git_commit_facets.py +222 -0
  81. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/github_resource_facets.py +512 -0
  82. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/gitlab_resource_facets.py +517 -0
  83. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/prepare_repo_memory.py +411 -0
  84. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/user_profile_memory.py +528 -0
  85. package/lib/memorax-code-codebuddy-adapter/skills/memorax-code/scripts/validate_memory.py +248 -0
  86. package/lib/memorax-code-codebuddy-adapter/src/cli.mjs +58 -0
  87. package/lib/memorax-code-codebuddy-adapter/src/config.mjs +277 -0
  88. package/lib/memorax-code-codebuddy-adapter/src/hook-manifest.mjs +47 -0
  89. package/lib/memorax-code-codebuddy-adapter/src/runtime-observation.mjs +65 -0
  90. package/lib/memorax-code-codex-adapter/.codex-plugin/plugin.json +1 -1
  91. package/lib/memorax-code-codex-adapter/hooks/runtime-shell.json +1 -1
  92. package/lib/memorax-code-codex-adapter/package.json +1 -1
  93. package/lib/memorax-code-codex-adapter/runtime-hooks/ensure-backend.mjs +4 -1
  94. package/lib/memorax-code-dsh-adapter/package.json +2 -1
  95. package/lib/memorax-code-dsh-adapter/src/profile-lifecycle.mjs +1 -0
  96. package/lib/memorax-code-opencode-adapter/package.json +1 -1
  97. package/lib/npm-invocation.mjs +51 -8
  98. package/lib/resolve-codebuddy-command.mjs +112 -0
  99. package/lib/run-entrypoint.mjs +25 -1
  100. package/lib/windows-cli-invocation.mjs +11 -1
  101. package/lib/windows-user-path.mjs +218 -0
  102. package/package.json +5 -3
@@ -0,0 +1,250 @@
1
+ import { readCodeBuddyInterruptedTranscriptTurn, readCodeBuddyTranscriptTurn, } from "./jsonl-history.js";
2
+ import { retrieveAutomaticMemoryContext } from "../../memory/automatic-retrieval.js";
3
+ import { createAutomaticMemoryWritebackRuntime } from "../../memory/automatic-writeback.js";
4
+ import { claimQuotaNotice, createPendingQuotaNoticeRuntime, } from "../../memory/quota-notice.js";
5
+ import { createMemoryTurnCoordinator } from "../../memory/turn-coordinator.js";
6
+ import { createRepositoryMemorySessionRuntime, resolvedRepoMemoryWorktree } from "../../memory/repository-session.js";
7
+ import { traceContextFromCodeBuddyHookBody } from "../../trace/context.js";
8
+ import { markCurrentCodeBuddyTurnOutcome, readOpenTraceTurn, recordCodeBuddyTraceEvent, traceTurnEventId, writeCurrentCodeBuddyTurn, } from "../../trace/store.js";
9
+ export function createCodeBuddyMemoryHookRuntime(options = {}) {
10
+ const now = options.now ?? (() => Date.now());
11
+ const pendingQuotaNotice = options.pendingQuotaNotice ?? createPendingQuotaNoticeRuntime({
12
+ claimQuotaNotice: options.claimQuotaNotice,
13
+ diagnosticLogger: options.diagnosticLogger,
14
+ env: options.env,
15
+ });
16
+ const ownsPendingQuotaNotice = options.pendingQuotaNotice === undefined;
17
+ const automatic = options.turnCoordinator ? undefined : options.automaticWriteback ? { enqueue: options.automaticWriteback, discardForScopeUpgrade: () => 0, drain: async () => undefined, close: () => undefined } : createAutomaticMemoryWritebackRuntime({ diagnosticLogger: options.diagnosticLogger, queueQuotaNotice: pendingQuotaNotice.queue });
18
+ const coordinator = options.turnCoordinator ?? createMemoryTurnCoordinator({ automaticWriteback: automatic.enqueue, now, ttlMs: options.ttlMs, maxEntries: options.maxEntries, cleanupIntervalMs: options.cleanupIntervalMs });
19
+ const repository = options.repositoryMemorySession ?? createRepositoryMemorySessionRuntime({ onScopeUpgrade: automatic?.discardForScopeUpgrade });
20
+ return {
21
+ async recordTurnStart(command) {
22
+ await reconcilePreviousInterruptedTurn(coordinator, command, options, now);
23
+ const memory = await resolveRepository(command, options, repository);
24
+ const traceContext = traceContextFromCodeBuddyHookBody(command, new Date(now()).toISOString());
25
+ coordinator.recordTurnStart({ client: "codebuddy", sessionId: command.sessionId, clientTurnId: command.turnId, cwd: command.cwd, workspaceKind: command.workspaceKind, transcriptPath: command.transcriptPath, createdAt: now(), traceContext, repositoryMemory: memory });
26
+ await recordTraceBestEffort("codebuddy_memory_hook.turn_start_event", recordCodeBuddyTraceEvent({
27
+ eventId: traceTurnEventId(traceContext, "turn_start"),
28
+ memoraxCodeHome: options.memoraxCodeHome,
29
+ env: options.env,
30
+ traceContext,
31
+ type: "turn_start",
32
+ source: "unknown",
33
+ operation: "query",
34
+ ok: true,
35
+ request: { prompt: command.prompt, cwd: command.cwd, transcriptPath: command.transcriptPath },
36
+ }), options.diagnosticLogger);
37
+ await recordTraceBestEffort("codebuddy_memory_hook.current_turn_write", writeCurrentCodeBuddyTurn(traceContext, {
38
+ memoraxCodeHome: options.memoraxCodeHome,
39
+ env: options.env,
40
+ now: () => new Date(now()),
41
+ }), options.diagnosticLogger);
42
+ const repoMemoryWorktree = resolvedRepoMemoryWorktree(memory);
43
+ const pendingUserNotice = memory.ok
44
+ ? await pendingQuotaNotice.claim(memory.memory.config)
45
+ : undefined;
46
+ const retrieval = await retrieveAutomaticMemoryContext({ diagnosticLogger: options.diagnosticLogger, claimQuotaNotice: options.claimQuotaNotice ?? claimQuotaNotice, env: options.env ?? process.env, fetchImpl: options.fetchImpl, memoryObservability: options.memoryObservability, memoryObservabilitySource: "codebuddy_hook_retrieval", query: command.prompt, repositoryMemory: memory, sessionKey: command.sessionId, traceContext: traceContextFromCodeBuddyHookBody(command) });
47
+ const userNotice = [pendingUserNotice, retrieval.userNotice].filter(Boolean).join("\n");
48
+ return { ok: true, ...(repoMemoryWorktree ? { repoMemoryWorktree } : {}), ...(retrieval.context ? { additionalContext: retrieval.context } : {}), ...(userNotice ? { userNotice } : {}) };
49
+ },
50
+ async writeback(command) {
51
+ const entry = coordinator.getTurn({ client: "codebuddy", sessionId: command.sessionId, clientTurnId: command.turnId });
52
+ if (!command.sessionId)
53
+ return { ok: true, scheduled: false, reason: "missing_session_id" };
54
+ if (!command.turnId)
55
+ return { ok: true, scheduled: false, reason: "turn_id_missing" };
56
+ if (entry?.transcriptPath && entry.transcriptPath !== command.transcriptPath) {
57
+ await recordCodeBuddyTurnEnd(options, traceContextForWriteback(command, entry), undefined, "transcript_path_mismatch");
58
+ return { ok: true, scheduled: false, reason: "transcript_path_mismatch" };
59
+ }
60
+ const traceContext = traceContextForWriteback(command, entry);
61
+ const transcript = await readWithRetry({ transcriptPath: command.transcriptPath, sessionId: command.sessionId, turnId: command.turnId }, options);
62
+ if (!transcript.ok) {
63
+ await recordCodeBuddyTurnEnd(options, traceContext, undefined, transcript.reason);
64
+ return { ok: true, scheduled: false, reason: transcript.reason };
65
+ }
66
+ await recordCodeBuddyTurnEnd(options, traceContext, transcript.turn);
67
+ const memory = await resolveRepository({ ...command, cwd: command.cwd ?? entry?.cwd, workspaceKind: command.workspaceKind ?? entry?.workspaceKind }, options, repository);
68
+ const completed = await coordinator.completeMaterializedTurn({ key: { client: "codebuddy", sessionId: command.sessionId, clientTurnId: command.turnId }, metadata: entry, resolveRepositoryMemory: async () => memory, userText: transcript.turn.userPrompt, assistantText: transcript.turn.assistantReply, writeback: { client: "codebuddy", sessionKey: command.sessionId, env: options.env ?? process.env, fetchImpl: options.fetchImpl, memoryObservability: options.memoryObservability, memoryObservabilitySource: "codebuddy_hook_writeback", traceContext: traceContextFromCodeBuddyHookBody(command) } });
69
+ await recordCodeBuddyTurnMaterialization(options, traceContext, transcript.turn);
70
+ return completed.scheduled ? { ok: true, scheduled: true } : { ok: true, scheduled: false, reason: completed.reason };
71
+ },
72
+ size() { return coordinator.size("codebuddy"); },
73
+ close() { if (!options.turnCoordinator)
74
+ coordinator.close(); if (!options.repositoryMemorySession)
75
+ repository.close(); automatic?.close?.(); if (ownsPendingQuotaNotice)
76
+ pendingQuotaNotice.close(); },
77
+ };
78
+ }
79
+ async function resolveRepository(command, options, repository) {
80
+ return repository.resolve({ client: "codebuddy", sessionId: command.sessionId, workspaceRoot: command.cwd, workspaceKind: command.workspaceKind, memoraxCodeHome: options.memoraxCodeHome ?? options.env?.MEMORAX_CODE_HOME, env: options.env });
81
+ }
82
+ async function readWithRetry(input, options) {
83
+ const attempts = options.transcriptReadAttempts ?? 6;
84
+ let result = await readCodeBuddyTranscriptTurn(input);
85
+ for (let i = 1; i < attempts && !result.ok && ["transcript_unavailable", "turn_not_found", "user_prompt_missing", "assistant_message_missing"].includes(result.reason); i += 1) {
86
+ await new Promise((resolve) => setTimeout(resolve, options.transcriptRetryDelayMs ?? 100));
87
+ result = await readCodeBuddyTranscriptTurn(input);
88
+ }
89
+ return result;
90
+ }
91
+ function traceContextForWriteback(command, entry) {
92
+ return traceContextFromCodeBuddyHookBody({
93
+ ...command,
94
+ cwd: command.cwd ?? entry?.cwd,
95
+ workspaceKind: command.workspaceKind ?? entry?.workspaceKind,
96
+ }) ?? entry?.traceContext;
97
+ }
98
+ async function recordCodeBuddyTurnEnd(options, traceContext, turn, failureReason) {
99
+ const outcome = turn ? "completed" : failureReason === "assistant_message_missing" ? "interrupted" : undefined;
100
+ const recorded = await recordTraceBestEffort("codebuddy_memory_hook.turn_end_event", recordCodeBuddyTraceEvent({
101
+ eventId: traceTurnEventId(traceContext, "turn_end"),
102
+ memoraxCodeHome: options.memoraxCodeHome,
103
+ env: options.env,
104
+ traceContext,
105
+ type: "turn_end",
106
+ source: turn ? "codebuddy-hook" : "codebuddy-transcript",
107
+ operation: "reply",
108
+ ok: Boolean(turn),
109
+ ...(outcome ? { outcome } : {}),
110
+ ...(turn ? {
111
+ activities: turn.activities,
112
+ sessionTurnIndex: turn.sessionTurnIndex,
113
+ request: { prompt: turn.userPrompt },
114
+ response: { assistantMessage: turn.assistantReply },
115
+ } : {
116
+ error: failureReason,
117
+ }),
118
+ }), options.diagnosticLogger);
119
+ if (outcome) {
120
+ await recordTraceBestEffort("codebuddy_memory_hook.current_turn_close", markCurrentCodeBuddyTurnOutcome(traceContext, outcome, { memoraxCodeHome: options.memoraxCodeHome, env: options.env }), options.diagnosticLogger);
121
+ }
122
+ return recorded;
123
+ }
124
+ async function recordCodeBuddyTurnMaterialization(options, traceContext, turn) {
125
+ const originalEventId = traceTurnEventId(traceContext, "turn_end");
126
+ if (!originalEventId)
127
+ return;
128
+ await recordTraceBestEffort("codebuddy_memory_hook.turn_materialized_event", recordCodeBuddyTraceEvent({
129
+ eventId: traceTurnEventId(traceContext, "turn_materialized"),
130
+ memoraxCodeHome: options.memoraxCodeHome,
131
+ env: options.env,
132
+ traceContext,
133
+ type: "turn_materialized",
134
+ source: "codebuddy-transcript",
135
+ operation: "reply",
136
+ ok: true,
137
+ outcome: "completed",
138
+ activities: turn.activities,
139
+ sessionTurnIndex: turn.sessionTurnIndex,
140
+ request: { original_event_id: originalEventId, prompt: turn.userPrompt },
141
+ response: { assistantMessage: turn.assistantReply },
142
+ }), options.diagnosticLogger);
143
+ }
144
+ async function reconcilePreviousInterruptedTurn(coordinator, currentTurn, options, now) {
145
+ const candidate = await previousInterruptedTurnCandidate(coordinator, currentTurn, options, now);
146
+ if (!candidate)
147
+ return;
148
+ const transcript = await readCodeBuddyInterruptedTranscriptTurn({
149
+ transcriptPath: candidate.transcriptPath,
150
+ sessionId: candidate.sessionId,
151
+ turnId: candidate.turnId,
152
+ });
153
+ if (!transcript.ok)
154
+ return;
155
+ await recordCodeBuddyInterruptedTurnEnd(options, candidate.traceContext, transcript.turn);
156
+ coordinator.discardTurn({
157
+ client: "codebuddy",
158
+ sessionId: candidate.sessionId,
159
+ clientTurnId: candidate.turnId,
160
+ }, "interrupted");
161
+ options.diagnosticLogger?.("codebuddy_memory_hook.interrupted_turn_reconciled", {
162
+ sessionId: candidate.sessionId,
163
+ turnId: candidate.turnId,
164
+ assistantChars: transcript.turn.assistantReply.length,
165
+ activityCount: transcript.turn.activities.length,
166
+ sessionTurnIndex: transcript.turn.sessionTurnIndex,
167
+ });
168
+ }
169
+ async function previousInterruptedTurnCandidate(coordinator, currentTurn, options, now) {
170
+ const open = await readOpenTraceTurn({
171
+ client: "codebuddy",
172
+ memoraxCodeHome: options.memoraxCodeHome,
173
+ env: options.env,
174
+ expectedSessionId: currentTurn.sessionId,
175
+ allowStale: true,
176
+ now: () => new Date(now()),
177
+ });
178
+ if (!open.ok && open.reason === "closed")
179
+ return undefined;
180
+ if (open.ok && open.traceContext.turnId && open.traceContext.turnId !== currentTurn.turnId) {
181
+ const turnId = open.traceContext.turnId;
182
+ const cached = coordinator.getTurn({ client: "codebuddy", sessionId: currentTurn.sessionId, clientTurnId: turnId });
183
+ return {
184
+ sessionId: currentTurn.sessionId,
185
+ turnId,
186
+ transcriptPath: cached?.transcriptPath
187
+ ?? open.traceContext.transcriptPath
188
+ ?? currentTurn.transcriptPath,
189
+ traceContext: cached?.traceContext ?? open.traceContext,
190
+ };
191
+ }
192
+ const cached = coordinator.latestTurn({
193
+ client: "codebuddy",
194
+ sessionId: currentTurn.sessionId,
195
+ excludeClientTurnId: currentTurn.turnId,
196
+ });
197
+ if (!cached)
198
+ return undefined;
199
+ const transcriptPath = cached.transcriptPath ?? currentTurn.transcriptPath;
200
+ const traceContext = cached.traceContext ?? traceContextFromCodeBuddyHookBody({
201
+ sessionId: cached.sessionId,
202
+ turnId: cached.clientTurnId,
203
+ cwd: cached.cwd,
204
+ workspaceKind: cached.workspaceKind,
205
+ transcriptPath,
206
+ }, new Date(cached.createdAt).toISOString());
207
+ if (!traceContext)
208
+ return undefined;
209
+ return {
210
+ sessionId: cached.sessionId,
211
+ turnId: cached.clientTurnId,
212
+ transcriptPath,
213
+ traceContext,
214
+ };
215
+ }
216
+ async function recordCodeBuddyInterruptedTurnEnd(options, traceContext, turn) {
217
+ const recorded = await recordTraceBestEffort("codebuddy_memory_hook.interrupted_turn_end_event", recordCodeBuddyTraceEvent({
218
+ eventId: traceTurnEventId(traceContext, "turn_end"),
219
+ memoraxCodeHome: options.memoraxCodeHome,
220
+ env: options.env,
221
+ traceContext,
222
+ type: "turn_end",
223
+ source: "codebuddy-transcript",
224
+ operation: "reply",
225
+ ok: true,
226
+ outcome: "interrupted",
227
+ activities: turn.activities,
228
+ sessionTurnIndex: turn.sessionTurnIndex,
229
+ request: { prompt: turn.userPrompt },
230
+ response: { assistantMessage: turn.assistantReply },
231
+ }), options.diagnosticLogger);
232
+ if (!recorded || (!recorded.written && recorded.reason !== "duplicate_event"))
233
+ return;
234
+ await recordTraceBestEffort("codebuddy_memory_hook.interrupted_current_turn_close", markCurrentCodeBuddyTurnOutcome(traceContext, "interrupted", {
235
+ memoraxCodeHome: options.memoraxCodeHome,
236
+ env: options.env,
237
+ }), options.diagnosticLogger);
238
+ }
239
+ async function recordTraceBestEffort(label, promise, diagnosticLogger) {
240
+ try {
241
+ return await promise;
242
+ }
243
+ catch (error) {
244
+ diagnosticLogger?.("codebuddy_trace.write_failed", {
245
+ label,
246
+ error: error instanceof Error ? error.message : String(error),
247
+ });
248
+ return undefined;
249
+ }
250
+ }
@@ -0,0 +1,16 @@
1
+ import { createHash } from "node:crypto";
2
+ export function parseCodeBuddyTurnId(input) {
3
+ const prefix = `${input.sessionId}:`;
4
+ if (!input.turnId.startsWith(prefix))
5
+ return undefined;
6
+ const match = /^(0|[1-9]\d*):([0-9a-f]{64})$/.exec(input.turnId.slice(prefix.length));
7
+ if (!match)
8
+ return undefined;
9
+ const boundary = Number(match[1]);
10
+ if (!Number.isSafeInteger(boundary))
11
+ return undefined;
12
+ return { boundary, promptDigest: match[2] };
13
+ }
14
+ export function codeBuddyPromptDigest(prompt) {
15
+ return createHash("sha256").update(prompt.trim()).digest("hex");
16
+ }
@@ -11,7 +11,9 @@ import { confirmHookTrust, listMemoraxCodeHooks, trustCodexPluginHookSelection,
11
11
  import { resolveWindowsCliInvocation } from "../../shared/windows-cli-invocation.js";
12
12
  const PLUGIN_NAME = "memorax-code-codex-adapter";
13
13
  const CLI_MARKETPLACE_NAME = "memorax-code";
14
+ const PLUGIN_ID = `${PLUGIN_NAME}@${CLI_MARKETPLACE_NAME}`;
14
15
  const ADAPTER_COMMON_NAME = "memorax-code-adapter-common";
16
+ const PLUGIN_LIST_TIMEOUT_MS = 10_000;
15
17
  export async function installCodexPlugin(options = {}) {
16
18
  const home = resolveHome(options.homeDir);
17
19
  const codexHome = resolveCodexHome(options.codexHome, home);
@@ -124,14 +126,25 @@ export async function activateCodexPlugin(options = {}) {
124
126
  if (!existsSync(join(cliMarketplaceRoot, ".agents", "plugins", "marketplace.json"))) {
125
127
  await stageCodexCliMarketplace(install);
126
128
  }
129
+ const registrationBefore = await inspectCodexPluginRegistration({
130
+ ...options,
131
+ codexHome: install.codexHome,
132
+ codexCommand,
133
+ workspace,
134
+ });
127
135
  let marketplaceAdd;
128
136
  let pluginAdd;
129
- if (install.registrationMode === "bootstrap") {
130
- marketplaceAdd = await runCommand(codexCommand, ["plugin", "marketplace", "add", cliMarketplaceRoot, "--json"], { cwd: workspace, env });
131
- if (!marketplaceAdd.ok) {
132
- throw new Error(`codex plugin marketplace add failed: ${marketplaceAdd.stderr || marketplaceAdd.stdout || "unknown error"}`);
137
+ if (!registrationBefore.registered || !registrationBefore.enabled) {
138
+ if (registrationBefore.available) {
139
+ marketplaceAdd = skippedPluginCommand("marketplace_registration_preserved");
140
+ }
141
+ else {
142
+ marketplaceAdd = await runCommand(codexCommand, ["plugin", "marketplace", "add", cliMarketplaceRoot, "--json"], { cwd: workspace, env });
143
+ if (!marketplaceAdd.ok) {
144
+ throw new Error(`codex plugin marketplace add failed: ${marketplaceAdd.stderr || marketplaceAdd.stdout || "unknown error"}`);
145
+ }
133
146
  }
134
- pluginAdd = await runCommand(codexCommand, ["plugin", "add", `${PLUGIN_NAME}@${CLI_MARKETPLACE_NAME}`, "--json"], { cwd: workspace, env });
147
+ pluginAdd = await runCommand(codexCommand, ["plugin", "add", PLUGIN_ID, "--json"], { cwd: workspace, env });
135
148
  if (!pluginAdd.ok) {
136
149
  throw new Error(`codex plugin add failed: ${pluginAdd.stderr || pluginAdd.stdout || "unknown error"}`);
137
150
  }
@@ -140,6 +153,19 @@ export async function activateCodexPlugin(options = {}) {
140
153
  marketplaceAdd = skippedPluginCommand("versioned_installation_preserved");
141
154
  pluginAdd = skippedPluginCommand("versioned_installation_preserved");
142
155
  }
156
+ const registration = await inspectCodexPluginRegistration({
157
+ ...options,
158
+ codexHome: install.codexHome,
159
+ codexCommand,
160
+ workspace,
161
+ });
162
+ const expectedVersion = stringField(await readJsonRecord(join(install.pluginSourcePath, ".codex-plugin", "plugin.json")), "version");
163
+ if (!registration.registered || !registration.enabled) {
164
+ throw new Error("Codex plugin registration was not enabled after activation");
165
+ }
166
+ if (!expectedVersion || registration.version !== expectedVersion) {
167
+ throw new Error(`Codex plugin registration version does not match ${expectedVersion ?? "the installed plugin"}`);
168
+ }
143
169
  await removePersonalMarketplaceEntry(bootstrapMarketplacePath);
144
170
  const hooks = await listMemoraxCodeHooks(codexCommand, workspace, env);
145
171
  if (hooks.length === 0) {
@@ -163,12 +189,37 @@ export async function activateCodexPlugin(options = {}) {
163
189
  workspace,
164
190
  marketplaceAdd,
165
191
  pluginAdd,
192
+ registrationBefore,
193
+ registration,
166
194
  hooks,
167
195
  trustedHooks: hooks.length,
168
196
  configPath,
169
197
  startsBackend: false,
170
198
  };
171
199
  }
200
+ export async function inspectCodexPluginRegistration(options = {}) {
201
+ const home = resolveHome(options.homeDir);
202
+ const codexHome = resolveCodexHome(options.codexHome, home);
203
+ const codexCommand = options.codexCommand ?? process.env.CODEX_CLI_PATH ?? "codex";
204
+ const workspace = resolve(options.workspace ?? process.cwd());
205
+ const result = await runCommand(codexCommand, ["plugin", "list", "--available", "--json"], {
206
+ cwd: workspace,
207
+ env: { ...process.env, HOME: home, CODEX_HOME: codexHome },
208
+ timeoutMs: PLUGIN_LIST_TIMEOUT_MS,
209
+ });
210
+ if (!result.ok) {
211
+ throw new Error(`codex plugin list failed: ${result.stderr || result.stdout || "unknown error"}`);
212
+ }
213
+ const state = parseCodexPluginList(result.stdout);
214
+ return {
215
+ ok: true,
216
+ action: "codex-plugin-registration",
217
+ codexHome,
218
+ codexCommand,
219
+ workspace,
220
+ ...state,
221
+ };
222
+ }
172
223
  export async function removeCodexPlugin(options = {}) {
173
224
  const home = resolveHome(options.homeDir);
174
225
  const codexHome = resolveCodexHome(options.codexHome, home);
@@ -368,10 +419,12 @@ function mjsFiles(root) {
368
419
  }
369
420
  async function writePluginMetadata(pluginSourcePath, codexCommand) {
370
421
  const normalizedCodexCommand = nonEmpty(codexCommand);
422
+ const npmExecPath = nonEmpty(process.env.MEMORAX_CODE_NPM_EXEC_PATH);
371
423
  const metadata = {
372
424
  version: 1,
373
425
  memoraxCodeCommand: process.argv[1],
374
426
  ...(normalizedCodexCommand ? { codexCommand: normalizedCodexCommand } : {}),
427
+ ...(npmExecPath ? { npmExecPath } : {}),
375
428
  writtenAt: new Date().toISOString(),
376
429
  };
377
430
  await writeFile(join(pluginSourcePath, ".memorax-code-package.json"), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
@@ -447,6 +500,55 @@ async function readMarketplace(marketplacePath) {
447
500
  function skippedPluginCommand(reason) {
448
501
  return { ok: true, stdout: "", stderr: "", skipped: true, reason };
449
502
  }
503
+ function parseCodexPluginList(stdout) {
504
+ let parsed;
505
+ try {
506
+ parsed = JSON.parse(stdout);
507
+ }
508
+ catch {
509
+ throw new Error("codex plugin list returned invalid JSON");
510
+ }
511
+ if (!isRecord(parsed) || !Array.isArray(parsed.installed) || !Array.isArray(parsed.available)) {
512
+ throw new Error("codex plugin list returned an invalid registry payload");
513
+ }
514
+ const installed = matchingPluginEntry(parsed.installed, "installed");
515
+ const available = matchingPluginEntry(parsed.available, "available");
516
+ if (!installed) {
517
+ return {
518
+ available: available !== undefined,
519
+ registered: false,
520
+ enabled: false,
521
+ };
522
+ }
523
+ const version = typeof installed.version === "string" ? nonEmpty(installed.version) : undefined;
524
+ if (installed.installed !== true
525
+ || typeof installed.enabled !== "boolean"
526
+ || !version) {
527
+ throw new Error("codex plugin list returned an invalid MemoraX Code registration");
528
+ }
529
+ return {
530
+ available: true,
531
+ registered: true,
532
+ enabled: installed.enabled,
533
+ version,
534
+ };
535
+ }
536
+ function matchingPluginEntry(entries, collection) {
537
+ const matches = entries.filter((entry) => isRecord(entry) && (entry.pluginId === PLUGIN_ID
538
+ || (entry.name === PLUGIN_NAME && entry.marketplaceName === CLI_MARKETPLACE_NAME)));
539
+ if (matches.length > 1) {
540
+ throw new Error(`codex plugin list returned duplicate MemoraX Code ${collection} entries`);
541
+ }
542
+ const entry = matches[0];
543
+ if (!entry)
544
+ return undefined;
545
+ if (entry.pluginId !== PLUGIN_ID
546
+ || entry.name !== PLUGIN_NAME
547
+ || entry.marketplaceName !== CLI_MARKETPLACE_NAME) {
548
+ throw new Error(`codex plugin list returned an invalid MemoraX Code ${collection} entry`);
549
+ }
550
+ return entry;
551
+ }
450
552
  function pluginEntry(sourcePath) {
451
553
  return {
452
554
  name: PLUGIN_NAME,
@@ -495,13 +597,33 @@ function runCommand(command, args, options) {
495
597
  });
496
598
  let stdout = "";
497
599
  let stderr = "";
600
+ let settled = false;
601
+ let timeout;
602
+ const finish = (result) => {
603
+ if (settled)
604
+ return;
605
+ settled = true;
606
+ if (timeout)
607
+ clearTimeout(timeout);
608
+ resolveResult(result);
609
+ };
610
+ if (options.timeoutMs) {
611
+ timeout = setTimeout(() => {
612
+ child.kill();
613
+ finish({
614
+ ok: false,
615
+ stdout,
616
+ stderr: stderr || `command timed out after ${options.timeoutMs}ms`,
617
+ });
618
+ }, options.timeoutMs);
619
+ }
498
620
  child.stdout.on("data", (chunk) => { stdout += String(chunk); });
499
621
  child.stderr.on("data", (chunk) => { stderr += String(chunk); });
500
622
  child.on("error", (error) => {
501
- resolveResult({ ok: false, stdout, stderr: stderr || error.message });
623
+ finish({ ok: false, stdout, stderr: stderr || error.message });
502
624
  });
503
625
  child.on("close", (code) => {
504
- resolveResult({ ok: code === 0, stdout, stderr });
626
+ finish({ ok: code === 0, stdout, stderr });
505
627
  });
506
628
  });
507
629
  }
@@ -32,6 +32,8 @@ export function codexRolloutTurnFromJsonLines(transcript, input) {
32
32
  if (!codexRolloutSessionMatches(scan, input.sessionId)) {
33
33
  return { ok: false, reason: "transcript_session_mismatch" };
34
34
  }
35
+ if (scan.turnMetadataMismatch)
36
+ return { ok: false, reason: "turn_metadata_mismatch" };
35
37
  if (!scan.targetSeen)
36
38
  return { ok: false, reason: "turn_not_found" };
37
39
  if (!scan.userPrompt)
@@ -48,6 +50,8 @@ export function codexInterruptedRolloutTurnFromJsonLines(transcript, input) {
48
50
  if (!codexRolloutSessionMatches(scan, input.sessionId)) {
49
51
  return { ok: false, reason: "transcript_session_mismatch" };
50
52
  }
53
+ if (scan.turnMetadataMismatch)
54
+ return { ok: false, reason: "turn_metadata_mismatch" };
51
55
  if (!scan.targetSeen)
52
56
  return { ok: false, reason: "turn_not_found" };
53
57
  if (!scan.userPrompt)
@@ -74,9 +78,12 @@ function scanCodexRolloutTurn(transcript, targetTurnId) {
74
78
  let targetBoundarySeen = false;
75
79
  let activeTurnId;
76
80
  let targetSeen = false;
81
+ let turnMetadataMismatch = false;
77
82
  let userPrompt;
78
83
  let assistantReply;
79
84
  const visibleAssistantMessages = [];
85
+ let responseItemUserPrompt;
86
+ let responseItemAssistantReply;
80
87
  let interrupted = false;
81
88
  let interruptedAt;
82
89
  let rolledBack = false;
@@ -145,6 +152,26 @@ function scanCodexRolloutTurn(transcript, targetTurnId) {
145
152
  const activities = activityCandidatesFromResponseItem(payload);
146
153
  if (activities.length > 0)
147
154
  targetActivityGroups.push(activities);
155
+ if (stringValue(payload.type) === "message") {
156
+ const role = stringValue(payload.role);
157
+ const message = responseItemMessageText(payload.content, role);
158
+ const authoritativeMessage = role === "user"
159
+ || (role === "assistant" && payload.phase === "final_answer");
160
+ if (authoritativeMessage && message) {
161
+ const responseTurnId = responseItemTurnId(payload);
162
+ if (responseTurnId && responseTurnId !== activeTurnId) {
163
+ turnMetadataMismatch = true;
164
+ continue;
165
+ }
166
+ if (role === "user") {
167
+ userMessageTurnIds.add(activeTurnId);
168
+ responseItemUserPrompt = message;
169
+ }
170
+ else {
171
+ responseItemAssistantReply = message;
172
+ }
173
+ }
174
+ }
148
175
  }
149
176
  continue;
150
177
  }
@@ -231,8 +258,9 @@ function scanCodexRolloutTurn(transcript, targetTurnId) {
231
258
  ambiguousSessionMetadata,
232
259
  composite,
233
260
  targetSeen,
234
- userPrompt,
235
- assistantReply,
261
+ turnMetadataMismatch,
262
+ userPrompt: responseItemUserPrompt ?? userPrompt,
263
+ assistantReply: responseItemAssistantReply ?? assistantReply,
236
264
  visibleAssistantMessages,
237
265
  interrupted,
238
266
  interruptedAt,
@@ -245,6 +273,28 @@ function scanCodexRolloutTurn(transcript, targetTurnId) {
245
273
  userMessageTurnIds,
246
274
  };
247
275
  }
276
+ function responseItemTurnId(payload) {
277
+ const metadata = isRecord(payload.internal_chat_message_metadata_passthrough)
278
+ ? payload.internal_chat_message_metadata_passthrough
279
+ : undefined;
280
+ return metadata
281
+ ? stringValue(metadata.turn_id) ?? stringValue(metadata.turnId)
282
+ : undefined;
283
+ }
284
+ function responseItemMessageText(value, role) {
285
+ if (!Array.isArray(value))
286
+ return undefined;
287
+ const expectedType = role === "user" ? "input_text" : role === "assistant" ? "output_text" : undefined;
288
+ if (!expectedType)
289
+ return undefined;
290
+ const parts = value.flatMap((item) => {
291
+ if (!isRecord(item) || stringValue(item.type) !== expectedType)
292
+ return [];
293
+ const text = nonBlankString(item.text);
294
+ return text ? [text] : [];
295
+ });
296
+ return parts.length > 0 ? parts.join("\n") : undefined;
297
+ }
248
298
  function codexRolloutSessionMatches(scan, sessionId) {
249
299
  return scan.authoritySessionId === sessionId && !scan.ambiguousSessionMetadata;
250
300
  }
@@ -70,6 +70,10 @@ export function renderDefaultMemoraxCodeConfig() {
70
70
  "enabled = true # Enable local OpenCode session memory trace collection.",
71
71
  "capture_content = true # Store content in local OpenCode trace events.",
72
72
  "",
73
+ "[trace.codebuddy]",
74
+ "enabled = true # Enable local CodeBuddy session memory trace collection.",
75
+ "capture_content = true # Store content in local CodeBuddy trace events.",
76
+ "",
73
77
  ].join("\n");
74
78
  }
75
79
  export async function seedMissingMemoraxCodeConfig(memoraxCodeHome = defaultMemoraxCodeHome(process.env)) {
@@ -146,12 +150,14 @@ function normalizeMemoraxCodeConfig(value) {
146
150
  const traceClaude = recordValue(trace?.claude);
147
151
  const traceDsh = recordValue(trace?.dsh);
148
152
  const traceOpenCode = recordValue(trace?.opencode);
153
+ const traceCodeBuddy = recordValue(trace?.codebuddy);
149
154
  return (prune({
150
155
  clients: prune({
151
156
  codex: booleanField(clients, "codex"),
152
157
  claude: booleanField(clients, "claude"),
153
158
  dsh: booleanField(clients, "dsh"),
154
159
  opencode: booleanField(clients, "opencode"),
160
+ codebuddy: booleanField(clients, "codebuddy"),
155
161
  }),
156
162
  memorax: prune({
157
163
  endpoint: stringField(memorax, "endpoint"),
@@ -230,6 +236,13 @@ function normalizeMemoraxCodeConfig(value) {
230
236
  max_event_chars: numberField(traceOpenCode, "max_event_chars"),
231
237
  max_file_bytes: numberField(traceOpenCode, "max_file_bytes"),
232
238
  }),
239
+ codebuddy: prune({
240
+ enabled: booleanField(traceCodeBuddy, "enabled"),
241
+ capture_content: booleanField(traceCodeBuddy, "capture_content"),
242
+ retention_days: numberField(traceCodeBuddy, "retention_days"),
243
+ max_event_chars: numberField(traceCodeBuddy, "max_event_chars"),
244
+ max_file_bytes: numberField(traceCodeBuddy, "max_file_bytes"),
245
+ }),
233
246
  }),
234
247
  }) ?? {});
235
248
  }
@@ -242,7 +255,7 @@ function validateRawLifecycleConfig(value, path) {
242
255
  const clients = tableValue(rawClients);
243
256
  if (!clients)
244
257
  throw invalidLifecycleConfig(path, "clients must be a table");
245
- for (const field of ["codex", "claude", "dsh", "opencode"]) {
258
+ for (const field of ["codex", "claude", "dsh", "opencode", "codebuddy"]) {
246
259
  if (clients[field] !== undefined && typeof clients[field] !== "boolean") {
247
260
  throw invalidLifecycleConfig(path, `clients.${field} must be a boolean`);
248
261
  }