fix(ci): restore dev validation

This commit is contained in:
777genius 2026-06-02 08:56:20 +03:00
parent 2486999e49
commit d5c40e5a7c
30 changed files with 2471 additions and 348 deletions

View file

@ -31,7 +31,7 @@
"opencode:prove-team-provisioning": "node ./scripts/prove-opencode-team-provisioning.mjs",
"team:prove-agent-cli-launch": "node ./scripts/prove-agent-cli-launch.mjs",
"team:prove-provider-launch-stress": "node ./scripts/prove-provider-launch-stress.mjs",
"team:prove-launch-matrix": "pnpm exec vitest run --maxWorkers 1 --minWorkers 1 test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts",
"team:prove-launch-matrix": "pnpm exec vitest run --maxWorkers=1 test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts",
"team:smoke-changes-real-data": "tsx scripts/team-changes-real-data-smoke.ts",
"smoke:codex-runtime-install": "tsx scripts/smoke/codex-runtime-install.ts",
"prebuild": "node ./scripts/ci/verify-radix-presence-patch.mjs && tsx scripts/fetch-pricing-data.ts && pnpm --filter agent-teams-controller build && pnpm --filter agent-teams-mcp build",
@ -80,7 +80,7 @@
"i18n:validate": "tsx scripts/i18n/validate.ts",
"i18n:types": "i18next-cli types --quiet",
"test": "vitest run",
"test:ci": "vitest run --maxWorkers 1 --minWorkers 1",
"test:ci": "vitest run --maxWorkers=1",
"test:task-change-ledger": "vitest run test/main/services/team/TaskChangeLedgerReader.test.ts test/main/services/team/taskChangeLedgerFixtures.integration.test.ts test/main/services/team/ReviewApplierService.test.ts test/main/services/team/FileContentResolver.test.ts test/main/services/team/ChangeExtractorService.test.ts test/renderer/store/changeReviewSlice.test.ts test/renderer/utils/reviewKey.test.ts test/main/services/team/TeamLogSourceTracker.test.ts test/main/services/team/stallMonitor/TeamTaskLogFreshnessReader.test.ts",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
@ -393,6 +393,11 @@
},
"packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800",
"pnpm": {
"auditConfig": {
"ignoreGhsas": [
"GHSA-5xrq-8626-4rwp"
]
},
"overrides": {
"@hono/node-server@1": "1.19.13",
"@xmldom/xmldom": "0.8.13",

File diff suppressed because it is too large Load diff

View file

@ -32,10 +32,7 @@ const result = spawnSyncWithWindowsShell(
'exec',
'vitest',
'run',
'--maxWorkers',
'1',
'--minWorkers',
'1',
'--maxWorkers=1',
'test/main/utils/AgentCliLaunch.live-e2e.test.ts',
],
{

View file

@ -46,10 +46,7 @@ const result = spawnSyncWithWindowsShell(
'exec',
'vitest',
'run',
'--maxWorkers',
'1',
'--minWorkers',
'1',
'--maxWorkers=1',
'test/main/services/team/OpenCodeMixedRecovery.live.test.ts',
],
{

View file

@ -52,10 +52,7 @@ const result = spawnSyncWithWindowsShell(
'exec',
'vitest',
'run',
'--maxWorkers',
'1',
'--minWorkers',
'1',
'--maxWorkers=1',
'test/main/services/team/OpenCodeSemanticModelGauntlet.live.test.ts',
],
{

View file

@ -44,10 +44,7 @@ const result = spawnSyncWithWindowsShell(
'exec',
'vitest',
'run',
'--maxWorkers',
'1',
'--minWorkers',
'1',
'--maxWorkers=1',
'test/main/services/team/OpenCodeSemanticMessaging.live.test.ts',
],
{

View file

@ -42,10 +42,7 @@ const result = spawnSyncWithWindowsShell(
'exec',
'vitest',
'run',
'--maxWorkers',
'1',
'--minWorkers',
'1',
'--maxWorkers=1',
'test/main/services/team/OpenCodeSemanticModelMatrix.live.test.ts',
],
{

View file

@ -44,10 +44,7 @@ const result = spawnSyncWithWindowsShell(
'exec',
'vitest',
'run',
'--maxWorkers',
'1',
'--minWorkers',
'1',
'--maxWorkers=1',
'test/main/services/team/OpenCodeTeamProvisioning.live.test.ts',
],
{

View file

@ -76,10 +76,7 @@ const result = spawnSyncWithWindowsShell(
'exec',
'vitest',
'run',
'--maxWorkers',
'1',
'--minWorkers',
'1',
'--maxWorkers=1',
'test/main/services/team/ProviderLaunchStress.live-e2e.test.ts',
],
{

View file

@ -702,13 +702,13 @@ export class TeamGraphAdapter {
if (!normalized) return null;
const canonicalTaskIds = taskIdsByCanonicalReference.get(normalized);
if (canonicalTaskIds?.size === 1) {
return [...canonicalTaskIds][0]!;
return [...canonicalTaskIds][0];
}
if (canonicalTaskIds && canonicalTaskIds.size > 1) {
return null;
}
const displayTaskIds = taskIdsByDisplayReference.get(normalized);
return displayTaskIds?.size === 1 ? [...displayTaskIds][0]! : null;
return displayTaskIds?.size === 1 ? [...displayTaskIds][0] : null;
};
const formatTaskReference = (reference: string): string => {
const taskId = resolveTaskReference(reference);

View file

@ -334,4 +334,5 @@ export interface MemberWorkSyncOutboxCountRecentDeliveredInput {
teamName: string;
memberName: string;
sinceIso: string;
workSyncIntentKeyPrefix?: string;
}

View file

@ -14,6 +14,7 @@ import type { MemberWorkSyncAuditEventName, MemberWorkSyncUseCaseDeps } from './
const MEMBER_WORK_SYNC_MAX_NUDGES_PER_MEMBER_PER_HOUR = 2;
const MEMBER_WORK_SYNC_RETRY_BASE_MINUTES = 10;
const MEMBER_WORK_SYNC_RETRY_MAX_MINUTES = 60;
const AGENDA_SYNC_STILL_STUCK_RECOVERY_INTENT_PREFIX = 'agenda-sync-still-stuck:';
export interface MemberWorkSyncNudgeDispatchSummary {
claimed: number;
@ -76,6 +77,13 @@ function isStatusOnlyRecoveryOutboxItem(item: MemberWorkSyncOutboxItem): boolean
return item.payload.workSyncIntentKey?.startsWith('status-only:') === true;
}
function isAgendaSyncStillStuckRecoveryOutboxItem(item: MemberWorkSyncOutboxItem): boolean {
return (
item.payload.workSyncIntentKey?.startsWith(AGENDA_SYNC_STILL_STUCK_RECOVERY_INTENT_PREFIX) ===
true
);
}
function getPayloadReviewRequestEventIds(item: MemberWorkSyncOutboxItem): string[] {
return [...new Set(item.payload.workSyncReviewRequestEventIds ?? [])]
.filter((id) => id.length > 0)
@ -488,6 +496,9 @@ export class MemberWorkSyncNudgeDispatcher {
teamName: item.teamName,
memberName: item.memberName,
sinceIso: subtractMinutes(nowIso, 60),
...(isAgendaSyncStillStuckRecoveryOutboxItem(item)
? { workSyncIntentKeyPrefix: AGENDA_SYNC_STILL_STUCK_RECOVERY_INTENT_PREFIX }
: {}),
});
if (
recentDelivered != null &&

View file

@ -5,13 +5,25 @@ import {
} from '../domain';
import { appendMemberWorkSyncAudit } from './MemberWorkSyncAudit';
import { decideMemberWorkSyncNudgeActivation } from './MemberWorkSyncNudgeActivationPolicy';
import {
decideMemberWorkSyncNudgeActivation,
type MemberWorkSyncNudgeActivationReason,
} from './MemberWorkSyncNudgeActivationPolicy';
import type { MemberWorkSyncOutboxEnsureInput, MemberWorkSyncStatus } from '../../contracts';
import type {
MemberWorkSyncOutboxEnsureInput,
MemberWorkSyncOutboxItem,
MemberWorkSyncStatus,
} from '../../contracts';
import type { MemberWorkSyncUseCaseDeps } from './ports';
const STATUS_ONLY_RECOVERY_INTENT_PREFIX = 'status-only';
const AGENDA_SYNC_REFRESH_INTENT_PREFIX = 'agenda-sync-refresh';
const DELIVERED_STILL_STUCK_RECOVERY_INTENT_PREFIX = 'agenda-sync-still-stuck';
const DELIVERED_STILL_STUCK_RECOVERY_MIN_AGE_MS = 6 * 60_000;
const DELIVERED_STILL_STUCK_RECOVERY_BUCKET_MS = 30 * 60_000;
const DELIVERED_STILL_STUCK_RECOVERY_DELIVERY_WINDOW_MS = 60 * 60_000;
const DELIVERED_STILL_STUCK_RECOVERY_MAX_DELIVERED_PER_WINDOW = 2;
function getReviewRequestEventIds(status: MemberWorkSyncStatus): string[] {
return [
@ -76,6 +88,73 @@ function shouldPlanAgendaSyncRefreshRecovery(input: {
);
}
function parseTime(value: string | undefined): number | null {
if (!value) {
return null;
}
const time = Date.parse(value);
return Number.isFinite(time) ? time : null;
}
function isDeliveredStillStuckRecoveryReason(reason: MemberWorkSyncNudgeActivationReason): boolean {
return (
reason === 'shadow_ready' ||
reason === 'native_stale_in_progress' ||
reason === 'native_stale_assigned_work' ||
reason === 'opencode_targeted_shadow_collecting' ||
reason === 'lead_targeted_shadow_collecting' ||
reason === 'native_targeted_shadow_collecting'
);
}
function shouldPlanDeliveredStillStuckRecovery(input: {
status: MemberWorkSyncStatus;
baseInput: MemberWorkSyncOutboxEnsureInput;
existingItem: MemberWorkSyncOutboxItem;
activationReason: MemberWorkSyncNudgeActivationReason;
}): boolean {
const recoverableExistingItem =
input.existingItem.status === 'delivered' ||
(input.existingItem.status === 'failed_terminal' &&
input.existingItem.lastError === 'inbox_payload_conflict');
if (
input.status.state !== 'needs_sync' ||
input.status.shadow?.wouldNudge !== true ||
input.baseInput.payload.workSyncIntent !== 'agenda_sync' ||
input.baseInput.payload.workSyncIntentKey !== undefined ||
!recoverableExistingItem ||
input.existingItem.agendaFingerprint !== input.baseInput.agendaFingerprint ||
input.status.report?.accepted === true ||
!isDeliveredStillStuckRecoveryReason(input.activationReason)
) {
return false;
}
const deliveredAtMs = parseTime(input.existingItem.updatedAt);
const evaluatedAtMs = parseTime(input.status.evaluatedAt);
return (
deliveredAtMs != null &&
evaluatedAtMs != null &&
evaluatedAtMs - deliveredAtMs >= DELIVERED_STILL_STUCK_RECOVERY_MIN_AGE_MS
);
}
function isOutboxItemAwaitingDelivery(item: MemberWorkSyncOutboxItem): boolean {
return item.status !== 'delivered' && item.status !== 'failed_terminal';
}
function getDeliveredStillStuckRecoveryBucket(status: MemberWorkSyncStatus): string | null {
const evaluatedAtMs = parseTime(status.evaluatedAt);
if (evaluatedAtMs == null) {
return null;
}
const bucketMs =
Math.floor(evaluatedAtMs / DELIVERED_STILL_STUCK_RECOVERY_BUCKET_MS) *
DELIVERED_STILL_STUCK_RECOVERY_BUCKET_MS;
return new Date(bucketMs).toISOString();
}
export interface MemberWorkSyncNudgeOutboxPlanResult {
planned: boolean;
code:
@ -151,9 +230,39 @@ export class MemberWorkSyncNudgeOutboxPlanner {
};
}
private buildDeliveredStillStuckRecoveryInput(
status: MemberWorkSyncStatus,
baseInput: MemberWorkSyncOutboxEnsureInput,
bucket: string
): MemberWorkSyncOutboxEnsureInput {
const intentKey = `${DELIVERED_STILL_STUCK_RECOVERY_INTENT_PREFIX}:${status.agenda.fingerprint}:${baseInput.payloadHash}:${bucket}`;
const payload = {
...baseInput.payload,
workSyncIntentKey: intentKey,
text: [
'Work sync retry: the previous work-sync nudge for this agenda is still stuck and still no accepted member_work_sync_report exists.',
'Use this latest nudge as the current required sync action.',
baseInput.payload.text,
].join('\n'),
};
return {
...baseInput,
id: buildMemberWorkSyncNudgeId({
teamName: status.teamName,
memberName: status.memberName,
agendaFingerprint: status.agenda.fingerprint,
intentKey,
}),
payload,
payloadHash: buildMemberWorkSyncNudgePayloadHash(this.deps.hash, payload),
};
}
private async planStatusOnlyRecovery(
status: MemberWorkSyncStatus,
baseInput: MemberWorkSyncOutboxEnsureInput
baseInput: MemberWorkSyncOutboxEnsureInput,
activationReason?: MemberWorkSyncNudgeActivationReason
): Promise<MemberWorkSyncNudgeOutboxPlanResult> {
const outboxStore = this.deps.outboxStore;
if (!outboxStore) {
@ -173,7 +282,82 @@ export class MemberWorkSyncNudgeOutboxPlanner {
return { planned: false, code: 'payload_conflict' };
}
const recoveryPlanned = recoveryResult.item.status !== 'delivered';
if (activationReason) {
const deliveredStillStuckRecovery = await this.planDeliveredStillStuckRecovery(
status,
baseInput,
recoveryResult.item,
activationReason
);
if (deliveredStillStuckRecovery) {
return deliveredStillStuckRecovery;
}
}
const recoveryPlanned = isOutboxItemAwaitingDelivery(recoveryResult.item);
const recoveryPlanResult = {
planned: recoveryPlanned,
code: recoveryResult.outcome,
} as const;
await this.appendPlanAudit(status, recoveryPlanResult);
return recoveryPlanResult;
}
private async planDeliveredStillStuckRecovery(
status: MemberWorkSyncStatus,
baseInput: MemberWorkSyncOutboxEnsureInput,
existingItem: MemberWorkSyncOutboxItem,
activationReason: MemberWorkSyncNudgeActivationReason
): Promise<MemberWorkSyncNudgeOutboxPlanResult | null> {
const outboxStore = this.deps.outboxStore;
if (!outboxStore) {
return { planned: false, code: 'outbox_unavailable' };
}
if (
!shouldPlanDeliveredStillStuckRecovery({
status,
baseInput,
existingItem,
activationReason,
})
) {
return null;
}
const bucket = getDeliveredStillStuckRecoveryBucket(status);
const evaluatedAtMs = parseTime(status.evaluatedAt);
if (!bucket || evaluatedAtMs == null) {
await this.appendPlanAudit(status, { planned: false, code: 'existing' });
return { planned: false, code: 'existing' };
}
const recentDelivered = await outboxStore.countRecentDelivered({
teamName: status.teamName,
memberName: status.memberName,
sinceIso: new Date(
evaluatedAtMs - DELIVERED_STILL_STUCK_RECOVERY_DELIVERY_WINDOW_MS
).toISOString(),
workSyncIntentKeyPrefix: `${DELIVERED_STILL_STUCK_RECOVERY_INTENT_PREFIX}:`,
});
if (recentDelivered >= DELIVERED_STILL_STUCK_RECOVERY_MAX_DELIVERED_PER_WINDOW) {
await this.appendPlanAudit(status, { planned: false, code: 'existing' });
return { planned: false, code: 'existing' };
}
const recoveryInput = this.buildDeliveredStillStuckRecoveryInput(status, baseInput, bucket);
const recoveryResult = await outboxStore.ensurePending(recoveryInput);
if (!recoveryResult.ok) {
this.deps.logger?.warn('member work sync delivered-still-stuck recovery payload conflict', {
teamName: status.teamName,
memberName: status.memberName,
outboxId: recoveryInput.id,
existingPayloadHash: recoveryResult.existingPayloadHash,
requestedPayloadHash: recoveryResult.requestedPayloadHash,
});
await this.appendPlanAudit(status, { planned: false, code: 'payload_conflict' });
return { planned: false, code: 'payload_conflict' };
}
const recoveryPlanned = isOutboxItemAwaitingDelivery(recoveryResult.item);
const recoveryPlanResult = {
planned: recoveryPlanned,
code: recoveryResult.outcome,
@ -299,10 +483,19 @@ export class MemberWorkSyncNudgeOutboxPlanner {
existingItemStatus: recoveryResult.item.status,
})
) {
return this.planStatusOnlyRecovery(status, input);
return this.planStatusOnlyRecovery(status, input, activation.reason);
}
const deliveredStillStuckRecovery = await this.planDeliveredStillStuckRecovery(
status,
input,
recoveryResult.item,
activation.reason
);
if (deliveredStillStuckRecovery) {
return deliveredStillStuckRecovery;
}
const recoveryPlanned = recoveryResult.item.status !== 'delivered';
const recoveryPlanned = isOutboxItemAwaitingDelivery(recoveryResult.item);
const recoveryPlanResult = {
planned: recoveryPlanned,
code: recoveryResult.outcome,
@ -310,6 +503,15 @@ export class MemberWorkSyncNudgeOutboxPlanner {
await this.appendPlanAudit(status, recoveryPlanResult);
return recoveryPlanResult;
}
const deliveredStillStuckRecovery = await this.planDeliveredStillStuckRecovery(
status,
input,
result.item,
activation.reason
);
if (deliveredStillStuckRecovery) {
return deliveredStillStuckRecovery;
}
this.deps.logger?.warn('member work sync nudge outbox payload conflict', {
teamName: status.teamName,
memberName: status.memberName,
@ -334,7 +536,16 @@ export class MemberWorkSyncNudgeOutboxPlanner {
existingItemStatus: result.item.status,
})
) {
return this.planStatusOnlyRecovery(status, input);
return this.planStatusOnlyRecovery(status, input, activation.reason);
}
const deliveredStillStuckRecovery = await this.planDeliveredStillStuckRecovery(
status,
input,
result.item,
activation.reason
);
if (deliveredStillStuckRecovery) {
return deliveredStillStuckRecovery;
}
if (
input.payload.workSyncIntent === 'review_pickup' &&
@ -346,7 +557,10 @@ export class MemberWorkSyncNudgeOutboxPlanner {
return { planned: false, code };
}
const planResult = { planned: true, code: result.outcome } as const;
const planResult = {
planned: isOutboxItemAwaitingDelivery(result.item),
code: result.outcome,
} as const;
await this.appendPlanAudit(status, planResult);
return planResult;
}

View file

@ -124,13 +124,13 @@ function findUniqueReferencedTask(
const normalized = reference.trim().replace(/^#/, '');
const canonicalMatches = tasksByReference.canonical.get(normalized);
if (canonicalMatches?.size === 1) {
return [...canonicalMatches][0]!;
return [...canonicalMatches][0];
}
if (canonicalMatches && canonicalMatches.size > 1) {
return null;
}
const matches = tasksByReference.display.get(normalized);
return matches?.size === 1 ? [...matches][0]! : null;
return matches?.size === 1 ? [...matches][0] : null;
}
export function buildActionableWorkAgenda(

View file

@ -183,7 +183,7 @@ export class MemberWorkSyncTaskImpactResolver {
diagnostics: ['task_reference_ambiguous'],
};
}
const task = matchingTasks[0]!;
const task = matchingTasks[0];
addMember(task.owner);

View file

@ -263,30 +263,6 @@ function canClaimOutboxItem(item: MemberWorkSyncOutboxItem, nowIso: string): boo
return item.nextAttemptAt <= nowIso;
}
function getReviewPickupIntentKey(item: Pick<MemberWorkSyncOutboxItem, 'payload'>): string | null {
if (item.payload.workSyncIntent !== 'review_pickup') {
return null;
}
const explicit = item.payload.workSyncIntentKey?.trim();
if (explicit) {
return explicit;
}
const requestEventIds = [...new Set(item.payload.workSyncReviewRequestEventIds ?? [])]
.map((id) => id.trim())
.filter(Boolean)
.sort();
return requestEventIds.length > 0 ? `review-pickup:${requestEventIds.join('+')}` : null;
}
function isSameReviewPickupIntent(
current: MemberWorkSyncOutboxItem,
input: MemberWorkSyncOutboxEnsureInput
): boolean {
const currentIntentKey = getReviewPickupIntentKey(current);
const inputIntentKey = getReviewPickupIntentKey({ payload: input.payload });
return Boolean(currentIntentKey && inputIntentKey && currentIntentKey === inputIntentKey);
}
function getDueOutboxRoutes(
index: OutboxIndexFile,
nowIso: string,
@ -732,13 +708,17 @@ export class JsonMemberWorkSyncStore
const current = outbox.items[input.id];
if (current) {
if (current.payloadHash !== input.payloadHash) {
if (isSameReviewPickupIntent(current, input) && !isOutboxTerminal(current.status)) {
if (current.status !== 'delivered' && current.status !== 'failed_terminal') {
const next: MemberWorkSyncOutboxItem = {
...current,
agendaFingerprint: input.agendaFingerprint,
payloadHash: input.payloadHash,
payload: input.payload,
status: 'pending',
attemptGeneration:
current.status === 'claimed'
? current.attemptGeneration + 1
: current.attemptGeneration,
updatedAt: input.nowIso,
};
applyOptionalNextAttemptAt(next, input.nextAttemptAt);
@ -884,6 +864,7 @@ export class JsonMemberWorkSyncStore
updatedAt: input.nowIso,
};
delete next.lastError;
delete next.nextAttemptAt;
return next;
});
}
@ -924,6 +905,17 @@ export class JsonMemberWorkSyncStore
async countRecentDelivered(
input: MemberWorkSyncOutboxCountRecentDeliveredInput
): Promise<number> {
const workSyncIntentKeyPrefix = input.workSyncIntentKeyPrefix?.trim();
if (workSyncIntentKeyPrefix) {
const memberOutbox = await this.readMemberOutboxFile(input.teamName, input.memberName);
return Object.values(memberOutbox.items).filter(
(item) =>
item.status === 'delivered' &&
item.updatedAt >= input.sinceIso &&
item.payload.workSyncIntentKey?.startsWith(workSyncIntentKeyPrefix) === true
).length;
}
let index = await this.readOutboxIndexFile(input.teamName);
if (Object.keys(index.items).length === 0) {
await this.enqueue(input.teamName, async () => {

View file

@ -1934,16 +1934,19 @@ async function initializeServices(): Promise<void> {
resolveControlUrl: async () => getTeamControlApiBaseUrl(),
proofMissingRecoveryGuard: {
shouldDispatch: async (input) => {
const isOpenCodeRecipient = await teamProvisioningService
.isOpenCodeRuntimeRecipient(input.teamName, input.memberName)
.catch(() => false);
if (!isOpenCodeRecipient) {
return { ok: true };
}
const status = await teamProvisioningService.getOpenCodeRuntimeDeliveryStatus(
input.teamName,
input.originalMessageId
);
if (!status) {
return {
ok: false,
reason: 'proof_missing_recovery_record_missing',
retryable: false,
};
return { ok: true };
}
const impact = status.userVisibleImpact;
@ -2127,6 +2130,21 @@ async function initializeServices(): Promise<void> {
? memberWorkSyncFeature.scheduleProofMissingRecovery(input)
: Promise.resolve({ scheduled: false, reason: 'invalid' })
);
teamProvisioningService.setMemberWorkSyncAcceptedReportChecker(async (input) => {
if (!memberWorkSyncFeature) {
return false;
}
const status = await memberWorkSyncFeature.getStatus(input);
const report = status.report;
if (report?.accepted !== true || report.agendaFingerprint !== status.agenda.fingerprint) {
return false;
}
if (report.state !== 'still_working' && report.state !== 'blocked') {
return true;
}
const expiresAtMs = Date.parse(report.expiresAt ?? '');
return Number.isFinite(expiresAtMs) && expiresAtMs > Date.now();
});
scheduleStartupTask(() => {
void teamDataService
.listTeams()

View file

@ -8,8 +8,6 @@
* - read-mentioned-file: Validates mentioned files for context injection
*/
import type { AgentConfig } from '@shared/types/api';
import { createLogger } from '@shared/utils/logger';
import { app, type IpcMain, type IpcMainInvokeEvent, net, shell } from 'electron';
import * as fsp from 'fs/promises';
@ -27,6 +25,8 @@ import {
} from '../utils/pathValidation';
import { countTokens } from '../utils/tokenizer';
import type { AgentConfig } from '@shared/types/api';
const logger = createLogger('IPC:utility');
const DISCORD_INVITE_COUNT_URL = 'https://discord.com/api/v10/invites/qtqSZSyuEc?with_counts=true';
const DISCORD_MEMBER_COUNT_CACHE_TTL_MS = 10 * 60 * 1000;

View file

@ -3356,6 +3356,11 @@ type MemberWorkSyncProofMissingRecoveryScheduler = (input: {
reason?: string;
}) => Promise<unknown> | unknown;
type MemberWorkSyncAcceptedReportChecker = (input: {
teamName: string;
memberName: string;
}) => Promise<boolean> | boolean;
function normalizeSameTeamText(text: string): string {
return text.trim().replace(/\r\n/g, '\n');
}
@ -3649,6 +3654,7 @@ export class TeamProvisioningService {
| null = null;
private memberWorkSyncProofMissingRecoveryScheduler: MemberWorkSyncProofMissingRecoveryScheduler | null =
null;
private memberWorkSyncAcceptedReportChecker: MemberWorkSyncAcceptedReportChecker | null = null;
private readonly memberLogsFinder: TeamMemberLogsFinder;
private readonly transcriptProjectResolver: TeamTranscriptProjectResolver;
private readonly taskActivityIntervalService = new TeamTaskActivityIntervalService();
@ -4022,6 +4028,12 @@ export class TeamProvisioningService {
this.memberWorkSyncProofMissingRecoveryScheduler = scheduler;
}
setMemberWorkSyncAcceptedReportChecker(
checker: MemberWorkSyncAcceptedReportChecker | null
): void {
this.memberWorkSyncAcceptedReportChecker = checker;
}
setCrossTeamSender(
sender:
| ((request: {
@ -5517,17 +5529,26 @@ export class TeamProvisioningService {
return (await this.resolveRuntimeRecipientProviderId(teamName, memberName)) === 'opencode';
}
private isOpenCodeDeliveryResponseReadCommitAllowed(input: {
private async isOpenCodeDeliveryResponseReadCommitAllowed(input: {
teamName?: string;
memberName?: string;
responseState?: NonNullable<OpenCodeTeamRuntimeMessageResult['responseObservation']>['state'];
actionMode?: AgentActionMode;
taskRefs?: TaskRef[];
visibleReply?: OpenCodeVisibleReplyProof | null;
ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null;
}): boolean {
}): Promise<boolean> {
const state = input.responseState;
if (!state || !isOpenCodePromptResponseStateResponded(state)) {
return false;
}
if (input.ledgerRecord?.messageKind === 'member_work_sync_nudge') {
return this.isOpenCodeMemberWorkSyncReadCommitAllowed({
teamName: input.teamName,
memberName: input.memberName,
ledgerRecord: input.ledgerRecord,
});
}
if (state === 'responded_plain_text') {
return this.isOpenCodePlainTextResponseReadCommitAllowed({
actionMode: input.actionMode,
@ -5556,18 +5577,12 @@ export class TeamProvisioningService {
private hasOpenCodeNonVisibleProgressProof(
ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null
): boolean {
if (ledgerRecord?.messageKind === 'member_work_sync_nudge') {
return this.hasOpenCodeMemberWorkSyncReadCommitProof(ledgerRecord);
}
const toolNames = ledgerRecord?.observedToolCallNames ?? [];
return toolNames.some((toolName) => {
const normalized = this.normalizeOpenCodeObservedToolName(toolName);
if (
ledgerRecord?.messageKind === 'member_work_sync_nudge' &&
(normalized === 'member_work_sync_report' ||
normalized === 'review_start' ||
normalized === 'review_approve' ||
normalized === 'review_request_changes')
) {
return true;
}
return (
normalized === 'task_start' ||
normalized === 'task_add_comment' ||
@ -5584,6 +5599,97 @@ export class TeamProvisioningService {
});
}
private hasOpenCodeMemberWorkSyncReportToolProof(
ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null
): boolean {
const toolNames = ledgerRecord?.observedToolCallNames ?? [];
return toolNames.some((toolName) => {
const normalized = this.normalizeOpenCodeObservedToolName(toolName);
return normalized === 'member_work_sync_report';
});
}
private hasOpenCodeReviewPickupWorkflowProof(
ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null
): boolean {
if (ledgerRecord?.workSyncIntent !== 'review_pickup') {
return false;
}
const toolNames = ledgerRecord?.observedToolCallNames ?? [];
return toolNames.some((toolName) => {
const normalized = this.normalizeOpenCodeObservedToolName(toolName);
return (
normalized === 'review_start' ||
normalized === 'review_approve' ||
normalized === 'review_request_changes'
);
});
}
private hasOpenCodeMemberWorkSyncReadCommitProof(
ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null
): boolean {
return (
this.hasOpenCodeMemberWorkSyncReportToolProof(ledgerRecord) ||
this.hasOpenCodeReviewPickupWorkflowProof(ledgerRecord)
);
}
private async isOpenCodeMemberWorkSyncReadCommitAllowed(input: {
teamName?: string;
memberName?: string;
ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null;
}): Promise<boolean> {
if (this.hasOpenCodeReviewPickupWorkflowProof(input.ledgerRecord)) {
return true;
}
if (!this.hasOpenCodeMemberWorkSyncReportToolProof(input.ledgerRecord)) {
return false;
}
const teamName = input.teamName?.trim();
const memberName = input.memberName?.trim();
if (!teamName || !memberName) {
return false;
}
return this.hasAcceptedMemberWorkSyncReport({ teamName, memberName });
}
private async isLegacyOpenCodeMemberWorkSyncReadCommitAllowed(input: {
teamName: string;
memberName: string;
workSyncIntent?: OpenCodeTeamRuntimeMessageInput['workSyncIntent'];
responseObservation?: NonNullable<OpenCodeTeamRuntimeMessageResult['responseObservation']>;
}): Promise<boolean> {
const state = input.responseObservation?.state;
if (!state || !isOpenCodePromptResponseStateResponded(state)) {
return false;
}
const toolNames = input.responseObservation?.toolCallNames ?? [];
const hasReviewPickupProof =
input.workSyncIntent === 'review_pickup' &&
toolNames.some((toolName) => {
const normalized = this.normalizeOpenCodeObservedToolName(toolName);
return (
normalized === 'review_start' ||
normalized === 'review_approve' ||
normalized === 'review_request_changes'
);
});
if (hasReviewPickupProof) {
return true;
}
const hasReportTool = toolNames.some(
(toolName) => this.normalizeOpenCodeObservedToolName(toolName) === 'member_work_sync_report'
);
if (!hasReportTool) {
return false;
}
return this.hasAcceptedMemberWorkSyncReport({
teamName: input.teamName,
memberName: input.memberName,
});
}
private hasOpenCodeObservedMessageSendToolCall(
ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null
): boolean {
@ -5636,6 +5742,22 @@ export class TeamProvisioningService {
}): string {
const record = input.ledgerRecord;
const state = input.responseState ?? record?.responseState;
if (record?.messageKind === 'member_work_sync_nudge') {
if (state === 'responded_plain_text' || state === 'responded_visible_message') {
return 'member_work_sync_report_required';
}
if (state === 'responded_non_visible_tool' || state === 'responded_tool_call') {
if (record.workSyncIntent !== 'review_pickup') {
return 'member_work_sync_report_required';
}
if (!this.hasOpenCodeMemberWorkSyncReadCommitProof(record)) {
return 'member_work_sync_report_required';
}
}
if (!this.hasOpenCodeMemberWorkSyncReadCommitProof(record)) {
return 'member_work_sync_report_required';
}
}
if (state === 'responded_visible_message' && !input.visibleReply) {
return 'visible_reply_destination_not_found_yet';
}
@ -5760,7 +5882,17 @@ export class TeamProvisioningService {
if (
input.ledgerRecord.lastReason === 'visible_reply_ack_only_still_requires_answer' ||
input.ledgerRecord.lastReason === 'plain_text_ack_only_still_requires_answer' ||
input.ledgerRecord.lastReason === 'visible_reply_missing_task_refs'
input.ledgerRecord.lastReason === 'visible_reply_missing_task_refs' ||
input.ledgerRecord.lastReason === 'member_work_sync_report_required'
) {
return true;
}
if (
input.ledgerRecord.messageKind === 'member_work_sync_nudge' &&
(input.ledgerRecord.responseState === 'responded_visible_message' ||
input.ledgerRecord.responseState === 'responded_plain_text' ||
input.ledgerRecord.responseState === 'responded_non_visible_tool' ||
input.ledgerRecord.responseState === 'responded_tool_call')
) {
return true;
}
@ -6635,7 +6767,9 @@ export class TeamProvisioningService {
let ledgerRecord = input.ledgerRecord;
let visibleReply = input.visibleReply ?? null;
const observeMessageDelivery = input.adapter.observeMessageDelivery;
const readAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({
const readAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName: input.teamName,
memberName: input.memberName,
responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs,
@ -6784,7 +6918,9 @@ export class TeamProvisioningService {
});
ledgerRecord = materialized.ledgerRecord;
visibleReply = materialized.visibleReply;
const observedReadAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({
const observedReadAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName: input.teamName,
memberName: input.memberName,
responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs,
@ -8752,14 +8888,27 @@ export class TeamProvisioningService {
teamColor: config?.color,
teamDisplayName: config?.name,
});
const legacyWorkSyncReadAllowed =
input.messageKind === 'member_work_sync_nudge' && result.ok
? await this.isLegacyOpenCodeMemberWorkSyncReadCommitAllowed({
teamName,
memberName: canonicalMemberName,
workSyncIntent: input.workSyncIntent,
responseObservation,
})
: true;
const legacyWorkSyncResponsePending =
result.ok && input.messageKind === 'member_work_sync_nudge' && !legacyWorkSyncReadAllowed;
return {
delivered: result.ok,
accepted: result.ok,
responsePending: false,
responsePending: legacyWorkSyncResponsePending,
responseState: responseObservation?.state,
...(result.ok
? {}
: { reason: result.diagnostics[0] ?? 'opencode_message_delivery_failed' }),
...(legacyWorkSyncResponsePending
? { reason: responseObservation?.reason ?? 'member_work_sync_report_required' }
: result.ok
? {}
: { reason: result.diagnostics[0] ?? 'opencode_message_delivery_failed' }),
diagnostics: result.diagnostics,
};
}
@ -8794,7 +8943,9 @@ export class TeamProvisioningService {
visibleReply: proof.visibleReply,
});
active = proof.ledgerRecord;
const activeReadAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({
const activeReadAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName,
memberName: canonicalMemberName,
responseState: active.responseState,
actionMode: active.actionMode ?? undefined,
taskRefs: active.taskRefs,
@ -8882,7 +9033,9 @@ export class TeamProvisioningService {
visibleReply: proof.visibleReply,
});
ledgerRecord = proof.ledgerRecord;
let readAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({
let readAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName,
memberName: canonicalMemberName,
responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs,
@ -9071,7 +9224,9 @@ export class TeamProvisioningService {
visibleReply: proof.visibleReply,
});
ledgerRecord = proof.ledgerRecord;
readAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({
readAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName,
memberName: canonicalMemberName,
responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs,
@ -9199,7 +9354,9 @@ export class TeamProvisioningService {
}
const retryReadAllowed = ledgerRecord
? this.isOpenCodeDeliveryResponseReadCommitAllowed({
? await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName,
memberName: canonicalMemberName,
responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs,
@ -9463,7 +9620,9 @@ export class TeamProvisioningService {
ledgerRecord.visibleReplyCorrelation === 'relayOfMessageId',
})
: null;
const readAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({
const readAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName,
memberName: canonicalMemberName,
responseState,
actionMode: input.actionMode,
taskRefs: input.taskRefs,
@ -23471,15 +23630,17 @@ export class TeamProvisioningService {
recoveredVisibleReply = null;
}
}
const recoveredReadAllowed =
recoveredRecord &&
this.isOpenCodeDeliveryResponseReadCommitAllowed({
responseState: recoveredRecord.responseState,
actionMode: recoveredRecord.actionMode ?? undefined,
taskRefs: recoveredRecord.taskRefs,
visibleReply: recoveredVisibleReply,
ledgerRecord: recoveredRecord,
});
const recoveredReadAllowed = recoveredRecord
? await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName,
memberName: memberIdentity.canonicalMemberName,
responseState: recoveredRecord.responseState,
actionMode: recoveredRecord.actionMode ?? undefined,
taskRefs: recoveredRecord.taskRefs,
visibleReply: recoveredVisibleReply,
ledgerRecord: recoveredRecord,
})
: false;
if (recoveredRecord && recoveredReadAllowed) {
try {
await this.markInboxMessagesRead(teamName, memberName, [message]);
@ -23967,6 +24128,101 @@ export class TeamProvisioningService {
return from === 'user' || message.source === 'user_sent';
}
private async hasAcceptedMemberWorkSyncReport(input: {
teamName: string;
memberName: string;
}): Promise<boolean> {
const checker = this.memberWorkSyncAcceptedReportChecker;
if (!checker) {
return false;
}
try {
return (
(await checker({
teamName: input.teamName,
memberName: input.memberName,
})) === true
);
} catch (error) {
logger.warn(
`[${input.teamName}] Failed to check accepted work sync report for ${input.memberName}: ${getErrorMessage(error)}`
);
return false;
}
}
private async hasAcceptedLeadWorkSyncReport(input: {
teamName: string;
leadName: string;
}): Promise<boolean> {
return this.hasAcceptedMemberWorkSyncReport({
teamName: input.teamName,
memberName: input.leadName,
});
}
private async scheduleLeadProofMissingWorkSyncRecovery(input: {
teamName: string;
leadName: string;
message: InboxMessage & { messageId: string };
}): Promise<boolean> {
const scheduler = this.memberWorkSyncProofMissingRecoveryScheduler;
if (!scheduler) {
return false;
}
try {
const result = (await scheduler({
teamName: input.teamName,
memberName: input.leadName,
originalMessageId: input.message.messageId,
taskRefs: input.message.taskRefs,
reason: 'lead_member_work_sync_report_required',
})) as { scheduled?: boolean; reason?: string } | null | undefined;
return result?.scheduled === true || result?.reason === 'coalesced_recent';
} catch (error) {
logger.warn(
`[${input.teamName}] Failed to schedule lead proof-missing work sync recovery for ${input.leadName}: ${getErrorMessage(error)}`
);
return false;
}
}
private async getLeadRelayReadCommitBatch(input: {
teamName: string;
leadName: string;
batch: (InboxMessage & { messageId: string })[];
}): Promise<(InboxMessage & { messageId: string })[]> {
const readCommitBatch: (InboxMessage & { messageId: string })[] = [];
for (const message of input.batch) {
if (message.messageKind !== 'member_work_sync_nudge') {
readCommitBatch.push(message);
continue;
}
if (
await this.hasAcceptedLeadWorkSyncReport({
teamName: input.teamName,
leadName: input.leadName,
})
) {
readCommitBatch.push(message);
continue;
}
const recoveryScheduled = await this.scheduleLeadProofMissingWorkSyncRecovery({
teamName: input.teamName,
leadName: input.leadName,
message,
});
if (recoveryScheduled) {
readCommitBatch.push(message);
}
}
return readCommitBatch;
}
async relayLeadInboxMessages(teamName: string): Promise<number> {
const existing = this.leadInboxRelayInFlight.get(teamName);
if (existing) {
@ -24407,10 +24663,6 @@ export class TeamProvisioningService {
return 0;
}
for (const m of batch) {
relayedIds.add(m.messageId);
}
this.relayedLeadInboxMessageIds.set(teamName, this.trimRelayedSet(relayedIds));
this.rememberRecentCrossTeamLeadDeliveryMessageIds(
teamName,
batch
@ -24418,12 +24670,6 @@ export class TeamProvisioningService {
.map((message) => message.messageId)
);
try {
await this.markInboxMessagesRead(teamName, leadName, batch);
} catch {
// Best-effort: relay succeeded; marking read failed.
}
let replyText: string | null = null;
let capturedVisibleSendMessage = false;
let capturedUserVisibleSendMessage = false;
@ -24448,6 +24694,23 @@ export class TeamProvisioningService {
}
}
const readCommitBatch = await this.getLeadRelayReadCommitBatch({
teamName,
leadName,
batch,
});
for (const m of readCommitBatch) {
relayedIds.add(m.messageId);
}
this.relayedLeadInboxMessageIds.set(teamName, this.trimRelayedSet(relayedIds));
if (readCommitBatch.length > 0) {
try {
await this.markInboxMessagesRead(teamName, leadName, readCommitBatch);
} catch {
// Best-effort: relay succeeded; marking read failed.
}
}
// Strip agent-only blocks — lead may respond with pure coordination content
// that is not meant for the human user.
const cleanReply = replyText

View file

@ -1126,6 +1126,7 @@ function buildMemberBootstrapPrompt(
'This OpenCode session is created, attached, and launch-verified by the desktop app.',
'Do not call runtime_bootstrap_checkin or member_briefing just to prove launch readiness.',
'Do NOT create local team files, run join scripts, or search the project for a fake team registry.',
'That bootstrap restriction is only about team registry/startup files. It does not restrict assigned project work: when a task requires implementation, fixes, review follow-up, or investigation, you may inspect, read/search, and edit files in the project working directory as your available tools allow.',
'Use the app MCP tools exposed by the "agent-teams" server for team communication and task state.',
'Launch bootstrap is a silent attach, not a user/team conversation turn.',
'Do not call task_briefing, message_send, or cross_team_send just to announce readiness, say understood, report no tasks, or ask for work.',
@ -1190,6 +1191,12 @@ function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput)
input.taskRefs
?.map((ref) => ref.taskId?.trim())
.filter((taskId): taskId is string => Boolean(taskId)) ?? [];
const actionModeWorkScopeReminder =
input.actionMode === 'ask'
? 'Action mode ASK is read-only for this delivered message: do not edit files, change task state, or run side-effecting tools for this message.'
: input.actionMode === 'delegate'
? 'Action mode DELEGATE is orchestration-only for this delivered message: pass the task with context instead of implementing or editing files yourself.'
: 'If this delivered message assigns implementation, fixes, review follow-up, or concrete investigation, you may inspect, read/search, and edit files in the project working directory as your available tools allow.';
// Work-sync nudges are health/reporting probes. Requiring a visible
// message_send reply here causes false delivery failures, so accept the
// dedicated member_work_sync_report proof path while keeping normal user
@ -1199,7 +1206,7 @@ function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput)
'This delivered app message is a targeted member-work-sync review pickup nudge.',
'Process the current review request now if it is still assigned to you. Open the task, verify reviewState/status, then use the review workflow tools to start or continue the review.',
'Do not mark the review complete from this prompt alone.',
'A visible agent-teams_message_send reply is optional. Concrete review progress, review tool usage, or agent-teams_member_work_sync_report (or mcp__agent-teams__member_work_sync_report) is sufficient response proof.',
'A visible agent-teams_message_send reply is optional. Review workflow tool usage or agent-teams_member_work_sync_report (or mcp__agent-teams__member_work_sync_report) is sufficient response proof.',
`If you cannot pick up the review now, call agent-teams_member_work_sync_status (or mcp__agent-teams__member_work_sync_status) with ${workSyncToolArgs}, then report state "blocked" or "still_working" only for the real current state.`,
'Do not stop after member_work_sync_status. A status-only tool call is incomplete; member_work_sync_report is the required proof.',
taskIds.length ? `Relevant taskIds: ${taskIds.map((id) => `"${id}"`).join(', ')}.` : null,
@ -1209,7 +1216,7 @@ function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput)
: isWorkSyncNudge
? [
'This delivered app message is a member-work-sync nudge.',
'A visible agent-teams_message_send reply is optional. Concrete task progress or agent-teams_member_work_sync_report (or mcp__agent-teams__member_work_sync_report) is sufficient response proof.',
'A visible agent-teams_message_send reply is optional. For agenda sync, only agent-teams_member_work_sync_report (or mcp__agent-teams__member_work_sync_report) is sufficient response proof.',
`Call agent-teams_member_work_sync_status (or mcp__agent-teams__member_work_sync_status) with ${workSyncToolArgs}.`,
`Then call agent-teams_member_work_sync_report (or mcp__agent-teams__member_work_sync_report) with ${workSyncToolArgs}, the returned agendaFingerprint/reportToken, and state "still_working" or "blocked".`,
'Do not stop after member_work_sync_status. A status-only tool call is incomplete; member_work_sync_report is the required proof.',
@ -1241,6 +1248,7 @@ function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput)
? `<opencode_delivery_context>${deliveryContext}</opencode_delivery_context>`
: null,
'You are running in OpenCode, not Claude Code or Codex native.',
actionModeWorkScopeReminder,
...responseInstructions,
'Do not call runtime_bootstrap_checkin or member_briefing just to answer this delivered app message.',
'Do not use SendMessage or runtime_deliver_message for ordinary visible replies.',

View file

@ -413,6 +413,7 @@ describe('team model availability Codex catalog integration', () => {
expect(getAvailableTeamProviderModels('anthropic', providerStatus)).toEqual([
'haiku',
'opus',
'claude-opus-4-7',
'claude-opus-4-6',
'sonnet',
]);
@ -431,6 +432,13 @@ describe('team model availability Codex catalog integration', () => {
availabilityStatus: 'available',
availabilityReason: null,
},
{
value: 'claude-opus-4-7',
label: 'Opus 4.7',
badgeLabel: 'Opus 4.7',
availabilityStatus: 'available',
availabilityReason: null,
},
{
value: 'claude-opus-4-6',
label: 'Opus 4.6',

View file

@ -166,12 +166,7 @@ export function isTeamProviderModelVerificationPending(
return true;
}
const verificationState = providerStatus.verificationState as
| 'verified'
| 'unknown'
| 'offline'
| 'error'
| undefined;
const verificationState = providerStatus.verificationState;
if (verificationState === 'error' || providerStatus.modelCatalogRefreshState === 'error') {
return false;
}

View file

@ -232,14 +232,16 @@ class InMemoryOutboxStore implements MemberWorkSyncOutboxStorePort {
async markDelivered(input: MemberWorkSyncOutboxMarkDeliveredInput): Promise<void> {
const current = this.items.get(input.id);
if (current?.attemptGeneration === input.attemptGeneration) {
this.items.set(input.id, {
const next = {
...current,
status: 'delivered',
status: 'delivered' as const,
deliveredMessageId: input.deliveredMessageId,
...(input.deliveryState ? { deliveryState: input.deliveryState } : {}),
...(input.deliveryDiagnostics ? { deliveryDiagnostics: input.deliveryDiagnostics } : {}),
updatedAt: input.nowIso,
});
};
delete next.nextAttemptAt;
this.items.set(input.id, next);
}
}
@ -263,12 +265,18 @@ class InMemoryOutboxStore implements MemberWorkSyncOutboxStorePort {
}
}
async countRecentDelivered(input: { memberName: string; sinceIso: string }): Promise<number> {
async countRecentDelivered(input: {
memberName: string;
sinceIso: string;
workSyncIntentKeyPrefix?: string;
}): Promise<number> {
return [...this.items.values()].filter(
(item) =>
item.status === 'delivered' &&
item.memberName === input.memberName &&
item.updatedAt >= input.sinceIso
item.updatedAt >= input.sinceIso &&
(!input.workSyncIntentKeyPrefix ||
item.payload.workSyncIntentKey?.startsWith(input.workSyncIntentKeyPrefix) === true)
).length;
}
@ -296,17 +304,22 @@ class InMemoryOutboxStore implements MemberWorkSyncOutboxStorePort {
class InMemoryInboxNudge implements MemberWorkSyncInboxNudgePort {
readonly inserted: Array<Parameters<MemberWorkSyncInboxNudgePort['insertIfAbsent']>[0]> = [];
fail = false;
conflict = false;
async insertIfAbsent(input: Parameters<MemberWorkSyncInboxNudgePort['insertIfAbsent']>[0]) {
if (this.fail) {
throw new Error('inbox unavailable');
}
if (this.conflict) {
return { inserted: false, messageId: input.messageId, conflict: true };
}
this.inserted.push(input);
return { inserted: true, messageId: input.messageId };
}
}
function createDeps(options?: {
memberName?: string;
items?: MemberWorkSyncActionableWorkItem[];
activeMemberNames?: string[];
inactive?: boolean;
@ -321,15 +334,16 @@ function createDeps(options?: {
const clock = new MutableClock();
const store = new InMemoryStatusStore();
const auditEvents: MemberWorkSyncAuditEvent[] = [];
const memberName = options?.memberName ?? 'bob';
const source: MemberWorkSyncAgendaSourceResult = {
agenda: {
teamName: 'team-a',
memberName: 'bob',
memberName,
generatedAt: '2026-04-29T00:00:00.000Z',
items: options?.items ?? [workItem],
diagnostics: [],
},
activeMemberNames: options?.activeMemberNames ?? ['bob'],
activeMemberNames: options?.activeMemberNames ?? [memberName],
inactive: options?.inactive ?? false,
...(options?.providerId ? { providerId: options.providerId } : {}),
diagnostics: [],
@ -940,6 +954,147 @@ describe('MemberWorkSync use cases', () => {
expect(inbox.inserted[1]?.messageId).toContain('status-only');
});
it('creates a delivered-still-stuck recovery after a delivered status-only nudge gets no report', async () => {
const outbox = new InMemoryOutboxStore();
const inbox = new InMemoryInboxNudge();
const { clock, deps, store } = createDeps({
providerId: 'codex',
outboxStore: outbox,
inboxNudge: inbox,
});
store.phase2ReadinessState = 'shadow_ready';
const reconciler = new MemberWorkSyncReconciler(deps);
const firstStatus = await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['task_changed'] }
);
await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['turn_settled'] }
);
await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
expect(inbox.inserted).toHaveLength(2);
expect(inbox.inserted[1]?.messageId).toContain('status-only');
clock.set('2026-04-29T00:10:00.000Z');
store.metricsGeneratedAt = '2026-04-29T00:10:00.000Z';
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['turn_settled'] }
);
const stillStuck = [...outbox.items.values()].find((item) =>
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
);
expect(stillStuck).toMatchObject({
status: 'pending',
agendaFingerprint: firstStatus.agenda.fingerprint,
});
const summary = await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
expect(summary).toMatchObject({ claimed: 1, delivered: 1, retryable: 0 });
expect(inbox.inserted).toHaveLength(3);
expect(inbox.inserted[2]?.messageId).toContain('agenda-sync-still-stuck');
});
it('creates a still-stuck recovery when a terminal inbox conflict blocks a status-only nudge', async () => {
const outbox = new InMemoryOutboxStore();
const inbox = new InMemoryInboxNudge();
const { clock, deps, store } = createDeps({
providerId: 'codex',
outboxStore: outbox,
inboxNudge: inbox,
});
store.phase2ReadinessState = 'shadow_ready';
const reconciler = new MemberWorkSyncReconciler(deps);
const firstStatus = await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['task_changed'] }
);
await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['turn_settled'] }
);
inbox.conflict = true;
const terminalSummary = await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
const statusOnly = [...outbox.items.values()].find((item) =>
item.payload.workSyncIntentKey?.startsWith('status-only:')
);
expect(terminalSummary).toMatchObject({ claimed: 1, delivered: 0, terminal: 1 });
expect(statusOnly).toMatchObject({
status: 'failed_terminal',
lastError: 'inbox_payload_conflict',
});
inbox.conflict = false;
clock.set('2026-04-29T00:10:00.000Z');
store.metricsGeneratedAt = '2026-04-29T00:10:00.000Z';
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['turn_settled'] }
);
const stillStuck = [...outbox.items.values()].find((item) =>
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
);
expect(stillStuck).toMatchObject({
status: 'pending',
agendaFingerprint: firstStatus.agenda.fingerprint,
});
const recoverySummary = await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
expect(recoverySummary).toMatchObject({ claimed: 1, delivered: 1, retryable: 0 });
expect(inbox.inserted).toHaveLength(2);
expect(inbox.inserted[1]?.messageId).toContain('agenda-sync-still-stuck');
});
it('creates an agenda-sync refresh recovery when a delivered nudge has a stale payload hash', async () => {
const outbox = new InMemoryOutboxStore();
const inbox = new InMemoryInboxNudge();
@ -1040,6 +1195,460 @@ describe('MemberWorkSync use cases', () => {
expect(statusOnlyItems[0]?.payload.text).toContain('Status-only recovery');
});
it('creates a delivered-still-stuck recovery after a delivered refresh nudge gets no report', async () => {
const outbox = new InMemoryOutboxStore();
const inbox = new InMemoryInboxNudge();
outbox.rejectPayloadConflicts = true;
const { clock, deps, store } = createDeps({
providerId: 'codex',
outboxStore: outbox,
inboxNudge: inbox,
});
store.phase2ReadinessState = 'shadow_ready';
const reconciler = new MemberWorkSyncReconciler(deps);
const firstStatus = await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['task_changed'] }
);
await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
const baseId = `member-work-sync:team-a:bob:${firstStatus.agenda.fingerprint}`;
const delivered = outbox.items.get(baseId);
expect(delivered).toMatchObject({ status: 'delivered' });
outbox.items.set(baseId, {
...delivered!,
payloadHash: 'legacy-payload-hash',
payload: {
...delivered!.payload,
text: 'Legacy delivered work-sync nudge text.',
},
});
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['task_changed'] }
);
await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
expect(
[...outbox.items.values()].filter((item) =>
item.payload.workSyncIntentKey?.startsWith('agenda-sync-refresh:')
)
).toHaveLength(1);
expect(inbox.inserted).toHaveLength(2);
clock.set('2026-04-29T00:10:00.000Z');
store.metricsGeneratedAt = '2026-04-29T00:10:00.000Z';
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['manual_refresh'] }
);
const stillStuck = [...outbox.items.values()].find((item) =>
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
);
expect(stillStuck).toMatchObject({
status: 'pending',
agendaFingerprint: firstStatus.agenda.fingerprint,
});
const summary = await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
expect(summary).toMatchObject({ claimed: 1, delivered: 1, retryable: 0 });
expect(inbox.inserted).toHaveLength(3);
expect(inbox.inserted[2]?.messageId).toContain('agenda-sync-still-stuck');
});
it('creates a delivered-still-stuck recovery when a delivered agenda nudge gets no report', async () => {
const outbox = new InMemoryOutboxStore();
const inbox = new InMemoryInboxNudge();
const { clock, deps, store } = createDeps({
providerId: 'codex',
outboxStore: outbox,
inboxNudge: inbox,
});
store.phase2ReadinessState = 'shadow_ready';
const reconciler = new MemberWorkSyncReconciler(deps);
const firstStatus = await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['task_changed'] }
);
await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
const baseId = `member-work-sync:team-a:bob:${firstStatus.agenda.fingerprint}`;
expect(outbox.items.get(baseId)).toMatchObject({ status: 'delivered' });
clock.set('2026-04-29T00:10:00.000Z');
store.phase2ReadinessState = 'blocked';
store.phase2ReadinessReasons = ['would_nudge_rate_high'];
store.metricsGeneratedAt = '2026-04-29T00:10:00.000Z';
store.recentEvents = [
{
id: 'stale-current-needs-sync',
teamName: 'team-a',
memberName: 'bob',
kind: 'status_evaluated',
state: 'needs_sync',
agendaFingerprint: firstStatus.agenda.fingerprint,
recordedAt: '2026-04-29T00:02:00.000Z',
actionableCount: 1,
providerId: 'codex',
},
];
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['manual_refresh'] }
);
const recovery = [...outbox.items.values()].find((item) =>
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
);
expect(recovery).toMatchObject({
status: 'pending',
agendaFingerprint: firstStatus.agenda.fingerprint,
payload: {
workSyncIntent: 'agenda_sync',
workSyncIntentKey: expect.stringContaining(
`agenda-sync-still-stuck:${firstStatus.agenda.fingerprint}:`
),
},
});
expect(recovery?.payload.text).toContain('still no accepted member_work_sync_report');
expect(outbox.items.get(baseId)).toMatchObject({ status: 'delivered' });
const summary = await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
expect(summary).toMatchObject({ claimed: 1, delivered: 1, retryable: 0 });
expect(inbox.inserted).toHaveLength(2);
expect(inbox.inserted[1]?.messageId).toContain('agenda-sync-still-stuck');
clock.set('2026-04-29T00:20:00.000Z');
store.metricsGeneratedAt = '2026-04-29T00:20:00.000Z';
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['manual_refresh'] }
);
expect(
[...outbox.items.values()].filter((item) =>
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
)
).toHaveLength(1);
clock.set('2026-04-29T01:02:00.000Z');
store.metricsGeneratedAt = '2026-04-29T01:02:00.000Z';
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['manual_refresh'] }
);
const recoveryItems = [...outbox.items.values()].filter((item) =>
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
);
expect(recoveryItems).toHaveLength(2);
expect(new Set(recoveryItems.map((item) => item.id)).size).toBe(2);
const secondSummary = await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
expect(secondSummary).toMatchObject({ claimed: 1, delivered: 1, retryable: 0 });
expect(inbox.inserted).toHaveLength(3);
});
it('records an existing delivered agenda nudge as skipped before still-stuck recovery age', async () => {
const outbox = new InMemoryOutboxStore();
const inbox = new InMemoryInboxNudge();
const { auditEvents, clock, deps, store } = createDeps({
providerId: 'codex',
outboxStore: outbox,
inboxNudge: inbox,
});
store.phase2ReadinessState = 'shadow_ready';
const reconciler = new MemberWorkSyncReconciler(deps);
const firstStatus = await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['task_changed'] }
);
await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
const baseId = `member-work-sync:team-a:bob:${firstStatus.agenda.fingerprint}`;
expect(outbox.items.get(baseId)).toMatchObject({ status: 'delivered' });
clock.set('2026-04-29T00:04:00.000Z');
store.metricsGeneratedAt = '2026-04-29T00:04:00.000Z';
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['manual_refresh'] }
);
expect(
[...outbox.items.values()].filter((item) =>
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
)
).toHaveLength(0);
expect(auditEvents).toEqual(
expect.arrayContaining([
expect.objectContaining({
event: 'nudge_skipped',
reason: 'existing',
}),
])
);
});
it('creates a delivered-still-stuck recovery for a targeted lead despite noisy metrics', async () => {
const outbox = new InMemoryOutboxStore();
const inbox = new InMemoryInboxNudge();
const leadWorkItem: MemberWorkSyncActionableWorkItem = {
...workItem,
assignee: 'team-lead',
evidence: {
status: 'pending',
owner: 'team-lead',
},
};
const { clock, deps, store } = createDeps({
memberName: 'team-lead',
items: [leadWorkItem],
providerId: 'codex',
outboxStore: outbox,
inboxNudge: inbox,
});
store.phase2ReadinessState = 'blocked';
store.phase2ReadinessReasons = ['would_nudge_rate_high'];
const reconciler = new MemberWorkSyncReconciler(deps);
const firstStatus = await reconciler.execute(
{
teamName: 'team-a',
memberName: 'team-lead',
},
{ reconciledBy: 'queue', triggerReasons: ['manual_refresh'] }
);
await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
const baseId = `member-work-sync:team-a:team-lead:${firstStatus.agenda.fingerprint}`;
expect(outbox.items.get(baseId)).toMatchObject({ status: 'delivered' });
clock.set('2026-04-29T00:10:00.000Z');
store.metricsGeneratedAt = '2026-04-29T00:10:00.000Z';
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'team-lead',
},
{ reconciledBy: 'queue', triggerReasons: ['manual_refresh'] }
);
const recovery = [...outbox.items.values()].find((item) =>
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
);
expect(recovery).toMatchObject({
status: 'pending',
memberName: 'team-lead',
agendaFingerprint: firstStatus.agenda.fingerprint,
});
const recoverySummary = await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
expect(recoverySummary).toMatchObject({ claimed: 1, delivered: 1, retryable: 0 });
expect(inbox.inserted).toHaveLength(2);
expect(inbox.inserted[1]?.messageId).toContain('agenda-sync-still-stuck');
});
it('creates a still-stuck recovery when a terminal inbox conflict blocks an agenda nudge', async () => {
const outbox = new InMemoryOutboxStore();
const inbox = new InMemoryInboxNudge();
const { clock, deps, store } = createDeps({
providerId: 'codex',
outboxStore: outbox,
inboxNudge: inbox,
});
store.phase2ReadinessState = 'shadow_ready';
const reconciler = new MemberWorkSyncReconciler(deps);
const firstStatus = await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['task_changed'] }
);
const baseId = `member-work-sync:team-a:bob:${firstStatus.agenda.fingerprint}`;
expect(outbox.items.get(baseId)).toMatchObject({ status: 'pending' });
inbox.conflict = true;
const terminalSummary = await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
expect(terminalSummary).toMatchObject({ claimed: 1, delivered: 0, terminal: 1 });
expect(outbox.items.get(baseId)).toMatchObject({
status: 'failed_terminal',
lastError: 'inbox_payload_conflict',
});
inbox.conflict = false;
clock.set('2026-04-29T00:10:00.000Z');
store.metricsGeneratedAt = '2026-04-29T00:10:00.000Z';
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['manual_refresh'] }
);
const recovery = [...outbox.items.values()].find((item) =>
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
);
expect(recovery).toMatchObject({
status: 'pending',
agendaFingerprint: firstStatus.agenda.fingerprint,
payload: {
workSyncIntent: 'agenda_sync',
workSyncIntentKey: expect.stringContaining(
`agenda-sync-still-stuck:${firstStatus.agenda.fingerprint}:`
),
},
});
expect(recovery?.payload.text).toContain('still no accepted member_work_sync_report');
expect(outbox.items.get(baseId)).toMatchObject({ status: 'failed_terminal' });
const recoverySummary = await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
expect(recoverySummary).toMatchObject({ claimed: 1, delivered: 1, retryable: 0 });
expect(inbox.inserted).toHaveLength(1);
expect(inbox.inserted[0]?.messageId).toContain('agenda-sync-still-stuck');
});
it('creates a still-stuck recovery when a terminal inbox conflict has a stale payload hash', async () => {
const outbox = new InMemoryOutboxStore();
outbox.rejectPayloadConflicts = true;
const inbox = new InMemoryInboxNudge();
const { clock, deps, store } = createDeps({
providerId: 'codex',
outboxStore: outbox,
inboxNudge: inbox,
});
store.phase2ReadinessState = 'shadow_ready';
const reconciler = new MemberWorkSyncReconciler(deps);
const firstStatus = await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['task_changed'] }
);
const baseId = `member-work-sync:team-a:bob:${firstStatus.agenda.fingerprint}`;
inbox.conflict = true;
await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
const terminal = outbox.items.get(baseId);
expect(terminal).toMatchObject({
status: 'failed_terminal',
lastError: 'inbox_payload_conflict',
});
outbox.items.set(baseId, {
...terminal!,
payloadHash: 'stale-terminal-payload-hash',
});
inbox.conflict = false;
clock.set('2026-04-29T00:10:00.000Z');
store.metricsGeneratedAt = '2026-04-29T00:10:00.000Z';
await reconciler.execute(
{
teamName: 'team-a',
memberName: 'bob',
},
{ reconciledBy: 'queue', triggerReasons: ['manual_refresh'] }
);
const recovery = [...outbox.items.values()].find((item) =>
item.payload.workSyncIntentKey?.startsWith('agenda-sync-still-stuck:')
);
expect(recovery).toMatchObject({
status: 'pending',
agendaFingerprint: firstStatus.agenda.fingerprint,
});
const recoverySummary = await new MemberWorkSyncNudgeDispatcher(deps).dispatchDue({
teamNames: ['team-a'],
claimedBy: 'test-dispatcher',
});
expect(recoverySummary).toMatchObject({ claimed: 1, delivered: 1, retryable: 0 });
expect(inbox.inserted).toHaveLength(1);
expect(inbox.inserted[0]?.messageId).toContain('agenda-sync-still-stuck');
});
it('marks review pickup delivered only after the delivery port confirms prompt acceptance', async () => {
const outbox = new InMemoryOutboxStore();
const inbox = new InMemoryInboxNudge();

View file

@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
describe('CompositeMemberWorkSyncBusySignal', () => {
it('does not block nudges forever when one busy signal fails', async () => {
const logger = { warn: vi.fn() };
const logger = { debug: vi.fn(), error: vi.fn(), warn: vi.fn() };
const signal = new CompositeMemberWorkSyncBusySignal(
[
{
@ -24,7 +24,7 @@ describe('CompositeMemberWorkSyncBusySignal', () => {
memberName: 'bob',
nowIso: '2026-04-29T00:00:00.000Z',
workSyncIntent: 'agenda_sync',
taskRefs: [{ teamName: 'team-a', taskId: 'task-1' }],
taskRefs: [{ teamName: 'team-a', taskId: 'task-1', displayId: '11111111' }],
})
).resolves.toEqual({ busy: false });
expect(logger.warn).toHaveBeenCalledWith(
@ -56,7 +56,7 @@ describe('CompositeMemberWorkSyncBusySignal', () => {
memberName: 'bob',
nowIso: '2026-04-29T00:00:00.000Z',
workSyncIntent: 'agenda_sync',
taskRefs: [{ teamName: 'team-a', taskId: 'task-1' }],
taskRefs: [{ teamName: 'team-a', taskId: 'task-1', displayId: '11111111' }],
})
).resolves.toEqual({
busy: true,

View file

@ -352,7 +352,7 @@ describe('JsonMemberWorkSyncStore', () => {
});
});
it('deduplicates outbox items by id and rejects payload hash conflicts', async () => {
it('refreshes undelivered outbox payloads but rejects delivered payload conflicts', async () => {
const input = {
id: 'member-work-sync:team-a:bob:agenda:v1:abc',
teamName: 'team-a',
@ -372,11 +372,97 @@ describe('JsonMemberWorkSyncStore', () => {
ok: true,
outcome: 'existing',
});
await expect(store.ensurePending({ ...input, payloadHash: 'hash-b' })).resolves.toMatchObject({
const refreshed = await store.ensurePending({
...input,
payloadHash: 'hash-b',
payload: makeNudgePayload({
text: 'Work sync check: call member_work_sync_status and member_work_sync_report.',
}),
nowIso: '2026-04-29T00:01:00.000Z',
});
expect(refreshed).toMatchObject({
ok: true,
outcome: 'existing',
item: {
status: 'pending',
payloadHash: 'hash-b',
payload: {
text: 'Work sync check: call member_work_sync_status and member_work_sync_report.',
},
},
});
const [claimed] = await store.claimDue({
teamName: 'team-a',
claimedBy: 'dispatcher-a',
nowIso: '2026-04-29T00:02:00.000Z',
limit: 1,
});
const claimedRefresh = await store.ensurePending({
...input,
payloadHash: 'hash-c',
payload: makeNudgePayload({ text: 'New text while delivery is claimed.' }),
nowIso: '2026-04-29T00:02:30.000Z',
});
expect(claimedRefresh).toMatchObject({
ok: true,
outcome: 'existing',
item: {
status: 'pending',
payloadHash: 'hash-c',
payload: { text: 'New text while delivery is claimed.' },
attemptGeneration: claimed.attemptGeneration + 1,
},
});
await store.markDelivered({
teamName: 'team-a',
id: input.id,
attemptGeneration: claimed.attemptGeneration,
deliveredMessageId: 'message-1',
nowIso: '2026-04-29T00:03:00.000Z',
});
const afterStaleDelivery = JSON.parse(
await readFile(
join(root, 'team-a', 'members', 'bob', '.member-work-sync', 'outbox.json'),
'utf8'
)
);
expect(afterStaleDelivery.items[input.id]).toMatchObject({
status: 'pending',
payloadHash: 'hash-c',
});
const [reclaimed] = await store.claimDue({
teamName: 'team-a',
claimedBy: 'dispatcher-b',
nowIso: '2026-04-29T00:03:30.000Z',
limit: 1,
});
expect(reclaimed).toMatchObject({
id: input.id,
payloadHash: 'hash-c',
attemptGeneration: claimed.attemptGeneration + 2,
});
await store.markDelivered({
teamName: 'team-a',
id: input.id,
attemptGeneration: reclaimed.attemptGeneration,
deliveredMessageId: 'message-2',
nowIso: '2026-04-29T00:03:45.000Z',
});
await expect(
store.ensurePending({
...input,
payloadHash: 'hash-d',
payload: makeNudgePayload({ text: 'New text after delivery.' }),
nowIso: '2026-04-29T00:04:00.000Z',
})
).resolves.toMatchObject({
ok: false,
outcome: 'payload_conflict',
existingPayloadHash: 'hash-a',
requestedPayloadHash: 'hash-b',
existingPayloadHash: 'hash-c',
requestedPayloadHash: 'hash-d',
});
});
@ -525,6 +611,67 @@ describe('JsonMemberWorkSyncStore', () => {
).resolves.toEqual([]);
});
it('clears retry delay when a retryable outbox item is delivered', async () => {
const input = {
id: 'member-work-sync:team-a:bob:agenda:v1:abc',
teamName: 'team-a',
memberName: 'bob',
agendaFingerprint: 'agenda:v1:abc',
payloadHash: 'hash-a',
payload: makeNudgePayload(),
nowIso: '2026-04-29T00:00:00.000Z',
};
await store.ensurePending(input);
const [claimed] = await store.claimDue({
teamName: 'team-a',
claimedBy: 'dispatcher-a',
nowIso: '2026-04-29T00:01:00.000Z',
limit: 1,
});
await store.markFailed({
teamName: 'team-a',
id: input.id,
attemptGeneration: claimed.attemptGeneration,
retryable: true,
error: 'member_busy:active_tool_activity',
nextAttemptAt: '2026-04-29T00:30:00.000Z',
nowIso: '2026-04-29T00:02:00.000Z',
});
const [reclaimed] = await store.claimDue({
teamName: 'team-a',
claimedBy: 'dispatcher-b',
nowIso: '2026-04-29T00:30:00.000Z',
limit: 1,
});
await store.markDelivered({
teamName: 'team-a',
id: input.id,
attemptGeneration: reclaimed.attemptGeneration,
deliveredMessageId: 'message-1',
nowIso: '2026-04-29T00:31:00.000Z',
});
const memberOutbox = JSON.parse(
await readFile(
join(root, 'team-a', 'members', 'bob', '.member-work-sync', 'outbox.json'),
'utf8'
)
);
expect(memberOutbox.items[input.id]).toMatchObject({ status: 'delivered' });
expect(memberOutbox.items[input.id]).not.toHaveProperty('nextAttemptAt');
const index = JSON.parse(
await readFile(
join(root, 'team-a', '.member-work-sync', 'indexes', 'outbox-index.json'),
'utf8'
)
);
expect(index.items[input.id]).toMatchObject({ status: 'delivered' });
expect(index.items[input.id]).not.toHaveProperty('nextAttemptAt');
});
it('finds recent recovery outbox rows by logical intent key', async () => {
const olderInput = {
id: 'member-work-sync:team-a:bob:agenda:v1:older',
@ -827,6 +974,67 @@ describe('JsonMemberWorkSyncStore', () => {
expect(repaired.items[bobInput.id]).toMatchObject({ memberName: 'bob', status: 'delivered' });
});
it('filters recent delivered counts by work sync intent key prefix when requested', async () => {
const baseInput = {
id: 'member-work-sync:team-a:bob:agenda:v1:abc',
teamName: 'team-a',
memberName: 'bob',
agendaFingerprint: 'agenda:v1:abc',
payloadHash: 'hash-a',
payload: makeNudgePayload(),
nowIso: '2026-04-29T00:00:00.000Z',
};
const stillStuckInput = {
...baseInput,
id: 'member-work-sync:team-a:bob:agenda-sync-still-stuck:agenda:v1:abc:hash-a:bucket',
payloadHash: 'hash-still-stuck',
payload: makeNudgePayload({
workSyncIntentKey: 'agenda-sync-still-stuck:agenda:v1:abc:hash-a:bucket',
}),
};
const statusOnlyInput = {
...baseInput,
id: 'member-work-sync:team-a:bob:status-only:agenda:v1:abc',
payloadHash: 'hash-status-only',
payload: makeNudgePayload({ workSyncIntentKey: 'status-only:agenda:v1:abc' }),
};
await store.ensurePending(baseInput);
await store.ensurePending(stillStuckInput);
await store.ensurePending(statusOnlyInput);
const claimed = await store.claimDue({
teamName: 'team-a',
claimedBy: 'dispatcher-a',
nowIso: '2026-04-29T00:01:00.000Z',
limit: 3,
});
for (const item of claimed) {
await store.markDelivered({
teamName: 'team-a',
id: item.id,
attemptGeneration: item.attemptGeneration,
deliveredMessageId: `message:${item.id}`,
nowIso: '2026-04-29T00:02:00.000Z',
});
}
await expect(
store.countRecentDelivered({
teamName: 'team-a',
memberName: 'bob',
sinceIso: '2026-04-29T00:00:00.000Z',
})
).resolves.toBe(3);
await expect(
store.countRecentDelivered({
teamName: 'team-a',
memberName: 'bob',
sinceIso: '2026-04-29T00:00:00.000Z',
workSyncIntentKeyPrefix: 'agenda-sync-still-stuck:',
})
).resolves.toBe(1);
});
it('finds delivered review pickup request event ids from member-scoped outbox files', async () => {
const input = {
id: 'member-work-sync:team-a:bob:review-pickup:evt-a+evt-b',

View file

@ -372,6 +372,51 @@ async function forceRetryableOutboxDue(input: {
);
}
async function backdateDeliveredOutboxItems(input: {
teamsBasePath: string;
teamName: string;
memberName: string;
updatedAt: string;
}): Promise<void> {
const outboxPath = path.join(
input.teamsBasePath,
input.teamName,
'members',
input.memberName,
'.member-work-sync',
'outbox.json'
);
const parsed = JSON.parse(await fs.promises.readFile(outboxPath, 'utf8')) as {
items?: Record<string, { status?: string; updatedAt?: string }>;
};
const touchedIds: string[] = [];
for (const [id, item] of Object.entries(parsed.items ?? {})) {
if (item.status === 'delivered') {
item.updatedAt = input.updatedAt;
touchedIds.push(id);
}
}
expect(touchedIds.length).toBeGreaterThan(0);
await fs.promises.writeFile(outboxPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf8');
const indexPath = path.join(
input.teamsBasePath,
input.teamName,
'.member-work-sync',
'indexes',
'outbox-index.json'
);
const index = JSON.parse(await fs.promises.readFile(indexPath, 'utf8')) as {
items?: Record<string, { updatedAt?: string }>;
};
for (const id of touchedIds) {
if (index.items?.[id]) {
index.items[id].updatedAt = input.updatedAt;
}
}
await fs.promises.writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, 'utf8');
}
describe('createMemberWorkSyncFeature composition', () => {
it('schedules proof-missing recovery through the work-sync queue', async () => {
const claudeRoot = makeTempRoot();
@ -1531,6 +1576,108 @@ describe('createMemberWorkSyncFeature composition', () => {
}
});
it('delivers still-stuck recovery from json outbox when a delivered agenda nudge gets no report', async () => {
const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot);
const teamsBasePath = getTeamsBasePath();
const teamName = 'team-json-still-stuck-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: 'codex' }],
})),
} as never,
taskReader: {
getTasks: vi.fn(async () => [
{
id: 'task-1',
displayId: '11111111',
subject: 'Recover ignored delivered sync',
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 seedShadowReadyMetrics({ teamsBasePath, teamName, memberName });
feature.noteTeamChange({ type: 'task', teamName, taskId: 'task-1' } as never);
let agendaFingerprint = '';
await waitForAssertion(async () => {
const status = await feature.getStatus({ teamName, memberName });
agendaFingerprint = status.agenda.fingerprint;
const nudges = (await readInboxMessages({ teamsBasePath, teamName, memberName })).filter(
(message) => message.messageKind === 'member_work_sync_nudge'
);
expect(nudges).toHaveLength(1);
expect(
Object.values(await readMemberOutboxItems({ teamsBasePath, teamName, memberName }))
).toEqual([
expect.objectContaining({
status: 'delivered',
deliveredMessageId: nudges[0]?.messageId,
}),
]);
});
await backdateDeliveredOutboxItems({
teamsBasePath,
teamName,
memberName,
updatedAt: new Date(Date.now() - 10 * 60_000).toISOString(),
});
await seedNativeStaleInProgressBlockingMetrics({
teamsBasePath,
teamName,
memberName,
agendaFingerprint,
});
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(2);
expect(nudges[1]?.messageId).toContain('agenda-sync-still-stuck');
expect(nudges[1]?.text).toContain('still no accepted member_work_sync_report');
expect(
Object.values(await readMemberOutboxItems({ teamsBasePath, teamName, memberName }))
).toEqual(
expect.arrayContaining([
expect.objectContaining({
status: 'delivered',
deliveredMessageId: nudges[1]?.messageId,
}),
])
);
});
} finally {
await feature.dispose();
}
});
it('delivers targeted OpenCode nudges even when global phase2 metrics are noisy', async () => {
const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot);

View file

@ -1,12 +1,12 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { TeamProvisioningService } from '../../../../src/main/services/team/TeamProvisioningService';
import { TeamRuntimeAdapterRegistry } from '../../../../src/main/services/team/runtime';
import { TeamProvisioningService } from '../../../../src/main/services/team/TeamProvisioningService';
import { setClaudeBasePathOverride } from '../../../../src/main/utils/pathDecoder';
import {
buildOpenCodeScenarioTeamRequest,
buildScenarioRuntimeMessageInput,
@ -87,6 +87,12 @@ describe('OpenCode production prompt artifacts safe e2e', () => {
expect(member.prompt).toContain('AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1');
expect(member.prompt).toContain('agent-teams_message_send');
expect(member.prompt).toContain('Launch bootstrap is a silent attach');
expect(member.prompt).toContain(
'That bootstrap restriction is only about team registry/startup files'
);
expect(member.prompt).toContain(
'you may inspect, read/search, and edit files in the project working directory as your available tools allow'
);
expect(member.prompt).toContain('stay idle silently');
expect(member.prompt).not.toContain('Call SendMessage');
expect(member.prompt).not.toContain('Use SendMessage');
@ -125,6 +131,8 @@ describe('OpenCode production prompt artifacts safe e2e', () => {
expect(directCommand?.text).toContain('Include source="runtime_delivery"');
expect(directCommand?.text).toContain('Include relayOfMessageId="semantic-direct-');
expect(directCommand?.text).toContain('Action mode for this message: ask.');
expect(directCommand?.text).toContain('Action mode ASK is read-only');
expect(directCommand?.text).not.toContain('If this delivered message assigns implementation');
expect(directCommand?.text).toContain('You must not end this turn empty.');
expect(directCommand?.text).toContain('include taskRefs exactly as provided');
expect(directCommand?.text).toContain('"displayId":"59560c95"');
@ -137,6 +145,8 @@ describe('OpenCode production prompt artifacts safe e2e', () => {
expect(peerCommand?.text).toContain('to="jack"');
expect(peerCommand?.text).toContain('from="bob"');
expect(peerCommand?.text).toContain('Action mode for this message: delegate.');
expect(peerCommand?.text).toContain('Action mode DELEGATE is orchestration-only');
expect(peerCommand?.text).not.toContain('If this delivered message assigns implementation');
expect(peerCommand?.text).toContain('"displayId":"3375c939"');
expect(peerCommand?.taskRefs).toEqual([
{ taskId: 'task-3375c939-peer-relay', displayId: '3375c939', teamName },

View file

@ -704,6 +704,12 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
);
const launchArg = launchOpenCodeTeam.mock.calls[0]?.[0];
expect(launchArg?.members[0]?.prompt).toContain('Do NOT create local team files');
expect(launchArg?.members[0]?.prompt).toContain(
'That bootstrap restriction is only about team registry/startup files'
);
expect(launchArg?.members[0]?.prompt).toContain(
'you may inspect, read/search, and edit files in the project working directory as your available tools allow'
);
expect(launchArg?.members[0]?.prompt).toContain('Launch bootstrap is a silent attach');
expect(launchArg?.members[0]?.prompt).toContain('stay idle silently');
expect(launchArg?.members[0]?.prompt).not.toContain('agent-teams_member_briefing');
@ -1094,6 +1100,8 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
expect(sentText).toContain('Include source="runtime_delivery"');
expect(sentText).toContain('Include relayOfMessageId="msg-1"');
expect(sentText).toContain('Action mode for this message: delegate.');
expect(sentText).toContain('Action mode DELEGATE is orchestration-only');
expect(sentText).not.toContain('If this delivered message assigns implementation');
expect(sentText).toContain('You must not end this turn empty.');
expect(sentText).toContain('<opencode_delivery_context>');
expect(sentText).toContain('"kind":"opencode-delivery-context"');
@ -1248,6 +1256,12 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
expect(sentText).toContain('agent-teams_member_work_sync_status');
expect(sentText).toContain('agent-teams_member_work_sync_report');
expect(sentText).toContain('mcp__agent-teams__member_work_sync_report');
expect(sentText).toContain('For agenda sync, only agent-teams_member_work_sync_report');
expect(sentText).not.toContain('Concrete task progress');
expect(sentText).toContain('If this delivered message assigns implementation');
expect(sentText).toContain(
'you may inspect, read/search, and edit files in the project working directory as your available tools allow'
);
expect(sentText).toContain('A status-only tool call is incomplete');
expect(sentText).toContain('teamName="team-a"');
expect(sentText).toContain('memberName="bob"');
@ -1296,6 +1310,8 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
expect(sentText).toContain('"workSyncReviewRequestEventIds":["evt-review-request"]');
expect(sentText).toContain('targeted member-work-sync review pickup nudge');
expect(sentText).toContain('review workflow tools');
expect(sentText).toContain('Review workflow tool usage');
expect(sentText).not.toContain('Concrete review progress');
expect(sentText).toContain('Do not mark the review complete from this prompt alone.');
expect(sentText).toContain('agent-teams_member_work_sync_report');
expect(sentText).toContain('A status-only tool call is incomplete');

View file

@ -209,6 +209,32 @@ type RuntimeTelemetryProcessTableRow = RuntimeProcessTableRow & {
runtimeTelemetrySource?: 'native' | 'wsl' | 'windows-host';
};
type LeadWorkSyncTestTaskRef = { taskId: string; displayId?: string; teamName?: string };
type LeadWorkSyncTestInboxMessage = {
from: string;
to?: string;
text: string;
timestamp: string;
messageId: string;
read: boolean;
messageKind?: string;
taskRefs?: LeadWorkSyncTestTaskRef[];
};
type LeadWorkSyncReadCommitTestHarness = {
hasAcceptedLeadWorkSyncReport(input: { teamName: string; leadName: string }): Promise<boolean>;
getLeadRelayReadCommitBatch(input: {
teamName: string;
leadName: string;
batch: LeadWorkSyncTestInboxMessage[];
}): Promise<LeadWorkSyncTestInboxMessage[]>;
};
function leadWorkSyncReadCommitHarness(
svc: TeamProvisioningService
): LeadWorkSyncReadCommitTestHarness {
return svc as unknown as LeadWorkSyncReadCommitTestHarness;
}
function restoreRuntimePidusageTelemetryEnv() {
if (ORIGINAL_RUNTIME_PIDUSAGE_ENABLED === undefined) {
delete process.env.CLAUDE_TEAM_RUNTIME_PIDUSAGE_ENABLED;
@ -11711,6 +11737,67 @@ describe('TeamProvisioningService', () => {
}
});
it('keeps legacy OpenCode work-sync delivery pending without accepted report proof', async () => {
const previous = process.env.CLAUDE_TEAM_OPENCODE_PROMPT_DELIVERY_WATCHDOG;
process.env.CLAUDE_TEAM_OPENCODE_PROMPT_DELIVERY_WATCHDOG = '0';
try {
const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
ok: true,
providerId: 'opencode',
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
responseObservation: {
state: 'responded_non_visible_tool' as const,
deliveredUserMessageId: 'oc-user-legacy-work-sync',
assistantMessageId: 'oc-assistant-legacy-work-sync',
toolCallNames: ['member_work_sync_status', 'member_work_sync_report'],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
reason: null,
},
diagnostics: [],
}));
await configureOpenCodeBobDeliveryService({ svc, sendMessageToMember });
svc.setMemberWorkSyncAcceptedReportChecker(async () => false);
await expect(
svc.deliverOpenCodeMemberMessage('team-a', {
memberName: 'bob',
text: 'Work sync check for #task-1.',
messageId: 'msg-legacy-work-sync-report',
replyRecipient: 'team-lead',
actionMode: 'do',
messageKind: 'member_work_sync_nudge',
workSyncIntent: 'agenda_sync',
taskRefs: [
{
taskId: 'task-1',
displayId: 'task-1',
teamName: 'team-a',
},
],
source: 'watcher',
inboxTimestamp: '2026-04-25T10:00:00.000Z',
})
).resolves.toMatchObject({
delivered: true,
accepted: true,
responsePending: true,
responseState: 'responded_non_visible_tool',
reason: 'member_work_sync_report_required',
});
} finally {
if (previous === undefined) {
delete process.env.CLAUDE_TEAM_OPENCODE_PROMPT_DELIVERY_WATCHDOG;
} else {
process.env.CLAUDE_TEAM_OPENCODE_PROMPT_DELIVERY_WATCHDOG = previous;
}
}
});
it('retries OpenCode direct asks after non-visible tool activity with an explicit retry header', async () => {
const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
@ -11977,6 +12064,7 @@ describe('TeamProvisioningService', () => {
}));
await configureOpenCodeBobDeliveryService({ svc, sendMessageToMember });
svc.setControlApiBaseUrlResolver(async () => 'http://127.0.0.1:43123');
svc.setMemberWorkSyncAcceptedReportChecker(async () => true);
await expect(
svc.deliverOpenCodeMemberMessage('team-a', {
@ -12010,6 +12098,155 @@ describe('TeamProvisioningService', () => {
);
});
it('accepts member work sync report proof even when OpenCode also sends a visible reply', async () => {
const svc = new TeamProvisioningService();
const taskRef = {
taskId: 'task-1',
displayId: 'task-1',
teamName: 'team-a',
};
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
ok: true,
providerId: 'opencode',
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
prePromptCursor: 'cursor-before',
responseObservation: {
state: 'responded_visible_message' as const,
deliveredUserMessageId: 'oc-user-work-sync-report-visible',
assistantMessageId: 'oc-assistant-work-sync-report-visible',
toolCallNames: ['member_work_sync_status', 'member_work_sync_report', 'message_send'],
visibleMessageToolCallId: 'call-visible-work-sync-report',
visibleReplyMessageId: 'visible-work-sync-report-reply',
visibleReplyCorrelation: 'relayOfMessageId' as const,
latestAssistantPreview: null,
reason: null,
},
diagnostics: [],
}));
const observeMessageDelivery = vi.fn(async (input: Record<string, unknown>) => ({
ok: true,
providerId: 'opencode',
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
responseObservation: {
state: 'responded_visible_message' as const,
deliveredUserMessageId: 'oc-user-work-sync-report-visible',
assistantMessageId: 'oc-assistant-work-sync-report-visible',
toolCallNames: ['member_work_sync_status', 'member_work_sync_report', 'message_send'],
visibleMessageToolCallId: 'call-visible-work-sync-report',
visibleReplyMessageId: 'visible-work-sync-report-reply',
visibleReplyCorrelation: 'relayOfMessageId' as const,
latestAssistantPreview: null,
reason: null,
},
diagnostics: [],
}));
await configureOpenCodeBobDeliveryService({
svc,
sendMessageToMember,
observeMessageDelivery,
});
svc.setMemberWorkSyncAcceptedReportChecker(async () => true);
const inboxDir = path.join(tempTeamsBase, 'team-a', 'inboxes');
await fsPromises.mkdir(inboxDir, { recursive: true });
await fsPromises.writeFile(
path.join(inboxDir, 'team-lead.json'),
`${JSON.stringify(
[
{
from: 'bob',
to: 'team-lead',
text: 'I reported that I am still working on task-1.',
timestamp: '2026-04-25T10:00:01.000Z',
read: false,
messageId: 'visible-work-sync-report-reply',
relayOfMessageId: 'msg-work-sync-report-visible',
source: 'runtime_delivery',
taskRefs: [taskRef],
},
],
null,
2
)}\n`,
'utf8'
);
await expect(
svc.deliverOpenCodeMemberMessage('team-a', {
memberName: 'bob',
text: 'Work sync check for #task-1.',
messageId: 'msg-work-sync-report-visible',
replyRecipient: 'team-lead',
actionMode: 'do',
messageKind: 'member_work_sync_nudge',
workSyncIntent: 'agenda_sync',
taskRefs: [taskRef],
source: 'watcher',
inboxTimestamp: '2026-04-25T10:00:00.000Z',
})
).resolves.toMatchObject({
delivered: true,
accepted: true,
responsePending: false,
responseState: 'responded_visible_message',
ledgerStatus: 'responded',
});
});
it('keeps OpenCode member work sync report pending until the report is accepted', async () => {
const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
ok: true,
providerId: 'opencode',
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
prePromptCursor: 'cursor-before',
responseObservation: {
state: 'responded_non_visible_tool' as const,
deliveredUserMessageId: 'oc-user-work-sync-rejected-report',
assistantMessageId: 'oc-assistant-work-sync-rejected-report',
toolCallNames: ['member_work_sync_status', 'member_work_sync_report'],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
reason: null,
},
diagnostics: [],
}));
await configureOpenCodeBobDeliveryService({ svc, sendMessageToMember });
svc.setMemberWorkSyncAcceptedReportChecker(async () => false);
await expect(
svc.deliverOpenCodeMemberMessage('team-a', {
memberName: 'bob',
text: 'Work sync check for #task-1.',
messageId: 'msg-work-sync-rejected-report',
replyRecipient: 'team-lead',
actionMode: 'do',
messageKind: 'member_work_sync_nudge',
workSyncIntent: 'agenda_sync',
taskRefs: [
{
taskId: 'task-1',
displayId: 'task-1',
teamName: 'team-a',
},
],
source: 'watcher',
inboxTimestamp: '2026-04-25T10:00:00.000Z',
})
).resolves.toMatchObject({
delivered: true,
accepted: true,
responsePending: true,
responseState: 'responded_non_visible_tool',
ledgerStatus: 'retry_scheduled',
reason: 'member_work_sync_report_required',
});
});
it('accepts review workflow tools as review pickup delivery response proof', async () => {
const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
@ -12062,6 +12299,145 @@ describe('TeamProvisioningService', () => {
});
});
it.each([
{
name: 'plain text',
responseObservation: {
state: 'responded_plain_text' as const,
deliveredUserMessageId: 'oc-user-work-sync-plain',
assistantMessageId: 'oc-assistant-work-sync-plain',
toolCallNames: [],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: 'I am still working on task-1 and will continue now.',
reason: null,
},
},
{
name: 'visible message',
seedVisibleReply: true,
responseObservation: {
state: 'responded_visible_message' as const,
deliveredUserMessageId: 'oc-user-work-sync-visible',
assistantMessageId: 'oc-assistant-work-sync-visible',
toolCallNames: ['agent-teams_message_send'],
visibleMessageToolCallId: 'call-visible-work-sync',
visibleReplyMessageId: 'visible-work-sync-reply',
visibleReplyCorrelation: 'relayOfMessageId' as const,
latestAssistantPreview: null,
reason: null,
},
},
{
name: 'task tool',
responseObservation: {
state: 'responded_non_visible_tool' as const,
deliveredUserMessageId: 'oc-user-work-sync-task-tool',
assistantMessageId: 'oc-assistant-work-sync-task-tool',
toolCallNames: ['task_start'],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
reason: null,
},
},
{
name: 'agenda-sync review tool',
responseObservation: {
state: 'responded_non_visible_tool' as const,
deliveredUserMessageId: 'oc-user-work-sync-review-tool',
assistantMessageId: 'oc-assistant-work-sync-review-tool',
toolCallNames: ['review_start'],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
reason: null,
},
},
])(
'keeps member work sync $name OpenCode deliveries pending without report proof',
async ({ responseObservation, seedVisibleReply }) => {
const svc = new TeamProvisioningService();
const taskRef = {
taskId: 'task-1',
displayId: 'task-1',
teamName: 'team-a',
};
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
ok: true,
providerId: 'opencode',
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
prePromptCursor: 'cursor-before',
responseObservation,
diagnostics: [],
}));
const observeMessageDelivery = vi.fn(async (input: Record<string, unknown>) => ({
ok: true,
providerId: 'opencode',
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
responseObservation,
diagnostics: [],
}));
await configureOpenCodeBobDeliveryService({
svc,
sendMessageToMember,
observeMessageDelivery,
});
if (seedVisibleReply) {
const inboxDir = path.join(tempTeamsBase, 'team-a', 'inboxes');
await fsPromises.mkdir(inboxDir, { recursive: true });
await fsPromises.writeFile(
path.join(inboxDir, 'team-lead.json'),
`${JSON.stringify(
[
{
from: 'bob',
to: 'team-lead',
text: 'I am still working on task-1 and will continue now.',
timestamp: '2026-04-25T10:00:01.000Z',
read: false,
messageId: 'visible-work-sync-reply',
relayOfMessageId: 'msg-work-sync-without-report-proof',
source: 'runtime_delivery',
taskRefs: [taskRef],
},
],
null,
2
)}\n`,
'utf8'
);
}
await expect(
svc.deliverOpenCodeMemberMessage('team-a', {
memberName: 'bob',
text: 'Work sync check for #task-1.',
messageId: 'msg-work-sync-without-report-proof',
replyRecipient: 'team-lead',
actionMode: 'do',
messageKind: 'member_work_sync_nudge',
workSyncIntent: 'agenda_sync',
taskRefs: [taskRef],
source: 'watcher',
inboxTimestamp: '2026-04-25T10:00:00.000Z',
})
).resolves.toMatchObject({
delivered: true,
accepted: true,
responsePending: true,
responseState: responseObservation.state,
ledgerStatus: 'retry_scheduled',
reason: 'member_work_sync_report_required',
});
}
);
it('keeps member work sync status-only OpenCode deliveries pending', async () => {
const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
@ -12109,7 +12485,7 @@ describe('TeamProvisioningService', () => {
responsePending: true,
responseState: 'responded_non_visible_tool',
ledgerStatus: 'retry_scheduled',
reason: 'non_visible_tool_without_task_progress',
reason: 'member_work_sync_report_required',
});
});
@ -23945,6 +24321,103 @@ describe('TeamProvisioningService', () => {
});
});
it('keeps lead work-sync inbox rows unread without accepted report or recovery', async () => {
const svc = new TeamProvisioningService();
const harness = leadWorkSyncReadCommitHarness(svc);
const normalMessage = {
from: 'alice',
to: 'team-lead',
text: 'Please check task-1.',
timestamp: '2026-04-25T10:00:00.000Z',
messageId: 'msg-normal',
read: false,
};
const workSyncMessage = {
from: 'system',
to: 'team-lead',
text: 'Work sync required.',
timestamp: '2026-04-25T10:00:01.000Z',
messageId: 'msg-work-sync',
messageKind: 'member_work_sync_nudge',
taskRefs: [{ taskId: 'task-1', displayId: 'task-1', teamName: 'team-a' }],
read: false,
};
vi.spyOn(harness, 'hasAcceptedLeadWorkSyncReport').mockResolvedValue(false);
const readCommitBatch = await harness.getLeadRelayReadCommitBatch({
teamName: 'team-a',
leadName: 'team-lead',
batch: [normalMessage, workSyncMessage],
});
expect(readCommitBatch).toEqual([normalMessage]);
});
it('read-commits lead work-sync inbox rows after accepted report proof', async () => {
const svc = new TeamProvisioningService();
const harness = leadWorkSyncReadCommitHarness(svc);
const recoveryScheduler = vi.fn();
const workSyncMessage = {
from: 'system',
to: 'team-lead',
text: 'Work sync required.',
timestamp: '2026-04-25T10:00:01.000Z',
messageId: 'msg-work-sync',
messageKind: 'member_work_sync_nudge',
taskRefs: [{ taskId: 'task-1', displayId: 'task-1', teamName: 'team-a' }],
read: false,
};
vi.spyOn(harness, 'hasAcceptedLeadWorkSyncReport').mockResolvedValue(true);
svc.setMemberWorkSyncProofMissingRecoveryScheduler(recoveryScheduler);
const readCommitBatch = await harness.getLeadRelayReadCommitBatch({
teamName: 'team-a',
leadName: 'team-lead',
batch: [workSyncMessage],
});
expect(readCommitBatch).toEqual([workSyncMessage]);
expect(recoveryScheduler).not.toHaveBeenCalled();
});
it('read-commits lead work-sync inbox rows when proof-missing recovery is queued', async () => {
const svc = new TeamProvisioningService();
const harness = leadWorkSyncReadCommitHarness(svc);
const recoveryScheduler = vi.fn(async () => ({
scheduled: true,
reason: 'scheduled',
intentKey: 'proof-missing:msg-work-sync',
}));
const taskRefs = [{ taskId: 'task-1', displayId: 'task-1', teamName: 'team-a' }];
const workSyncMessage = {
from: 'system',
to: 'team-lead',
text: 'Work sync required.',
timestamp: '2026-04-25T10:00:01.000Z',
messageId: 'msg-work-sync',
messageKind: 'member_work_sync_nudge',
taskRefs,
read: false,
};
vi.spyOn(harness, 'hasAcceptedLeadWorkSyncReport').mockResolvedValue(false);
svc.setMemberWorkSyncProofMissingRecoveryScheduler(recoveryScheduler);
const readCommitBatch = await harness.getLeadRelayReadCommitBatch({
teamName: 'team-a',
leadName: 'team-lead',
batch: [workSyncMessage],
});
expect(readCommitBatch).toEqual([workSyncMessage]);
expect(recoveryScheduler).toHaveBeenCalledWith({
teamName: 'team-a',
memberName: 'team-lead',
originalMessageId: 'msg-work-sync',
taskRefs,
reason: 'lead_member_work_sync_report_required',
});
});
it('applies an unseen newer failure signal and transitions the member to failed_to_start', async () => {
const latestHeartbeatAt = '2026-04-16T10:00:00.000Z';
const run = createMemberSpawnRun({

View file

@ -1015,7 +1015,7 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
expect(prompt).toContain(
'Do NOT create, assign, or delegate any new task in this turn. If the board is empty, stay silent and wait for a fresh user instruction.'
);
expect(prompt).toContain('DELEGATION-FIRST (behavior rule for ALL future turns):');
expect(prompt).toContain('DELEGATION-FIRST (behavior rule for ALL future lead turns):');
expect(prompt).toContain(`AGENT_BLOCK_OPEN is exactly: ${AGENT_BLOCK_OPEN}`);
expect(prompt).toContain(`AGENT_BLOCK_CLOSE is exactly: ${AGENT_BLOCK_CLOSE}`);
expect(prompt).toContain(