chore(runtime): pin orchestrator 0.0.20

This commit is contained in:
777genius 2026-05-06 20:20:26 +03:00
parent e96a74f4fa
commit 9e1abb0332
4 changed files with 785 additions and 13 deletions

View file

@ -1,27 +1,27 @@
{
"version": "0.0.19",
"sourceRef": "v0.0.19",
"version": "0.0.20",
"sourceRef": "v0.0.20",
"sourceRepository": "777genius/agent_teams_orchestrator",
"releaseRepository": "777genius/claude_agent_teams_ui",
"releaseTag": "v1.2.0",
"assets": {
"darwin-arm64": {
"file": "agent-teams-runtime-darwin-arm64-v0.0.19.tar.gz",
"file": "agent-teams-runtime-darwin-arm64-v0.0.20.tar.gz",
"archiveKind": "tar.gz",
"binaryName": "claude-multimodel"
},
"darwin-x64": {
"file": "agent-teams-runtime-darwin-x64-v0.0.19.tar.gz",
"file": "agent-teams-runtime-darwin-x64-v0.0.20.tar.gz",
"archiveKind": "tar.gz",
"binaryName": "claude-multimodel"
},
"linux-x64": {
"file": "agent-teams-runtime-linux-x64-v0.0.19.tar.gz",
"file": "agent-teams-runtime-linux-x64-v0.0.20.tar.gz",
"archiveKind": "tar.gz",
"binaryName": "claude-multimodel"
},
"win32-x64": {
"file": "agent-teams-runtime-win32-x64-v0.0.19.zip",
"file": "agent-teams-runtime-win32-x64-v0.0.20.zip",
"archiveKind": "zip",
"binaryName": "claude-multimodel.exe"
}

View file

@ -11064,16 +11064,23 @@ export class TeamProvisioningService {
return { busy: true, reason: 'opencode_no_active_lane', retryAfterIso };
}
const activeRecord = await this.createOpenCodePromptDeliveryLedger(
input.teamName,
identity.laneId
)
.getActiveForMember({
let activeRecord: OpenCodePromptDeliveryLedgerRecord | null;
try {
activeRecord = await this.createOpenCodePromptDeliveryLedger(
input.teamName,
identity.laneId
).getActiveForMember({
teamName: input.teamName,
memberName: identity.canonicalMemberName,
laneId: identity.laneId,
})
.catch(() => null);
});
} catch {
return {
busy: true,
reason: 'opencode_prompt_ledger_unavailable',
retryAfterIso,
};
}
if (activeRecord) {
return {
busy: true,

View file

@ -74,6 +74,111 @@ async function seedShadowReadyMetrics(input: {
);
}
async function seedNonBlockingShadowCollectingMetrics(input: {
teamsBasePath: string;
teamName: string;
memberName: string;
}): Promise<void> {
const metricsPath = path.join(
input.teamsBasePath,
input.teamName,
'.member-work-sync',
'indexes',
'metrics.json'
);
await fs.promises.mkdir(path.dirname(metricsPath), { recursive: true });
await fs.promises.writeFile(
metricsPath,
`${JSON.stringify(
{
schemaVersion: 2,
members: {
[input.memberName]: {
memberName: input.memberName,
state: 'caught_up',
agendaFingerprint: 'agenda:v1:seed',
actionableCount: 0,
evaluatedAt: '2026-01-01T00:00:00.000Z',
},
},
recentEvents: Array.from({ length: 18 }, (_, index) => ({
id: `seed-status-${index}`,
teamName: input.teamName,
memberName: input.memberName,
kind: 'status_evaluated',
state: 'caught_up',
agendaFingerprint: `agenda:v1:seed-${index}`,
recordedAt: new Date(Date.UTC(2026, 0, 1, index * 6)).toISOString(),
actionableCount: 0,
})),
},
null,
2
)}\n`,
'utf8'
);
}
async function seedBlockingShadowCollectingMetrics(input: {
teamsBasePath: string;
teamName: string;
memberName: string;
}): Promise<void> {
const nowMs = Date.now();
const firstObservedAt = new Date(nowMs - 1_000).toISOString();
const secondObservedAt = new Date(nowMs).toISOString();
const metricsPath = path.join(
input.teamsBasePath,
input.teamName,
'.member-work-sync',
'indexes',
'metrics.json'
);
await fs.promises.mkdir(path.dirname(metricsPath), { recursive: true });
await fs.promises.writeFile(
metricsPath,
`${JSON.stringify(
{
schemaVersion: 2,
members: {
[input.memberName]: {
memberName: input.memberName,
state: 'needs_sync',
agendaFingerprint: 'agenda:v1:seed',
actionableCount: 1,
evaluatedAt: firstObservedAt,
},
},
recentEvents: [
{
id: 'seed-status-0',
teamName: input.teamName,
memberName: input.memberName,
kind: 'status_evaluated',
state: 'needs_sync',
agendaFingerprint: 'agenda:v1:seed',
recordedAt: firstObservedAt,
actionableCount: 1,
},
{
id: 'seed-would-nudge-0',
teamName: input.teamName,
memberName: input.memberName,
kind: 'would_nudge',
state: 'needs_sync',
agendaFingerprint: 'agenda:v1:seed',
recordedAt: secondObservedAt,
actionableCount: 1,
},
],
},
null,
2
)}\n`,
'utf8'
);
}
async function waitForAssertion(assertion: () => Promise<void> | void): Promise<void> {
const deadline = Date.now() + 2_000;
let lastError: unknown;
@ -443,6 +548,492 @@ describe('createMemberWorkSyncFeature composition', () => {
}
});
it('delivers targeted OpenCode nudges during shadow collection and schedules a delivery wake', async () => {
const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot);
const teamsBasePath = getTeamsBasePath();
const teamName = 'team-opencode-targeted';
const memberName = 'alice';
const nudgeDeliveryWake = {
schedule: vi.fn(async () => undefined),
};
const feature = createMemberWorkSyncFeature({
teamsBasePath,
configReader: {
getConfig: vi.fn(async () => ({
name: teamName,
members: [{ name: memberName, providerId: 'opencode' }],
})),
} as never,
taskReader: {
getTasks: vi.fn(async () => [
{
id: 'task-1',
displayId: '11111111',
subject: 'Ship OpenCode targeted nudge',
status: 'pending',
owner: memberName,
},
]),
} as never,
kanbanManager: {
getState: vi.fn(async () => ({
teamName,
reviewers: [],
tasks: {},
})),
} as never,
membersMetaStore: {
getMembers: vi.fn(async () => []),
} as never,
isTeamActive: vi.fn(async () => true),
nudgeDeliveryWake,
queueQuietWindowMs: 1,
});
try {
await seedNonBlockingShadowCollectingMetrics({ teamsBasePath, teamName, memberName });
feature.noteTeamChange({ type: 'task', teamName, taskId: 'task-1' } as never);
await waitForAssertion(async () => {
expect(feature.getQueueDiagnostics()).toMatchObject({ reconciled: 1 });
const nudges = (await readInboxMessages({ teamsBasePath, teamName, memberName })).filter(
(message) => message.messageKind === 'member_work_sync_nudge'
);
expect(nudges).toHaveLength(1);
expect(nudges[0]?.text).toContain('11111111');
expect(nudgeDeliveryWake.schedule).toHaveBeenCalledTimes(1);
expect(nudgeDeliveryWake.schedule).toHaveBeenCalledWith({
teamName,
memberName,
messageId: nudges[0]?.messageId,
providerId: 'opencode',
reason: 'member_work_sync_nudge_inserted',
delayMs: 500,
});
await expect(feature.getMetrics({ teamName })).resolves.toMatchObject({
phase2Readiness: { state: 'collecting_shadow_data' },
});
await expect(feature.getStatus({ teamName, memberName })).resolves.toMatchObject({
state: 'needs_sync',
providerId: 'opencode',
shadow: { wouldNudge: true },
});
expect(
Object.values(await readMemberOutboxItems({ teamsBasePath, teamName, memberName }))
).toEqual([
expect.objectContaining({
status: 'delivered',
deliveredMessageId: nudges[0]?.messageId,
}),
]);
});
const journal = await fs.promises.readFile(
path.join(
teamsBasePath,
teamName,
'members',
memberName,
'.member-work-sync',
'journal.jsonl'
),
'utf8'
);
expect(journal).toContain('"event":"nudge_delivered"');
expect(journal).not.toContain('"reason":"phase2_not_ready"');
} finally {
await feature.dispose();
}
});
it('does not apply the OpenCode shadow-collection exception to Codex members', async () => {
const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot);
const teamsBasePath = getTeamsBasePath();
const teamName = 'team-codex-shadow-gated';
const memberName = 'bob';
const feature = createMemberWorkSyncFeature({
teamsBasePath,
configReader: {
getConfig: vi.fn(async () => ({
name: teamName,
members: [{ name: memberName, providerId: 'codex' }],
})),
} as never,
taskReader: {
getTasks: vi.fn(async () => [
{
id: 'task-1',
displayId: '11111111',
subject: 'Keep Codex gated during shadow collection',
status: 'pending',
owner: memberName,
},
]),
} as never,
kanbanManager: {
getState: vi.fn(async () => ({
teamName,
reviewers: [],
tasks: {},
})),
} as never,
membersMetaStore: {
getMembers: vi.fn(async () => []),
} as never,
isTeamActive: vi.fn(async () => true),
queueQuietWindowMs: 1,
});
try {
await seedNonBlockingShadowCollectingMetrics({ teamsBasePath, teamName, memberName });
feature.noteTeamChange({ type: 'task', teamName, taskId: 'task-1' } as never);
await waitForAssertion(async () => {
expect(feature.getQueueDiagnostics()).toMatchObject({ reconciled: 1 });
expect(await readInboxMessages({ teamsBasePath, teamName, memberName })).toEqual([]);
expect(await readMemberOutboxItems({ teamsBasePath, teamName, memberName })).toEqual({});
await expect(feature.getMetrics({ teamName })).resolves.toMatchObject({
phase2Readiness: { state: 'collecting_shadow_data' },
});
await expect(feature.getStatus({ teamName, memberName })).resolves.toMatchObject({
state: 'needs_sync',
providerId: 'codex',
shadow: { wouldNudge: true },
});
});
const journal = await fs.promises.readFile(
path.join(
teamsBasePath,
teamName,
'members',
memberName,
'.member-work-sync',
'journal.jsonl'
),
'utf8'
);
expect(journal).toContain('"event":"nudge_skipped"');
expect(journal).toContain('"reason":"phase2_not_ready"');
expect(journal).not.toContain('"event":"nudge_delivered"');
} finally {
await feature.dispose();
}
});
it('blocks targeted OpenCode nudges when phase2 metrics are unsafe', async () => {
const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot);
const teamsBasePath = getTeamsBasePath();
const teamName = 'team-opencode-blocking-metrics';
const memberName = 'alice';
const nudgeDeliveryWake = {
schedule: vi.fn(async () => undefined),
};
const feature = createMemberWorkSyncFeature({
teamsBasePath,
configReader: {
getConfig: vi.fn(async () => ({
name: teamName,
members: [{ name: memberName, providerId: 'opencode' }],
})),
} as never,
taskReader: {
getTasks: vi.fn(async () => [
{
id: 'task-1',
displayId: '11111111',
subject: 'Do not nudge when metrics are unsafe',
status: 'pending',
owner: memberName,
},
]),
} as never,
kanbanManager: {
getState: vi.fn(async () => ({
teamName,
reviewers: [],
tasks: {},
})),
} as never,
membersMetaStore: {
getMembers: vi.fn(async () => []),
} as never,
isTeamActive: vi.fn(async () => true),
nudgeDeliveryWake,
queueQuietWindowMs: 1,
});
try {
await seedBlockingShadowCollectingMetrics({ teamsBasePath, teamName, memberName });
feature.noteTeamChange({ type: 'task', teamName, taskId: 'task-1' } as never);
await waitForAssertion(async () => {
expect(feature.getQueueDiagnostics()).toMatchObject({ reconciled: 1 });
expect(await readInboxMessages({ teamsBasePath, teamName, memberName })).toEqual([]);
expect(await readMemberOutboxItems({ teamsBasePath, teamName, memberName })).toEqual({});
expect(nudgeDeliveryWake.schedule).not.toHaveBeenCalled();
await expect(feature.getMetrics({ teamName })).resolves.toMatchObject({
phase2Readiness: {
reasons: expect.arrayContaining(['would_nudge_rate_high']),
},
});
});
const journal = await fs.promises.readFile(
path.join(
teamsBasePath,
teamName,
'members',
memberName,
'.member-work-sync',
'journal.jsonl'
),
'utf8'
);
expect(journal).toContain('"event":"nudge_skipped"');
expect(journal).toContain('"reason":"phase2_not_ready"');
expect(journal).not.toContain('"event":"nudge_delivered"');
} finally {
await feature.dispose();
}
});
it('recovers targeted OpenCode nudge delivery after unsafe metrics become ready', async () => {
const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot);
const teamsBasePath = getTeamsBasePath();
const teamName = 'team-opencode-metrics-recovery';
const memberName = 'alice';
const nudgeDeliveryWake = {
schedule: vi.fn(async () => undefined),
};
const feature = createMemberWorkSyncFeature({
teamsBasePath,
configReader: {
getConfig: vi.fn(async () => ({
name: teamName,
members: [{ name: memberName, providerId: 'opencode' }],
})),
} as never,
taskReader: {
getTasks: vi.fn(async () => [
{
id: 'task-1',
displayId: '11111111',
subject: 'Recover OpenCode nudge after metrics ready',
status: 'pending',
owner: memberName,
},
]),
} as never,
kanbanManager: {
getState: vi.fn(async () => ({
teamName,
reviewers: [],
tasks: {},
})),
} as never,
membersMetaStore: {
getMembers: vi.fn(async () => []),
} as never,
isTeamActive: vi.fn(async () => true),
nudgeDeliveryWake,
queueQuietWindowMs: 1,
});
try {
await seedBlockingShadowCollectingMetrics({ teamsBasePath, teamName, memberName });
feature.noteTeamChange({ type: 'task', teamName, taskId: 'task-1' } as never);
await waitForAssertion(async () => {
expect(feature.getQueueDiagnostics()).toMatchObject({ reconciled: 1 });
expect(await readInboxMessages({ teamsBasePath, teamName, memberName })).toEqual([]);
expect(await readMemberOutboxItems({ teamsBasePath, teamName, memberName })).toEqual({});
expect(nudgeDeliveryWake.schedule).not.toHaveBeenCalled();
});
await seedShadowReadyMetrics({ teamsBasePath, teamName, memberName });
feature.noteTeamChange({ type: 'task', teamName, taskId: 'task-1' } as never);
await waitForAssertion(async () => {
const nudges = (await readInboxMessages({ teamsBasePath, teamName, memberName })).filter(
(message) => message.messageKind === 'member_work_sync_nudge'
);
expect(nudges).toHaveLength(1);
expect(nudges[0]?.text).toContain('11111111');
expect(nudgeDeliveryWake.schedule).toHaveBeenCalledTimes(1);
expect(nudgeDeliveryWake.schedule).toHaveBeenCalledWith({
teamName,
memberName,
messageId: nudges[0]?.messageId,
providerId: 'opencode',
reason: 'member_work_sync_nudge_inserted',
delayMs: 500,
});
expect(
Object.values(await readMemberOutboxItems({ teamsBasePath, teamName, memberName }))
).toEqual([
expect.objectContaining({
status: 'delivered',
deliveredMessageId: nudges[0]?.messageId,
}),
]);
});
} finally {
await feature.dispose();
}
});
it('keeps targeted OpenCode nudges retryable when prompt delivery is busy', async () => {
const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot);
const teamsBasePath = getTeamsBasePath();
const teamName = 'team-opencode-busy';
const memberName = 'alice';
const nudgeDeliveryWake = {
schedule: vi.fn(async () => undefined),
};
let promptDeliveryBusy = true;
const promptDeliveryBusySignal = {
isBusy: vi.fn(async () =>
promptDeliveryBusy
? {
busy: true,
reason: 'opencode_prompt_delivery_active',
retryAfterIso: '2026-05-05T12:05:00.000Z',
}
: { busy: false }
),
};
const feature = createMemberWorkSyncFeature({
teamsBasePath,
configReader: {
getConfig: vi.fn(async () => ({
name: teamName,
members: [{ name: memberName, providerId: 'opencode' }],
})),
} as never,
taskReader: {
getTasks: vi.fn(async () => [
{
id: 'task-1',
displayId: '11111111',
subject: 'Ship OpenCode busy nudge',
status: 'pending',
owner: memberName,
},
]),
} as never,
kanbanManager: {
getState: vi.fn(async () => ({
teamName,
reviewers: [],
tasks: {},
})),
} as never,
membersMetaStore: {
getMembers: vi.fn(async () => []),
} as never,
isTeamActive: vi.fn(async () => true),
extraBusySignals: [promptDeliveryBusySignal],
nudgeDeliveryWake,
queueQuietWindowMs: 1,
});
try {
await seedNonBlockingShadowCollectingMetrics({ teamsBasePath, teamName, memberName });
feature.noteTeamChange({ type: 'task', teamName, taskId: 'task-1' } as never);
await waitForAssertion(async () => {
expect(feature.getQueueDiagnostics()).toMatchObject({ reconciled: 1 });
expect(await readInboxMessages({ teamsBasePath, teamName, memberName })).toEqual([]);
expect(nudgeDeliveryWake.schedule).not.toHaveBeenCalled();
expect(
Object.values(await readMemberOutboxItems({ teamsBasePath, teamName, memberName }))
).toEqual([
expect.objectContaining({
status: 'failed_retryable',
lastError: 'member_busy:opencode_prompt_delivery_active',
nextAttemptAt: '2026-05-05T12:05:00.000Z',
}),
]);
});
const journal = await fs.promises.readFile(
path.join(
teamsBasePath,
teamName,
'members',
memberName,
'.member-work-sync',
'journal.jsonl'
),
'utf8'
);
expect(journal).toContain('"event":"member_busy"');
expect(journal).toContain('"reason":"member_busy:opencode_prompt_delivery_active"');
expect(journal).not.toContain('"event":"nudge_delivered"');
promptDeliveryBusy = false;
await forceRetryableOutboxDue({
teamsBasePath,
teamName,
memberName,
nextAttemptAt: new Date(Date.now() - 1_000).toISOString(),
});
await expect(feature.dispatchDueNudges([teamName])).resolves.toEqual({
claimed: 1,
delivered: 1,
superseded: 0,
retryable: 0,
terminal: 0,
});
await waitForAssertion(async () => {
const nudges = (await readInboxMessages({ teamsBasePath, teamName, memberName })).filter(
(message) => message.messageKind === 'member_work_sync_nudge'
);
expect(nudges).toHaveLength(1);
expect(nudges[0]?.text).toContain('11111111');
expect(nudgeDeliveryWake.schedule).toHaveBeenCalledTimes(1);
expect(nudgeDeliveryWake.schedule).toHaveBeenCalledWith({
teamName,
memberName,
messageId: nudges[0]?.messageId,
providerId: 'opencode',
reason: 'member_work_sync_nudge_inserted',
delayMs: 500,
});
expect(
Object.values(await readMemberOutboxItems({ teamsBasePath, teamName, memberName }))
).toEqual([
expect.objectContaining({
status: 'delivered',
deliveredMessageId: nudges[0]?.messageId,
}),
]);
});
const recoveredJournal = await fs.promises.readFile(
path.join(
teamsBasePath,
teamName,
'members',
memberName,
'.member-work-sync',
'journal.jsonl'
),
'utf8'
);
expect(recoveredJournal).toContain('"event":"nudge_delivered"');
} finally {
await feature.dispose();
}
});
it('keeps nudges gated until shadow readiness is reached, then delivers on the next reconcile', async () => {
const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot);

View file

@ -156,8 +156,10 @@ vi.mock('agent-teams-controller', () => ({
}));
import { buildLegacyInboxMessageId } from '../../../../src/main/services/team/inboxMessageIdentity';
import * as OpenCodeRuntimeStore from '../../../../src/main/services/team/opencode/store/OpenCodeRuntimeManifestEvidenceReader';
import { TeamConfigReader } from '../../../../src/main/services/team/TeamConfigReader';
import { TeamProvisioningService } from '../../../../src/main/services/team/TeamProvisioningService';
import { getTeamsBasePath } from '../../../../src/main/utils/pathDecoder';
function seedConfig(teamName: string): void {
hoisted.files.set(
@ -2738,4 +2740,176 @@ Messages:
const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]');
expect(rows[0].read).toBe(false);
});
it('fails closed when OpenCode prompt ledger cannot be inspected for work-sync busy checks', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';
const laneId = 'secondary:opencode:jack';
const teamsBasePath = getTeamsBasePath();
hoisted.files.set(
`${teamsBasePath}/${teamName}/config.json`,
JSON.stringify({
name: teamName,
projectPath: '/tmp/my-team',
members: [
{ name: 'team-lead', agentType: 'team-lead' },
{ name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' },
],
})
);
hoisted.files.set(
OpenCodeRuntimeStore.getOpenCodeRuntimeLaneIndexPath(teamsBasePath, teamName),
JSON.stringify({
version: 1,
updatedAt: '2026-02-23T17:30:00.000Z',
lanes: {
primary: {
laneId: 'primary',
state: 'active',
updatedAt: '2026-02-23T17:30:00.000Z',
},
[laneId]: {
laneId,
state: 'active',
updatedAt: '2026-02-23T17:30:00.000Z',
},
},
})
);
vi.spyOn(OpenCodeRuntimeStore, 'readOpenCodeRuntimeLaneIndex').mockResolvedValue({
version: 1,
updatedAt: '2026-02-23T17:30:00.000Z',
lanes: {
[laneId]: {
laneId,
state: 'active',
updatedAt: '2026-02-23T17:30:00.000Z',
},
},
});
hoisted.files.set(`${teamsBasePath}/${teamName}/inboxes/jack.json`, JSON.stringify([]));
(service as any).resolveOpenCodeMemberDeliveryIdentity = vi.fn(async () => ({
ok: true,
canonicalMemberName: 'jack',
laneId,
}));
vi.spyOn(service as any, 'createOpenCodePromptDeliveryLedger').mockReturnValue({
getActiveForMember: vi.fn(async () => {
throw new Error('ledger read failed');
}),
});
const busy = await service.getOpenCodeMemberDeliveryBusyStatus({
teamName,
memberName: 'jack',
nowIso: '2026-02-23T17:31:00.000Z',
});
expect(busy).toMatchObject({
busy: true,
reason: 'opencode_prompt_ledger_unavailable',
});
});
it('treats unread OpenCode foreground inbox messages as busy for work-sync checks', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';
const teamsBasePath = getTeamsBasePath();
hoisted.files.set(
`${teamsBasePath}/${teamName}/config.json`,
JSON.stringify({
name: teamName,
projectPath: '/tmp/my-team',
members: [
{ name: 'team-lead', agentType: 'team-lead' },
{ name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' },
],
})
);
hoisted.files.set(
`${teamsBasePath}/${teamName}/inboxes/jack.json`,
JSON.stringify([
{
from: 'user',
to: 'jack',
text: 'Please check the current issue.',
timestamp: '2026-02-23T17:31:00.000Z',
read: false,
messageId: 'foreground-message-1',
messageKind: 'direct',
},
])
);
const busy = await service.getOpenCodeMemberDeliveryBusyStatus({
teamName,
memberName: 'jack',
nowIso: '2026-02-23T17:31:10.000Z',
});
expect(busy).toMatchObject({
busy: true,
reason: 'opencode_foreground_inbox_unread',
activeMessageId: 'foreground-message-1',
});
});
it('does not treat unread OpenCode work-sync nudges as foreground busy blockers', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';
const laneId = 'secondary:opencode:jack';
const teamsBasePath = getTeamsBasePath();
hoisted.files.set(
`${teamsBasePath}/${teamName}/config.json`,
JSON.stringify({
name: teamName,
projectPath: '/tmp/my-team',
members: [
{ name: 'team-lead', agentType: 'team-lead' },
{ name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' },
],
})
);
hoisted.files.set(
`${teamsBasePath}/${teamName}/inboxes/jack.json`,
JSON.stringify([
{
from: 'system',
to: 'jack',
text: 'Work sync check.',
timestamp: '2026-02-23T17:31:00.000Z',
read: false,
messageId: 'work-sync-nudge-1',
messageKind: 'member_work_sync_nudge',
},
])
);
(service as any).resolveOpenCodeMemberDeliveryIdentity = vi.fn(async () => ({
ok: true,
canonicalMemberName: 'jack',
laneId,
}));
vi.spyOn(OpenCodeRuntimeStore, 'readOpenCodeRuntimeLaneIndex').mockResolvedValue({
version: 1,
updatedAt: '2026-02-23T17:30:00.000Z',
lanes: {
[laneId]: {
laneId,
state: 'active',
updatedAt: '2026-02-23T17:30:00.000Z',
},
},
});
vi.spyOn(service as any, 'createOpenCodePromptDeliveryLedger').mockReturnValue({
getActiveForMember: vi.fn(async () => null),
});
const busy = await service.getOpenCodeMemberDeliveryBusyStatus({
teamName,
memberName: 'jack',
nowIso: '2026-02-23T17:31:10.000Z',
});
expect(busy).toEqual({ busy: false });
});
});