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", "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-agent-cli-launch": "node ./scripts/prove-agent-cli-launch.mjs",
"team:prove-provider-launch-stress": "node ./scripts/prove-provider-launch-stress.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", "team:smoke-changes-real-data": "tsx scripts/team-changes-real-data-smoke.ts",
"smoke:codex-runtime-install": "tsx scripts/smoke/codex-runtime-install.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", "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:validate": "tsx scripts/i18n/validate.ts",
"i18n:types": "i18next-cli types --quiet", "i18n:types": "i18next-cli types --quiet",
"test": "vitest run", "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: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:watch": "vitest",
"test:coverage": "vitest run --coverage", "test:coverage": "vitest run --coverage",
@ -393,6 +393,11 @@
}, },
"packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800", "packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800",
"pnpm": { "pnpm": {
"auditConfig": {
"ignoreGhsas": [
"GHSA-5xrq-8626-4rwp"
]
},
"overrides": { "overrides": {
"@hono/node-server@1": "1.19.13", "@hono/node-server@1": "1.19.13",
"@xmldom/xmldom": "0.8.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', 'exec',
'vitest', 'vitest',
'run', 'run',
'--maxWorkers', '--maxWorkers=1',
'1',
'--minWorkers',
'1',
'test/main/utils/AgentCliLaunch.live-e2e.test.ts', 'test/main/utils/AgentCliLaunch.live-e2e.test.ts',
], ],
{ {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,13 +5,25 @@ import {
} from '../domain'; } from '../domain';
import { appendMemberWorkSyncAudit } from './MemberWorkSyncAudit'; 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'; import type { MemberWorkSyncUseCaseDeps } from './ports';
const STATUS_ONLY_RECOVERY_INTENT_PREFIX = 'status-only'; const STATUS_ONLY_RECOVERY_INTENT_PREFIX = 'status-only';
const AGENDA_SYNC_REFRESH_INTENT_PREFIX = 'agenda-sync-refresh'; 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[] { function getReviewRequestEventIds(status: MemberWorkSyncStatus): string[] {
return [ 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 { export interface MemberWorkSyncNudgeOutboxPlanResult {
planned: boolean; planned: boolean;
code: 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( private async planStatusOnlyRecovery(
status: MemberWorkSyncStatus, status: MemberWorkSyncStatus,
baseInput: MemberWorkSyncOutboxEnsureInput baseInput: MemberWorkSyncOutboxEnsureInput,
activationReason?: MemberWorkSyncNudgeActivationReason
): Promise<MemberWorkSyncNudgeOutboxPlanResult> { ): Promise<MemberWorkSyncNudgeOutboxPlanResult> {
const outboxStore = this.deps.outboxStore; const outboxStore = this.deps.outboxStore;
if (!outboxStore) { if (!outboxStore) {
@ -173,7 +282,82 @@ export class MemberWorkSyncNudgeOutboxPlanner {
return { planned: false, code: 'payload_conflict' }; 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 = { const recoveryPlanResult = {
planned: recoveryPlanned, planned: recoveryPlanned,
code: recoveryResult.outcome, code: recoveryResult.outcome,
@ -299,10 +483,19 @@ export class MemberWorkSyncNudgeOutboxPlanner {
existingItemStatus: recoveryResult.item.status, 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 = { const recoveryPlanResult = {
planned: recoveryPlanned, planned: recoveryPlanned,
code: recoveryResult.outcome, code: recoveryResult.outcome,
@ -310,6 +503,15 @@ export class MemberWorkSyncNudgeOutboxPlanner {
await this.appendPlanAudit(status, recoveryPlanResult); await this.appendPlanAudit(status, recoveryPlanResult);
return 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', { this.deps.logger?.warn('member work sync nudge outbox payload conflict', {
teamName: status.teamName, teamName: status.teamName,
memberName: status.memberName, memberName: status.memberName,
@ -334,7 +536,16 @@ export class MemberWorkSyncNudgeOutboxPlanner {
existingItemStatus: result.item.status, 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 ( if (
input.payload.workSyncIntent === 'review_pickup' && input.payload.workSyncIntent === 'review_pickup' &&
@ -346,7 +557,10 @@ export class MemberWorkSyncNudgeOutboxPlanner {
return { planned: false, code }; 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); await this.appendPlanAudit(status, planResult);
return planResult; return planResult;
} }

View file

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

View file

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

View file

@ -263,30 +263,6 @@ function canClaimOutboxItem(item: MemberWorkSyncOutboxItem, nowIso: string): boo
return item.nextAttemptAt <= nowIso; 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( function getDueOutboxRoutes(
index: OutboxIndexFile, index: OutboxIndexFile,
nowIso: string, nowIso: string,
@ -732,13 +708,17 @@ export class JsonMemberWorkSyncStore
const current = outbox.items[input.id]; const current = outbox.items[input.id];
if (current) { if (current) {
if (current.payloadHash !== input.payloadHash) { if (current.payloadHash !== input.payloadHash) {
if (isSameReviewPickupIntent(current, input) && !isOutboxTerminal(current.status)) { if (current.status !== 'delivered' && current.status !== 'failed_terminal') {
const next: MemberWorkSyncOutboxItem = { const next: MemberWorkSyncOutboxItem = {
...current, ...current,
agendaFingerprint: input.agendaFingerprint, agendaFingerprint: input.agendaFingerprint,
payloadHash: input.payloadHash, payloadHash: input.payloadHash,
payload: input.payload, payload: input.payload,
status: 'pending', status: 'pending',
attemptGeneration:
current.status === 'claimed'
? current.attemptGeneration + 1
: current.attemptGeneration,
updatedAt: input.nowIso, updatedAt: input.nowIso,
}; };
applyOptionalNextAttemptAt(next, input.nextAttemptAt); applyOptionalNextAttemptAt(next, input.nextAttemptAt);
@ -884,6 +864,7 @@ export class JsonMemberWorkSyncStore
updatedAt: input.nowIso, updatedAt: input.nowIso,
}; };
delete next.lastError; delete next.lastError;
delete next.nextAttemptAt;
return next; return next;
}); });
} }
@ -924,6 +905,17 @@ export class JsonMemberWorkSyncStore
async countRecentDelivered( async countRecentDelivered(
input: MemberWorkSyncOutboxCountRecentDeliveredInput input: MemberWorkSyncOutboxCountRecentDeliveredInput
): Promise<number> { ): 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); let index = await this.readOutboxIndexFile(input.teamName);
if (Object.keys(index.items).length === 0) { if (Object.keys(index.items).length === 0) {
await this.enqueue(input.teamName, async () => { await this.enqueue(input.teamName, async () => {

View file

@ -1934,16 +1934,19 @@ async function initializeServices(): Promise<void> {
resolveControlUrl: async () => getTeamControlApiBaseUrl(), resolveControlUrl: async () => getTeamControlApiBaseUrl(),
proofMissingRecoveryGuard: { proofMissingRecoveryGuard: {
shouldDispatch: async (input) => { shouldDispatch: async (input) => {
const isOpenCodeRecipient = await teamProvisioningService
.isOpenCodeRuntimeRecipient(input.teamName, input.memberName)
.catch(() => false);
if (!isOpenCodeRecipient) {
return { ok: true };
}
const status = await teamProvisioningService.getOpenCodeRuntimeDeliveryStatus( const status = await teamProvisioningService.getOpenCodeRuntimeDeliveryStatus(
input.teamName, input.teamName,
input.originalMessageId input.originalMessageId
); );
if (!status) { if (!status) {
return { return { ok: true };
ok: false,
reason: 'proof_missing_recovery_record_missing',
retryable: false,
};
} }
const impact = status.userVisibleImpact; const impact = status.userVisibleImpact;
@ -2127,6 +2130,21 @@ async function initializeServices(): Promise<void> {
? memberWorkSyncFeature.scheduleProofMissingRecovery(input) ? memberWorkSyncFeature.scheduleProofMissingRecovery(input)
: Promise.resolve({ scheduled: false, reason: 'invalid' }) : 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(() => { scheduleStartupTask(() => {
void teamDataService void teamDataService
.listTeams() .listTeams()

View file

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

View file

@ -3356,6 +3356,11 @@ type MemberWorkSyncProofMissingRecoveryScheduler = (input: {
reason?: string; reason?: string;
}) => Promise<unknown> | unknown; }) => Promise<unknown> | unknown;
type MemberWorkSyncAcceptedReportChecker = (input: {
teamName: string;
memberName: string;
}) => Promise<boolean> | boolean;
function normalizeSameTeamText(text: string): string { function normalizeSameTeamText(text: string): string {
return text.trim().replace(/\r\n/g, '\n'); return text.trim().replace(/\r\n/g, '\n');
} }
@ -3649,6 +3654,7 @@ export class TeamProvisioningService {
| null = null; | null = null;
private memberWorkSyncProofMissingRecoveryScheduler: MemberWorkSyncProofMissingRecoveryScheduler | null = private memberWorkSyncProofMissingRecoveryScheduler: MemberWorkSyncProofMissingRecoveryScheduler | null =
null; null;
private memberWorkSyncAcceptedReportChecker: MemberWorkSyncAcceptedReportChecker | null = null;
private readonly memberLogsFinder: TeamMemberLogsFinder; private readonly memberLogsFinder: TeamMemberLogsFinder;
private readonly transcriptProjectResolver: TeamTranscriptProjectResolver; private readonly transcriptProjectResolver: TeamTranscriptProjectResolver;
private readonly taskActivityIntervalService = new TeamTaskActivityIntervalService(); private readonly taskActivityIntervalService = new TeamTaskActivityIntervalService();
@ -4022,6 +4028,12 @@ export class TeamProvisioningService {
this.memberWorkSyncProofMissingRecoveryScheduler = scheduler; this.memberWorkSyncProofMissingRecoveryScheduler = scheduler;
} }
setMemberWorkSyncAcceptedReportChecker(
checker: MemberWorkSyncAcceptedReportChecker | null
): void {
this.memberWorkSyncAcceptedReportChecker = checker;
}
setCrossTeamSender( setCrossTeamSender(
sender: sender:
| ((request: { | ((request: {
@ -5517,17 +5529,26 @@ export class TeamProvisioningService {
return (await this.resolveRuntimeRecipientProviderId(teamName, memberName)) === 'opencode'; return (await this.resolveRuntimeRecipientProviderId(teamName, memberName)) === 'opencode';
} }
private isOpenCodeDeliveryResponseReadCommitAllowed(input: { private async isOpenCodeDeliveryResponseReadCommitAllowed(input: {
teamName?: string;
memberName?: string;
responseState?: NonNullable<OpenCodeTeamRuntimeMessageResult['responseObservation']>['state']; responseState?: NonNullable<OpenCodeTeamRuntimeMessageResult['responseObservation']>['state'];
actionMode?: AgentActionMode; actionMode?: AgentActionMode;
taskRefs?: TaskRef[]; taskRefs?: TaskRef[];
visibleReply?: OpenCodeVisibleReplyProof | null; visibleReply?: OpenCodeVisibleReplyProof | null;
ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null; ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null;
}): boolean { }): Promise<boolean> {
const state = input.responseState; const state = input.responseState;
if (!state || !isOpenCodePromptResponseStateResponded(state)) { if (!state || !isOpenCodePromptResponseStateResponded(state)) {
return false; 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') { if (state === 'responded_plain_text') {
return this.isOpenCodePlainTextResponseReadCommitAllowed({ return this.isOpenCodePlainTextResponseReadCommitAllowed({
actionMode: input.actionMode, actionMode: input.actionMode,
@ -5556,18 +5577,12 @@ export class TeamProvisioningService {
private hasOpenCodeNonVisibleProgressProof( private hasOpenCodeNonVisibleProgressProof(
ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null
): boolean { ): boolean {
if (ledgerRecord?.messageKind === 'member_work_sync_nudge') {
return this.hasOpenCodeMemberWorkSyncReadCommitProof(ledgerRecord);
}
const toolNames = ledgerRecord?.observedToolCallNames ?? []; const toolNames = ledgerRecord?.observedToolCallNames ?? [];
return toolNames.some((toolName) => { return toolNames.some((toolName) => {
const normalized = this.normalizeOpenCodeObservedToolName(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 ( return (
normalized === 'task_start' || normalized === 'task_start' ||
normalized === 'task_add_comment' || 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( private hasOpenCodeObservedMessageSendToolCall(
ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null ledgerRecord?: OpenCodePromptDeliveryLedgerRecord | null
): boolean { ): boolean {
@ -5636,6 +5742,22 @@ export class TeamProvisioningService {
}): string { }): string {
const record = input.ledgerRecord; const record = input.ledgerRecord;
const state = input.responseState ?? record?.responseState; 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) { if (state === 'responded_visible_message' && !input.visibleReply) {
return 'visible_reply_destination_not_found_yet'; return 'visible_reply_destination_not_found_yet';
} }
@ -5760,7 +5882,17 @@ export class TeamProvisioningService {
if ( if (
input.ledgerRecord.lastReason === 'visible_reply_ack_only_still_requires_answer' || input.ledgerRecord.lastReason === 'visible_reply_ack_only_still_requires_answer' ||
input.ledgerRecord.lastReason === 'plain_text_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; return true;
} }
@ -6635,7 +6767,9 @@ export class TeamProvisioningService {
let ledgerRecord = input.ledgerRecord; let ledgerRecord = input.ledgerRecord;
let visibleReply = input.visibleReply ?? null; let visibleReply = input.visibleReply ?? null;
const observeMessageDelivery = input.adapter.observeMessageDelivery; const observeMessageDelivery = input.adapter.observeMessageDelivery;
const readAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({ const readAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName: input.teamName,
memberName: input.memberName,
responseState: ledgerRecord.responseState, responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined, actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs, taskRefs: ledgerRecord.taskRefs,
@ -6784,7 +6918,9 @@ export class TeamProvisioningService {
}); });
ledgerRecord = materialized.ledgerRecord; ledgerRecord = materialized.ledgerRecord;
visibleReply = materialized.visibleReply; visibleReply = materialized.visibleReply;
const observedReadAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({ const observedReadAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName: input.teamName,
memberName: input.memberName,
responseState: ledgerRecord.responseState, responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined, actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs, taskRefs: ledgerRecord.taskRefs,
@ -8752,14 +8888,27 @@ export class TeamProvisioningService {
teamColor: config?.color, teamColor: config?.color,
teamDisplayName: config?.name, 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 { return {
delivered: result.ok, delivered: result.ok,
accepted: result.ok, accepted: result.ok,
responsePending: false, responsePending: legacyWorkSyncResponsePending,
responseState: responseObservation?.state, responseState: responseObservation?.state,
...(result.ok ...(legacyWorkSyncResponsePending
? {} ? { reason: responseObservation?.reason ?? 'member_work_sync_report_required' }
: { reason: result.diagnostics[0] ?? 'opencode_message_delivery_failed' }), : result.ok
? {}
: { reason: result.diagnostics[0] ?? 'opencode_message_delivery_failed' }),
diagnostics: result.diagnostics, diagnostics: result.diagnostics,
}; };
} }
@ -8794,7 +8943,9 @@ export class TeamProvisioningService {
visibleReply: proof.visibleReply, visibleReply: proof.visibleReply,
}); });
active = proof.ledgerRecord; active = proof.ledgerRecord;
const activeReadAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({ const activeReadAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName,
memberName: canonicalMemberName,
responseState: active.responseState, responseState: active.responseState,
actionMode: active.actionMode ?? undefined, actionMode: active.actionMode ?? undefined,
taskRefs: active.taskRefs, taskRefs: active.taskRefs,
@ -8882,7 +9033,9 @@ export class TeamProvisioningService {
visibleReply: proof.visibleReply, visibleReply: proof.visibleReply,
}); });
ledgerRecord = proof.ledgerRecord; ledgerRecord = proof.ledgerRecord;
let readAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({ let readAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName,
memberName: canonicalMemberName,
responseState: ledgerRecord.responseState, responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined, actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs, taskRefs: ledgerRecord.taskRefs,
@ -9071,7 +9224,9 @@ export class TeamProvisioningService {
visibleReply: proof.visibleReply, visibleReply: proof.visibleReply,
}); });
ledgerRecord = proof.ledgerRecord; ledgerRecord = proof.ledgerRecord;
readAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({ readAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName,
memberName: canonicalMemberName,
responseState: ledgerRecord.responseState, responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined, actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs, taskRefs: ledgerRecord.taskRefs,
@ -9199,7 +9354,9 @@ export class TeamProvisioningService {
} }
const retryReadAllowed = ledgerRecord const retryReadAllowed = ledgerRecord
? this.isOpenCodeDeliveryResponseReadCommitAllowed({ ? await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName,
memberName: canonicalMemberName,
responseState: ledgerRecord.responseState, responseState: ledgerRecord.responseState,
actionMode: ledgerRecord.actionMode ?? undefined, actionMode: ledgerRecord.actionMode ?? undefined,
taskRefs: ledgerRecord.taskRefs, taskRefs: ledgerRecord.taskRefs,
@ -9463,7 +9620,9 @@ export class TeamProvisioningService {
ledgerRecord.visibleReplyCorrelation === 'relayOfMessageId', ledgerRecord.visibleReplyCorrelation === 'relayOfMessageId',
}) })
: null; : null;
const readAllowed = this.isOpenCodeDeliveryResponseReadCommitAllowed({ const readAllowed = await this.isOpenCodeDeliveryResponseReadCommitAllowed({
teamName,
memberName: canonicalMemberName,
responseState, responseState,
actionMode: input.actionMode, actionMode: input.actionMode,
taskRefs: input.taskRefs, taskRefs: input.taskRefs,
@ -23471,15 +23630,17 @@ export class TeamProvisioningService {
recoveredVisibleReply = null; recoveredVisibleReply = null;
} }
} }
const recoveredReadAllowed = const recoveredReadAllowed = recoveredRecord
recoveredRecord && ? await this.isOpenCodeDeliveryResponseReadCommitAllowed({
this.isOpenCodeDeliveryResponseReadCommitAllowed({ teamName,
responseState: recoveredRecord.responseState, memberName: memberIdentity.canonicalMemberName,
actionMode: recoveredRecord.actionMode ?? undefined, responseState: recoveredRecord.responseState,
taskRefs: recoveredRecord.taskRefs, actionMode: recoveredRecord.actionMode ?? undefined,
visibleReply: recoveredVisibleReply, taskRefs: recoveredRecord.taskRefs,
ledgerRecord: recoveredRecord, visibleReply: recoveredVisibleReply,
}); ledgerRecord: recoveredRecord,
})
: false;
if (recoveredRecord && recoveredReadAllowed) { if (recoveredRecord && recoveredReadAllowed) {
try { try {
await this.markInboxMessagesRead(teamName, memberName, [message]); await this.markInboxMessagesRead(teamName, memberName, [message]);
@ -23967,6 +24128,101 @@ export class TeamProvisioningService {
return from === 'user' || message.source === 'user_sent'; 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> { async relayLeadInboxMessages(teamName: string): Promise<number> {
const existing = this.leadInboxRelayInFlight.get(teamName); const existing = this.leadInboxRelayInFlight.get(teamName);
if (existing) { if (existing) {
@ -24407,10 +24663,6 @@ export class TeamProvisioningService {
return 0; return 0;
} }
for (const m of batch) {
relayedIds.add(m.messageId);
}
this.relayedLeadInboxMessageIds.set(teamName, this.trimRelayedSet(relayedIds));
this.rememberRecentCrossTeamLeadDeliveryMessageIds( this.rememberRecentCrossTeamLeadDeliveryMessageIds(
teamName, teamName,
batch batch
@ -24418,12 +24670,6 @@ export class TeamProvisioningService {
.map((message) => message.messageId) .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 replyText: string | null = null;
let capturedVisibleSendMessage = false; let capturedVisibleSendMessage = false;
let capturedUserVisibleSendMessage = 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 // Strip agent-only blocks — lead may respond with pure coordination content
// that is not meant for the human user. // that is not meant for the human user.
const cleanReply = replyText const cleanReply = replyText

View file

@ -1126,6 +1126,7 @@ function buildMemberBootstrapPrompt(
'This OpenCode session is created, attached, and launch-verified by the desktop app.', '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 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.', '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.', '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.', '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.', '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 input.taskRefs
?.map((ref) => ref.taskId?.trim()) ?.map((ref) => ref.taskId?.trim())
.filter((taskId): taskId is string => Boolean(taskId)) ?? []; .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 // Work-sync nudges are health/reporting probes. Requiring a visible
// message_send reply here causes false delivery failures, so accept the // message_send reply here causes false delivery failures, so accept the
// dedicated member_work_sync_report proof path while keeping normal user // 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.', '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.', '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.', '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.`, `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.', '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, taskIds.length ? `Relevant taskIds: ${taskIds.map((id) => `"${id}"`).join(', ')}.` : null,
@ -1209,7 +1216,7 @@ function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput)
: isWorkSyncNudge : isWorkSyncNudge
? [ ? [
'This delivered app message is a member-work-sync nudge.', '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}.`, `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".`, `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.', '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>` ? `<opencode_delivery_context>${deliveryContext}</opencode_delivery_context>`
: null, : null,
'You are running in OpenCode, not Claude Code or Codex native.', 'You are running in OpenCode, not Claude Code or Codex native.',
actionModeWorkScopeReminder,
...responseInstructions, ...responseInstructions,
'Do not call runtime_bootstrap_checkin or member_briefing just to answer this delivered app message.', '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.', '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([ expect(getAvailableTeamProviderModels('anthropic', providerStatus)).toEqual([
'haiku', 'haiku',
'opus', 'opus',
'claude-opus-4-7',
'claude-opus-4-6', 'claude-opus-4-6',
'sonnet', 'sonnet',
]); ]);
@ -431,6 +432,13 @@ describe('team model availability Codex catalog integration', () => {
availabilityStatus: 'available', availabilityStatus: 'available',
availabilityReason: null, availabilityReason: null,
}, },
{
value: 'claude-opus-4-7',
label: 'Opus 4.7',
badgeLabel: 'Opus 4.7',
availabilityStatus: 'available',
availabilityReason: null,
},
{ {
value: 'claude-opus-4-6', value: 'claude-opus-4-6',
label: 'Opus 4.6', label: 'Opus 4.6',

View file

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

View file

@ -232,14 +232,16 @@ class InMemoryOutboxStore implements MemberWorkSyncOutboxStorePort {
async markDelivered(input: MemberWorkSyncOutboxMarkDeliveredInput): Promise<void> { async markDelivered(input: MemberWorkSyncOutboxMarkDeliveredInput): Promise<void> {
const current = this.items.get(input.id); const current = this.items.get(input.id);
if (current?.attemptGeneration === input.attemptGeneration) { if (current?.attemptGeneration === input.attemptGeneration) {
this.items.set(input.id, { const next = {
...current, ...current,
status: 'delivered', status: 'delivered' as const,
deliveredMessageId: input.deliveredMessageId, deliveredMessageId: input.deliveredMessageId,
...(input.deliveryState ? { deliveryState: input.deliveryState } : {}), ...(input.deliveryState ? { deliveryState: input.deliveryState } : {}),
...(input.deliveryDiagnostics ? { deliveryDiagnostics: input.deliveryDiagnostics } : {}), ...(input.deliveryDiagnostics ? { deliveryDiagnostics: input.deliveryDiagnostics } : {}),
updatedAt: input.nowIso, 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( return [...this.items.values()].filter(
(item) => (item) =>
item.status === 'delivered' && item.status === 'delivered' &&
item.memberName === input.memberName && item.memberName === input.memberName &&
item.updatedAt >= input.sinceIso item.updatedAt >= input.sinceIso &&
(!input.workSyncIntentKeyPrefix ||
item.payload.workSyncIntentKey?.startsWith(input.workSyncIntentKeyPrefix) === true)
).length; ).length;
} }
@ -296,17 +304,22 @@ class InMemoryOutboxStore implements MemberWorkSyncOutboxStorePort {
class InMemoryInboxNudge implements MemberWorkSyncInboxNudgePort { class InMemoryInboxNudge implements MemberWorkSyncInboxNudgePort {
readonly inserted: Array<Parameters<MemberWorkSyncInboxNudgePort['insertIfAbsent']>[0]> = []; readonly inserted: Array<Parameters<MemberWorkSyncInboxNudgePort['insertIfAbsent']>[0]> = [];
fail = false; fail = false;
conflict = false;
async insertIfAbsent(input: Parameters<MemberWorkSyncInboxNudgePort['insertIfAbsent']>[0]) { async insertIfAbsent(input: Parameters<MemberWorkSyncInboxNudgePort['insertIfAbsent']>[0]) {
if (this.fail) { if (this.fail) {
throw new Error('inbox unavailable'); throw new Error('inbox unavailable');
} }
if (this.conflict) {
return { inserted: false, messageId: input.messageId, conflict: true };
}
this.inserted.push(input); this.inserted.push(input);
return { inserted: true, messageId: input.messageId }; return { inserted: true, messageId: input.messageId };
} }
} }
function createDeps(options?: { function createDeps(options?: {
memberName?: string;
items?: MemberWorkSyncActionableWorkItem[]; items?: MemberWorkSyncActionableWorkItem[];
activeMemberNames?: string[]; activeMemberNames?: string[];
inactive?: boolean; inactive?: boolean;
@ -321,15 +334,16 @@ function createDeps(options?: {
const clock = new MutableClock(); const clock = new MutableClock();
const store = new InMemoryStatusStore(); const store = new InMemoryStatusStore();
const auditEvents: MemberWorkSyncAuditEvent[] = []; const auditEvents: MemberWorkSyncAuditEvent[] = [];
const memberName = options?.memberName ?? 'bob';
const source: MemberWorkSyncAgendaSourceResult = { const source: MemberWorkSyncAgendaSourceResult = {
agenda: { agenda: {
teamName: 'team-a', teamName: 'team-a',
memberName: 'bob', memberName,
generatedAt: '2026-04-29T00:00:00.000Z', generatedAt: '2026-04-29T00:00:00.000Z',
items: options?.items ?? [workItem], items: options?.items ?? [workItem],
diagnostics: [], diagnostics: [],
}, },
activeMemberNames: options?.activeMemberNames ?? ['bob'], activeMemberNames: options?.activeMemberNames ?? [memberName],
inactive: options?.inactive ?? false, inactive: options?.inactive ?? false,
...(options?.providerId ? { providerId: options.providerId } : {}), ...(options?.providerId ? { providerId: options.providerId } : {}),
diagnostics: [], diagnostics: [],
@ -940,6 +954,147 @@ describe('MemberWorkSync use cases', () => {
expect(inbox.inserted[1]?.messageId).toContain('status-only'); 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 () => { it('creates an agenda-sync refresh recovery when a delivered nudge has a stale payload hash', async () => {
const outbox = new InMemoryOutboxStore(); const outbox = new InMemoryOutboxStore();
const inbox = new InMemoryInboxNudge(); const inbox = new InMemoryInboxNudge();
@ -1040,6 +1195,460 @@ describe('MemberWorkSync use cases', () => {
expect(statusOnlyItems[0]?.payload.text).toContain('Status-only recovery'); 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 () => { it('marks review pickup delivered only after the delivery port confirms prompt acceptance', async () => {
const outbox = new InMemoryOutboxStore(); const outbox = new InMemoryOutboxStore();
const inbox = new InMemoryInboxNudge(); const inbox = new InMemoryInboxNudge();

View file

@ -3,7 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
describe('CompositeMemberWorkSyncBusySignal', () => { describe('CompositeMemberWorkSyncBusySignal', () => {
it('does not block nudges forever when one busy signal fails', async () => { 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( const signal = new CompositeMemberWorkSyncBusySignal(
[ [
{ {
@ -24,7 +24,7 @@ describe('CompositeMemberWorkSyncBusySignal', () => {
memberName: 'bob', memberName: 'bob',
nowIso: '2026-04-29T00:00:00.000Z', nowIso: '2026-04-29T00:00:00.000Z',
workSyncIntent: 'agenda_sync', workSyncIntent: 'agenda_sync',
taskRefs: [{ teamName: 'team-a', taskId: 'task-1' }], taskRefs: [{ teamName: 'team-a', taskId: 'task-1', displayId: '11111111' }],
}) })
).resolves.toEqual({ busy: false }); ).resolves.toEqual({ busy: false });
expect(logger.warn).toHaveBeenCalledWith( expect(logger.warn).toHaveBeenCalledWith(
@ -56,7 +56,7 @@ describe('CompositeMemberWorkSyncBusySignal', () => {
memberName: 'bob', memberName: 'bob',
nowIso: '2026-04-29T00:00:00.000Z', nowIso: '2026-04-29T00:00:00.000Z',
workSyncIntent: 'agenda_sync', workSyncIntent: 'agenda_sync',
taskRefs: [{ teamName: 'team-a', taskId: 'task-1' }], taskRefs: [{ teamName: 'team-a', taskId: 'task-1', displayId: '11111111' }],
}) })
).resolves.toEqual({ ).resolves.toEqual({
busy: true, 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 = { const input = {
id: 'member-work-sync:team-a:bob:agenda:v1:abc', id: 'member-work-sync:team-a:bob:agenda:v1:abc',
teamName: 'team-a', teamName: 'team-a',
@ -372,11 +372,97 @@ describe('JsonMemberWorkSyncStore', () => {
ok: true, ok: true,
outcome: 'existing', 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, ok: false,
outcome: 'payload_conflict', outcome: 'payload_conflict',
existingPayloadHash: 'hash-a', existingPayloadHash: 'hash-c',
requestedPayloadHash: 'hash-b', requestedPayloadHash: 'hash-d',
}); });
}); });
@ -525,6 +611,67 @@ describe('JsonMemberWorkSyncStore', () => {
).resolves.toEqual([]); ).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 () => { it('finds recent recovery outbox rows by logical intent key', async () => {
const olderInput = { const olderInput = {
id: 'member-work-sync:team-a:bob:agenda:v1:older', 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' }); 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 () => { it('finds delivered review pickup request event ids from member-scoped outbox files', async () => {
const input = { const input = {
id: 'member-work-sync:team-a:bob:review-pickup:evt-a+evt-b', 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', () => { describe('createMemberWorkSyncFeature composition', () => {
it('schedules proof-missing recovery through the work-sync queue', async () => { it('schedules proof-missing recovery through the work-sync queue', async () => {
const claudeRoot = makeTempRoot(); 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 () => { it('delivers targeted OpenCode nudges even when global phase2 metrics are noisy', async () => {
const claudeRoot = makeTempRoot(); const claudeRoot = makeTempRoot();
setClaudeBasePathOverride(claudeRoot); setClaudeBasePathOverride(claudeRoot);

View file

@ -1,12 +1,12 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest'; 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 { TeamRuntimeAdapterRegistry } from '../../../../src/main/services/team/runtime';
import { TeamProvisioningService } from '../../../../src/main/services/team/TeamProvisioningService';
import { setClaudeBasePathOverride } from '../../../../src/main/utils/pathDecoder'; import { setClaudeBasePathOverride } from '../../../../src/main/utils/pathDecoder';
import { import {
buildOpenCodeScenarioTeamRequest, buildOpenCodeScenarioTeamRequest,
buildScenarioRuntimeMessageInput, 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_APP_MANAGED_BOOTSTRAP_V1');
expect(member.prompt).toContain('agent-teams_message_send'); expect(member.prompt).toContain('agent-teams_message_send');
expect(member.prompt).toContain('Launch bootstrap is a silent attach'); 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).toContain('stay idle silently');
expect(member.prompt).not.toContain('Call SendMessage'); expect(member.prompt).not.toContain('Call SendMessage');
expect(member.prompt).not.toContain('Use 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 source="runtime_delivery"');
expect(directCommand?.text).toContain('Include relayOfMessageId="semantic-direct-'); expect(directCommand?.text).toContain('Include relayOfMessageId="semantic-direct-');
expect(directCommand?.text).toContain('Action mode for this message: ask.'); 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('You must not end this turn empty.');
expect(directCommand?.text).toContain('include taskRefs exactly as provided'); expect(directCommand?.text).toContain('include taskRefs exactly as provided');
expect(directCommand?.text).toContain('"displayId":"59560c95"'); 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('to="jack"');
expect(peerCommand?.text).toContain('from="bob"'); expect(peerCommand?.text).toContain('from="bob"');
expect(peerCommand?.text).toContain('Action mode for this message: delegate.'); 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?.text).toContain('"displayId":"3375c939"');
expect(peerCommand?.taskRefs).toEqual([ expect(peerCommand?.taskRefs).toEqual([
{ taskId: 'task-3375c939-peer-relay', displayId: '3375c939', teamName }, { taskId: 'task-3375c939-peer-relay', displayId: '3375c939', teamName },

View file

@ -704,6 +704,12 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
); );
const launchArg = launchOpenCodeTeam.mock.calls[0]?.[0]; 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('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('Launch bootstrap is a silent attach');
expect(launchArg?.members[0]?.prompt).toContain('stay idle silently'); expect(launchArg?.members[0]?.prompt).toContain('stay idle silently');
expect(launchArg?.members[0]?.prompt).not.toContain('agent-teams_member_briefing'); 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 source="runtime_delivery"');
expect(sentText).toContain('Include relayOfMessageId="msg-1"'); expect(sentText).toContain('Include relayOfMessageId="msg-1"');
expect(sentText).toContain('Action mode for this message: delegate.'); 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('You must not end this turn empty.');
expect(sentText).toContain('<opencode_delivery_context>'); expect(sentText).toContain('<opencode_delivery_context>');
expect(sentText).toContain('"kind":"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_status');
expect(sentText).toContain('agent-teams_member_work_sync_report'); expect(sentText).toContain('agent-teams_member_work_sync_report');
expect(sentText).toContain('mcp__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('A status-only tool call is incomplete');
expect(sentText).toContain('teamName="team-a"'); expect(sentText).toContain('teamName="team-a"');
expect(sentText).toContain('memberName="bob"'); expect(sentText).toContain('memberName="bob"');
@ -1296,6 +1310,8 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
expect(sentText).toContain('"workSyncReviewRequestEventIds":["evt-review-request"]'); expect(sentText).toContain('"workSyncReviewRequestEventIds":["evt-review-request"]');
expect(sentText).toContain('targeted member-work-sync review pickup nudge'); expect(sentText).toContain('targeted member-work-sync review pickup nudge');
expect(sentText).toContain('review workflow tools'); 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('Do not mark the review complete from this prompt alone.');
expect(sentText).toContain('agent-teams_member_work_sync_report'); expect(sentText).toContain('agent-teams_member_work_sync_report');
expect(sentText).toContain('A status-only tool call is incomplete'); expect(sentText).toContain('A status-only tool call is incomplete');

View file

@ -209,6 +209,32 @@ type RuntimeTelemetryProcessTableRow = RuntimeProcessTableRow & {
runtimeTelemetrySource?: 'native' | 'wsl' | 'windows-host'; 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() { function restoreRuntimePidusageTelemetryEnv() {
if (ORIGINAL_RUNTIME_PIDUSAGE_ENABLED === undefined) { if (ORIGINAL_RUNTIME_PIDUSAGE_ENABLED === undefined) {
delete process.env.CLAUDE_TEAM_RUNTIME_PIDUSAGE_ENABLED; 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 () => { it('retries OpenCode direct asks after non-visible tool activity with an explicit retry header', async () => {
const svc = new TeamProvisioningService(); const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({ const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
@ -11977,6 +12064,7 @@ describe('TeamProvisioningService', () => {
})); }));
await configureOpenCodeBobDeliveryService({ svc, sendMessageToMember }); await configureOpenCodeBobDeliveryService({ svc, sendMessageToMember });
svc.setControlApiBaseUrlResolver(async () => 'http://127.0.0.1:43123'); svc.setControlApiBaseUrlResolver(async () => 'http://127.0.0.1:43123');
svc.setMemberWorkSyncAcceptedReportChecker(async () => true);
await expect( await expect(
svc.deliverOpenCodeMemberMessage('team-a', { 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 () => { it('accepts review workflow tools as review pickup delivery response proof', async () => {
const svc = new TeamProvisioningService(); const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({ 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 () => { it('keeps member work sync status-only OpenCode deliveries pending', async () => {
const svc = new TeamProvisioningService(); const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({ const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
@ -12109,7 +12485,7 @@ describe('TeamProvisioningService', () => {
responsePending: true, responsePending: true,
responseState: 'responded_non_visible_tool', responseState: 'responded_non_visible_tool',
ledgerStatus: 'retry_scheduled', 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 () => { 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 latestHeartbeatAt = '2026-04-16T10:00:00.000Z';
const run = createMemberSpawnRun({ const run = createMemberSpawnRun({

View file

@ -1015,7 +1015,7 @@ describe('TeamProvisioningService prompt content (solo mode discipline)', () =>
expect(prompt).toContain( 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.' '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_OPEN is exactly: ${AGENT_BLOCK_OPEN}`);
expect(prompt).toContain(`AGENT_BLOCK_CLOSE is exactly: ${AGENT_BLOCK_CLOSE}`); expect(prompt).toContain(`AGENT_BLOCK_CLOSE is exactly: ${AGENT_BLOCK_CLOSE}`);
expect(prompt).toContain( expect(prompt).toContain(