fix(team): ignore replayed bootstrap progress events
This commit is contained in:
parent
ac3475d3be
commit
ad56f0e337
3 changed files with 106 additions and 0 deletions
|
|
@ -660,6 +660,8 @@ interface ProvisioningRun {
|
|||
>;
|
||||
/** Agent tool_use_id -> teammate name for persistent teammate spawns. */
|
||||
memberSpawnToolUseIds: Map<string, string>;
|
||||
/** Highest accepted deterministic bootstrap event sequence for this run. */
|
||||
lastDeterministicBootstrapSeq: number;
|
||||
/** Throttles config/inbox audit work triggered by frequent status polling. */
|
||||
lastMemberSpawnAuditAt: number;
|
||||
/** Throttles repeated audit warnings when config.json is temporarily unreadable. */
|
||||
|
|
@ -707,6 +709,33 @@ function createInitialMemberSpawnStatusEntry(): MemberSpawnStatusEntry {
|
|||
};
|
||||
}
|
||||
|
||||
export function shouldAcceptDeterministicBootstrapEvent(params: {
|
||||
runId: string;
|
||||
teamName: string;
|
||||
lastSeq: number;
|
||||
msg: Record<string, unknown>;
|
||||
}): { accept: boolean; nextSeq: number } {
|
||||
const msgRunId = typeof params.msg.run_id === 'string' ? params.msg.run_id.trim() : '';
|
||||
if (msgRunId && msgRunId !== params.runId) {
|
||||
return { accept: false, nextSeq: params.lastSeq };
|
||||
}
|
||||
|
||||
const msgTeamName = typeof params.msg.team_name === 'string' ? params.msg.team_name.trim() : '';
|
||||
if (msgTeamName && msgTeamName !== params.teamName) {
|
||||
return { accept: false, nextSeq: params.lastSeq };
|
||||
}
|
||||
|
||||
const seq = typeof params.msg.seq === 'number' ? params.msg.seq : NaN;
|
||||
if (Number.isFinite(seq)) {
|
||||
if (!Number.isInteger(seq) || seq <= params.lastSeq) {
|
||||
return { accept: false, nextSeq: params.lastSeq };
|
||||
}
|
||||
return { accept: true, nextSeq: seq };
|
||||
}
|
||||
|
||||
return { accept: true, nextSeq: params.lastSeq };
|
||||
}
|
||||
|
||||
function deriveMemberLaunchState(entry: {
|
||||
agentToolAccepted?: boolean;
|
||||
runtimeAlive?: boolean;
|
||||
|
|
@ -4386,6 +4415,7 @@ export class TeamProvisioningService {
|
|||
request.members.map((m) => [m.name, createInitialMemberSpawnStatusEntry()])
|
||||
),
|
||||
memberSpawnToolUseIds: new Map(),
|
||||
lastDeterministicBootstrapSeq: 0,
|
||||
lastMemberSpawnAuditAt: 0,
|
||||
lastMemberSpawnAuditConfigReadWarningAt: 0,
|
||||
lastMemberSpawnAuditMissingWarningAt: new Map(),
|
||||
|
|
@ -4899,6 +4929,7 @@ export class TeamProvisioningService {
|
|||
expectedMembers.map((name) => [name, createInitialMemberSpawnStatusEntry()])
|
||||
),
|
||||
memberSpawnToolUseIds: new Map(),
|
||||
lastDeterministicBootstrapSeq: 0,
|
||||
lastMemberSpawnAuditAt: 0,
|
||||
lastMemberSpawnAuditConfigReadWarningAt: 0,
|
||||
lastMemberSpawnAuditMissingWarningAt: new Map(),
|
||||
|
|
@ -7069,6 +7100,17 @@ export class TeamProvisioningService {
|
|||
return false;
|
||||
}
|
||||
|
||||
const acceptance = shouldAcceptDeterministicBootstrapEvent({
|
||||
runId: run.runId,
|
||||
teamName: run.teamName,
|
||||
lastSeq: run.lastDeterministicBootstrapSeq,
|
||||
msg,
|
||||
});
|
||||
if (!acceptance.accept) {
|
||||
return true;
|
||||
}
|
||||
run.lastDeterministicBootstrapSeq = acceptance.nextSeq;
|
||||
|
||||
const event = typeof msg.event === 'string' ? msg.event : undefined;
|
||||
if (!event) {
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ describe('TeamProvisioningService', () => {
|
|||
fs.mkdirSync(tempProjectsBase, { recursive: true });
|
||||
});
|
||||
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { shouldAcceptDeterministicBootstrapEvent } from '@main/services/team/TeamProvisioningService';
|
||||
|
||||
describe('TeamProvisioningService deterministic bootstrap event ordering', () => {
|
||||
it('accepts newer in-order bootstrap events', () => {
|
||||
expect(
|
||||
shouldAcceptDeterministicBootstrapEvent({
|
||||
runId: 'run-1',
|
||||
teamName: 'atlas-hq',
|
||||
lastSeq: 2,
|
||||
msg: {
|
||||
run_id: 'run-1',
|
||||
team_name: 'atlas-hq',
|
||||
seq: 3,
|
||||
},
|
||||
})
|
||||
).toEqual({ accept: true, nextSeq: 3 });
|
||||
});
|
||||
|
||||
it('rejects replayed or out-of-order bootstrap events', () => {
|
||||
expect(
|
||||
shouldAcceptDeterministicBootstrapEvent({
|
||||
runId: 'run-1',
|
||||
teamName: 'atlas-hq',
|
||||
lastSeq: 3,
|
||||
msg: {
|
||||
run_id: 'run-1',
|
||||
team_name: 'atlas-hq',
|
||||
seq: 2,
|
||||
},
|
||||
})
|
||||
).toEqual({ accept: false, nextSeq: 3 });
|
||||
});
|
||||
|
||||
it('rejects bootstrap events for another run or team', () => {
|
||||
expect(
|
||||
shouldAcceptDeterministicBootstrapEvent({
|
||||
runId: 'run-1',
|
||||
teamName: 'atlas-hq',
|
||||
lastSeq: 1,
|
||||
msg: {
|
||||
run_id: 'run-2',
|
||||
team_name: 'atlas-hq',
|
||||
seq: 2,
|
||||
},
|
||||
})
|
||||
).toEqual({ accept: false, nextSeq: 1 });
|
||||
|
||||
expect(
|
||||
shouldAcceptDeterministicBootstrapEvent({
|
||||
runId: 'run-1',
|
||||
teamName: 'atlas-hq',
|
||||
lastSeq: 1,
|
||||
msg: {
|
||||
run_id: 'run-1',
|
||||
team_name: 'forge-labs',
|
||||
seq: 2,
|
||||
},
|
||||
})
|
||||
).toEqual({ accept: false, nextSeq: 1 });
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue