ai-whisper 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -59,7 +59,7 @@ You pair any two of four agents — `claude`, `codex`, `ezio`, and `agy`. ai-whi
59
59
  - **ezio** *(optional)* — bundled with ai-whisper; mount it with `whisper collab mount ezio`, no separate install.
60
60
  - **agy** *(optional)* — the `agy` command (Antigravity CLI), signed in. Mount it with `whisper collab mount agy` (manual-mount parity; no auto-launch via `collab start`).
61
61
  - **Node.js 22+**.
62
- - **An LLM evaluator with credentials** — workflows are gated by it and refuse to start without it. See [Evaluator configuration](docs/evaluator-configuration.md).
62
+ - **An LLM evaluator with credentials** — workflows are gated by it and refuse to start without it. It supports four providers: **Anthropic** (default), a local **Ollama** model, **OpenAI** (and any OpenAI-compatible backend via a `baseURL` — Azure, OpenRouter, vLLM, LM Studio, …), or reusing an **already-mounted agent CLI** (`claude`/`codex`/`agy`) in non-interactive mode (no separate key — it reuses that CLI's own auth). See [Evaluator configuration](docs/evaluator-configuration.md).
63
63
  - **tmux** *(optional)* — only for `whisper collab start`, which auto-arranges both agents into panes. The mount flow below does not need it.
64
64
 
65
65
  > **Platform support:** ai-whisper is terminal-native and Unix-oriented — it drives interactive PTY sessions, so it runs on **macOS and Linux**. It is **not supported natively on Windows**: `whisper collab mount` / `reconnect` require a Unix tty-backed shell and will exit with an error pointing here. On Windows, run ai-whisper inside **[WSL2](https://learn.microsoft.com/windows/wsl/install)** — install Node, your agent CLI, and ai-whisper inside the WSL2 distro and run the commands there, where everything works as-is.
@@ -633,6 +633,40 @@ function countActiveWorkflowsForCollab(db, collabId2) {
633
633
  const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId2);
634
634
  return row.n;
635
635
  }
636
+ var COUNTED_STATUSES = ["done", "halted"];
637
+ function getHandsOffStats(db) {
638
+ const placeholders = COUNTED_STATUSES.map(() => "?").join(",");
639
+ const rows = db.prepare(`SELECT status, created_at, updated_at
640
+ FROM workflows
641
+ WHERE status IN (${placeholders})`).all(...COUNTED_STATUSES);
642
+ const stats = {
643
+ totalMs: 0,
644
+ count: 0,
645
+ byStatus: {
646
+ done: { count: 0, totalMs: 0 },
647
+ halted: { count: 0, totalMs: 0 }
648
+ },
649
+ earliestKickoffAt: null,
650
+ skipped: 0
651
+ };
652
+ for (const row of rows) {
653
+ const start = Date.parse(row.created_at);
654
+ const end = Date.parse(row.updated_at);
655
+ if (Number.isNaN(start) || Number.isNaN(end)) {
656
+ stats.skipped += 1;
657
+ continue;
658
+ }
659
+ const elapsed = Math.max(0, end - start);
660
+ stats.totalMs += elapsed;
661
+ stats.count += 1;
662
+ stats.byStatus[row.status].count += 1;
663
+ stats.byStatus[row.status].totalMs += elapsed;
664
+ if (stats.earliestKickoffAt === null || row.created_at < stats.earliestKickoffAt) {
665
+ stats.earliestKickoffAt = row.created_at;
666
+ }
667
+ }
668
+ return stats;
669
+ }
636
670
 
637
671
  // ../broker/dist/runtime/workspace-snapshot.js
638
672
  import { execFileSync } from "node:child_process";
@@ -4352,6 +4386,9 @@ function createControlService(db, events) {
4352
4386
  ...now !== void 0 ? { now } : {}
4353
4387
  });
4354
4388
  },
4389
+ getHandsOffStats() {
4390
+ return getHandsOffStats(db);
4391
+ },
4355
4392
  listRunCostRows(collabId2, workflowId) {
4356
4393
  return listRunCostRows(db, { collabId: collabId2, workflowId });
4357
4394
  },
@@ -5310,19 +5347,20 @@ async function sweepStaleBrokerDaemons(input) {
5310
5347
  }
5311
5348
  return { deleted };
5312
5349
  }
5313
- function defaultIsAlive(pid) {
5350
+ function isPidAlive(pid) {
5314
5351
  try {
5315
5352
  process.kill(pid, 0);
5316
- return Promise.resolve({ alive: true, startTime: null });
5353
+ return true;
5317
5354
  } catch (err) {
5318
5355
  const code = err.code;
5319
- if (code === "ESRCH")
5320
- return Promise.resolve({ alive: false, startTime: null });
5321
5356
  if (code === "EPERM")
5322
- return Promise.resolve({ alive: true, startTime: null });
5323
- return Promise.resolve({ alive: false, startTime: null });
5357
+ return true;
5358
+ return false;
5324
5359
  }
5325
5360
  }
5361
+ function defaultIsAlive(pid) {
5362
+ return Promise.resolve({ alive: isPidAlive(pid), startTime: null });
5363
+ }
5326
5364
 
5327
5365
  // ../broker/dist/runtime/daemon-heartbeat.js
5328
5366
  function createDaemonHeartbeat(input) {
@@ -5918,7 +5956,45 @@ function createWorkflowEventBridge(input) {
5918
5956
  import * as crypto3 from "node:crypto";
5919
5957
  import Anthropic from "@anthropic-ai/sdk";
5920
5958
  import { Ollama } from "ollama";
5959
+ import OpenAI from "openai";
5921
5960
  import { z as z18 } from "zod";
5961
+
5962
+ // src/runtime/agent-cli-presets.ts
5963
+ import { spawn as nodeSpawn } from "node:child_process";
5964
+ import { accessSync, constants, statSync as statSync3 } from "node:fs";
5965
+ import { delimiter, join as join6 } from "node:path";
5966
+ var defaultSpawn = (command, args, options) => nodeSpawn(command, [...args], options);
5967
+ var PRESETS = {
5968
+ claude: { executable: "claude", execArgs: ["-p"], promptVia: "arg" },
5969
+ codex: { executable: "codex", execArgs: ["exec"], promptVia: "arg" },
5970
+ agy: { executable: "agy", execArgs: ["-p"], promptVia: "arg" }
5971
+ };
5972
+ function resolveAgentCliInvocation(input) {
5973
+ const preset = PRESETS[input.agent];
5974
+ return {
5975
+ executable: input.executable ?? preset.executable,
5976
+ execArgs: input.execArgs ?? preset.execArgs,
5977
+ promptVia: input.promptVia ?? preset.promptVia
5978
+ };
5979
+ }
5980
+ function isExecutableOnPath(executable) {
5981
+ const isExecFile = (p) => {
5982
+ try {
5983
+ if (!statSync3(p).isFile()) return false;
5984
+ accessSync(p, constants.X_OK);
5985
+ return true;
5986
+ } catch {
5987
+ return false;
5988
+ }
5989
+ };
5990
+ if (executable.includes("/") || executable.includes("\\")) return isExecFile(executable);
5991
+ const dirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
5992
+ const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
5993
+ return dirs.some((dir) => exts.some((ext) => isExecFile(join6(dir, executable + ext))));
5994
+ }
5995
+
5996
+ // src/runtime/relay-orchestrator-evaluator.ts
5997
+ var defaultOpenAIFactory = (opts) => new OpenAI(opts);
5922
5998
  var baseFields = {
5923
5999
  confidence: z18.number().min(0).max(1),
5924
6000
  reason: z18.string()
@@ -6158,7 +6234,10 @@ function selectBranch(payload) {
6158
6234
  }
6159
6235
  return legacyBranch;
6160
6236
  }
6237
+ var ProviderUnavailableError = class extends Error {
6238
+ };
6161
6239
  function isProviderUnavailableError(error) {
6240
+ if (error instanceof ProviderUnavailableError) return true;
6162
6241
  if (!(error instanceof Error)) return false;
6163
6242
  const code = error.code;
6164
6243
  if (code === "ECONNREFUSED" || code === "ENOTFOUND" || code === "ETIMEDOUT" || code === "ECONNRESET")
@@ -6222,6 +6301,77 @@ function buildOllamaCaller(config) {
6222
6301
  return { raw: response.message.content };
6223
6302
  };
6224
6303
  }
6304
+ function buildOpenAICaller(config) {
6305
+ const client = (config.createClient ?? defaultOpenAIFactory)({
6306
+ apiKey: config.apiKey,
6307
+ ...config.baseURL ? { baseURL: config.baseURL } : {}
6308
+ });
6309
+ const model = config.model;
6310
+ return async function(systemPrompt, payload, jsonSchema) {
6311
+ const response = await client.chat.completions.create({
6312
+ model,
6313
+ messages: [
6314
+ { role: "system", content: systemPrompt },
6315
+ { role: "user", content: JSON.stringify(payload) }
6316
+ ],
6317
+ // Non-strict json_schema: schema-shaped guidance only; branch.parse() is authoritative.
6318
+ // `strict` is intentionally omitted (see spec §5.2.1) — do not set it true.
6319
+ response_format: { type: "json_schema", json_schema: { name: "verdict", schema: jsonSchema } },
6320
+ temperature: 0.3
6321
+ });
6322
+ const raw = response.choices[0]?.message.content ?? "";
6323
+ return {
6324
+ raw,
6325
+ ...typeof response.usage?.prompt_tokens === "number" ? { inputTokens: response.usage.prompt_tokens } : {},
6326
+ ...typeof response.usage?.completion_tokens === "number" ? { outputTokens: response.usage.completion_tokens } : {}
6327
+ };
6328
+ };
6329
+ }
6330
+ function buildAgentCliCaller(config) {
6331
+ const { executable, execArgs, promptVia } = resolveAgentCliInvocation(config);
6332
+ const spawnImpl = config.spawnImpl ?? defaultSpawn;
6333
+ return function(systemPrompt, payload) {
6334
+ const prompt = `${systemPrompt}
6335
+
6336
+ === INPUT (JSON) ===
6337
+ ${JSON.stringify(payload)}
6338
+ === END INPUT ===
6339
+
6340
+ Reply with ONLY the JSON object described above \u2014 no prose, no code fence.`;
6341
+ return new Promise((resolve2, reject) => {
6342
+ const child = promptVia === "arg" ? spawnImpl(executable, [...execArgs, prompt], { stdio: ["ignore", "pipe", "pipe"] }) : spawnImpl(executable, execArgs, { stdio: ["pipe", "pipe", "pipe"] });
6343
+ let stdout = "";
6344
+ let stderr = "";
6345
+ let settled = false;
6346
+ child.stdout.on("data", (chunk) => {
6347
+ stdout += String(chunk);
6348
+ });
6349
+ child.stderr.on("data", (chunk) => {
6350
+ stderr += String(chunk);
6351
+ });
6352
+ child.on("error", (err) => {
6353
+ if (settled) return;
6354
+ settled = true;
6355
+ reject(new ProviderUnavailableError(`agent-cli spawn failed (${executable}): ${err.message}`));
6356
+ });
6357
+ child.on("close", (code) => {
6358
+ if (settled) return;
6359
+ settled = true;
6360
+ if (code !== 0) {
6361
+ reject(new ProviderUnavailableError(`agent-cli ${executable} exited with code ${code}: ${stderr.trim()}`));
6362
+ return;
6363
+ }
6364
+ resolve2({ raw: stdout });
6365
+ });
6366
+ if (promptVia === "stdin") {
6367
+ child.stdin.on("error", () => {
6368
+ });
6369
+ child.stdin.write(prompt);
6370
+ child.stdin.end();
6371
+ }
6372
+ });
6373
+ };
6374
+ }
6225
6375
  function buildSingleProviderCaller(config) {
6226
6376
  if (config.provider === "anthropic") {
6227
6377
  const call2 = buildAnthropicCaller(config);
@@ -6264,6 +6414,46 @@ function buildSingleProviderCaller(config) {
6264
6414
  }
6265
6415
  };
6266
6416
  }
6417
+ if (config.provider === "openai") {
6418
+ const call2 = buildOpenAICaller(config);
6419
+ return async function(payload, branch) {
6420
+ const started = Date.now();
6421
+ let providerResult;
6422
+ try {
6423
+ providerResult = await call2(branch.systemPrompt, payload, branch.jsonSchema);
6424
+ } catch (callErr) {
6425
+ const providerLatencyMs2 = Date.now() - started;
6426
+ return { ok: false, error: callErr instanceof Error ? callErr : new Error(String(callErr)), raw: null, inputTokens: null, outputTokens: null, providerLatencyMs: providerLatencyMs2 };
6427
+ }
6428
+ const providerLatencyMs = Date.now() - started;
6429
+ try {
6430
+ const verdict = branch.parse(providerResult.raw);
6431
+ return { ok: true, verdict, raw: providerResult.raw, inputTokens: providerResult.inputTokens ?? null, outputTokens: providerResult.outputTokens ?? null, providerLatencyMs };
6432
+ } catch (parseErr) {
6433
+ return { ok: false, error: parseErr instanceof Error ? parseErr : new Error(String(parseErr)), raw: providerResult.raw, inputTokens: providerResult.inputTokens ?? null, outputTokens: providerResult.outputTokens ?? null, providerLatencyMs };
6434
+ }
6435
+ };
6436
+ }
6437
+ if (config.provider === "agent-cli") {
6438
+ const call2 = buildAgentCliCaller(config);
6439
+ return async function(payload, branch) {
6440
+ const started = Date.now();
6441
+ let raw;
6442
+ try {
6443
+ ({ raw } = await call2(branch.systemPrompt, payload));
6444
+ } catch (callErr) {
6445
+ const providerLatencyMs2 = Date.now() - started;
6446
+ return { ok: false, error: callErr instanceof Error ? callErr : new Error(String(callErr)), raw: null, inputTokens: null, outputTokens: null, providerLatencyMs: providerLatencyMs2 };
6447
+ }
6448
+ const providerLatencyMs = Date.now() - started;
6449
+ try {
6450
+ const verdict = branch.parse(raw);
6451
+ return { ok: true, verdict, raw, inputTokens: null, outputTokens: null, providerLatencyMs };
6452
+ } catch (parseErr) {
6453
+ return { ok: false, error: parseErr instanceof Error ? parseErr : new Error(String(parseErr)), raw, inputTokens: null, outputTokens: null, providerLatencyMs };
6454
+ }
6455
+ };
6456
+ }
6267
6457
  const call = buildOllamaCaller(config);
6268
6458
  return async function(payload, branch) {
6269
6459
  const started = Date.now();
@@ -6710,8 +6900,8 @@ function writeOwnPidToBrokerDaemon(db, input) {
6710
6900
  }
6711
6901
 
6712
6902
  // src/runtime/evaluator-config.ts
6713
- import { readFileSync as readFileSync2, statSync as statSync3 } from "node:fs";
6714
- import { join as join6 } from "node:path";
6903
+ import { readFileSync as readFileSync2, statSync as statSync4 } from "node:fs";
6904
+ import { join as join7 } from "node:path";
6715
6905
 
6716
6906
  // src/runtime/state-root.ts
6717
6907
  import os from "node:os";
@@ -6745,7 +6935,7 @@ function warnIfLoosePerms(path2) {
6745
6935
  if (process.platform === "win32") return;
6746
6936
  let mode;
6747
6937
  try {
6748
- mode = statSync3(path2).mode;
6938
+ mode = statSync4(path2).mode;
6749
6939
  } catch {
6750
6940
  return;
6751
6941
  }
@@ -6773,31 +6963,46 @@ function loadEvaluatorConfig() {
6773
6963
  const root = getStateRoot();
6774
6964
  let dotenv = {};
6775
6965
  try {
6776
- dotenv = parseDotEnv(readFileSync2(join6(root, ".env"), "utf8"));
6966
+ dotenv = parseDotEnv(readFileSync2(join7(root, ".env"), "utf8"));
6777
6967
  } catch (err) {
6778
6968
  if (err.code !== "ENOENT") throw err;
6779
6969
  }
6780
6970
  const envGet = (k) => process.env[k] ?? dotenv[k];
6781
- const authPath = join6(root, "auth.json");
6971
+ const authPath = join7(root, "auth.json");
6782
6972
  warnIfLoosePerms(authPath);
6783
6973
  const auth = readJsonFile(authPath, "auth.json");
6784
- const config = readJsonFile(join6(root, "config.json"), "config.json");
6974
+ const config = readJsonFile(join7(root, "config.json"), "config.json");
6785
6975
  const evalCfg = config?.evaluator ?? {};
6786
6976
  const evalOllama = evalCfg.ollama ?? {};
6787
- const provider = (envGet("AI_WHISPER_EVALUATOR_PROVIDER") ?? evalCfg.provider) === "ollama" ? "ollama" : "anthropic";
6977
+ const provider = envGet("AI_WHISPER_EVALUATOR_PROVIDER") ?? evalCfg.provider;
6978
+ const resolvedProvider = provider === "ollama" || provider === "openai" || provider === "agent-cli" ? provider : "anthropic";
6788
6979
  const rawFallback = envGet("AI_WHISPER_EVALUATOR_FALLBACK") ?? evalCfg.fallback;
6789
- const fallback = rawFallback === "anthropic" || rawFallback === "ollama" ? rawFallback : null;
6980
+ const fallback = rawFallback === "anthropic" || rawFallback === "ollama" || rawFallback === "openai" || rawFallback === "agent-cli" ? rawFallback : null;
6790
6981
  const apiKey = envGet("ANTHROPIC_API_KEY") ?? auth?.ANTHROPIC_API_KEY ?? null;
6982
+ const evalOpenai = evalCfg.openai ?? {};
6983
+ const openaiKey = envGet("OPENAI_API_KEY") ?? auth?.OPENAI_API_KEY ?? null;
6984
+ const evalAgentCli = evalCfg.agentCli ?? {};
6985
+ const rawAgent = envGet("AI_WHISPER_EVALUATOR_AGENT_CLI_AGENT") ?? evalAgentCli.agent;
6986
+ const agent = rawAgent === "claude" || rawAgent === "codex" || rawAgent === "agy" ? rawAgent : null;
6791
6987
  return {
6792
- provider,
6988
+ provider: resolvedProvider,
6793
6989
  fallback,
6794
- anthropic: {
6795
- apiKey: apiKey && apiKey.length > 0 ? apiKey : null,
6796
- model: evalCfg.anthropicModel ?? null
6797
- },
6990
+ anthropic: { apiKey: apiKey && apiKey.length > 0 ? apiKey : null, model: evalCfg.anthropicModel ?? null },
6798
6991
  ollama: {
6799
6992
  host: envGet("AI_WHISPER_EVALUATOR_OLLAMA_HOST") ?? evalOllama.host ?? null,
6800
6993
  model: envGet("AI_WHISPER_EVALUATOR_OLLAMA_MODEL") ?? evalOllama.model ?? null
6994
+ },
6995
+ openai: {
6996
+ apiKey: openaiKey && openaiKey.length > 0 ? openaiKey : null,
6997
+ model: envGet("AI_WHISPER_EVALUATOR_OPENAI_MODEL") ?? evalOpenai.model ?? null,
6998
+ baseURL: envGet("AI_WHISPER_EVALUATOR_OPENAI_BASE_URL") ?? evalOpenai.baseURL ?? null
6999
+ },
7000
+ agentCli: {
7001
+ agent,
7002
+ executable: evalAgentCli.executable ?? null,
7003
+ execArgs: Array.isArray(evalAgentCli.execArgs) ? evalAgentCli.execArgs : null,
7004
+ promptVia: evalAgentCli.promptVia === "arg" || evalAgentCli.promptVia === "stdin" ? evalAgentCli.promptVia : null,
7005
+ model: evalAgentCli.model ?? null
6801
7006
  }
6802
7007
  };
6803
7008
  }
@@ -6807,6 +7012,21 @@ function computeEvaluatorStatus(cfg, ctx) {
6807
7012
  if (cfg.provider === "anthropic" && cfg.anthropic.apiKey === null) {
6808
7013
  return "missing_anthropic_key";
6809
7014
  }
7015
+ if (cfg.provider === "openai") {
7016
+ if (cfg.openai.apiKey === null) return "missing_openai_key";
7017
+ if (cfg.openai.model === null) return "invalid_config";
7018
+ }
7019
+ if (cfg.provider === "agent-cli") {
7020
+ if (cfg.agentCli.agent === null) return "invalid_config";
7021
+ const { executable } = resolveAgentCliInvocation({
7022
+ agent: cfg.agentCli.agent,
7023
+ executable: cfg.agentCli.executable,
7024
+ execArgs: cfg.agentCli.execArgs,
7025
+ promptVia: cfg.agentCli.promptVia
7026
+ });
7027
+ const exists = (ctx.executableExists ?? isExecutableOnPath)(executable);
7028
+ if (!exists) return "agent_cli_unavailable";
7029
+ }
6810
7030
  return "ready";
6811
7031
  }
6812
7032
 
@@ -6815,7 +7035,9 @@ var NOT_CONFIGURED = {
6815
7035
  provider: "anthropic",
6816
7036
  fallback: null,
6817
7037
  anthropic: { apiKey: null, model: null },
6818
- ollama: { host: null, model: null }
7038
+ ollama: { host: null, model: null },
7039
+ openai: { apiKey: null, model: null, baseURL: null },
7040
+ agentCli: { agent: null, executable: null, execArgs: null, promptVia: null, model: null }
6819
7041
  };
6820
7042
  function recordEvaluatorStatus(db, input) {
6821
7043
  const status = computeEvaluatorStatus(input.resolved ?? NOT_CONFIGURED, {
@@ -6828,7 +7050,7 @@ function recordEvaluatorStatus(db, input) {
6828
7050
 
6829
7051
  // src/bin/broker-daemon.ts
6830
7052
  import { execFile as execFile2 } from "node:child_process";
6831
- import { join as join7 } from "node:path";
7053
+ import { join as join8 } from "node:path";
6832
7054
 
6833
7055
  // src/runtime/cli-package-info.ts
6834
7056
  import { readFileSync as readFileSync3 } from "node:fs";
@@ -6878,7 +7100,7 @@ if (collabId) {
6878
7100
  });
6879
7101
  workflowEventBridge.start();
6880
7102
  eventSocket = await createEventSocketServer({
6881
- socketPath: join7(getStateSocketsDir(), `events-${collabId}.sock`),
7103
+ socketPath: join8(getStateSocketsDir(), `events-${collabId}.sock`),
6882
7104
  events: [broker.events, externalEvents],
6883
7105
  engineVersion: resolveCliVersion()
6884
7106
  });
@@ -6905,10 +7127,30 @@ function providerConfigFrom(kind) {
6905
7127
  if (!resolved || resolved.anthropic.apiKey === null) return null;
6906
7128
  return { provider: "anthropic", apiKey: resolved.anthropic.apiKey };
6907
7129
  }
7130
+ if (kind === "openai") {
7131
+ if (!resolved || resolved.openai.apiKey === null || resolved.openai.model === null) return null;
7132
+ return {
7133
+ provider: "openai",
7134
+ apiKey: resolved.openai.apiKey,
7135
+ model: resolved.openai.model,
7136
+ ...resolved.openai.baseURL ? { baseURL: resolved.openai.baseURL } : {}
7137
+ };
7138
+ }
7139
+ if (kind === "ollama") {
7140
+ return {
7141
+ provider: "ollama",
7142
+ ...resolved?.ollama.host ? { host: resolved.ollama.host } : {},
7143
+ ...resolved?.ollama.model ? { model: resolved.ollama.model } : {}
7144
+ };
7145
+ }
7146
+ if (!resolved || resolved.agentCli.agent === null) return null;
6908
7147
  return {
6909
- provider: "ollama",
6910
- ...resolved?.ollama.host ? { host: resolved.ollama.host } : {},
6911
- ...resolved?.ollama.model ? { model: resolved.ollama.model } : {}
7148
+ provider: "agent-cli",
7149
+ agent: resolved.agentCli.agent,
7150
+ ...resolved.agentCli.executable ? { executable: resolved.agentCli.executable } : {},
7151
+ ...resolved.agentCli.execArgs ? { execArgs: resolved.agentCli.execArgs } : {},
7152
+ ...resolved.agentCli.promptVia ? { promptVia: resolved.agentCli.promptVia } : {},
7153
+ ...resolved.agentCli.model ? { model: resolved.agentCli.model } : {}
6912
7154
  };
6913
7155
  }
6914
7156
  var collab = broker.control.getCollab(collabId);
@@ -666,6 +666,40 @@ function countActiveWorkflowsForCollab(db, collabId) {
666
666
  const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId);
667
667
  return row.n;
668
668
  }
669
+ var COUNTED_STATUSES = ["done", "halted"];
670
+ function getHandsOffStats(db) {
671
+ const placeholders = COUNTED_STATUSES.map(() => "?").join(",");
672
+ const rows = db.prepare(`SELECT status, created_at, updated_at
673
+ FROM workflows
674
+ WHERE status IN (${placeholders})`).all(...COUNTED_STATUSES);
675
+ const stats = {
676
+ totalMs: 0,
677
+ count: 0,
678
+ byStatus: {
679
+ done: { count: 0, totalMs: 0 },
680
+ halted: { count: 0, totalMs: 0 }
681
+ },
682
+ earliestKickoffAt: null,
683
+ skipped: 0
684
+ };
685
+ for (const row of rows) {
686
+ const start = Date.parse(row.created_at);
687
+ const end = Date.parse(row.updated_at);
688
+ if (Number.isNaN(start) || Number.isNaN(end)) {
689
+ stats.skipped += 1;
690
+ continue;
691
+ }
692
+ const elapsed = Math.max(0, end - start);
693
+ stats.totalMs += elapsed;
694
+ stats.count += 1;
695
+ stats.byStatus[row.status].count += 1;
696
+ stats.byStatus[row.status].totalMs += elapsed;
697
+ if (stats.earliestKickoffAt === null || row.created_at < stats.earliestKickoffAt) {
698
+ stats.earliestKickoffAt = row.created_at;
699
+ }
700
+ }
701
+ return stats;
702
+ }
669
703
 
670
704
  // ../broker/dist/runtime/workspace-snapshot.js
671
705
  import { execFileSync } from "node:child_process";
@@ -4372,6 +4406,9 @@ function createControlService(db, events) {
4372
4406
  ...now !== void 0 ? { now } : {}
4373
4407
  });
4374
4408
  },
4409
+ getHandsOffStats() {
4410
+ return getHandsOffStats(db);
4411
+ },
4375
4412
  listRunCostRows(collabId, workflowId) {
4376
4413
  return listRunCostRows(db, { collabId, workflowId });
4377
4414
  },
@@ -5320,19 +5357,20 @@ async function sweepStaleBrokerDaemons(input) {
5320
5357
  }
5321
5358
  return { deleted };
5322
5359
  }
5323
- function defaultIsAlive(pid) {
5360
+ function isPidAlive(pid) {
5324
5361
  try {
5325
5362
  process.kill(pid, 0);
5326
- return Promise.resolve({ alive: true, startTime: null });
5363
+ return true;
5327
5364
  } catch (err) {
5328
5365
  const code = err.code;
5329
- if (code === "ESRCH")
5330
- return Promise.resolve({ alive: false, startTime: null });
5331
5366
  if (code === "EPERM")
5332
- return Promise.resolve({ alive: true, startTime: null });
5333
- return Promise.resolve({ alive: false, startTime: null });
5367
+ return true;
5368
+ return false;
5334
5369
  }
5335
5370
  }
5371
+ function defaultIsAlive(pid) {
5372
+ return Promise.resolve({ alive: isPidAlive(pid), startTime: null });
5373
+ }
5336
5374
 
5337
5375
  // ../broker/dist/runtime/daemon-heartbeat.js
5338
5376
  function createDaemonHeartbeat(input) {
@@ -7506,6 +7544,8 @@ function haxSpawnEnv(base = process.env, transcriptPath) {
7506
7544
  const env = { ...base, HAX_EXTRA_SKILLS_DIR: aiEzioGlobalSkillsDir(base) };
7507
7545
  if (transcriptPath)
7508
7546
  env.HAX_TRANSCRIPT = transcriptPath;
7547
+ if (env.HAX_COMPACT_AUTO === void 0)
7548
+ env.HAX_COMPACT_AUTO = "0";
7509
7549
  return env;
7510
7550
  }
7511
7551
  function spawnHax(options = {}) {
@@ -7828,7 +7868,9 @@ var Session = class {
7828
7868
  async spawnAndPump(options) {
7829
7869
  this._transcriptPath = options.transcriptPath;
7830
7870
  const gen = this.generation;
7831
- const spawned = (this.options.spawn ?? spawnHax)(options);
7871
+ const overrides = this.options.engineEnvOverrides;
7872
+ const spawnOptions = overrides ? { ...options, env: { ...options.env ?? process.env, ...overrides } } : options;
7873
+ const spawned = (this.options.spawn ?? spawnHax)(spawnOptions);
7832
7874
  this.spawned = spawned;
7833
7875
  spawned.child.on("exit", (code, signal) => {
7834
7876
  if (gen !== this.generation)
@@ -8861,6 +8903,7 @@ function profileEnv(profile, parentEnv) {
8861
8903
  env.HAX_REASONING_EFFORT = profile.effort;
8862
8904
  else
8863
8905
  delete env.HAX_REASONING_EFFORT;
8906
+ env.HAX_COMPACT_AUTO = "1";
8864
8907
  return env;
8865
8908
  }
8866
8909
  function validateProfile(profile, parentEnv) {
@@ -9948,6 +9991,14 @@ function builtinCommands(listCommands) {
9948
9991
  {
9949
9992
  name: "compact",
9950
9993
  summary: "summarize old history and free context",
9994
+ // NOTE (post-hax-sync): upstream hax now ships its own `/compact` slash
9995
+ // command (`agent_compact`) that silently rewrites conversation history
9996
+ // inside the engine. This ezio-owned handler intercepts `/compact` here,
9997
+ // routes it to `ctx.compactor.compactNow()` (the harness-controlled protocol
9998
+ // `compact` control), and returns `action: "handled"` — so the literal text
9999
+ // "/compact" is NEVER forwarded as a raw `submit` to the hax session where
10000
+ // the engine's own slash registry would trigger `agent_compact`.
10001
+ // Do NOT remove this command or change its return path to `action: "submit"`.
9951
10002
  run: async (ctx) => {
9952
10003
  if (!ctx.compactor) {
9953
10004
  ctx.write("compaction unavailable\n");
@@ -636,6 +636,40 @@ function countActiveWorkflowsForCollab(db, collabId2) {
636
636
  const row = db.prepare("SELECT COUNT(*) AS n FROM workflows WHERE collab_id = ? AND status IN ('running', 'paused')").get(collabId2);
637
637
  return row.n;
638
638
  }
639
+ var COUNTED_STATUSES = ["done", "halted"];
640
+ function getHandsOffStats(db) {
641
+ const placeholders = COUNTED_STATUSES.map(() => "?").join(",");
642
+ const rows = db.prepare(`SELECT status, created_at, updated_at
643
+ FROM workflows
644
+ WHERE status IN (${placeholders})`).all(...COUNTED_STATUSES);
645
+ const stats = {
646
+ totalMs: 0,
647
+ count: 0,
648
+ byStatus: {
649
+ done: { count: 0, totalMs: 0 },
650
+ halted: { count: 0, totalMs: 0 }
651
+ },
652
+ earliestKickoffAt: null,
653
+ skipped: 0
654
+ };
655
+ for (const row of rows) {
656
+ const start = Date.parse(row.created_at);
657
+ const end = Date.parse(row.updated_at);
658
+ if (Number.isNaN(start) || Number.isNaN(end)) {
659
+ stats.skipped += 1;
660
+ continue;
661
+ }
662
+ const elapsed = Math.max(0, end - start);
663
+ stats.totalMs += elapsed;
664
+ stats.count += 1;
665
+ stats.byStatus[row.status].count += 1;
666
+ stats.byStatus[row.status].totalMs += elapsed;
667
+ if (stats.earliestKickoffAt === null || row.created_at < stats.earliestKickoffAt) {
668
+ stats.earliestKickoffAt = row.created_at;
669
+ }
670
+ }
671
+ return stats;
672
+ }
639
673
 
640
674
  // ../broker/dist/runtime/workspace-snapshot.js
641
675
  import { execFileSync } from "node:child_process";
@@ -4342,6 +4376,9 @@ function createControlService(db, events) {
4342
4376
  ...now !== void 0 ? { now } : {}
4343
4377
  });
4344
4378
  },
4379
+ getHandsOffStats() {
4380
+ return getHandsOffStats(db);
4381
+ },
4345
4382
  listRunCostRows(collabId2, workflowId) {
4346
4383
  return listRunCostRows(db, { collabId: collabId2, workflowId });
4347
4384
  },
@@ -5294,19 +5331,20 @@ async function sweepStaleBrokerDaemons(input) {
5294
5331
  }
5295
5332
  return { deleted };
5296
5333
  }
5297
- function defaultIsAlive(pid) {
5334
+ function isPidAlive(pid) {
5298
5335
  try {
5299
5336
  process.kill(pid, 0);
5300
- return Promise.resolve({ alive: true, startTime: null });
5337
+ return true;
5301
5338
  } catch (err) {
5302
5339
  const code = err.code;
5303
- if (code === "ESRCH")
5304
- return Promise.resolve({ alive: false, startTime: null });
5305
5340
  if (code === "EPERM")
5306
- return Promise.resolve({ alive: true, startTime: null });
5307
- return Promise.resolve({ alive: false, startTime: null });
5341
+ return true;
5342
+ return false;
5308
5343
  }
5309
5344
  }
5345
+ function defaultIsAlive(pid) {
5346
+ return Promise.resolve({ alive: isPidAlive(pid), startTime: null });
5347
+ }
5310
5348
 
5311
5349
  // ../broker/dist/runtime/daemon-heartbeat.js
5312
5350
  function createDaemonHeartbeat(input) {