refactor: remove read-time repair from canonical hydrate paths

This commit is contained in:
ejclaw
2026-04-10 21:18:47 +09:00
parent 4e05f60400
commit 644b38f2f6
7 changed files with 513 additions and 46 deletions

View File

@@ -1311,6 +1311,57 @@ describe('paired task state', () => {
}); });
}); });
it('fails fast when a paired task row loses canonical agent metadata after init', () => {
const tempDir = fs.mkdtempSync('/tmp/ejclaw-paired-task-strict-read-');
const dbPath = path.join(tempDir, 'messages.db');
try {
const fileDb = new Database(dbPath);
initializeDatabaseSchema(fileDb);
fileDb.close();
_initTestDatabaseFromFile(dbPath);
createPairedTask({
id: 'paired-task-strict-read',
chat_jid: 'dc:paired-task-strict-read',
group_folder: 'paired-task-strict-read',
owner_service_id: CODEX_MAIN_SERVICE_ID,
reviewer_service_id: CLAUDE_SERVICE_ID,
owner_agent_type: 'codex',
reviewer_agent_type: 'claude-code',
arbiter_agent_type: null,
title: 'strict read task',
source_ref: null,
plan_notes: null,
review_requested_at: null,
round_trip_count: 0,
status: 'active',
arbiter_verdict: null,
arbiter_requested_at: null,
completion_reason: null,
created_at: '2026-04-10T00:00:00.000Z',
updated_at: '2026-04-10T00:00:00.000Z',
});
const rawDb = new Database(dbPath);
rawDb
.prepare(
`UPDATE paired_tasks
SET reviewer_agent_type = NULL
WHERE id = ?`,
)
.run('paired-task-strict-read');
rawDb.close();
expect(() => getPairedTaskById('paired-task-strict-read')).toThrow(
/cannot read reviewer_agent_type from stored row metadata/,
);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
_initTestDatabase();
}
});
it('preserves stored reviewer service ids during init even when reviewer agent metadata exists', () => { it('preserves stored reviewer service ids during init even when reviewer agent metadata exists', () => {
const tempDir = fs.mkdtempSync( const tempDir = fs.mkdtempSync(
'/tmp/ejclaw-channel-owner-stored-reviewer-', '/tmp/ejclaw-channel-owner-stored-reviewer-',
@@ -1371,6 +1422,44 @@ describe('paired task state', () => {
}); });
}); });
it('fails fast when a channel owner lease row loses canonical reviewer metadata after init', () => {
const tempDir = fs.mkdtempSync('/tmp/ejclaw-channel-owner-strict-read-');
const dbPath = path.join(tempDir, 'messages.db');
try {
const fileDb = new Database(dbPath);
initializeDatabaseSchema(fileDb);
fileDb.close();
_initTestDatabaseFromFile(dbPath);
setChannelOwnerLease({
chat_jid: 'dc:channel-owner-strict-read',
owner_service_id: CODEX_MAIN_SERVICE_ID,
reviewer_service_id: CLAUDE_SERVICE_ID,
owner_agent_type: 'codex',
reviewer_agent_type: 'claude-code',
reason: 'strict read setup',
});
const rawDb = new Database(dbPath);
rawDb
.prepare(
`UPDATE channel_owner
SET reviewer_agent_type = NULL
WHERE chat_jid = ?`,
)
.run('dc:channel-owner-strict-read');
rawDb.close();
expect(() =>
getChannelOwnerLease('dc:channel-owner-strict-read'),
).toThrow(/cannot read reviewer_agent_type from stored row metadata/);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
_initTestDatabase();
}
});
it('fails init when stored paired task metadata conflicts with stored service ids even if room settings differ', () => { it('fails init when stored paired task metadata conflicts with stored service ids even if room settings differ', () => {
const tempDir = fs.mkdtempSync('/tmp/ejclaw-paired-task-ssot-'); const tempDir = fs.mkdtempSync('/tmp/ejclaw-paired-task-ssot-');
const dbPath = path.join(tempDir, 'messages.db'); const dbPath = path.join(tempDir, 'messages.db');
@@ -3381,6 +3470,48 @@ describe('service handoff completion', () => {
); );
}); });
it('fails fast when a service handoff row loses canonical target metadata after init', () => {
const tempDir = fs.mkdtempSync('/tmp/ejclaw-handoff-strict-read-');
const dbPath = path.join(tempDir, 'messages.db');
try {
const fileDb = new Database(dbPath);
initializeDatabaseSchema(fileDb);
fileDb.close();
_initTestDatabaseFromFile(dbPath);
const handoff = createServiceHandoff({
chat_jid: 'dc:handoff-strict-read',
group_folder: 'handoff-strict-read',
source_service_id: CLAUDE_SERVICE_ID,
target_service_id: CODEX_REVIEW_SERVICE_ID,
source_role: 'owner',
target_role: 'reviewer',
source_agent_type: 'claude-code',
target_agent_type: 'codex',
prompt: 'strict read handoff',
intended_role: 'reviewer',
});
const rawDb = new Database(dbPath);
rawDb
.prepare(
`UPDATE service_handoffs
SET target_agent_type = ''
WHERE id = ?`,
)
.run(handoff.id);
rawDb.close();
expect(() => getPendingServiceHandoffs(CODEX_REVIEW_SERVICE_ID)).toThrow(
/cannot read target_agent_type from stored row metadata/,
);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
_initTestDatabase();
}
});
it('preserves an explicit reviewer target service id when creating a new handoff', () => { it('preserves an explicit reviewer target service id when creating a new handoff', () => {
const handoff = createServiceHandoff({ const handoff = createServiceHandoff({
chat_jid: 'dc:handoff-stored-reviewer', chat_jid: 'dc:handoff-stored-reviewer',
@@ -8686,6 +8817,49 @@ describe('work items', () => {
); );
}); });
it('fails fast when a work item row loses canonical agent metadata after init', () => {
const tempDir = fs.mkdtempSync('/tmp/ejclaw-work-item-strict-read-');
const dbPath = path.join(tempDir, 'messages.db');
try {
const fileDb = new Database(dbPath);
initializeDatabaseSchema(fileDb);
fileDb.close();
_initTestDatabaseFromFile(dbPath);
const item = createProducedWorkItem({
group_folder: 'discord_test',
chat_jid: 'dc:work-item-strict-read',
agent_type: 'codex',
service_id: CODEX_REVIEW_SERVICE_ID,
delivery_role: 'reviewer',
start_seq: 1,
end_seq: 2,
result_payload: 'strict read work item',
});
const rawDb = new Database(dbPath);
rawDb
.prepare(
`UPDATE work_items
SET agent_type = ''
WHERE id = ?`,
)
.run(item.id);
rawDb.close();
expect(() =>
getOpenWorkItemForChat(
'dc:work-item-strict-read',
CODEX_REVIEW_SERVICE_ID,
),
).toThrow(/cannot read agent_type from stored row metadata/);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
_initTestDatabase();
}
});
it('backfills work item service ids during init on a canonical work_items schema without service_id columns', () => { it('backfills work item service ids during init on a canonical work_items schema without service_id columns', () => {
const tempDir = fs.mkdtempSync('/tmp/ejclaw-work-items-canonical-'); const tempDir = fs.mkdtempSync('/tmp/ejclaw-work-items-canonical-');
const dbPath = path.join(tempDir, 'messages.db'); const dbPath = path.join(tempDir, 'messages.db');

View File

@@ -22,7 +22,9 @@ interface RequiredRoleMetadataInput {
interface OptionalRoleMetadataInput extends RequiredRoleMetadataInput {} interface OptionalRoleMetadataInput extends RequiredRoleMetadataInput {}
function resolveRequiredAgentType(input: RequiredRoleMetadataInput): AgentType { function resolveFilledRequiredAgentType(
input: RequiredRoleMetadataInput,
): AgentType {
const persistedAgentType = normalizeStoredAgentType(input.storedAgentType); const persistedAgentType = normalizeStoredAgentType(input.storedAgentType);
const inferredAgentType = inferAgentTypeFromServiceShadow( const inferredAgentType = inferAgentTypeFromServiceShadow(
input.storedServiceId, input.storedServiceId,
@@ -48,7 +50,33 @@ function resolveRequiredAgentType(input: RequiredRoleMetadataInput): AgentType {
); );
} }
function resolveRequiredServiceId( function readStoredRequiredAgentType(
input: RequiredRoleMetadataInput,
): AgentType {
const persistedAgentType = normalizeStoredAgentType(input.storedAgentType);
const inferredAgentType = inferAgentTypeFromServiceShadow(
input.storedServiceId,
);
if (
persistedAgentType &&
inferredAgentType &&
persistedAgentType !== inferredAgentType
) {
throw new Error(
`${input.context}: ${input.role}_agent_type conflicts with ${input.role}_service_id`,
);
}
if (persistedAgentType) {
return persistedAgentType;
}
throw new Error(
`${input.context}: cannot read ${input.role}_agent_type from stored row metadata`,
);
}
function resolveFilledRequiredServiceId(
context: string, context: string,
role: PairedRoomRole, role: PairedRoomRole,
agentType: AgentType, agentType: AgentType,
@@ -65,7 +93,21 @@ function resolveRequiredServiceId(
); );
} }
function resolveOptionalRoleMetadata(input: OptionalRoleMetadataInput): { function readStoredRequiredServiceId(
context: string,
role: PairedRoomRole,
storedServiceId?: string | null,
): string {
if (storedServiceId) {
return storedServiceId;
}
throw new Error(
`${context}: cannot read ${role}_service_id from stored row metadata`,
);
}
function resolveFilledOptionalRoleMetadata(input: OptionalRoleMetadataInput): {
agentType: AgentType | null; agentType: AgentType | null;
serviceId: string | null; serviceId: string | null;
} { } {
@@ -73,10 +115,10 @@ function resolveOptionalRoleMetadata(input: OptionalRoleMetadataInput): {
return { agentType: null, serviceId: null }; return { agentType: null, serviceId: null };
} }
const agentType = resolveRequiredAgentType(input); const agentType = resolveFilledRequiredAgentType(input);
return { return {
agentType, agentType,
serviceId: resolveRequiredServiceId( serviceId: resolveFilledRequiredServiceId(
input.context, input.context,
input.role, input.role,
agentType, agentType,
@@ -85,6 +127,25 @@ function resolveOptionalRoleMetadata(input: OptionalRoleMetadataInput): {
}; };
} }
function readStoredOptionalRoleMetadata(input: OptionalRoleMetadataInput): {
agentType: AgentType | null;
serviceId: string | null;
} {
if (!input.storedAgentType && !input.storedServiceId) {
return { agentType: null, serviceId: null };
}
const agentType = readStoredRequiredAgentType(input);
return {
agentType,
serviceId: readStoredRequiredServiceId(
input.context,
input.role,
input.storedServiceId,
),
};
}
export interface CanonicalPairedTaskMetadata { export interface CanonicalPairedTaskMetadata {
ownerAgentType: AgentType; ownerAgentType: AgentType;
reviewerAgentType: AgentType; reviewerAgentType: AgentType;
@@ -93,7 +154,7 @@ export interface CanonicalPairedTaskMetadata {
reviewerServiceId: string; reviewerServiceId: string;
} }
export function canonicalizePairedTaskMetadata(input: { export function fillCanonicalPairedTaskMetadata(input: {
id: string; id: string;
owner_service_id?: string | null; owner_service_id?: string | null;
reviewer_service_id?: string | null; reviewer_service_id?: string | null;
@@ -102,14 +163,14 @@ export function canonicalizePairedTaskMetadata(input: {
arbiter_agent_type?: string | null; arbiter_agent_type?: string | null;
}): CanonicalPairedTaskMetadata { }): CanonicalPairedTaskMetadata {
const context = `paired_tasks(${input.id})`; const context = `paired_tasks(${input.id})`;
const ownerAgentType = resolveRequiredAgentType({ const ownerAgentType = resolveFilledRequiredAgentType({
context, context,
role: 'owner', role: 'owner',
storedAgentType: input.owner_agent_type, storedAgentType: input.owner_agent_type,
storedServiceId: input.owner_service_id, storedServiceId: input.owner_service_id,
fallbackAgentType: OWNER_AGENT_TYPE, fallbackAgentType: OWNER_AGENT_TYPE,
}); });
const reviewerAgentType = resolveRequiredAgentType({ const reviewerAgentType = resolveFilledRequiredAgentType({
context, context,
role: 'reviewer', role: 'reviewer',
storedAgentType: input.reviewer_agent_type, storedAgentType: input.reviewer_agent_type,
@@ -124,13 +185,13 @@ export function canonicalizePairedTaskMetadata(input: {
normalizeStoredAgentType(input.arbiter_agent_type) ?? normalizeStoredAgentType(input.arbiter_agent_type) ??
ARBITER_AGENT_TYPE ?? ARBITER_AGENT_TYPE ??
null, null,
ownerServiceId: resolveRequiredServiceId( ownerServiceId: resolveFilledRequiredServiceId(
context, context,
'owner', 'owner',
ownerAgentType, ownerAgentType,
input.owner_service_id, input.owner_service_id,
), ),
reviewerServiceId: resolveRequiredServiceId( reviewerServiceId: resolveFilledRequiredServiceId(
context, context,
'reviewer', 'reviewer',
reviewerAgentType, reviewerAgentType,
@@ -139,6 +200,46 @@ export function canonicalizePairedTaskMetadata(input: {
}; };
} }
export function readCanonicalPairedTaskMetadata(input: {
id: string;
owner_service_id?: string | null;
reviewer_service_id?: string | null;
owner_agent_type?: string | null;
reviewer_agent_type?: string | null;
arbiter_agent_type?: string | null;
}): CanonicalPairedTaskMetadata {
const context = `paired_tasks(${input.id})`;
const ownerAgentType = readStoredRequiredAgentType({
context,
role: 'owner',
storedAgentType: input.owner_agent_type,
storedServiceId: input.owner_service_id,
});
const reviewerAgentType = readStoredRequiredAgentType({
context,
role: 'reviewer',
storedAgentType: input.reviewer_agent_type,
storedServiceId: input.reviewer_service_id,
});
return {
ownerAgentType,
reviewerAgentType,
arbiterAgentType:
normalizeStoredAgentType(input.arbiter_agent_type) ?? null,
ownerServiceId: readStoredRequiredServiceId(
context,
'owner',
input.owner_service_id,
),
reviewerServiceId: readStoredRequiredServiceId(
context,
'reviewer',
input.reviewer_service_id,
),
};
}
export interface CanonicalChannelOwnerLeaseMetadata { export interface CanonicalChannelOwnerLeaseMetadata {
ownerAgentType: AgentType; ownerAgentType: AgentType;
reviewerAgentType: AgentType | null; reviewerAgentType: AgentType | null;
@@ -148,7 +249,7 @@ export interface CanonicalChannelOwnerLeaseMetadata {
arbiterServiceId: string | null; arbiterServiceId: string | null;
} }
export function canonicalizeChannelOwnerLeaseMetadata(input: { export function fillCanonicalChannelOwnerLeaseMetadata(input: {
chat_jid: string; chat_jid: string;
owner_service_id?: string | null; owner_service_id?: string | null;
reviewer_service_id?: string | null; reviewer_service_id?: string | null;
@@ -158,19 +259,19 @@ export function canonicalizeChannelOwnerLeaseMetadata(input: {
arbiter_agent_type?: string | null; arbiter_agent_type?: string | null;
}): CanonicalChannelOwnerLeaseMetadata { }): CanonicalChannelOwnerLeaseMetadata {
const context = `channel_owner(${input.chat_jid})`; const context = `channel_owner(${input.chat_jid})`;
const ownerAgentType = resolveRequiredAgentType({ const ownerAgentType = resolveFilledRequiredAgentType({
context, context,
role: 'owner', role: 'owner',
storedAgentType: input.owner_agent_type, storedAgentType: input.owner_agent_type,
storedServiceId: input.owner_service_id, storedServiceId: input.owner_service_id,
}); });
const reviewer = resolveOptionalRoleMetadata({ const reviewer = resolveFilledOptionalRoleMetadata({
context, context,
role: 'reviewer', role: 'reviewer',
storedAgentType: input.reviewer_agent_type, storedAgentType: input.reviewer_agent_type,
storedServiceId: input.reviewer_service_id, storedServiceId: input.reviewer_service_id,
}); });
const arbiter = resolveOptionalRoleMetadata({ const arbiter = resolveFilledOptionalRoleMetadata({
context, context,
role: 'arbiter', role: 'arbiter',
storedAgentType: input.arbiter_agent_type, storedAgentType: input.arbiter_agent_type,
@@ -181,7 +282,7 @@ export function canonicalizeChannelOwnerLeaseMetadata(input: {
ownerAgentType, ownerAgentType,
reviewerAgentType: reviewer.agentType, reviewerAgentType: reviewer.agentType,
arbiterAgentType: arbiter.agentType, arbiterAgentType: arbiter.agentType,
ownerServiceId: resolveRequiredServiceId( ownerServiceId: resolveFilledRequiredServiceId(
context, context,
'owner', 'owner',
ownerAgentType, ownerAgentType,
@@ -192,6 +293,49 @@ export function canonicalizeChannelOwnerLeaseMetadata(input: {
}; };
} }
export function readCanonicalChannelOwnerLeaseMetadata(input: {
chat_jid: string;
owner_service_id?: string | null;
reviewer_service_id?: string | null;
arbiter_service_id?: string | null;
owner_agent_type?: string | null;
reviewer_agent_type?: string | null;
arbiter_agent_type?: string | null;
}): CanonicalChannelOwnerLeaseMetadata {
const context = `channel_owner(${input.chat_jid})`;
const ownerAgentType = readStoredRequiredAgentType({
context,
role: 'owner',
storedAgentType: input.owner_agent_type,
storedServiceId: input.owner_service_id,
});
const reviewer = readStoredOptionalRoleMetadata({
context,
role: 'reviewer',
storedAgentType: input.reviewer_agent_type,
storedServiceId: input.reviewer_service_id,
});
const arbiter = readStoredOptionalRoleMetadata({
context,
role: 'arbiter',
storedAgentType: input.arbiter_agent_type,
storedServiceId: input.arbiter_service_id,
});
return {
ownerAgentType,
reviewerAgentType: reviewer.agentType,
arbiterAgentType: arbiter.agentType,
ownerServiceId: readStoredRequiredServiceId(
context,
'owner',
input.owner_service_id,
),
reviewerServiceId: reviewer.serviceId,
arbiterServiceId: arbiter.serviceId,
};
}
function normalizeHandoffRole( function normalizeHandoffRole(
role: string | null | undefined, role: string | null | undefined,
): PairedRoomRole | null { ): PairedRoomRole | null {
@@ -232,7 +376,7 @@ export interface CanonicalServiceHandoffMetadata {
targetServiceId: string; targetServiceId: string;
} }
export function canonicalizeServiceHandoffMetadata(input: { export function fillCanonicalServiceHandoffMetadata(input: {
id: number | string; id: number | string;
chat_jid: string; chat_jid: string;
source_service_id?: string | null; source_service_id?: string | null;
@@ -265,8 +409,6 @@ export function canonicalizeServiceHandoffMetadata(input: {
`${context}: source_agent_type conflicts with source_service_id`, `${context}: source_agent_type conflicts with source_service_id`,
); );
} }
const sourceAgentType =
storedSourceAgentType ?? inferredSourceAgentType ?? null;
const storedTargetAgentType = normalizeStoredAgentType( const storedTargetAgentType = normalizeStoredAgentType(
input.target_agent_type, input.target_agent_type,
); );
@@ -301,12 +443,133 @@ export function canonicalizeServiceHandoffMetadata(input: {
); );
} }
const sourceAgentTypeForShadow =
storedSourceAgentType ?? inferredSourceAgentType ?? null;
const sourceServiceId = const sourceServiceId =
input.source_service_id ?? input.source_service_id ??
(sourceRole != null && sourceAgentType != null (sourceRole != null && sourceAgentTypeForShadow != null
? resolveRoleServiceShadow(sourceRole, sourceAgentType) ? resolveRoleServiceShadow(sourceRole, sourceAgentTypeForShadow)
: null) ?? : null) ??
SERVICE_SESSION_SCOPE; SERVICE_SESSION_SCOPE;
const sourceAgentType =
storedSourceAgentType ??
inferredSourceAgentType ??
inferAgentTypeFromServiceShadow(sourceServiceId) ??
null;
assertStoredRoleMatchesServiceShadow({
context,
storedRole: sourceRole,
agentType: sourceAgentType,
serviceId: sourceServiceId,
roleField: 'source_role',
serviceField: 'source_service_id',
});
assertStoredRoleMatchesServiceShadow({
context,
storedRole: targetRole,
agentType: targetAgentType,
serviceId: targetServiceId,
roleField: 'target_role',
serviceField: 'target_service_id',
});
return {
sourceRole,
targetRole,
sourceAgentType,
targetAgentType,
sourceServiceId,
targetServiceId,
};
}
function readStoredHandoffAgentType(args: {
context: string;
field: 'source_agent_type' | 'target_agent_type';
serviceField: 'source_service_id' | 'target_service_id';
storedAgentType?: string | null;
storedServiceId?: string | null;
}): AgentType {
const persistedAgentType = normalizeStoredAgentType(args.storedAgentType);
const inferredAgentType = inferAgentTypeFromServiceShadow(
args.storedServiceId,
);
if (
persistedAgentType &&
inferredAgentType &&
persistedAgentType !== inferredAgentType
) {
throw new Error(
`${args.context}: ${args.field} conflicts with ${args.serviceField}`,
);
}
if (persistedAgentType) {
return persistedAgentType;
}
throw new Error(
`${args.context}: cannot read ${args.field} from stored row metadata`,
);
}
function readStoredHandoffServiceId(args: {
context: string;
field: 'source_service_id' | 'target_service_id';
storedServiceId?: string | null;
}): string {
if (args.storedServiceId) {
return args.storedServiceId;
}
throw new Error(
`${args.context}: cannot read ${args.field} from stored row metadata`,
);
}
export function readCanonicalServiceHandoffMetadata(input: {
id: number | string;
chat_jid: string;
source_service_id?: string | null;
target_service_id?: string | null;
source_role?: string | null;
target_role?: string | null;
intended_role?: string | null;
source_agent_type?: string | null;
target_agent_type?: string | null;
}): CanonicalServiceHandoffMetadata {
const context = `service_handoffs(${input.id})`;
const sourceRole =
normalizeHandoffRole(input.source_role) ??
normalizeHandoffRole(input.intended_role);
const targetRole =
normalizeHandoffRole(input.target_role) ??
normalizeHandoffRole(input.intended_role);
const sourceServiceId = readStoredHandoffServiceId({
context,
field: 'source_service_id',
storedServiceId: input.source_service_id,
});
const targetServiceId = readStoredHandoffServiceId({
context,
field: 'target_service_id',
storedServiceId: input.target_service_id,
});
const sourceAgentType = readStoredHandoffAgentType({
context,
field: 'source_agent_type',
serviceField: 'source_service_id',
storedAgentType: input.source_agent_type,
storedServiceId: sourceServiceId,
});
const targetAgentType = readStoredHandoffAgentType({
context,
field: 'target_agent_type',
serviceField: 'target_service_id',
storedAgentType: input.target_agent_type,
storedServiceId: targetServiceId,
});
assertStoredRoleMatchesServiceShadow({ assertStoredRoleMatchesServiceShadow({
context, context,
@@ -351,7 +614,7 @@ export function inferExecutionLeaseServiceIdFromCanonicalMetadata(
): string | null { ): string | null {
switch (row.role) { switch (row.role) {
case 'owner': { case 'owner': {
const owner = resolveOptionalRoleMetadata({ const owner = resolveFilledOptionalRoleMetadata({
context: `paired_task_execution_leases(${row.rowid})`, context: `paired_task_execution_leases(${row.rowid})`,
role: 'owner', role: 'owner',
storedAgentType: row.owner_agent_type, storedAgentType: row.owner_agent_type,
@@ -360,7 +623,7 @@ export function inferExecutionLeaseServiceIdFromCanonicalMetadata(
return owner.serviceId; return owner.serviceId;
} }
case 'reviewer': { case 'reviewer': {
const reviewer = resolveOptionalRoleMetadata({ const reviewer = resolveFilledOptionalRoleMetadata({
context: `paired_task_execution_leases(${row.rowid})`, context: `paired_task_execution_leases(${row.rowid})`,
role: 'reviewer', role: 'reviewer',
storedAgentType: row.reviewer_agent_type, storedAgentType: row.reviewer_agent_type,
@@ -369,7 +632,7 @@ export function inferExecutionLeaseServiceIdFromCanonicalMetadata(
return reviewer.serviceId; return reviewer.serviceId;
} }
case 'arbiter': { case 'arbiter': {
const arbiter = resolveOptionalRoleMetadata({ const arbiter = resolveFilledOptionalRoleMetadata({
context: `paired_task_execution_leases(${row.rowid})`, context: `paired_task_execution_leases(${row.rowid})`,
role: 'arbiter', role: 'arbiter',
storedAgentType: row.arbiter_agent_type, storedAgentType: row.arbiter_agent_type,

View File

@@ -1,7 +1,10 @@
import { Database } from 'bun:sqlite'; import { Database } from 'bun:sqlite';
import { AgentType } from '../types.js'; import { AgentType } from '../types.js';
import { canonicalizeChannelOwnerLeaseMetadata } from './canonical-role-metadata.js'; import {
fillCanonicalChannelOwnerLeaseMetadata,
readCanonicalChannelOwnerLeaseMetadata,
} from './canonical-role-metadata.js';
export interface ChannelOwnerLeaseRow { export interface ChannelOwnerLeaseRow {
chat_jid: string; chat_jid: string;
@@ -49,7 +52,7 @@ function hydrateChannelOwnerLeaseRow(
ownerServiceId, ownerServiceId,
reviewerServiceId, reviewerServiceId,
arbiterServiceId, arbiterServiceId,
} = canonicalizeChannelOwnerLeaseMetadata({ } = readCanonicalChannelOwnerLeaseMetadata({
chat_jid: row.chat_jid, chat_jid: row.chat_jid,
owner_service_id: row.owner_service_id, owner_service_id: row.owner_service_id,
reviewer_service_id: row.reviewer_service_id, reviewer_service_id: row.reviewer_service_id,
@@ -102,7 +105,7 @@ export function setChannelOwnerLeaseInDatabase(
ownerServiceId, ownerServiceId,
reviewerServiceId, reviewerServiceId,
arbiterServiceId, arbiterServiceId,
} = canonicalizeChannelOwnerLeaseMetadata({ } = fillCanonicalChannelOwnerLeaseMetadata({
chat_jid: input.chat_jid, chat_jid: input.chat_jid,
owner_service_id: input.owner_service_id, owner_service_id: input.owner_service_id,
reviewer_service_id: input.reviewer_service_id, reviewer_service_id: input.reviewer_service_id,

View File

@@ -9,7 +9,10 @@ import {
buildPairedTurnIdentity, buildPairedTurnIdentity,
resolvePairedTurnRole, resolvePairedTurnRole,
} from '../paired-turn-identity.js'; } from '../paired-turn-identity.js';
import { canonicalizePairedTaskMetadata } from './canonical-role-metadata.js'; import {
fillCanonicalPairedTaskMetadata,
readCanonicalPairedTaskMetadata,
} from './canonical-role-metadata.js';
import { import {
ensurePairedTurnQueuedInDatabase, ensurePairedTurnQueuedInDatabase,
markPairedTurnRunningInDatabase, markPairedTurnRunningInDatabase,
@@ -74,7 +77,7 @@ function hydratePairedTaskRow(
arbiterAgentType, arbiterAgentType,
ownerServiceId, ownerServiceId,
reviewerServiceId, reviewerServiceId,
} = canonicalizePairedTaskMetadata({ } = readCanonicalPairedTaskMetadata({
id: row.id, id: row.id,
owner_service_id: row.owner_service_id, owner_service_id: row.owner_service_id,
reviewer_service_id: row.reviewer_service_id, reviewer_service_id: row.reviewer_service_id,
@@ -142,7 +145,7 @@ export function createPairedTaskInDatabase(
arbiterAgentType, arbiterAgentType,
ownerServiceId, ownerServiceId,
reviewerServiceId, reviewerServiceId,
} = canonicalizePairedTaskMetadata({ } = fillCanonicalPairedTaskMetadata({
id: task.id, id: task.id,
owner_service_id: task.owner_service_id, owner_service_id: task.owner_service_id,
reviewer_service_id: task.reviewer_service_id, reviewer_service_id: task.reviewer_service_id,

View File

@@ -6,9 +6,9 @@ import {
resolveRoleServiceShadow, resolveRoleServiceShadow,
} from '../role-service-shadow.js'; } from '../role-service-shadow.js';
import { import {
canonicalizeChannelOwnerLeaseMetadata, fillCanonicalChannelOwnerLeaseMetadata,
canonicalizePairedTaskMetadata, fillCanonicalPairedTaskMetadata,
canonicalizeServiceHandoffMetadata, fillCanonicalServiceHandoffMetadata,
inferExecutionLeaseServiceIdFromCanonicalMetadata, inferExecutionLeaseServiceIdFromCanonicalMetadata,
} from './canonical-role-metadata.js'; } from './canonical-role-metadata.js';
import { import {
@@ -2320,7 +2320,7 @@ function backfillCanonicalPairedTaskServiceIds(database: Database): void {
arbiterAgentType, arbiterAgentType,
ownerServiceId, ownerServiceId,
reviewerServiceId, reviewerServiceId,
} = canonicalizePairedTaskMetadata({ } = fillCanonicalPairedTaskMetadata({
id: row.id, id: row.id,
owner_service_id: row.owner_service_id, owner_service_id: row.owner_service_id,
reviewer_service_id: row.reviewer_service_id, reviewer_service_id: row.reviewer_service_id,
@@ -2400,7 +2400,7 @@ function backfillCanonicalChannelOwnerServiceIds(database: Database): void {
ownerServiceId, ownerServiceId,
reviewerServiceId, reviewerServiceId,
arbiterServiceId, arbiterServiceId,
} = canonicalizeChannelOwnerLeaseMetadata({ } = fillCanonicalChannelOwnerLeaseMetadata({
chat_jid: row.chat_jid, chat_jid: row.chat_jid,
owner_service_id: row.owner_service_id, owner_service_id: row.owner_service_id,
reviewer_service_id: row.reviewer_service_id, reviewer_service_id: row.reviewer_service_id,
@@ -2486,7 +2486,7 @@ function backfillCanonicalServiceHandoffServiceIds(database: Database): void {
targetAgentType, targetAgentType,
sourceServiceId, sourceServiceId,
targetServiceId, targetServiceId,
} = canonicalizeServiceHandoffMetadata({ } = fillCanonicalServiceHandoffMetadata({
id: row.id, id: row.id,
chat_jid: row.chat_jid, chat_jid: row.chat_jid,
source_service_id: row.source_service_id, source_service_id: row.source_service_id,

View File

@@ -19,7 +19,10 @@ import {
getLatestMessageSeqAtOrBeforeFromDatabase, getLatestMessageSeqAtOrBeforeFromDatabase,
normalizeSeqCursor, normalizeSeqCursor,
} from './messages.js'; } from './messages.js';
import { canonicalizeServiceHandoffMetadata } from './canonical-role-metadata.js'; import {
fillCanonicalServiceHandoffMetadata,
readCanonicalServiceHandoffMetadata,
} from './canonical-role-metadata.js';
import { import {
getRouterStateFromDatabase, getRouterStateFromDatabase,
setRouterStateInDatabase, setRouterStateInDatabase,
@@ -133,7 +136,7 @@ function hydrateServiceHandoffRow(
targetAgentType, targetAgentType,
sourceServiceId, sourceServiceId,
targetServiceId, targetServiceId,
} = canonicalizeServiceHandoffMetadata({ } = readCanonicalServiceHandoffMetadata({
id: row.id, id: row.id,
chat_jid: row.chat_jid, chat_jid: row.chat_jid,
source_service_id: row.source_service_id, source_service_id: row.source_service_id,
@@ -249,7 +252,7 @@ export function createServiceHandoffInDatabase(
targetAgentType, targetAgentType,
sourceServiceId, sourceServiceId,
targetServiceId, targetServiceId,
} = canonicalizeServiceHandoffMetadata({ } = fillCanonicalServiceHandoffMetadata({
id: 'new', id: 'new',
chat_jid: input.chat_jid, chat_jid: input.chat_jid,
source_service_id: input.source_service_id, source_service_id: input.source_service_id,

View File

@@ -53,7 +53,7 @@ function normalizeStoredAgentType(
return undefined; return undefined;
} }
function resolveStoredWorkItemServiceId(args: { function fillCanonicalWorkItemServiceId(args: {
agentType: AgentType; agentType: AgentType;
deliveryRole?: PairedRoomRole | null; deliveryRole?: PairedRoomRole | null;
serviceId?: string | null; serviceId?: string | null;
@@ -65,16 +65,37 @@ function resolveStoredWorkItemServiceId(args: {
); );
} }
function readStoredWorkItemAgentType(
row: Pick<StoredWorkItemRow, 'id' | 'agent_type'>,
): AgentType {
const agentType = normalizeStoredAgentType(row.agent_type);
if (agentType) {
return agentType;
}
throw new Error(
`work_items(${row.id}): cannot read agent_type from stored row metadata`,
);
}
function readStoredWorkItemServiceId(
row: Pick<StoredWorkItemRow, 'id' | 'service_id'>,
): string {
if (row.service_id) {
return normalizeServiceId(row.service_id);
}
throw new Error(
`work_items(${row.id}): cannot read service_id from stored row metadata`,
);
}
function hydrateWorkItemRow(row: StoredWorkItemRow): WorkItem { function hydrateWorkItemRow(row: StoredWorkItemRow): WorkItem {
const agentType = normalizeStoredAgentType(row.agent_type) ?? 'claude-code'; const agentType = readStoredWorkItemAgentType(row);
return { return {
...row, ...row,
agent_type: agentType, agent_type: agentType,
service_id: resolveStoredWorkItemServiceId({ service_id: readStoredWorkItemServiceId(row),
agentType,
deliveryRole: row.delivery_role,
serviceId: row.service_id,
}),
}; };
} }
@@ -182,7 +203,7 @@ export function createProducedWorkItemInDatabase(
): WorkItem { ): WorkItem {
const now = new Date().toISOString(); const now = new Date().toISOString();
const agentType = input.agent_type || 'claude-code'; const agentType = input.agent_type || 'claude-code';
const serviceId = resolveStoredWorkItemServiceId({ const serviceId = fillCanonicalWorkItemServiceId({
agentType, agentType,
deliveryRole: input.delivery_role, deliveryRole: input.delivery_role,
serviceId: input.service_id, serviceId: input.service_id,