opencode: notifier wrapper gains the packet id (id <n>)

This commit is contained in:
2026-09-25 15:06:31 +00:00
parent 34e7bfe0bb
commit 88b617f8b7
4 changed files with 30 additions and 23 deletions
+8 -6
View File
@@ -699,7 +699,9 @@ describe('MeshService opencode notifier', () => {
let notifyMock: ReturnType<typeof vi.fn>;
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;
}
+10 -5
View File
@@ -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;
+10 -10
View File
@@ -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();
}
});
+2 -2
View File
@@ -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<void> {
async notify(id: number, text: string, from?: number): Promise<void> {
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;