From 9007a9dcfa8cac7fa3566d03a6214befab9b3dbd Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Sat, 25 Apr 2026 00:18:17 +0200 Subject: [PATCH] Keep Codex lanes from losing owner identity Active-session telemetry can arrive with agentName and cliName recorded as unknown even when the Guardex branch/session key still carries agent/codex. Hivemind now treats unknown as missing and derives owner fields from concrete branch, session, or CLI signals before falling back. Constraint: Viewer and MCP consumers use normalized HivemindSession.agent/cli values directly. Rejected: Patch only the viewer label | other consumers would still receive unknown owners. Confidence: high Scope-risk: narrow Directive: Do not pass literal unknown owner fields through when branch or session identity is concrete. Tested: node /home/deadpool/Documents/recodee/agents-hivemind/node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.17/node_modules/vitest/vitest.mjs run Tested: node /home/deadpool/Documents/recodee/agents-hivemind/node_modules/.pnpm/@biomejs+biome@1.9.4/node_modules/@biomejs/biome/bin/biome check packages/core/src/hivemind.ts packages/core/test/hivemind.test.ts Not-tested: pnpm --filter @colony/core typecheck; baseline missing @types/better-sqlite3 in this workspace --- packages/core/src/hivemind.ts | 49 +++++++++++++++++++++++-- packages/core/test/hivemind.test.ts | 55 +++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 packages/core/test/hivemind.test.ts diff --git a/packages/core/src/hivemind.ts b/packages/core/src/hivemind.ts index 3a2971c..5466431 100644 --- a/packages/core/src/hivemind.ts +++ b/packages/core/src/hivemind.ts @@ -1,5 +1,6 @@ import { type Dirent, existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; import { basename, delimiter, join, resolve } from 'node:path'; +import { inferIdeFromSessionId } from './infer-ide.js'; const ACTIVE_SESSIONS_RELATIVE_DIR = join('.omx', 'state', 'active-sessions'); const FILE_LOCKS_RELATIVE_PATH = join('.omx', 'state', 'agent-file-locks.json'); @@ -174,6 +175,9 @@ function normalizeActiveSession( const activity = classifyActiveSession({ state, lastHeartbeatAt, pidAlive, now }); const latestTaskPreview = readString(input.latestTaskPreview) || readString(input.latest_task_preview); + const sessionKey = readString(input.sessionKey) || readString(input.session_key); + const agent = activeSessionAgent(input, branch, sessionKey); + const cli = activeSessionCli(input, branch, sessionKey); return { repo_root: repoRoot, @@ -182,8 +186,8 @@ function normalizeActiveSession( task: latestTaskPreview || taskName, task_name: taskName, latest_task_preview: latestTaskPreview, - agent: readString(input.agentName) || readString(input.agent_name) || 'agent', - cli: readString(input.cliName) || readString(input.cli_name) || 'codex', + agent, + cli, state, activity, activity_summary: activeActivitySummary(activity, state, lastHeartbeatAt, pidAlive, now), @@ -199,7 +203,7 @@ function normalizeActiveSession( routing_reason: readString(input.taskRoutingReason) || readString(input.routing_reason), snapshot_name: '', project_name: '', - session_key: readString(input.sessionKey) || readString(input.session_key), + session_key: sessionKey, locked_file_count: 0, locked_file_preview: [], file_path: filePath, @@ -656,6 +660,11 @@ function readString(value: unknown): string { return typeof value === 'string' ? value.trim() : ''; } +function readConcreteString(value: unknown): string { + const text = readString(value); + return text && text.toLowerCase() !== 'unknown' ? text : ''; +} + function normalizeState(value: string): string { const normalized = value.toLowerCase(); return ['working', 'thinking', 'idle'].includes(normalized) ? normalized : ''; @@ -699,6 +708,40 @@ function deriveAgentName(branch: string): string { return parts[0] === 'agent' && parts[1] ? parts[1] : 'agent'; } +function activeSessionAgent(input: JsonRecord, branch: string, sessionKey: string): string { + const recorded = readConcreteString(input.agentName) || readConcreteString(input.agent_name); + if (recorded && recorded !== 'agent') return agentFromIde(recorded); + + const branchAgent = deriveAgentName(branch); + if (branchAgent !== 'agent') return branchAgent; + + const sessionAgent = agentFromIde(inferIdeFromSessionId(sessionKey) ?? ''); + if (sessionAgent) return sessionAgent; + + const cliAgent = agentFromIde( + readConcreteString(input.cliName) || readConcreteString(input.cli_name), + ); + return cliAgent || recorded || 'agent'; +} + +function activeSessionCli(input: JsonRecord, branch: string, sessionKey: string): string { + const recorded = readConcreteString(input.cliName) || readConcreteString(input.cli_name); + if (recorded) return recorded; + + const inferred = inferIdeFromSessionId(sessionKey); + if (inferred) return inferred; + + const branchAgent = deriveAgentName(branch); + if (branchAgent !== 'agent') return branchAgent === 'claude' ? 'claude-code' : branchAgent; + + return 'codex'; +} + +function agentFromIde(ide: string): string { + if (ide === 'claude-code') return 'claude'; + return ide; +} + function isRecord(value: unknown): value is JsonRecord { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); } diff --git a/packages/core/test/hivemind.test.ts b/packages/core/test/hivemind.test.ts new file mode 100644 index 0000000..0d8c626 --- /dev/null +++ b/packages/core/test/hivemind.test.ts @@ -0,0 +1,55 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { readHivemind } from '../src/hivemind.js'; + +let dir = ''; + +afterEach(() => { + if (dir) rmSync(dir, { recursive: true, force: true }); + dir = ''; +}); + +describe('readHivemind', () => { + it('derives codex owner when active-session telemetry says unknown', () => { + dir = mkdtempSync(join(tmpdir(), 'colony-hivemind-')); + const repoRoot = join(dir, 'repo'); + const worktreePath = join(repoRoot, '.omx', 'agent-worktrees', 'agent__codex__owner-task'); + const activeSessionDir = join(repoRoot, '.omx', 'state', 'active-sessions'); + const now = new Date().toISOString(); + mkdirSync(activeSessionDir, { recursive: true }); + mkdirSync(worktreePath, { recursive: true }); + writeFileSync( + join(activeSessionDir, 'agent__codex__owner-task.json'), + `${JSON.stringify( + { + schemaVersion: 1, + repoRoot, + branch: 'agent/codex/owner-task', + taskName: 'Fix owner label', + latestTaskPreview: 'Render Codex instead of unknown', + agentName: 'unknown', + cliName: 'unknown', + sessionKey: 'agent/codex/owner-task', + worktreePath, + pid: process.pid, + startedAt: now, + lastHeartbeatAt: now, + state: 'working', + }, + null, + 2, + )}\n`, + 'utf8', + ); + + const snapshot = readHivemind({ repoRoot, now: Date.parse(now) }); + + expect(snapshot.sessions[0]).toMatchObject({ + branch: 'agent/codex/owner-task', + agent: 'codex', + cli: 'codex', + }); + }); +});