Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
130 commits
Select commit Hold shift + click to select a range
56c9b77
fix: reset activatedViaWakeWord flag on voice mode activation failure…
alex-nork Feb 24, 2026
2ada379
fix: use effective base URL for polling and cancel task on dismiss (#…
ashleeradka Feb 24, 2026
7a8b719
fix: derive segment count from manifest in media diagnostics (#8187)
alex-nork Feb 24, 2026
f2c44e5
fix: enforce payload size limit on pairing proxy endpoints (#8188)
ashleeradka Feb 24, 2026
c2cd8af
fix: restore approvalConversationGenerator in RuntimeHttpServer (#8189)
ashleeradka Feb 24, 2026
6f177c5
fix: approved devices race condition and optimistic clear (#8190)
ashleeradka Feb 24, 2026
4b2dded
fix: move zero-frame check before atomic rename in preprocess (#8191)
alex-nork Feb 24, 2026
897dfda
fix: include context field in map cache config hash (#8192)
alex-nork Feb 24, 2026
2e06a32
fix: align lastPairedAt type to Int matching generated IPC types (#8193)
ashleeradka Feb 24, 2026
37e710c
improve Gmail OAuth setup UX with auto-detection and clearer messagin…
asharma53 Feb 24, 2026
fc0b5e1
fix: reset activatedViaWakeWord flag outside wakeWordEnabled guard (#…
alex-nork Feb 24, 2026
5f84e1c
fix: send deny on PairingApprovalWindow close and supersede (#8196)
ashleeradka Feb 24, 2026
54e14f0
fix: clear stale pairing overrides during v4 migration (#8197)
ashleeradka Feb 24, 2026
25190e2
fix: re-register QR pairing before TTL and handle missing daemon (#8199)
ashleeradka Feb 24, 2026
b37b7e0
fix: honor task cancellation in QRPairingSheet pairing flow (#8210)
ashleeradka Feb 24, 2026
9f0de84
fix: enforce Content-Length pre-check in pairing proxy (#8212)
ashleeradka Feb 24, 2026
e987ec2
fix: roll back optimistic removal in removeApprovedDevice on IPC fail…
ashleeradka Feb 24, 2026
e844801
feat: reduce maxTokens default from 64000 to 16000 (#8217)
alex-nork Feb 24, 2026
4e11e3b
feat: add streamThinking config flag and filter thinking deltas (#8219)
alex-nork Feb 24, 2026
f4bdc28
fix: skip deny on same-ID retry in PairingApprovalWindow (#8223)
ashleeradka Feb 24, 2026
35a7e2c
fix: update maxTokens schema default and tests to 16000 (#8224)
alex-nork Feb 24, 2026
1285794
fix: separate iOS override migration and guard macOS cleanup (#8225)
ashleeradka Feb 24, 2026
a0c1421
fix: avoid QR flicker during refresh and register on daemon connect (…
ashleeradka Feb 24, 2026
734e605
fix: handle floating promise and add timeout in title generation (#8228)
siddseethepalli Feb 24, 2026
50d6d73
refactor: deduplicate isPlainObject into shared utility (#8229)
siddseethepalli Feb 24, 2026
17a3546
perf: add database indexes on memorySegments table (#8230)
siddseethepalli Feb 24, 2026
9b66c3f
feat: add backpressure and metrics to session message queue (#8231)
siddseethepalli Feb 24, 2026
88be750
perf: cache shell-parsing and risk classification results (#8232)
siddseethepalli Feb 24, 2026
a4fcae5
security: add pino log serializer to scrub sensitive data (#8233)
siddseethepalli Feb 24, 2026
b8af96d
refactor: replace unsafe type assertions with proper type guards (#8234)
siddseethepalli Feb 24, 2026
04eced2
feat: add circuit breaker to gateway runtime client (#8236)
siddseethepalli Feb 24, 2026
b4f0187
refactor: create centralized environment variable registry (#8235)
siddseethepalli Feb 24, 2026
a8c3cdb
feat: add status indication to macOS menu bar icon (#8237)
siddseethepalli Feb 24, 2026
4b4d646
refactor: replace any types with proper interfaces in test files (#8241)
siddseethepalli Feb 24, 2026
a3f4741
fix: remove iosPairingUseOverride deletion from v4 migration (#8239)
ashleeradka Feb 24, 2026
5553988
fix: remove Porcupine iOS-only SPM dep that breaks Xcode 26.2 build (…
alex-nork Feb 24, 2026
4a0777d
perf: add database indexes on memoryItems table (#8244)
siddseethepalli Feb 24, 2026
147d341
fix: add per-surface serialization to prevent race conditions (#8245)
siddseethepalli Feb 24, 2026
15f9702
feat: source filter for Available Skills + platform catalog API (#8097)
dvargasfuertes Feb 24, 2026
8021f70
fix: add AbortSignal support to tool implementations (#8246)
siddseethepalli Feb 24, 2026
c85ac5b
refactor: split http-server.ts into focused middleware and route modu…
siddseethepalli Feb 24, 2026
5e06bcb
fix: handle QR refresh failure and remove dead onChange (#8248)
ashleeradka Feb 24, 2026
9672bc4
feat: add response style section, trim verbose system prompt sections…
alex-nork Feb 24, 2026
8b7fa08
security: enforce 0o600 permissions on log files (#8252)
siddseethepalli Feb 24, 2026
1fbdae8
fix: propagate AbortSignal through permission checking (#8253)
siddseethepalli Feb 24, 2026
bbada1a
feat: complete ingress config schema with validation (#8254)
siddseethepalli Feb 24, 2026
6572d67
perf: add database indexes on remaining tables (#8255)
siddseethepalli Feb 24, 2026
76cf18d
feat(macos): add secret dev mode toggle (#8226)
dvargasfuertes Feb 24, 2026
614a819
fix: reset refresh timer and failure counter on QR retry (#8257)
ashleeradka Feb 24, 2026
84d16a0
refactor: split lifecycle.ts into focused modules (#8259)
siddseethepalli Feb 24, 2026
933bc2c
feat: make global hotkey configurable in macOS client (#8258)
siddseethepalli Feb 24, 2026
dc8f213
feat: add structured error serialization to pino logger (#8260)
siddseethepalli Feb 24, 2026
94c7991
fix: add numeric bounds validation for memory item confidence/importa…
siddseethepalli Feb 24, 2026
f3ca3fa
feat: make hardcoded daemon timeout values configurable (#8265)
siddseethepalli Feb 24, 2026
4716191
feat: add Zod schema for message metadata validation (#8268)
siddseethepalli Feb 24, 2026
7607422
refactor: separate migration code from platform utilities (#8272)
siddseethepalli Feb 24, 2026
096bfda
refactor: use consistent pino logging throughout migration and startu…
siddseethepalli Feb 24, 2026
774570a
fix: resolve all CI type-check and lint errors (#8276)
siddseethepalli Feb 24, 2026
9b3a468
feat(macos): add health check indicator to Platform URL row (#8271)
dvargasfuertes Feb 24, 2026
45fada4
fix: move response style directives to SOUL.md, remove buildResponseS…
alex-nork Feb 24, 2026
60813ba
Guardian Cross-Channel Approval UX Polish (#8208)
noanflaherty Feb 24, 2026
1a09025
refactor: remove assistant inbox feature flag gating from desktop UI …
NgoHarrison Feb 24, 2026
d912d55
docs: add provider abstraction and approval resilience rules to AGENT…
awlevin Feb 24, 2026
c54b8c9
refactor: remove assistantInbox config schema and defaults (#8282)
NgoHarrison Feb 24, 2026
bce77d1
refactor: make ingress ACL enforcement always-on (remove feature flag…
NgoHarrison Feb 24, 2026
4247f97
docs: add rule for agents to keep AGENTS.md up to date (#8284)
awlevin Feb 24, 2026
e5defe0
refactor: remove assistantInbox feature-flag check from ingress ACL e…
NgoHarrison Feb 24, 2026
66d7e07
docs: update inbox docs to reflect always-on behavior (#8293)
NgoHarrison Feb 24, 2026
6e141f3
fix: change private to internal for properties accessed from AppDeleg…
vincent0426 Feb 24, 2026
3e32633
feat: increase maxInputTokens from 180k to 200k (#8295)
vincent0426 Feb 24, 2026
61e6087
fix: resolve CI type-check and lint errors in ingress config and impo…
siddseethepalli Feb 24, 2026
965c2ff
fix: use Chrome debugger API for CSP-bypass eval, fix Swift Result<Vo…
marinatrajk Feb 24, 2026
ef5afb6
refactor: remove QuickChat feature (#8300)
vincent0426 Feb 24, 2026
02ddcc4
fix: remove stale relayPort from storage when port field is cleared (…
marinatrajk Feb 24, 2026
b66b6f2
fix(macos): use consistent paragraph style for placeholder height mea…
noanflaherty Feb 24, 2026
b0a317f
fix(macos): resolve CI build errors in settings views (#8306)
siddseethepalli Feb 24, 2026
07035d4
fix: make error message bubbles span full chat width (#8312)
asharma53 Feb 24, 2026
c1d1861
feat: add Quick Input bar (Cmd+/) (#8309)
vincent0426 Feb 24, 2026
40d26a6
fix(lint): resolve lint errors in influencer client (#8316)
siddseethepalli Feb 24, 2026
05d6bb0
feat: request 16kHz mono format in AlwaysOnAudioMonitor audio tap (#8…
alex-nork Feb 24, 2026
3e4c4ad
feat: add PorcupineBinding.swift — dlopen wrapper for Porcupine C API…
alex-nork Feb 24, 2026
357b9ee
feat: bundle Porcupine dylib, model, and keywords in build.sh (#8321)
alex-nork Feb 24, 2026
838e44a
fix: always start daemon HTTP server for iOS pairing (#8322)
ashleeradka Feb 24, 2026
1862284
feat: replace PorcupineWakeWordEngine stub with real Porcupine C SDK …
alex-nork Feb 24, 2026
dfa254a
feat: add wake word keyword selection and fix APIKeyManager usage (#8…
alex-nork Feb 24, 2026
8957faf
fix(swift): fix IPC Unix socket protocol and reconnect on cancel (#8325)
ashleeradka Feb 24, 2026
549f15e
fix: use AVAudioConverter for 16kHz resampling instead of custom tap …
alex-nork Feb 24, 2026
aafaa0d
fix: move FRAMEWORKS_DIR definition before Porcupine staleness check …
alex-nork Feb 24, 2026
ea504cd
fix: add input validation and handle cleanup in PorcupineBinding (#8327)
alex-nork Feb 24, 2026
eb95f68
fix: prevent double dismiss of Quick Input panel on submit (#8315)
vincent0426 Feb 24, 2026
d63fcfd
Release v0.3.6 (#8329)
vellum-automation[bot] Feb 24, 2026
d427947
fix: move wake word callback outside lock and use dynamic frame lengt…
alex-nork Feb 24, 2026
829a582
fix: prevent duplicate audio in AVAudioConverter and use ceil for buf…
alex-nork Feb 24, 2026
3a810c0
perf: disable LLM reranking for memory recall (#8067)
vincent0426 Feb 24, 2026
7cbf1a7
fix(swift): restore NWProtocolTCP on Unix socket, keep cancelled reco…
ashleeradka Feb 24, 2026
83bc75c
fix: add explicit self. for property captures in PorcupineWakeWordEng…
vincent0426 Feb 24, 2026
c5c1622
fix: add explicit self for keyword reference in os.Logger closure (#8…
alex-nork Feb 24, 2026
29bec9f
fix: resolve CI failures blocking v0.3.7 release (#8344)
ashleeradka Feb 24, 2026
07ebae8
fix: broadcast pairing approval to HTTP/SSE clients (#8348)
ashleeradka Feb 24, 2026
c533260
feat(macos): add Restart menu item to status bar menu (#8347)
Jasonnnz Feb 24, 2026
fb7013c
feat: add memory degradation banner to component gallery with #F5F3EB…
TirmanSidhu Feb 24, 2026
8e25925
Add rename option to thread right-click context menu (#8351)
ZeebBoyBlue Feb 24, 2026
4f8af53
fix: remove redundant conversation_id index on memory_segments (#8357)
siddseethepalli Feb 24, 2026
a57a664
fix: remove unscoped queue-full error and guard expireStale against e…
siddseethepalli Feb 24, 2026
7bbbf46
fix: preserve statusCode fallback when status is undefined in getErro…
siddseethepalli Feb 24, 2026
b4dd4d2
fix: preserve Error name/message/stack in log redaction serializer (#…
siddseethepalli Feb 24, 2026
4d3921c
fix: include workingDir and manifestOverride in risk cache key (#8361)
siddseethepalli Feb 24, 2026
6e44446
fix: remove redundant fingerprint index from memoryItems migration (#…
siddseethepalli Feb 24, 2026
1e722cb
fix: add missing legitimate env vars to KNOWN_VELLUM_VARS (#8363)
siddseethepalli Feb 24, 2026
0f52dfa
fix: clean up settled surface mutex entries to prevent memory leak (#…
siddseethepalli Feb 24, 2026
1412e2c
fix: return immediately on aborted CAPTCHA wait instead of breaking (…
siddseethepalli Feb 24, 2026
8f550c6
fix: rebind menu bar connection observer after client replacement and…
siddseethepalli Feb 24, 2026
b9ebcd1
fix: limit half-open probes to single attempt and propagate CircuitBr…
siddseethepalli Feb 24, 2026
336efb5
fix: read daemon timeouts without triggering loadConfig/migration sid…
siddseethepalli Feb 24, 2026
b94f815
fix: enforce log file permissions on existing files at startup (#8371)
siddseethepalli Feb 24, 2026
8e3208c
fix: auto-start SSE on HTTP transport connect for system events (#8370)
ashleeradka Feb 24, 2026
5be1bde
fix: throw typed cancellation error from permission checks (#8372)
siddseethepalli Feb 24, 2026
df63e9b
feat: add pre-commit warning when modifying system-prompt.ts (#8354)
awlevin Feb 24, 2026
8960688
Release v0.3.7 (#8373)
vellum-automation[bot] Feb 24, 2026
6949bb7
fix: initialize pairing handlers so approval responses are processed …
ashleeradka Feb 24, 2026
50de834
feat: improve media analysis skill defaults and add best practices (#…
alex-nork Feb 24, 2026
db66019
feat: add holistic Codex review phase to safe-blitz command (#8387)
noanflaherty Feb 24, 2026
b0cdf92
fix: watch http-token file so gateway picks up daemon token changes (…
ashleeradka Feb 24, 2026
bed3c5a
fix: drop legacy conversation_id index and remove from Drizzle schema…
siddseethepalli Feb 24, 2026
3c76155
fix: unreserve dedup cache entries on CircuitBreakerOpenError before …
siddseethepalli Feb 24, 2026
e522387
fix: validate daemon timeout bounds in readDaemonTimeouts (#8395)
siddseethepalli Feb 24, 2026
b24f698
fix: propagate queue-full status through subagent message path (#8397)
siddseethepalli Feb 24, 2026
c1fcee2
fix: respect env var precedence in http-token watcher (#8399)
ashleeradka Feb 24, 2026
868931f
feat: add queue-if-busy behavior and hub publishing to POST /v1/messa…
awlevin Feb 24, 2026
0b7fb25
Merge remote-tracking branch 'origin/main' into swarm/voice-session-l…
noanflaherty Feb 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 284 additions & 0 deletions assistant/src/__tests__/send-endpoint-busy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
/**
* Tests for POST /v1/messages queue-if-busy behavior and hub publishing.
*
* Validates that:
* - Messages are accepted (202) when the session is idle, with hub events published.
* - Messages are queued (202, queued: true) when the session is busy, not 409.
* - SSE subscribers receive events from messages sent via this endpoint.
*/
import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test';
import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import type { ServerMessage } from '../daemon/ipc-protocol.js';
import type { Session } from '../daemon/session.js';

const testDir = realpathSync(mkdtempSync(join(tmpdir(), 'send-endpoint-busy-test-')));

mock.module('../util/platform.js', () => ({
getRootDir: () => testDir,
getDataDir: () => testDir,
isMacOS: () => process.platform === 'darwin',
isLinux: () => process.platform === 'linux',
isWindows: () => process.platform === 'win32',
getSocketPath: () => join(testDir, 'test.sock'),
getPidPath: () => join(testDir, 'test.pid'),
getDbPath: () => join(testDir, 'test.db'),
getLogPath: () => join(testDir, 'test.log'),
ensureDataDir: () => {},
}));

mock.module('../util/logger.js', () => ({
getLogger: () => new Proxy({} as Record<string, unknown>, {
get: () => () => {},
}),
}));

mock.module('../config/loader.js', () => ({
getConfig: () => ({
model: 'test',
provider: 'test',
apiKeys: {},
memory: { enabled: false },
rateLimit: { maxRequestsPerMinute: 0, maxTokensPerSession: 0 },
secretDetection: { enabled: false },
}),
}));

import { initializeDb, getDb, resetDb } from '../memory/db.js';
import { RuntimeHttpServer } from '../runtime/http-server.js';
import { AssistantEventHub } from '../runtime/assistant-event-hub.js';
import type { AssistantEvent } from '../runtime/assistant-event.js';

initializeDb();

// ---------------------------------------------------------------------------
// Session helpers
// ---------------------------------------------------------------------------

/** Session that completes its agent loop quickly and emits a text delta + message_complete. */
function makeCompletingSession(): Session {
let processing = false;
return {
isProcessing: () => processing,
persistUserMessage: (_content: string, _attachments: unknown[], requestId?: string) => {
processing = true;
return requestId ?? 'msg-1';
},
memoryPolicy: { scopeId: 'default', includeDefaultFallback: false, strictSideEffects: false },
setChannelCapabilities: () => {},
setAssistantId: () => {},
setGuardianContext: () => {},
setCommandIntent: () => {},
updateClient: () => {},
enqueueMessage: () => ({ queued: false, requestId: 'noop' }),
runAgentLoop: async (_content: string, _messageId: string, onEvent: (msg: ServerMessage) => void) => {
onEvent({ type: 'assistant_text_delta', text: 'Hello!' });
onEvent({ type: 'message_complete', sessionId: 'test-session' });
processing = false;
},
handleConfirmationResponse: () => {},
handleSecretResponse: () => {},
} as unknown as Session;
}

/** Session that hangs forever in the agent loop (simulates a busy session). */
function makeHangingSession(): Session {
let processing = false;
const enqueuedMessages: Array<{ content: string; onEvent: (msg: ServerMessage) => void; requestId: string }> = [];
return {
isProcessing: () => processing,
persistUserMessage: (_content: string, _attachments: unknown[], requestId?: string) => {
processing = true;
return requestId ?? 'msg-1';
},
memoryPolicy: { scopeId: 'default', includeDefaultFallback: false, strictSideEffects: false },
setChannelCapabilities: () => {},
setAssistantId: () => {},
setGuardianContext: () => {},
setCommandIntent: () => {},
updateClient: () => {},
enqueueMessage: (content: string, _attachments: unknown[], onEvent: (msg: ServerMessage) => void, requestId: string) => {
enqueuedMessages.push({ content, onEvent, requestId });
return { queued: true, requestId };
},
runAgentLoop: async () => {
// Hang forever
await new Promise<void>(() => {});
},
handleConfirmationResponse: () => {},
handleSecretResponse: () => {},
_enqueuedMessages: enqueuedMessages,
} as unknown as Session;
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

const TEST_TOKEN = 'test-bearer-token-send';
const AUTH_HEADERS = { Authorization: `Bearer ${TEST_TOKEN}` };

describe('POST /v1/messages — queue-if-busy and hub publishing', () => {
let server: RuntimeHttpServer;
let port: number;
let eventHub: AssistantEventHub;

beforeEach(() => {
const db = getDb();
db.run('DELETE FROM messages');
db.run('DELETE FROM conversations');
db.run('DELETE FROM conversation_keys');
eventHub = new AssistantEventHub();
});

afterAll(() => {
resetDb();
try { rmSync(testDir, { recursive: true, force: true }); } catch { /* best effort */ }
});

async function startServer(sessionFactory: () => Session): Promise<void> {
port = 19000 + Math.floor(Math.random() * 1000);
server = new RuntimeHttpServer({
port,
bearerToken: TEST_TOKEN,
sendMessageDeps: {
getOrCreateSession: async () => sessionFactory(),
assistantEventHub: eventHub,
resolveAttachments: () => [],
},
});
await server.start();
}

async function stopServer(): Promise<void> {
await server?.stop();
}

function messagesUrl(): string {
return `http://127.0.0.1:${port}/v1/messages`;
}

// ── Idle session: immediate processing ──────────────────────────────

test('returns 202 with accepted: true and messageId when session is idle', async () => {
await startServer(() => makeCompletingSession());

const res = await fetch(messagesUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
body: JSON.stringify({ conversationKey: 'conv-idle', content: 'Hello', sourceChannel: 'macos' }),
});
const body = await res.json() as { accepted: boolean; messageId: string };

expect(res.status).toBe(202);
expect(body.accepted).toBe(true);
expect(body.messageId).toBeDefined();

await stopServer();
});

test('publishes events to assistantEventHub when session is idle', async () => {
const publishedEvents: AssistantEvent[] = [];

await startServer(() => makeCompletingSession());

eventHub.subscribe(
{ assistantId: 'self' },
(event) => { publishedEvents.push(event); },
);

const res = await fetch(messagesUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
body: JSON.stringify({ conversationKey: 'conv-hub', content: 'Hello hub', sourceChannel: 'macos' }),
});
expect(res.status).toBe(202);

// Wait for the async agent loop to complete and events to be published
await new Promise((r) => setTimeout(r, 100));

// Should have received assistant_text_delta and message_complete
const types = publishedEvents.map((e) => e.message.type);
expect(types).toContain('assistant_text_delta');
expect(types).toContain('message_complete');

await stopServer();
});

// ── Busy session: queue-if-busy ─────────────────────────────────────

test('returns 202 with queued: true when session is busy (not 409)', async () => {
const session = makeHangingSession();
await startServer(() => session);

// First message starts the agent loop and makes the session busy
const res1 = await fetch(messagesUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
body: JSON.stringify({ conversationKey: 'conv-busy', content: 'First', sourceChannel: 'macos' }),
});
expect(res1.status).toBe(202);
const body1 = await res1.json() as { accepted: boolean; messageId: string };
expect(body1.accepted).toBe(true);
expect(body1.messageId).toBeDefined();

// Wait for the agent loop to start
await new Promise((r) => setTimeout(r, 30));

// Second message should be queued, not rejected
const res2 = await fetch(messagesUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
body: JSON.stringify({ conversationKey: 'conv-busy', content: 'Second', sourceChannel: 'macos' }),
});
const body2 = await res2.json() as { accepted: boolean; queued: boolean };

expect(res2.status).toBe(202);
expect(body2.accepted).toBe(true);
expect(body2.queued).toBe(true);

await stopServer();
});

// ── Validation ──────────────────────────────────────────────────────

test('returns 400 when sourceChannel is missing', async () => {
await startServer(() => makeCompletingSession());

const res = await fetch(messagesUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
body: JSON.stringify({ conversationKey: 'conv-val', content: 'Hello' }),
});
expect(res.status).toBe(400);

await stopServer();
});

test('returns 400 when content is empty', async () => {
await startServer(() => makeCompletingSession());

const res = await fetch(messagesUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
body: JSON.stringify({ conversationKey: 'conv-empty', content: '', sourceChannel: 'macos' }),
});
expect(res.status).toBe(400);

await stopServer();
});

test('returns 400 when conversationKey is missing', async () => {
await startServer(() => makeCompletingSession());

const res = await fetch(messagesUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...AUTH_HEADERS },
body: JSON.stringify({ content: 'Hello', sourceChannel: 'macos' }),
});
expect(res.status).toBe(400);

await stopServer();
});
});
15 changes: 14 additions & 1 deletion assistant/src/daemon/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import { QdrantManager } from '../memory/qdrant-manager.js';
import { initQdrantClient } from '../memory/qdrant-client.js';
import { startScheduler } from '../schedule/scheduler.js';
import { RuntimeHttpServer } from '../runtime/http-server.js';
import { assistantEventHub } from '../runtime/assistant-event-hub.js';
import * as attachmentsStore from '../memory/attachments-store.js';
import { getHookManager } from '../hooks/manager.js';
import { installTemplates } from '../hooks/templates.js';
import { installCliLaunchers } from './install-cli-launchers.js';
Expand Down Expand Up @@ -263,13 +265,24 @@ export async function runDaemon(): Promise<void> {
interfacesDir: getInterfacesDir(),
approvalCopyGenerator: createApprovalCopyGenerator(),
approvalConversationGenerator: createApprovalConversationGenerator(),
sendMessageDeps: {
getOrCreateSession: (conversationId) =>
server.getSessionForMessages(conversationId),
assistantEventHub,
resolveAttachments: (attachmentIds) =>
attachmentsStore.getAttachmentsByIds(attachmentIds).map((a) => ({
id: a.id,
filename: a.originalFilename,
mimeType: a.mimeType,
data: a.dataBase64,
})),
},
});

// Inject the voice bridge orchestrator BEFORE attempting to start the HTTP
// server. The bridge only needs the RunOrchestrator instance (already created
// above) and must be available even when the HTTP server fails to bind.
setVoiceBridgeOrchestrator(runOrchestrator);

try {
await runtimeHttp.start();
setRelayBroadcast((msg) => server.broadcast(msg));
Expand Down
8 changes: 8 additions & 0 deletions assistant/src/daemon/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,14 @@ export class DaemonServer {
return { messageId };
}

/**
* Expose session lookup for the POST /v1/messages handler.
* The handler manages busy-state checking and queueing itself.
*/
async getSessionForMessages(conversationId: string): Promise<Session> {
return this.getOrCreateSession(conversationId, undefined, true);
}

createRunOrchestrator(): RunOrchestrator {
return new RunOrchestrator({
getOrCreateSession: (conversationId, transport) =>
Expand Down
5 changes: 5 additions & 0 deletions assistant/src/runtime/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export type {
RuntimeAttachmentMetadata,
ApprovalCopyGenerator,
ApprovalConversationGenerator,
SendMessageDeps,
} from './http-types.js';

import type {
Expand All @@ -129,6 +130,7 @@ import type {
RuntimeHttpServerOptions,
ApprovalCopyGenerator,
ApprovalConversationGenerator,
SendMessageDeps,
} from './http-types.js';

const log = getLogger('runtime-http');
Expand Down Expand Up @@ -156,6 +158,7 @@ export class RuntimeHttpServer {
private sweepInProgress = false;
private pairingStore = new PairingStore();
private pairingBroadcast?: (msg: ServerMessage) => void;
private sendMessageDeps?: SendMessageDeps;

constructor(options: RuntimeHttpServerOptions = {}) {
this.port = options.port ?? DEFAULT_PORT;
Expand All @@ -167,6 +170,7 @@ export class RuntimeHttpServer {
this.approvalCopyGenerator = options.approvalCopyGenerator;
this.approvalConversationGenerator = options.approvalConversationGenerator;
this.interfacesDir = options.interfacesDir ?? null;
this.sendMessageDeps = options.sendMessageDeps;
}

/** The port the server is actually listening on (resolved after start). */
Expand Down Expand Up @@ -558,6 +562,7 @@ export class RuntimeHttpServer {
return await handleSendMessage(req, {
processMessage: this.processMessage,
persistAndProcessMessage: this.persistAndProcessMessage,
sendMessageDeps: this.sendMessageDeps,
});
}

Expand Down
Loading
Loading