From 9b6f72b9d785ae0973e043f73e953994c05d51ea Mon Sep 17 00:00:00 2001 From: twaldin Date: Wed, 13 May 2026 23:18:48 -0700 Subject: [PATCH] feat: harden external parent event envelopes --- src/commands/send.ts | 21 ++++++++++++++- tests/unit/external-orchestrator.test.ts | 34 +++++++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/commands/send.ts b/src/commands/send.ts index 51c0bed..6f355ec 100644 --- a/src/commands/send.ts +++ b/src/commands/send.ts @@ -179,9 +179,28 @@ export function appendExternalEvent(from: string, to: string, message: string): try { const dir = getStateDir() mkdirSync(dir, { recursive: true }) - const event = JSON.stringify({ type: 'message', from, to, message, ts: new Date().toISOString() }) + const refs = externalEventRefs() + const event = JSON.stringify({ + version: 1, + type: 'message', + from, + to, + text: message, + message, + ts: Date.now(), + ...(Object.keys(refs).length > 0 ? { refs } : {}), + }) appendFileSync(join(dir, 'events.jsonl'), event + '\n', 'utf-8') } catch { // Best-effort — never throw from event logging } } + +function externalEventRefs(): Record { + const refs: Record = {} + for (const key of ['HERMES_KANBAN_TASK', 'HERMES_KANBAN_RUN_ID', 'HERMES_KANBAN_PARENT_REF']) { + const value = process.env[key] + if (value && value.length > 0) refs[key] = value + } + return refs +} diff --git a/tests/unit/external-orchestrator.test.ts b/tests/unit/external-orchestrator.test.ts index dd15b0d..c3e3e9f 100644 --- a/tests/unit/external-orchestrator.test.ts +++ b/tests/unit/external-orchestrator.test.ts @@ -132,11 +132,13 @@ describe('sendDirect — external parent routing', () => { const lines = readFileSync(eventsPath, 'utf-8').trim().split('\n') expect(lines.length).toBe(1) const evt = JSON.parse(lines[0]) + expect(evt.version).toBe(1) expect(evt.type).toBe('message') expect(evt.from).toBe('my-agent') expect(evt.to).toBe('hermes') + expect(evt.text).toBe('done with task') expect(evt.message).toBe('done with task') - expect(typeof evt.ts).toBe('string') + expect(typeof evt.ts).toBe('number') }) it('does NOT write to events.jsonl when parent is human', async () => { @@ -194,9 +196,12 @@ describe('appendExternalEvent', () => { const eventsPath = join(tmpHome, '.flt', 'events.jsonl') expect(existsSync(eventsPath)).toBe(true) const evt = JSON.parse(readFileSync(eventsPath, 'utf-8').trim()) + expect(evt.version).toBe(1) expect(evt.from).toBe('agent-x') expect(evt.to).toBe('hermes') + expect(evt.text).toBe('test message') expect(evt.message).toBe('test message') + expect(typeof evt.ts).toBe('number') }) it('appends multiple events as separate lines', () => { @@ -208,4 +213,31 @@ describe('appendExternalEvent', () => { expect(JSON.parse(lines[0]).message).toBe('first') expect(JSON.parse(lines[1]).message).toBe('second') }) + + it('forwards Hermes kanban refs opaquely when present', () => { + const prevTask = process.env.HERMES_KANBAN_TASK + const prevRun = process.env.HERMES_KANBAN_RUN_ID + const prevParent = process.env.HERMES_KANBAN_PARENT_REF + process.env.HERMES_KANBAN_TASK = 't_abc123' + process.env.HERMES_KANBAN_RUN_ID = '42' + process.env.HERMES_KANBAN_PARENT_REF = '{"kind":"flt-event-sink","board":"kanban-flt-events"}' + try { + appendExternalEvent('agent-x', 'hermes', 'done') + } finally { + if (prevTask === undefined) delete process.env.HERMES_KANBAN_TASK + else process.env.HERMES_KANBAN_TASK = prevTask + if (prevRun === undefined) delete process.env.HERMES_KANBAN_RUN_ID + else process.env.HERMES_KANBAN_RUN_ID = prevRun + if (prevParent === undefined) delete process.env.HERMES_KANBAN_PARENT_REF + else process.env.HERMES_KANBAN_PARENT_REF = prevParent + } + + const eventsPath = join(tmpHome, '.flt', 'events.jsonl') + const evt = JSON.parse(readFileSync(eventsPath, 'utf-8').trim()) + expect(evt.refs).toEqual({ + HERMES_KANBAN_TASK: 't_abc123', + HERMES_KANBAN_RUN_ID: '42', + HERMES_KANBAN_PARENT_REF: '{"kind":"flt-event-sink","board":"kanban-flt-events"}', + }) + }) })