Fix repo Face active queue accounting
This commit is contained in:
parent
92385bba4c
commit
1ad70a2e6f
4 changed files with 32 additions and 8 deletions
|
|
@ -177,7 +177,7 @@ async function processPendingJobs(): Promise<void> {
|
|||
resolve("config/system-messages.json"),
|
||||
);
|
||||
providerRegistry = await buildProviderRegistry(systemMessages);
|
||||
const jobs = await jobQueue.claimRunnableJobs();
|
||||
const jobs = await jobQueue.claimRunnableJobs(1);
|
||||
|
||||
for (const job of jobs) {
|
||||
await processJob(job);
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ interface FaceHeartbeatState {
|
|||
|
||||
interface ActiveTurnScan {
|
||||
active: Map<string, string>;
|
||||
queued: Set<string>;
|
||||
staleRecovered: StaleActiveTurn[];
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +223,7 @@ async function main(): Promise<void> {
|
|||
const now = new Date();
|
||||
advanceInitiativeClockFromWallClock(state, now);
|
||||
const activeTurnScan = dryRun
|
||||
? { active: new Map<string, string>(), staleRecovered: [] }
|
||||
? { active: new Map<string, string>(), queued: new Set<string>(), staleRecovered: [] }
|
||||
: await listExistingActiveTurns(config.databaseDsn, config.stateStorageBackend, config);
|
||||
for (const stale of activeTurnScan.staleRecovered) {
|
||||
state.history.push({
|
||||
|
|
@ -253,6 +254,7 @@ async function main(): Promise<void> {
|
|||
applyActiveTurnFreeze(
|
||||
participant,
|
||||
activeTurnScan.active.get(participant.identityId),
|
||||
activeTurnScan.queued,
|
||||
state,
|
||||
completedThisTick,
|
||||
),
|
||||
|
|
@ -270,6 +272,7 @@ async function main(): Promise<void> {
|
|||
state,
|
||||
config.repoFaceHeartbeats.maxJobsPerTick,
|
||||
completedThisTick,
|
||||
activeTurnScan.queued,
|
||||
restStates,
|
||||
idleCooling,
|
||||
);
|
||||
|
|
@ -323,11 +326,12 @@ async function main(): Promise<void> {
|
|||
if (turn.created) {
|
||||
queuedIdentityIds.push(participant.identityId);
|
||||
participant.lastQueuedAt = queuedAt;
|
||||
participant.activeTurnStartedAt = state.initiativeClock;
|
||||
participant.activeJobId = turn.activeJobId;
|
||||
participant.lastTurnAt = state.initiativeClock;
|
||||
participant.queuedCount += 1;
|
||||
participant.currentLoad = 1;
|
||||
participant.currentLoad = 0;
|
||||
participant.activeTurnStartedAt = undefined;
|
||||
participant.activeJobId = undefined;
|
||||
participant.nextTurnAt = Math.max(participant.nextTurnAt, state.initiativeClock + recoveryFor(participant));
|
||||
if (pendingMentions.length > 0) {
|
||||
const consumedIds = new Set(pendingMentions.map((entry) => entry.id));
|
||||
state.pendingMentions = state.pendingMentions.filter((entry) => !consumedIds.has(entry.id));
|
||||
|
|
@ -1313,6 +1317,7 @@ async function listExistingActiveTurns(
|
|||
try {
|
||||
const jobs = await storage.jobQueue.listByStates(["approved", "running"]);
|
||||
const active = new Map<string, string>();
|
||||
const queued = new Set<string>();
|
||||
const staleRecovered: StaleActiveTurn[] = [];
|
||||
const staleAfterMs = Math.max(MIN_STALE_ACTIVE_JOB_MS, config.codexExecTimeoutMs * 3);
|
||||
const nowMs = Date.now();
|
||||
|
|
@ -1326,6 +1331,11 @@ async function listExistingActiveTurns(
|
|||
job.requestMessageId?.match(/^repo-face-heartbeat:([^:]+):/) ??
|
||||
job.requestMessageId?.match(/:repo-face:([^:]+):\d+$/);
|
||||
if (match) {
|
||||
if (job.state === "approved") {
|
||||
queued.add(match[1]);
|
||||
continue;
|
||||
}
|
||||
|
||||
const updatedMs = Date.parse(job.updatedAt);
|
||||
const ageMs = Number.isFinite(updatedMs) ? nowMs - updatedMs : Number.POSITIVE_INFINITY;
|
||||
if (ageMs > staleAfterMs) {
|
||||
|
|
@ -1351,7 +1361,7 @@ async function listExistingActiveTurns(
|
|||
if (voidLock) {
|
||||
active.set("void", "lock:moderation-rumination");
|
||||
}
|
||||
return { active, staleRecovered };
|
||||
return { active, queued, staleRecovered };
|
||||
} finally {
|
||||
await storage.close();
|
||||
}
|
||||
|
|
@ -1447,6 +1457,7 @@ function reconcileParticipants(
|
|||
function applyActiveTurnFreeze(
|
||||
participant: FaceHeartbeatParticipant,
|
||||
activeJobId: string | undefined,
|
||||
queuedIdentities: Set<string>,
|
||||
state: FaceHeartbeatState,
|
||||
completedThisTick: Set<string>,
|
||||
): FaceHeartbeatParticipant {
|
||||
|
|
@ -1459,6 +1470,16 @@ function applyActiveTurnFreeze(
|
|||
};
|
||||
}
|
||||
|
||||
if (queuedIdentities.has(participant.identityId)) {
|
||||
return {
|
||||
...participant,
|
||||
currentLoad: 0,
|
||||
activeTurnStartedAt: undefined,
|
||||
activeJobId: undefined,
|
||||
nextTurnAt: Math.max(participant.nextTurnAt, state.initiativeClock + recoveryFor(participant)),
|
||||
};
|
||||
}
|
||||
|
||||
if (participant.currentLoad >= 1 || participant.activeTurnStartedAt !== undefined || participant.activeJobId) {
|
||||
const completedTurnStartedAt = participant.activeTurnStartedAt ?? participant.lastTurnAt ?? state.initiativeClock;
|
||||
const unfrozen = {
|
||||
|
|
@ -1493,6 +1514,7 @@ function selectReadyParticipants(
|
|||
state: FaceHeartbeatState,
|
||||
maxJobs: number,
|
||||
completedThisTick: Set<string>,
|
||||
queuedIdentities: Set<string>,
|
||||
restStates: Map<string, RepoFaceRestSnapshot>,
|
||||
idleCooling: IdleCoolingSnapshot,
|
||||
): FaceHeartbeatParticipant[] {
|
||||
|
|
@ -1504,6 +1526,7 @@ function selectReadyParticipants(
|
|||
return (
|
||||
participant.status === "active" &&
|
||||
participant.currentLoad < 1 &&
|
||||
!queuedIdentities.has(participant.identityId) &&
|
||||
!completedThisTick.has(participant.identityId) &&
|
||||
!(
|
||||
participant.participantKind === "repo_face" &&
|
||||
|
|
|
|||
|
|
@ -329,3 +329,4 @@
|
|||
{"ts":"2026-06-03T01:46:49Z","type":"verification","status":"ok","note":"Repo Face speech boundary proof passed before unpause: agents:speech-boundary-smoke verified raw Would say drafts stay private, silence markers/full-message code fences are rejected, and legitimate SAY with inline code parses; typecheck, build, interpreter-loop smoke, and stack restart passed with fresh worker PID 21572."}
|
||||
{"ts":"2026-06-03T01:53:24Z","type":"verification","status":"ok","note":"Follow-up operator report was traced to pre-repair Discord messages; post-repair Mimir logs showed Interpreter-owned public text, and the repo Face mouth was hardened further so inline Markdown code spans normalize to quoted text before Bifrost posting."}
|
||||
{"ts":"2026-06-03T02:14:18Z","type":"documentation","status":"ok","note":"Documented VoidBot Verse service contract: VoidBot owns Discord cognition/archive/source/typed self-state/repo Face compatibility; Huginn owns Persona/.cc runtime inspection stewardship; Bifrost owns governed work transport; Odin discovers provider-owned CultMesh surfaces; Eve/CultUI is the presentation contract. Current provider is cultmesh://voidbot.local/eve/providers/voidbot.swarm with .cc witness at .voidbot/status/cultmesh/voidbot-swarm-state.cc."}
|
||||
{"ts":"2026-06-04T10:51:11Z","type":"implementation","status":"ok","note":"Fixed repo Face active-stack fault: worker now claims one runnable job per poll, and heartbeat reconciliation treats running jobs as active while approved jobs only block duplicate queueing. Reset eight over-claimed running jobs to approved and restarted worker PID 36736; visible active count dropped to one."}
|
||||
|
|
|
|||
|
|
@ -253,5 +253,5 @@ rejected_paths:
|
|||
reason: sleep/distillation that collapses concrete claims into generic abstraction creates memory nonsense and then forces later cleanup compensators
|
||||
|
||||
current_status:
|
||||
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, and the repo Face swarm has been resumed after the silence/code-block public-post fault. Public speech is now owned by parent Interpreter SAY blocks: the obsolete raw child draft-speech fallback is cut, silence markers and full-message markdown fences are rejected at the transport boundary, and inline Markdown code spans are normalized to quoted human text before Bifrost posting. Live inspection after the follow-up operator report showed the visible code-block examples were pre-repair Discord messages, not fresh post-boundary output. GameCult Local Orchestrator remains the single pulse owner, and the repo Face heartbeat path still obeys `state/agent-swarm-paused.json`. Bifrost work routing remains paused by default.
|
||||
next_action: Watch the next Discord-visible repo Face heartbeat turns for actual public behavior: no raw silence phrases, no full-message code fences, no child draft speech bypassing Interpreter SAY, and no revival of Bifrost/governance routing while that lane is disabled.
|
||||
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, and the repo Face swarm active-stack fault has been repaired. The worker had died, and a direct restart exposed that `claimRunnableJobs()` batch-claimed the approved backlog while processing it sequentially, making many Faces appear active even though only one turn could move. The worker now claims one runnable job per poll, and heartbeat reconciliation treats `running` jobs as active while `approved` jobs are queued blockers only. Direct worker PID 36736 is running after the repair; visible active count dropped to one. Public speech is still parent-Interpreter owned, silence/code-fence transport guards remain in place, GameCult Local Orchestrator remains the pulse owner, and Bifrost work routing remains paused by default.
|
||||
next_action: Watch the current single running repo Face job and the remaining approved backlog drain. If chat motion still looks absent, distinguish ordinary Interpreter `say=0` decisions and idle cooling from transport failure; investigate the separate CultCache/CultMesh decode error (`Extra ... byte(s) found at buffer[1]`) if it recurs on a `say=1` job or before a Discord bridge artifact is written.
|
||||
|
|
|
|||
Loading…
Reference in a new issue