fix(agent-runner): open inbound.db fresh per messages_in read
Cached singleton can return stale rows on virtiofs/NFS mounts, causing follow-up messages to silently never be polled. Add openInboundDb() with mmap_size=0 and switch the three messages_in readers to it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,11 +28,37 @@ let _inbound: Database | null = null;
|
|||||||
let _outbound: Database | null = null;
|
let _outbound: Database | null = null;
|
||||||
let _heartbeatPath: string = DEFAULT_HEARTBEAT_PATH;
|
let _heartbeatPath: string = DEFAULT_HEARTBEAT_PATH;
|
||||||
|
|
||||||
/** Inbound DB — container opens read-only (host is the sole writer). */
|
/**
|
||||||
|
* Avoid all cached db reads; open inbound.db read-only with mmap and page cache disabled.
|
||||||
|
*
|
||||||
|
* Use this (not getInboundDb) for readers that need to see host-written rows
|
||||||
|
* promptly — e.g. messages_in polling. Caller must .close() the returned
|
||||||
|
* connection (try/finally).
|
||||||
|
*
|
||||||
|
* Needed for mounts where host writes don't reliably invalidate
|
||||||
|
* SQLite's caches: virtiofs (Colima, Lima, Podman Machine, Apple
|
||||||
|
* Container), NFS.
|
||||||
|
*
|
||||||
|
* Cost is microseconds per query, so safe for universal use.
|
||||||
|
*/
|
||||||
|
export function openInboundDb(): Database {
|
||||||
|
const db = new Database(DEFAULT_INBOUND_PATH, { readonly: true });
|
||||||
|
db.exec('PRAGMA busy_timeout = 5000');
|
||||||
|
db.exec('PRAGMA mmap_size = 0');
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inbound DB — long-lived singleton, OK for tables the host writes once
|
||||||
|
* at spawn and never again (destinations, session_routing). For
|
||||||
|
* messages_in polling — where the host writes continuously and a stale
|
||||||
|
* view causes the pollHandle hang — use `openInboundDb()` instead.
|
||||||
|
*/
|
||||||
export function getInboundDb(): Database {
|
export function getInboundDb(): Database {
|
||||||
if (!_inbound) {
|
if (!_inbound) {
|
||||||
_inbound = new Database(DEFAULT_INBOUND_PATH, { readonly: true });
|
_inbound = new Database(DEFAULT_INBOUND_PATH, { readonly: true });
|
||||||
_inbound.exec('PRAGMA busy_timeout = 5000');
|
_inbound.exec('PRAGMA busy_timeout = 5000');
|
||||||
|
_inbound.exec('PRAGMA mmap_size = 0');
|
||||||
}
|
}
|
||||||
return _inbound;
|
return _inbound;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
* processing_ack. The host reads processing_ack to sync message lifecycle.
|
* processing_ack. The host reads processing_ack to sync message lifecycle.
|
||||||
*/
|
*/
|
||||||
import { getConfig } from '../config.js';
|
import { getConfig } from '../config.js';
|
||||||
import { getInboundDb, getOutboundDb } from './connection.js';
|
import { openInboundDb, getOutboundDb } from './connection.js';
|
||||||
|
|
||||||
export interface MessageInRow {
|
export interface MessageInRow {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -50,9 +50,10 @@ function getMaxMessagesPerPrompt(): number {
|
|||||||
* trigger=1 separately (see src/db/session-db.ts).
|
* trigger=1 separately (see src/db/session-db.ts).
|
||||||
*/
|
*/
|
||||||
export function getPendingMessages(): MessageInRow[] {
|
export function getPendingMessages(): MessageInRow[] {
|
||||||
const inbound = getInboundDb();
|
const inbound = openInboundDb();
|
||||||
const outbound = getOutboundDb();
|
const outbound = getOutboundDb();
|
||||||
|
|
||||||
|
try {
|
||||||
const pending = inbound
|
const pending = inbound
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT * FROM messages_in
|
`SELECT * FROM messages_in
|
||||||
@@ -75,6 +76,9 @@ export function getPendingMessages(): MessageInRow[] {
|
|||||||
// Reverse: we fetched DESC to take the most recent N, but the agent
|
// Reverse: we fetched DESC to take the most recent N, but the agent
|
||||||
// should see them in chronological order (oldest first).
|
// should see them in chronological order (oldest first).
|
||||||
return pending.filter((m) => !ackedIds.has(m.id)).reverse();
|
return pending.filter((m) => !ackedIds.has(m.id)).reverse();
|
||||||
|
} finally {
|
||||||
|
inbound.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mark messages as processing — writes to processing_ack in outbound.db. */
|
/** Mark messages as processing — writes to processing_ack in outbound.db. */
|
||||||
@@ -112,7 +116,12 @@ export function markFailed(id: string): void {
|
|||||||
|
|
||||||
/** Get a message by ID (read from inbound.db). */
|
/** Get a message by ID (read from inbound.db). */
|
||||||
export function getMessageIn(id: string): MessageInRow | undefined {
|
export function getMessageIn(id: string): MessageInRow | undefined {
|
||||||
return getInboundDb().prepare('SELECT * FROM messages_in WHERE id = ?').get(id) as MessageInRow | undefined;
|
const inbound = openInboundDb();
|
||||||
|
try {
|
||||||
|
return inbound.prepare('SELECT * FROM messages_in WHERE id = ?').get(id) as MessageInRow | undefined;
|
||||||
|
} finally {
|
||||||
|
inbound.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,9 +129,10 @@ export function getMessageIn(id: string): MessageInRow | undefined {
|
|||||||
* Reads from inbound.db, checks processing_ack to skip already-handled responses.
|
* Reads from inbound.db, checks processing_ack to skip already-handled responses.
|
||||||
*/
|
*/
|
||||||
export function findQuestionResponse(questionId: string): MessageInRow | undefined {
|
export function findQuestionResponse(questionId: string): MessageInRow | undefined {
|
||||||
const inbound = getInboundDb();
|
const inbound = openInboundDb();
|
||||||
const outbound = getOutboundDb();
|
const outbound = getOutboundDb();
|
||||||
|
|
||||||
|
try {
|
||||||
const response = inbound
|
const response = inbound
|
||||||
.prepare("SELECT * FROM messages_in WHERE status = 'pending' AND content LIKE ?")
|
.prepare("SELECT * FROM messages_in WHERE status = 'pending' AND content LIKE ?")
|
||||||
.get(`%"questionId":"${questionId}"%`) as MessageInRow | undefined;
|
.get(`%"questionId":"${questionId}"%`) as MessageInRow | undefined;
|
||||||
@@ -134,5 +144,8 @@ export function findQuestionResponse(questionId: string): MessageInRow | undefin
|
|||||||
if (acked) return undefined;
|
if (acked) return undefined;
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
} finally {
|
||||||
|
inbound.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user