Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 49 additions & 15 deletions .agentworkforce/agents/factory-feature-guardian/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,14 +280,16 @@ class DelayedSlackTransport extends IdempotentSlackTransport {
}
}

class ReceiptlessSlackTransport extends IdempotentSlackTransport {
class LateReceiptReplaySlackTransport extends IdempotentSlackTransport {
private readonly receiptlessKeys = new Set<string>();

override async write(request: RelayTransportWriteRequest): Promise<WritebackResult> {
this.attempts.push(request);
return {
path: '/slack/draft.json',
absolutePath: '/slack/draft.json',
receipt: { id: 'mountcmd-without-provider-ts' },
};
const result = await super.write(request);
const body = request.body as { idempotencyKey?: string };
const key = body.idempotencyKey ?? `unkeyed:${this.attempts.length}`;
if (this.receiptlessKeys.has(key)) return result;
this.receiptlessKeys.add(key);
return { ...result, receipt: { id: 'mountcmd-without-provider-ts' } };
}
}

Expand Down Expand Up @@ -1095,19 +1097,51 @@ describe('factory-feature-guardian delayed Slack receipts', () => {
}
});

it('rejects and never advances on a receipt-shaped draft without provider ts', async () => {
const transport = new ReceiptlessSlackTransport();
it('keeps a delayed receipt retryable and replays the stable key without a second Slack post', async () => {
const capabilityFeatures = [
manifestFeatures[0]!,
{
id: 'capabilities-register',
name: 'Register Capabilities',
cli: 'relay capabilities register',
description: 'Registers broker capabilities.',
tier: 1,
},
];
const capabilityState: ProgressState = {
kind: 'factory-feature-guardian:progress',
version: 3,
generation: 1,
checkedIds: ['broker-up'],
cycleStartedAt: '2026-07-18T10:26:47.981Z',
totalFeatures: capabilityFeatures.length,
lastPost: { featureId: 'broker-up', ts: '17843701.029509' },
};
const formerFatalError =
'Slack post failed: no timestamp returned for feature capabilities-register';
const transport = new LateReceiptReplaySlackTransport();
const restore = bindPreviewTransport(transport);
const { ctx, files } = exactStateContext(JSON.stringify(progressState(1)));
const { ctx, files } = exactStateContext(
JSON.stringify(capabilityState),
renderManifest(capabilityFeatures)
);
try {
await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).rejects.toThrow(
'Slack post failed: no timestamp returned for feature broker-status'
);
await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).resolves.toBeUndefined();
expect(JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}').checkedIds).toEqual(['broker-up']);
expect(ctx.log).toHaveBeenCalledWith('error', 'factory-feature-guardian.post-failed', {
expect(ctx.log).toHaveBeenCalledWith('warn', 'factory-feature-guardian.post-receipt-pending', {
channel: 'C0BHWJSF309',
feature: 'broker-status',
feature: 'capabilities-register',
path: expect.any(String),
});
expect(JSON.stringify(vi.mocked(ctx.log).mock.calls)).not.toContain(formerFatalError);

await expect(guardian.handler(ctx, { type: 'cron.tick' } as never)).resolves.toBeUndefined();
const state = JSON.parse(files.get(CYCLE_STATE_PATH) ?? '{}') as ProgressState;
expect(state.checkedIds).toEqual(['broker-up', 'capabilities-register']);
expect(state.lastPost?.ts).toBe('1710000001.000100');
expect(transport.providerCreates).toBe(1);
expect(transport.attempts).toHaveLength(2);
expect(transport.attempts[0]?.body).toMatchObject(transport.attempts[1]?.body as object);
} finally {
restore();
}
Expand Down
12 changes: 10 additions & 2 deletions .agentworkforce/agents/factory-feature-guardian/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,8 +808,16 @@ export async function runGuardian(
}
const ts = deliveredSlackTs(result);
if (!ts) {
ctx.log('error', 'factory-feature-guardian.post-failed', { channel, feature: feature.id });
throw new Error(`Slack post failed: no timestamp returned for feature ${feature.id}`);
// A successful helper return means the draft was admitted, not that the
// provider receipt is already visible. Leave the exact checkpoint alone so
// the next tick replays the stable idempotency key instead of turning an
// eventually-consistent receipt into a terminal handler failure.
ctx.log('warn', 'factory-feature-guardian.post-receipt-pending', {
channel,
feature: feature.id,
path: result.path,
});
return;
}

// Checkpoint immediately after the confirmed provider receipt. The stable
Expand Down