diff --git a/src/mesh/mesh.service.spec.ts b/src/mesh/mesh.service.spec.ts index 967b694..4fa4703 100644 --- a/src/mesh/mesh.service.spec.ts +++ b/src/mesh/mesh.service.spec.ts @@ -699,7 +699,9 @@ describe('MeshService opencode notifier', () => { let notifyMock: ReturnType; beforeEach(() => { - notifyMock = vi.fn(async (_text: string, _from?: number) => {}); + notifyMock = vi.fn( + async (_id: number, _text: string, _from?: number) => {}, + ); }); // myInfo frame as streamed during the initial sync — sets svc.myNodeNum @@ -737,7 +739,7 @@ describe('MeshService opencode notifier', () => { await waitFor(() => notifyMock.mock.calls.length === 1); expect(notifyMock).toHaveBeenCalledTimes(1); - expect(notifyMock).toHaveBeenCalledWith('ping', KAMIL_NUM); + expect(notifyMock).toHaveBeenCalledWith(1, 'ping', KAMIL_NUM); // packet id threaded through } finally { delete process.env.MESH_NOTIFY_CHANNEL; svc.onModuleDestroy(); @@ -773,10 +775,10 @@ describe('MeshService opencode notifier', () => { process.env.MESH_NOTIFY_CHANNEL = '0'; const svc = makeSvc(); try { - svc.injectMessage('hi', 42); + const r = svc.injectMessage('hi', 42); expect(notifyMock).toHaveBeenCalledTimes(1); - expect(notifyMock).toHaveBeenCalledWith('hi', 42); + expect(notifyMock).toHaveBeenCalledWith(r.id, 'hi', 42); // generated record id threaded through } finally { delete process.env.MESH_NOTIFY_CHANNEL; } @@ -786,10 +788,10 @@ describe('MeshService opencode notifier', () => { process.env.MESH_NOTIFY_CHANNEL = '0'; const svc = makeSvc(); try { - svc.injectMessage('hi'); + const r = svc.injectMessage('hi'); expect(notifyMock).toHaveBeenCalledTimes(1); - expect(notifyMock).toHaveBeenCalledWith('hi', undefined); + expect(notifyMock).toHaveBeenCalledWith(r.id, 'hi', undefined); } finally { delete process.env.MESH_NOTIFY_CHANNEL; } diff --git a/src/mesh/mesh.service.ts b/src/mesh/mesh.service.ts index da3229d..eae173d 100644 --- a/src/mesh/mesh.service.ts +++ b/src/mesh/mesh.service.ts @@ -433,19 +433,24 @@ export class MeshService implements OnModuleInit, OnModuleDestroy { }; this.pushHistory(rec); // injected records ride channel 0 — the notifier fires only when MESH_NOTIFY_CHANNEL is set to match - this.notifyRx(0, text, from); + this.notifyRx(0, text, from, rec.id); return { id: rec.id }; } - /** Fire-and-forget opencode notifier push for an rx text on `channel` (no-op unless channels match and a notifier is wired). */ - private notifyRx(channel: number, text: string, from?: number): void { + /** Fire-and-forget opencode notifier push for an rx text on `channel` (no-op unless channels match and a notifier is wired). Both call sites pass the packet id. */ + private notifyRx( + channel: number, + text: string, + from?: number, + id?: number, + ): void { if (!this.opencode || channel !== this.notifyChannel) return; // self-echo filter: our own node must never trigger a notification of itself if (from !== undefined && from === this.myNodeNum) { this.logger.debug(`notify skipped: rx from our own node ${from}`); return; } - void this.opencode.notify(text, from); + void this.opencode.notify(id!, text, from); // both call sites always pass the packet id } /** @@ -631,7 +636,7 @@ export class MeshService implements OnModuleInit, OnModuleDestroy { channel: pkt.channel ?? 0, at: Date.now(), }); - this.notifyRx(pkt.channel ?? 0, text, pkt.from); + this.notifyRx(pkt.channel ?? 0, text, pkt.from, pkt.id); } else if (decoded && decoded.portnum === PORT_POSITION_APP) { // live position update: the Position is serialized in Data.payload let pos: Proto.Mesh.Position | undefined; diff --git a/src/opencode/opencode.service.spec.ts b/src/opencode/opencode.service.spec.ts index 0574654..0d6d6cc 100644 --- a/src/opencode/opencode.service.spec.ts +++ b/src/opencode/opencode.service.spec.ts @@ -68,7 +68,7 @@ describe('OpencodeService.notify', () => { .mockResolvedValueOnce(json([WORKSPACE])) .mockResolvedValueOnce(json({})); - await svc.notify('hi', 42); + await svc.notify(7, 'hi', 42); expect(fetchMock).toHaveBeenCalledTimes(2); const [url, init] = fetchMock.mock.calls[1]; @@ -89,7 +89,7 @@ describe('OpencodeService.notify', () => { parts: [ { type: 'text', - text: 'New mesh message from node 42: "hi" — please react to it.', + text: 'New mesh message (id 7) from node 42: "hi" — please react to it.', }, ], }), @@ -106,7 +106,7 @@ describe('OpencodeService.notify', () => { .mockResolvedValueOnce(json([WORKSPACE])) .mockResolvedValueOnce(json({})); - await svc.notify('hi'); + await svc.notify(7, 'hi'); const [, init] = fetchMock.mock.calls[1]; expect(String((init as { body?: string }).body)).toBe( @@ -114,7 +114,7 @@ describe('OpencodeService.notify', () => { parts: [ { type: 'text', - text: 'New mesh message: "hi" — please react to it.', + text: 'New mesh message (id 7): "hi" — please react to it.', }, ], }), @@ -146,7 +146,7 @@ describe('OpencodeService.notify', () => { ); fetchMock.mockResolvedValueOnce(json({})); - await svc.notify('hi'); + await svc.notify(7, 'hi'); expect(fetchMock.mock.calls[1][0]).toBe( `${API}/proxy/abc/session/ses_pinned/message`, @@ -179,7 +179,7 @@ describe('OpencodeService.notify', () => { ); fetchMock.mockResolvedValueOnce(json({})); - await svc.notify('hi'); + await svc.notify(7, 'hi'); expect(fetchMock.mock.calls[1][0]).toBe( `${API}/proxy/b/session/ses_pinned/message`, @@ -198,7 +198,7 @@ describe('OpencodeService.notify', () => { ) .mockResolvedValueOnce(json({})); - await svc.notify('hi'); + await svc.notify(7, 'hi'); expect(fetchMock.mock.calls[1][0]).toBe(`${API}/proxy/abc/session`); expect(fetchMock.mock.calls[2][0]).toBe( @@ -213,7 +213,7 @@ describe('OpencodeService.notify', () => { .mockResolvedValueOnce(json([{ id: 'ses_a' }, { id: 'ses_b', time: {} }])) .mockResolvedValueOnce(json({})); - await svc.notify('hi'); + await svc.notify(7, 'hi'); expect(fetchMock.mock.calls[2][0]).toBe( `${API}/proxy/abc/session/ses_a/message`, @@ -230,7 +230,7 @@ describe('OpencodeService.notify', () => { .mockResolvedValueOnce(json([WORKSPACE])) .mockResolvedValueOnce(json({})); - await svc.notify('hi'); + await svc.notify(7, 'hi'); // workspaces GET + message POST only — no /session discovery GET expect(fetchMock).toHaveBeenCalledTimes(2); @@ -309,7 +309,7 @@ describe('OpencodeService.notify', () => { string >[]) { const svc = makeService(env); - await expect(svc.notify('hi', 42)).resolves.toBeUndefined(); + await expect(svc.notify(7, 'hi', 42)).resolves.toBeUndefined(); expect(fetchMock).not.toHaveBeenCalled(); } }); diff --git a/src/opencode/opencode.service.ts b/src/opencode/opencode.service.ts index cd5dc69..d040701 100644 --- a/src/opencode/opencode.service.ts +++ b/src/opencode/opencode.service.ts @@ -49,10 +49,10 @@ export class OpencodeService { * up to RETRY_DELAYS_MS.length + 1 attempts total; the last failure logs `opencode notify dropped * after N attempts`. No-op when disabled (no credentials). */ - async notify(text: string, from?: number): Promise { + async notify(id: number, text: string, from?: number): Promise { if (!this.enabled) return; const msg = - 'New mesh message' + + `New mesh message (id ${id})` + (from != null ? ` from node ${from}` : '') + `: "${text}" — please react to it.`; const maxAttempts = RETRY_DELAYS_MS.length + 1;