/**
* Command/narration event emitter for the coordination layer.
*
* `writeSessionEvent` records command spans in the canonical V3 ledger.
*/
import { randomBytes } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { resolve } from "node:path";
// Kept dependency-light: vendored verbatim into a downstream consumer, so no coordEnv import.
import {
LIVE_COMMAND_V3_PRODUCER_ID,
liveInstanceIdV3,
livePlatformV3,
resolveLiveEventLedgerRouteV3,
} from "../events/v3/live-routing.ts";
import type { CommandObservationV3, CommandSignalV3 } from "../events/v3/producers/command.ts";
import { recordCommandSignalV3 } from "../events/v3/producers/command-recorder.ts";
import { writeProducerDiagnosticV3 } from "../events/v3/producers/intake.ts";
import { readHookProducerStateByInstanceV3 } from "../events/v3/producers/recorder.ts";
import { resolveEmitRoot } from "./canonical-emit.ts";
/** Event types accepted by `writeSessionEvent`. */
export type SessionEventType = "command.started" | "command.output_observed" | "command.completed";
/**
* Resolved path of the ndjson sidecar file. Lives inside `.harnery/` so a
* containerized reader can pick it up through a single bind mount.
*/
export function coordinationRootPath(): string {
const root = resolveEmitRoot();
return root ?? resolve(process.env.HOME || "/tmp");
}
/** Random 8-char hex id for grouping output lines under a single command. */
export function newCmdId(): string {
return randomBytes(4).toString("hex");
}
/**
* Read the model's most recent `...` declaration from the
* intent-stamp file written by the PreToolUse hook. Returns null when the
* file is missing, empty, or contains the explicit `(no intent)` sentinel.
* Callers fall back to whatever default they want in that case.
*
* Path: `.harnery/.last-intent.` next to the agent's heartbeat.
*/
export function readLastIntent(instanceId?: string): string | null {
if (!instanceId) return null;
// Same superproject-aware root resolution as coordinationRootPath(): the
// intent stamp is written by the PreToolUse hook into the SUPERPROJECT's
// .harnery/, so a nested-`.harnery/` cwd must not redirect the read.
const root = resolveEmitRoot();
if (!root) return null;
const agentsDir = resolve(root, ".harnery");
const intentPath = resolve(agentsDir, `.last-intent.${instanceId}`);
if (!existsSync(intentPath)) return null;
try {
const raw = readFileSync(intentPath, "utf8").trim();
if (!raw || raw === "(no intent)") return null;
return raw;
} catch {
return null;
}
}
const outputSequence = new Map();
/** Emit a command/narration event to the canonical stream. Swallows every
* error and skips when identity can't be resolved: telemetry must never break
* or slow down a command. */
function recordCommandObservation(type: SessionEventType, fields: Record): void {
const instanceId = typeof fields.instance_id === "string" ? fields.instance_id : undefined;
if (!instanceId) return;
try {
const coordRoot = coordinationRootPath();
const route = resolveLiveEventLedgerRouteV3(coordRoot);
if (route.state === "blocked") return;
const liveInstanceId = liveInstanceIdV3(instanceId);
const hook = readHookProducerStateByInstanceV3(coordRoot, liveInstanceId);
if (!hook) {
writeProducerDiagnosticV3(coordRoot, "command_emit_unjoinable", {
type,
instance_id: instanceId,
reason: "hook_generation_not_found",
});
return;
}
const command = commandSignalAndObservation(type, fields);
if (!command) return;
const result = recordCommandSignalV3({
coordRoot,
mode: route.mode,
signal: command.signal,
observation: command.observation,
adapter: hook.adapter,
instance_id: liveInstanceId,
producer_id: LIVE_COMMAND_V3_PRODUCER_ID,
build_id: route.build_id,
platform: livePlatformV3(),
...(fields.bridge === "codex-wsl" ? { bridge: "codex-wsl" as const } : {}),
monotonic_ns: process.hrtime.bigint().toString(),
});
if (result.state === "generation_unavailable") {
const expectedLifecycleReopenGap =
result.reason === "turn_not_started" &&
hook.session_start_derivation === "approved_lifecycle_reopen";
if (expectedLifecycleReopenGap) return;
writeProducerDiagnosticV3(coordRoot, "command_emit_unjoinable", {
type,
instance_id: instanceId,
signal: command.signal,
reason: result.reason,
});
} else if (result.state !== "recorded" && result.state !== "already_recorded") {
writeProducerDiagnosticV3(coordRoot, "command_emit_rejected", {
type,
instance_id: instanceId,
signal: command.signal,
result_state: result.state,
});
}
} catch (error) {
// Telemetry must never break the command, but the loss is preserved.
try {
const coordRoot = coordinationRootPath();
writeProducerDiagnosticV3(coordRoot, "command_emit_failed", {
type,
instance_id: instanceId,
error: String(error),
});
} catch {
/* diagnostics are best-effort */
}
}
}
function commandSignalAndObservation(
type: SessionEventType,
fields: Record,
): { signal: CommandSignalV3; observation: CommandObservationV3 } | undefined {
const commandId = typeof fields.cmd_id === "string" ? fields.cmd_id : undefined;
if (!commandId) return undefined;
if (type === "command.started") {
outputSequence.set(commandId, 0);
const command = typeof fields.cmd === "string" ? fields.cmd : "";
const executable = command.trim().split(/\s+/, 1)[0] || "unknown";
return {
signal: "command.started",
observation: {
native_command_id: commandId,
executable,
executable_class: "cli",
// This module runs inside the command process, so preserve the actual
// argument boundaries for the HMAC instead of hashing a clamped line.
argv: process.argv.slice(2),
intent: typeof fields.intent === "string" ? fields.intent : undefined,
intent_kind: typeof fields.intent === "string" ? "declared" : "unknown",
sensitive_argument_count: 0,
},
};
}
if (type === "command.output_observed") {
const sequence = (outputSequence.get(commandId) ?? 0) + 1;
outputSequence.set(commandId, sequence);
const line = typeof fields.line === "string" ? fields.line : "";
const stream =
fields.stream === "stdout" || fields.stream === "stderr" ? fields.stream : "combined";
return {
signal: "command.output_observed",
observation: {
native_command_id: commandId,
native_observation_id: `${commandId}:output:${sequence}`,
stream,
output: line,
output_bytes: Buffer.byteLength(line, "utf8"),
output_lines: line ? 1 : 0,
},
};
}
outputSequence.delete(commandId);
const exitCode =
typeof fields.exit === "number" && Number.isSafeInteger(fields.exit) ? fields.exit : undefined;
return {
signal: "command.completed",
observation: {
native_command_id: commandId,
...(exitCode === undefined ? {} : { exit_code: exitCode }),
outcome: exitCode === undefined ? "unknown" : exitCode === 0 ? "succeeded" : "failed",
duration_ms:
typeof fields.duration_ms === "number" && fields.duration_ms >= 0
? fields.duration_ms
: undefined,
signal: typeof fields.signal === "string" ? fields.signal : undefined,
},
};
}
/**
* Record a command event in V3. Best-effort, never throws into the caller;
* telemetry must not break or slow a command.
*/
export function writeSessionEvent(
type: SessionEventType,
fields: Record = {},
): void {
recordCommandObservation(type, fields);
}
/** Trim long values to keep individual events small. */
export function clampField(v: string, max = 1024): string {
if (v.length <= max) return v;
return `${v.slice(0, max - 1)}…`;
}