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. */
|
/** Agent tool_use_id -> teammate name for persistent teammate spawns. */
|
||||||
memberSpawnToolUseIds: Map<string, string>;
|
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. */
|
/** Throttles config/inbox audit work triggered by frequent status polling. */
|
||||||
lastMemberSpawnAuditAt: number;
|
lastMemberSpawnAuditAt: number;
|
||||||
/** Throttles repeated audit warnings when config.json is temporarily unreadable. */
|
/** 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: {
|
function deriveMemberLaunchState(entry: {
|
||||||
agentToolAccepted?: boolean;
|
agentToolAccepted?: boolean;
|
||||||
runtimeAlive?: boolean;
|
runtimeAlive?: boolean;
|
||||||
|
|
@ -4386,6 +4415,7 @@ export class TeamProvisioningService {
|
||||||
request.members.map((m) => [m.name, createInitialMemberSpawnStatusEntry()])
|
request.members.map((m) => [m.name, createInitialMemberSpawnStatusEntry()])
|
||||||
),
|
),
|
||||||
memberSpawnToolUseIds: new Map(),
|
memberSpawnToolUseIds: new Map(),
|
||||||
|
lastDeterministicBootstrapSeq: 0,
|
||||||
lastMemberSpawnAuditAt: 0,
|
lastMemberSpawnAuditAt: 0,
|
||||||
lastMemberSpawnAuditConfigReadWarningAt: 0,
|
lastMemberSpawnAuditConfigReadWarningAt: 0,
|
||||||
lastMemberSpawnAuditMissingWarningAt: new Map(),
|
lastMemberSpawnAuditMissingWarningAt: new Map(),
|
||||||
|
|
@ -4899,6 +4929,7 @@ export class TeamProvisioningService {
|
||||||
expectedMembers.map((name) => [name, createInitialMemberSpawnStatusEntry()])
|
expectedMembers.map((name) => [name, createInitialMemberSpawnStatusEntry()])
|
||||||
),
|
),
|
||||||
memberSpawnToolUseIds: new Map(),
|
memberSpawnToolUseIds: new Map(),
|
||||||
|
lastDeterministicBootstrapSeq: 0,
|
||||||
lastMemberSpawnAuditAt: 0,
|
lastMemberSpawnAuditAt: 0,
|
||||||
lastMemberSpawnAuditConfigReadWarningAt: 0,
|
lastMemberSpawnAuditConfigReadWarningAt: 0,
|
||||||
lastMemberSpawnAuditMissingWarningAt: new Map(),
|
lastMemberSpawnAuditMissingWarningAt: new Map(),
|
||||||
|
|
@ -7069,6 +7100,17 @@ export class TeamProvisioningService {
|
||||||
return false;
|
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;
|
const event = typeof msg.event === 'string' ? msg.event : undefined;
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,7 @@ describe('TeamProvisioningService', () => {
|
||||||
fs.mkdirSync(tempProjectsBase, { recursive: true });
|
fs.mkdirSync(tempProjectsBase, { recursive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
try {
|
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