opencode-codex-memory 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +26 -28
  2. package/dist/opencode.json +1 -1
  3. package/dist/src/citation.d.ts +9 -0
  4. package/dist/src/citation.js +68 -11
  5. package/dist/src/db.js +10 -0
  6. package/dist/src/host-client.d.ts +1 -0
  7. package/dist/src/host-client.js +1 -0
  8. package/dist/src/index.d.ts +12 -2
  9. package/dist/src/index.js +32 -5
  10. package/dist/src/llm.d.ts +6 -0
  11. package/dist/src/llm.js +21 -10
  12. package/dist/src/phase2.d.ts +2 -0
  13. package/dist/src/phase2.js +1 -1
  14. package/dist/src/rollout-input.d.ts +6 -0
  15. package/dist/src/rollout-input.js +111 -0
  16. package/dist/src/store.d.ts +17 -1
  17. package/dist/src/store.js +90 -4
  18. package/dist/src/v2/agents.d.ts +53 -0
  19. package/dist/src/v2/agents.js +204 -0
  20. package/dist/src/v2/citation-overlay.d.ts +7 -0
  21. package/dist/src/v2/citation-overlay.js +52 -0
  22. package/dist/src/v2/index.d.ts +7 -0
  23. package/dist/src/v2/index.js +10 -0
  24. package/dist/src/v2/injection.d.ts +14 -0
  25. package/dist/src/v2/injection.js +19 -0
  26. package/dist/src/v2/plugin.d.ts +7 -0
  27. package/dist/src/v2/plugin.js +482 -0
  28. package/dist/src/v2/service.d.ts +74 -0
  29. package/dist/src/v2/service.js +173 -0
  30. package/dist/src/v2/shim.d.ts +47 -0
  31. package/dist/src/v2/shim.js +581 -0
  32. package/dist/src/v2/status-rpc.d.ts +197 -0
  33. package/dist/src/v2/status-rpc.js +159 -0
  34. package/dist/src/v2/status.d.ts +3 -0
  35. package/dist/src/v2/status.js +83 -0
  36. package/dist/src/v2/tools.d.ts +33 -0
  37. package/dist/src/v2/tools.js +57 -0
  38. package/dist/src/v2/tui.d.ts +3 -0
  39. package/dist/src/v2/tui.js +750 -0
  40. package/opencode.json +1 -1
  41. package/package.json +38 -2
@@ -0,0 +1,482 @@
1
+ /**
2
+ * opencode2 plugin setup.
3
+ *
4
+ * The V1 pipeline (phase1/phase2/capture/llm/…) runs unchanged on top of the
5
+ * V1-client shim (./shim.ts). This module only translates V2 host surfaces
6
+ * into the same calls the V1 hooks made:
7
+ *
8
+ * - prompt hook → turn-start stamp + phase-1 pump (was chat.message)
9
+ * - context hook → memory injection (was system.transform) + citation
10
+ * record/strip (was text.complete/messages.transform)
11
+ * - tool.execute.before→ external-context pollution mark (unchanged name)
12
+ * - execution.succeeded→ phase-1 pump (was session.status idle/session.idle)
13
+ * - agent.transform → memorize sub-agent provisioning (was config hook)
14
+ * - tool.transform → memory tool registration (was returned tool map)
15
+ *
16
+ * Known V2 adaptations (see docs/opencode2.md): global session reads use the
17
+ * registered public service; finalized citations are accounted from durable
18
+ * text events and stripped from model-bound context; config documents are
19
+ * adapted for the shared model resolver.
20
+ */
21
+ import { ensureMemoryLayout, buildMemorySystemPrompt, invalidateCache } from "../source.js";
22
+ import { stripCitations, extractCitedSessionIds, hasCitationMarkup } from "../citation.js";
23
+ import { MemoryStore } from "../store.js";
24
+ import { runPhase1 } from "../phase1.js";
25
+ import { runPhase2 } from "../phase2.js";
26
+ import { setPluginInput, setSubSessionDirectory, cleanupOldSubSessions, isMemorySubSession, abortActiveSubSessions, } from "../llm.js";
27
+ import { pluginOptions, clearConfigWarnings, resetPluginOptions } from "../options.js";
28
+ import { beginPluginShutdown, isPluginShuttingDown, resetPluginLifecycle } from "../lifecycle.js";
29
+ import { hostMcpStatus } from "../host-client.js";
30
+ import { recordDiagnostic } from "../diagnostics.js";
31
+ import { resetAgentHealth } from "../agent-health.js";
32
+ import { applyPluginOptions, handleSessionDeleted } from "../index.js";
33
+ import { setV2Context, buildV1ClientShim, } from "./shim.js";
34
+ import { ensureV2Agents } from "./agents.js";
35
+ import { buildV2Tools } from "./tools.js";
36
+ import { MemoryStatusRpc } from "./status-rpc.js";
37
+ import { readMemoryStatus } from "./status.js";
38
+ import { recordInjection, resetInjectionStats } from "./injection.js";
39
+ import { overlayV2CitationInstructions } from "./citation-overlay.js";
40
+ import { estimateTokens } from "../token.js";
41
+ let phase1InFlight = false;
42
+ let shimClient = null;
43
+ const backgroundTasks = new Set();
44
+ const statusListeners = new Set();
45
+ function notifyStatusChanged() {
46
+ for (const notify of statusListeners)
47
+ notify();
48
+ }
49
+ function trackBackgroundTask(task) {
50
+ const tracked = task.catch((err) => {
51
+ console.error("[opencode-codex-memory] background task error:", err);
52
+ });
53
+ backgroundTasks.add(tracked);
54
+ void tracked.then(() => backgroundTasks.delete(tracked));
55
+ }
56
+ /** Test seam: wait for hook-launched work to settle. */
57
+ export async function waitForV2BackgroundTasks() {
58
+ while (backgroundTasks.size > 0) {
59
+ await Promise.all([...backgroundTasks]);
60
+ }
61
+ }
62
+ /** Test seam: reset module state between tests. */
63
+ export function resetV2ModuleStateForTest() {
64
+ phase1InFlight = false;
65
+ statusListeners.clear();
66
+ backgroundTasks.clear();
67
+ seenTurnSessions.clear();
68
+ mcpStatusInFlight = null;
69
+ }
70
+ function getStore() {
71
+ return new MemoryStore();
72
+ }
73
+ function recordV2Citations(sessionId, assistantMessageId, text) {
74
+ if (!hasCitationMarkup(text))
75
+ return;
76
+ const ids = extractCitedSessionIds(text);
77
+ if (ids.length > 0)
78
+ getStore().recordUsageOnce(sessionId, assistantMessageId, ids);
79
+ }
80
+ function stripAndReconcileCitations(sessionId, messages) {
81
+ for (const [i, msg] of (messages ?? []).entries()) {
82
+ if (msg?.type !== "assistant" || !Array.isArray(msg.content))
83
+ continue;
84
+ for (const part of msg.content) {
85
+ if (part?.type !== "text" || typeof part.text !== "string")
86
+ continue;
87
+ if (!hasCitationMarkup(part.text))
88
+ continue;
89
+ try {
90
+ recordV2Citations(sessionId, String(msg.id ?? `context-part-${i}`), part.text);
91
+ }
92
+ catch (e) {
93
+ console.error("[opencode-codex-memory] citation recording failed:", e);
94
+ }
95
+ part.text = stripCitations(part.text);
96
+ }
97
+ }
98
+ }
99
+ function sessionIdFromV2Event(data) {
100
+ if (typeof data.sessionID === "string")
101
+ return data.sessionID;
102
+ if (typeof data.info?.id === "string")
103
+ return data.info.id;
104
+ if (typeof data.id === "string")
105
+ return data.id;
106
+ return "";
107
+ }
108
+ function lruSet(map, key, value, max) {
109
+ map.delete(key);
110
+ map.set(key, value);
111
+ if (map.size > max) {
112
+ const oldest = map.keys().next().value;
113
+ if (oldest !== undefined)
114
+ map.delete(oldest);
115
+ }
116
+ return value;
117
+ }
118
+ // One stamp+pump per session per process from the prompt hook.
119
+ const seenTurnSessions = new Map();
120
+ const MAX_TRACKED_TURN_SESSIONS = 1000;
121
+ export function markV2TurnSeen(sessionId) {
122
+ const first = seenTurnSessions.get(sessionId);
123
+ lruSet(seenTurnSessions, sessionId, first ?? Date.now(), MAX_TRACKED_TURN_SESSIONS);
124
+ return first === undefined;
125
+ }
126
+ let mcpStatusInFlight = null;
127
+ const MCP_STATUS_TIMEOUT_MS = 1_000;
128
+ async function mcpToolPrefixes() {
129
+ if (!shimClient)
130
+ return null;
131
+ if (!mcpStatusInFlight) {
132
+ mcpStatusInFlight = (async () => {
133
+ const controller = new AbortController();
134
+ let timer;
135
+ try {
136
+ const res = await Promise.race([
137
+ hostMcpStatus(shimClient, controller.signal),
138
+ new Promise((_, reject) => {
139
+ timer = setTimeout(() => {
140
+ controller.abort();
141
+ reject(new Error(`mcp status timed out after ${MCP_STATUS_TIMEOUT_MS}ms`));
142
+ }, MCP_STATUS_TIMEOUT_MS);
143
+ }),
144
+ ]);
145
+ if (!res || res.error)
146
+ return null;
147
+ const servers = res.data;
148
+ if (!servers || typeof servers !== "object" || Array.isArray(servers))
149
+ return null;
150
+ const prefixes = [];
151
+ for (const [server, status] of Object.entries(servers)) {
152
+ if (!status || typeof status !== "object" || typeof status.status !== "string")
153
+ continue;
154
+ prefixes.push(server.replace(/[^a-zA-Z0-9_-]/g, "_"));
155
+ }
156
+ return prefixes;
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ finally {
162
+ clearTimeout(timer);
163
+ mcpStatusInFlight = null;
164
+ }
165
+ })();
166
+ }
167
+ return mcpStatusInFlight;
168
+ }
169
+ async function classifyExternalContextTool(toolName) {
170
+ if (toolName === "websearch" || toolName === "webfetch")
171
+ return true;
172
+ const prefixes = await mcpToolPrefixes();
173
+ if (prefixes === null)
174
+ return null;
175
+ for (const prefix of prefixes) {
176
+ if (toolName.startsWith(`${prefix}_`) || toolName.startsWith(`mcp_${prefix}_`))
177
+ return true;
178
+ }
179
+ return false;
180
+ }
181
+ function stampAndPump(sid) {
182
+ try {
183
+ getStore().stampMemoryModeIfAbsent(sid, pluginOptions.generate_memories ? "enabled" : "disabled");
184
+ }
185
+ catch (e) {
186
+ console.error("[opencode-codex-memory] stampMemoryModeIfAbsent failed:", e);
187
+ }
188
+ trackBackgroundTask(triggerPhase1(sid));
189
+ }
190
+ async function triggerPhase1(currentSessionId) {
191
+ if (phase1InFlight || !pluginOptions.generate_memories || isPluginShuttingDown())
192
+ return;
193
+ phase1InFlight = true;
194
+ notifyStatusChanged();
195
+ try {
196
+ await runPhase1(getStore(), {
197
+ maxAgeDays: pluginOptions.max_rollout_age_days,
198
+ minIdleHours: pluginOptions.min_rollout_idle_hours,
199
+ maxClaimed: pluginOptions.max_rollouts_per_startup,
200
+ maxUnusedDays: pluginOptions.max_unused_days,
201
+ excludeSession: currentSessionId,
202
+ extractModel: pluginOptions.extract_model,
203
+ });
204
+ }
205
+ catch (err) {
206
+ console.error("[opencode-codex-memory] phase1 error:", err);
207
+ recordDiagnostic("error", "phase1", err instanceof Error ? err.message : String(err));
208
+ }
209
+ finally {
210
+ phase1InFlight = false;
211
+ notifyStatusChanged();
212
+ }
213
+ trackBackgroundTask(triggerPhase2().then(() => { }));
214
+ }
215
+ async function triggerPhase2(bypassCooldown = false) {
216
+ if (isPluginShuttingDown())
217
+ return "shutting_down";
218
+ try {
219
+ const result = await runPhase2(getStore(), {
220
+ maxRaw: pluginOptions.max_raw_memories_for_consolidation,
221
+ maxUnusedDays: pluginOptions.max_unused_days,
222
+ extensionRetentionDays: 7,
223
+ consolidationModel: pluginOptions.consolidation_model,
224
+ codexInterop: pluginOptions.codex_interop,
225
+ claudeImport: pluginOptions.claude_import,
226
+ bypassCooldown,
227
+ });
228
+ if (result.status !== "already_running" && result.status !== "skipped_cooldown" && result.status !== "skipped_running") {
229
+ recordDiagnostic(result.status === "succeeded" || result.status === "no_workspace_changes" ? "info" : "warn", "phase2", result.status);
230
+ }
231
+ return result.status;
232
+ }
233
+ catch (err) {
234
+ console.error("[opencode-codex-memory] phase2 error:", err);
235
+ recordDiagnostic("error", "phase2", err instanceof Error ? err.message : String(err));
236
+ return "failed";
237
+ }
238
+ finally {
239
+ notifyStatusChanged();
240
+ }
241
+ }
242
+ /** /memory "Consolidate now": one phase-1 pass over idle sessions, then phase 2 without cooldown. */
243
+ async function consolidateNow() {
244
+ if (phase1InFlight)
245
+ return "already_running";
246
+ if (!pluginOptions.generate_memories)
247
+ return "generation_disabled";
248
+ phase1InFlight = true;
249
+ notifyStatusChanged();
250
+ try {
251
+ await runPhase1(getStore(), {
252
+ maxAgeDays: pluginOptions.max_rollout_age_days,
253
+ minIdleHours: pluginOptions.min_rollout_idle_hours,
254
+ maxClaimed: pluginOptions.max_rollouts_per_startup,
255
+ maxUnusedDays: pluginOptions.max_unused_days,
256
+ extractModel: pluginOptions.extract_model,
257
+ });
258
+ }
259
+ catch (err) {
260
+ recordDiagnostic("error", "phase1", err instanceof Error ? err.message : String(err));
261
+ }
262
+ finally {
263
+ phase1InFlight = false;
264
+ notifyStatusChanged();
265
+ }
266
+ return triggerPhase2(true);
267
+ }
268
+ export async function setup(ctx) {
269
+ resetPluginLifecycle();
270
+ resetInjectionStats();
271
+ setV2Context(ctx);
272
+ shimClient = buildV1ClientShim();
273
+ setPluginInput({ client: shimClient });
274
+ setSubSessionDirectory(ctx.location.directory);
275
+ resetAgentHealth();
276
+ mcpStatusInFlight = null;
277
+ clearConfigWarnings();
278
+ if (ctx.options)
279
+ applyPluginOptions(ctx.options);
280
+ else
281
+ resetPluginOptions();
282
+ await ensureV2Agents(ctx);
283
+ const statusRegistration = await ctx.rpc.register(MemoryStatusRpc, {
284
+ status: async (input) => {
285
+ const sessionID = input?.sessionID;
286
+ return readMemoryStatus(typeof sessionID === "string" ? sessionID : null);
287
+ },
288
+ setOption: async (input) => {
289
+ const { key, value } = (input ?? {});
290
+ if ((key !== "use_memories" && key !== "generate_memories") || typeof value !== "boolean")
291
+ return { ok: false };
292
+ if (key === "generate_memories" && value && !pluginOptions.generate_memories) {
293
+ pluginOptions.generate_memories = true;
294
+ try {
295
+ await ensureV2Agents(ctx);
296
+ }
297
+ catch (error) {
298
+ pluginOptions.generate_memories = false;
299
+ console.error("[opencode-codex-memory] failed to provision V2 agents while enabling memory:", error);
300
+ return { ok: false };
301
+ }
302
+ }
303
+ pluginOptions[key] = value;
304
+ invalidateCache();
305
+ notifyStatusChanged();
306
+ return { ok: true };
307
+ },
308
+ setSessionMode: async (input) => {
309
+ const { sessionID, mode } = (input ?? {});
310
+ if (typeof sessionID !== "string" || (mode !== "enabled" && mode !== "disabled"))
311
+ return { ok: false };
312
+ getStore().setMemoryMode(sessionID, mode);
313
+ notifyStatusChanged();
314
+ return { ok: true };
315
+ },
316
+ consolidateNow: async () => {
317
+ // Runs detached so the dialog does not block on a multi-minute turn.
318
+ const run = consolidateNow();
319
+ trackBackgroundTask(run.then(() => { }));
320
+ return { status: "started" };
321
+ },
322
+ });
323
+ const publishStatus = () => {
324
+ void statusRegistration.events.emit("changed", {}).catch((err) => {
325
+ console.warn("[opencode-codex-memory] status notification failed:", err);
326
+ });
327
+ };
328
+ await ctx.tool.transform((editor) => {
329
+ for (const t of buildV2Tools())
330
+ editor.add(t);
331
+ });
332
+ await ctx.session.hook("prompt", (ev) => {
333
+ try {
334
+ const sid = ev?.sessionID;
335
+ if (!sid || isMemorySubSession(sid))
336
+ return;
337
+ if (!markV2TurnSeen(sid))
338
+ return;
339
+ stampAndPump(sid);
340
+ }
341
+ catch (err) {
342
+ console.error("[opencode-codex-memory] v2 prompt hook error:", err);
343
+ }
344
+ });
345
+ const handleModelBoundSession = (ev, inject) => {
346
+ const sid = ev?.sessionID;
347
+ if (!sid || isMemorySubSession(sid))
348
+ return;
349
+ try {
350
+ stripAndReconcileCitations(sid, ev.messages);
351
+ }
352
+ catch (e) {
353
+ console.error("[opencode-codex-memory] v2 citation handling failed:", e);
354
+ }
355
+ if (!inject || !pluginOptions.use_memories)
356
+ return;
357
+ ensureMemoryLayout();
358
+ const memoryPrompt = buildMemorySystemPrompt(pluginOptions.dedicated_tools);
359
+ if (!memoryPrompt)
360
+ return;
361
+ const text = overlayV2CitationInstructions(memoryPrompt);
362
+ if (!Array.isArray(ev.system))
363
+ ev.system = [];
364
+ ev.system.push({ type: "text", text });
365
+ recordInjection(sid, estimateTokens(text));
366
+ notifyStatusChanged();
367
+ };
368
+ await ctx.session.hook("context", (ev) => {
369
+ try {
370
+ handleModelBoundSession(ev, true);
371
+ }
372
+ catch (err) {
373
+ console.error("[opencode-codex-memory] v2 context hook error:", err);
374
+ }
375
+ });
376
+ await ctx.session.hook("compaction", (ev) => {
377
+ try {
378
+ handleModelBoundSession(ev, false);
379
+ }
380
+ catch (err) {
381
+ console.error("[opencode-codex-memory] v2 compaction hook error:", err);
382
+ }
383
+ });
384
+ await ctx.session.hook("generate", (ev) => {
385
+ try {
386
+ handleModelBoundSession(ev, false);
387
+ }
388
+ catch (err) {
389
+ console.error("[opencode-codex-memory] v2 generate hook error:", err);
390
+ }
391
+ });
392
+ await ctx.tool.hook("execute.before", async (ev) => {
393
+ try {
394
+ if (!pluginOptions.disable_on_external_context || !ev?.sessionID)
395
+ return;
396
+ if ((await classifyExternalContextTool(ev.tool)) !== true)
397
+ return;
398
+ getStore().markPolluted(ev.sessionID);
399
+ }
400
+ catch (err) {
401
+ console.error("[opencode-codex-memory] v2 tool hook error:", err);
402
+ }
403
+ });
404
+ // Bounded reseed before the event loop can see leftover helpers as user sessions.
405
+ await cleanupOldSubSessions();
406
+ try {
407
+ if (getStore().releaseOrphanedPhase2Job()) {
408
+ console.warn("[opencode-codex-memory] released a consolidation lease orphaned by a dead process");
409
+ }
410
+ }
411
+ catch (err) {
412
+ console.warn("[opencode-codex-memory] orphaned phase2 sweep failed:", err);
413
+ }
414
+ statusListeners.add(publishStatus);
415
+ // Event loop: execution.succeeded pumps phase 1 the way V1's idle events did.
416
+ const eventAbort = new AbortController();
417
+ void (async () => {
418
+ while (!eventAbort.signal.aborted && !isPluginShuttingDown()) {
419
+ try {
420
+ for await (const raw of ctx.event.subscribe({ signal: eventAbort.signal })) {
421
+ const e = raw;
422
+ try {
423
+ const data = e.data ?? e;
424
+ if (e.type === "session.text.ended") {
425
+ const sid = typeof data.sessionID === "string" ? data.sessionID : "";
426
+ const messageId = typeof data.assistantMessageID === "string" ? data.assistantMessageID : "";
427
+ if (sid && messageId && !isMemorySubSession(sid) && typeof data.text === "string") {
428
+ try {
429
+ recordV2Citations(sid, messageId, data.text);
430
+ }
431
+ catch (err) {
432
+ console.error("[opencode-codex-memory] durable citation accounting failed:", err);
433
+ }
434
+ }
435
+ }
436
+ if (e.type === "session.execution.succeeded" || e.type === "session.execution.ended") {
437
+ const sid = sessionIdFromV2Event(data);
438
+ if (sid && !isMemorySubSession(sid)) {
439
+ trackBackgroundTask(triggerPhase1(sid));
440
+ }
441
+ }
442
+ if (e.type === "session.deleted") {
443
+ const sid = sessionIdFromV2Event(data);
444
+ if (sid) {
445
+ try {
446
+ handleSessionDeleted(sid, getStore(), () => {
447
+ if (pluginOptions.generate_memories)
448
+ trackBackgroundTask(triggerPhase2().then(() => { }));
449
+ });
450
+ }
451
+ catch (err) {
452
+ console.error("[opencode-codex-memory] v2 session.deleted handling failed:", err);
453
+ }
454
+ }
455
+ }
456
+ }
457
+ catch (err) {
458
+ console.error("[opencode-codex-memory] v2 event handling error:", err);
459
+ }
460
+ }
461
+ break;
462
+ }
463
+ catch (err) {
464
+ if (eventAbort.signal.aborted || isPluginShuttingDown())
465
+ break;
466
+ console.error("[opencode-codex-memory] v2 event subscription error:", err);
467
+ await new Promise((r) => setTimeout(r, 5000));
468
+ }
469
+ }
470
+ })().catch((err) => console.error("[opencode-codex-memory] v2 event loop error:", err));
471
+ return () => {
472
+ statusListeners.delete(publishStatus);
473
+ eventAbort.abort();
474
+ beginPluginShutdown();
475
+ setSubSessionDirectory();
476
+ setV2Context(null);
477
+ void abortActiveSubSessions().catch((err) => {
478
+ console.warn("[opencode-codex-memory] v2 dispose abort of sub-sessions failed:", err);
479
+ });
480
+ invalidateCache();
481
+ };
482
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The supported OpenCode 2 connection boundary.
3
+ *
4
+ * Server plugins do not receive the complete public client. The registered
5
+ * local service does: the XDG `service.json` file is the discovery contract
6
+ * (read-only — never Service.ensure()). Auth headers are preserved, and
7
+ * GET /api/status pid must match this process. 2.0.5 dropped JSON
8
+ * /api/health (404 HTML/empty); 2.0.3 Service.discover() still probes that
9
+ * path and throws on a non-object body, so this module never calls it.
10
+ */
11
+ export interface V2ServiceEndpoint {
12
+ url: string;
13
+ auth?: {
14
+ type: "basic";
15
+ username: string;
16
+ password: string;
17
+ };
18
+ }
19
+ export interface V2ServiceStatus {
20
+ version: string;
21
+ pid: number;
22
+ }
23
+ export interface V2ServiceClient {
24
+ session: Record<string, (input?: unknown, options?: {
25
+ signal?: AbortSignal;
26
+ }) => Promise<unknown>>;
27
+ message?: Record<string, (input?: unknown, options?: {
28
+ signal?: AbortSignal;
29
+ }) => Promise<unknown>>;
30
+ config?: Record<string, (input?: unknown, options?: {
31
+ signal?: AbortSignal;
32
+ }) => Promise<unknown>>;
33
+ generate?: {
34
+ text: (input: unknown, options?: {
35
+ signal?: AbortSignal;
36
+ }) => Promise<unknown>;
37
+ };
38
+ /** Test/legacy only. Production probes /api/status, not this. */
39
+ health?: {
40
+ get: (options?: {
41
+ signal?: AbortSignal;
42
+ }) => Promise<unknown>;
43
+ };
44
+ }
45
+ export interface V2ServiceDependencies {
46
+ service: {
47
+ discover: () => Promise<V2ServiceEndpoint | undefined>;
48
+ headers: (endpoint: V2ServiceEndpoint) => Record<string, string> | undefined;
49
+ };
50
+ make: (options: {
51
+ baseUrl: string;
52
+ headers?: Record<string, string>;
53
+ }) => V2ServiceClient;
54
+ probe?: (endpoint: V2ServiceEndpoint, signal?: AbortSignal) => Promise<V2ServiceStatus>;
55
+ }
56
+ /** Test seam: replace discovery without changing the production connection path. */
57
+ export declare function setV2ServiceDependenciesForTest(dependencies: V2ServiceDependencies | null): void;
58
+ /** Forget a cached endpoint after a service restart or failed request. */
59
+ export declare function invalidateOwnService(): void;
60
+ export declare function parseReadyStatus(body: unknown): V2ServiceStatus | null;
61
+ export declare function readRegisteredEndpoint(file?: string): Promise<V2ServiceEndpoint | undefined>;
62
+ export declare function fetchServiceStatus(endpoint: V2ServiceEndpoint, headers: Record<string, string> | undefined, signal?: AbortSignal): Promise<V2ServiceStatus>;
63
+ /**
64
+ * Find a ready, registered OpenCode service without starting or replacing one.
65
+ * A missing service is a normal unavailable result; a PID mismatch is a
66
+ * safety failure because it would make global memory operate on another host.
67
+ */
68
+ export declare function discoverOwnService(dependencies?: V2ServiceDependencies, timeoutMs?: number): Promise<{
69
+ endpoint: V2ServiceEndpoint;
70
+ client: V2ServiceClient;
71
+ health: V2ServiceStatus;
72
+ } | null>;
73
+ /** Resolve the registered client once per live service; never start a service. */
74
+ export declare function ownServiceClient(): Promise<V2ServiceClient | null>;