@drisp/cli 0.4.5 → 0.5.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.
@@ -13,6 +13,10 @@ import {
13
13
  isValidHookEventEnvelope,
14
14
  resolveHookSocketPath
15
15
  } from "./chunk-BTKQ67RE.js";
16
+ import {
17
+ createInstanceSocketClient,
18
+ writeAttachmentMirror
19
+ } from "./chunk-ZVOGOZNT.js";
16
20
  import {
17
21
  TransportUnreachableError,
18
22
  createUdsClientTransport,
@@ -23,16 +27,46 @@ import {
23
27
  traceGatewayFrame,
24
28
  trackGatewayTransportReconnect,
25
29
  writeGatewayTrace
26
- } from "./chunk-4CRZXLIP.js";
30
+ } from "./chunk-BTY7MYYT.js";
27
31
  import {
28
32
  compileWorkflowPlan,
29
33
  createWorkflowRunner,
34
+ installWorkflowFromSource,
30
35
  readConfig,
31
36
  readGlobalConfig,
32
37
  resolveActiveWorkflow,
33
38
  resolveWorkflow,
39
+ resolveWorkflowInstall,
34
40
  resolveWorkflowPlugins
35
- } from "./chunk-5VK2ZMVV.js";
41
+ } from "./chunk-A54HGVML.js";
42
+
43
+ // src/infra/daemon/stateDir.ts
44
+ import fs from "fs";
45
+ import os from "os";
46
+ import path from "path";
47
+ function daemonStatePaths(env = process.env) {
48
+ const xdg = env["XDG_STATE_HOME"];
49
+ const home = env["HOME"] ?? os.homedir();
50
+ const base = xdg && xdg.length > 0 ? xdg : path.join(home, ".local", "state");
51
+ const dir = path.join(base, "drisp");
52
+ return {
53
+ dir,
54
+ pidPath: path.join(dir, "dashboard-daemon.pid"),
55
+ logPath: path.join(dir, "dashboard-daemon.log"),
56
+ socketPath: path.join(dir, "dashboard-daemon.sock")
57
+ };
58
+ }
59
+ function ensureDaemonStateDir(env = process.env) {
60
+ const paths = daemonStatePaths(env);
61
+ fs.mkdirSync(paths.dir, { recursive: true, mode: 448 });
62
+ if (process.platform !== "win32") {
63
+ try {
64
+ fs.chmodSync(paths.dir, 448);
65
+ } catch {
66
+ }
67
+ }
68
+ return paths;
69
+ }
36
70
 
37
71
  // src/shared/utils/processRegistry.ts
38
72
  var ProcessRegistry = class {
@@ -146,12 +180,12 @@ var processRegistry = new ProcessRegistry();
146
180
 
147
181
  // src/harnesses/claude/runtime/server.ts
148
182
  import * as net from "net";
149
- import * as fs2 from "fs";
150
- import * as path2 from "path";
183
+ import * as fs3 from "fs";
184
+ import * as path3 from "path";
151
185
 
152
186
  // src/harnesses/claude/runtime/cleanupStaleSockets.ts
153
- import * as fs from "fs";
154
- import * as path from "path";
187
+ import * as fs2 from "fs";
188
+ import * as path2 from "path";
155
189
  var SOCK_PATTERN = /^ink-(\d+)\.sock$/;
156
190
  function isPidAlive(pid) {
157
191
  try {
@@ -165,7 +199,7 @@ function isPidAlive(pid) {
165
199
  function cleanupStaleSockets(sockDir) {
166
200
  let entries;
167
201
  try {
168
- entries = fs.readdirSync(sockDir);
202
+ entries = fs2.readdirSync(sockDir);
169
203
  } catch {
170
204
  return [];
171
205
  }
@@ -176,7 +210,7 @@ function cleanupStaleSockets(sockDir) {
176
210
  const pid = parseInt(match[1], 10);
177
211
  if (isPidAlive(pid)) continue;
178
212
  try {
179
- fs.unlinkSync(path.join(sockDir, entry));
213
+ fs2.unlinkSync(path2.join(sockDir, entry));
180
214
  removed.push(entry);
181
215
  } catch {
182
216
  }
@@ -355,7 +389,15 @@ var RULES = {
355
389
  },
356
390
  unknown: DEFAULT_HINTS
357
391
  };
358
- function getInteractionHints(kind) {
392
+ var ASK_USER_QUESTION_HINTS = {
393
+ expectsDecision: true,
394
+ defaultTimeoutMs: null,
395
+ canBlock: true
396
+ };
397
+ function getInteractionHints(kind, toolName) {
398
+ if (kind === "tool.pre" && toolName === "AskUserQuestion") {
399
+ return ASK_USER_QUESTION_HINTS;
400
+ }
359
401
  const maybeRule = RULES[kind];
360
402
  return maybeRule ?? DEFAULT_HINTS;
361
403
  }
@@ -664,7 +706,7 @@ function mapEnvelopeToRuntimeEvent(envelope) {
664
706
  agentId: translated.agentId,
665
707
  agentType: translated.agentType,
666
708
  context,
667
- interaction: getInteractionHints(translated.kind),
709
+ interaction: getInteractionHints(translated.kind, translated.toolName),
668
710
  payload: safePayload,
669
711
  display: buildClaudeDisplay(translated.kind, translated.data)
670
712
  };
@@ -754,6 +796,13 @@ var BoundedLineParser = class {
754
796
  }
755
797
  };
756
798
 
799
+ // src/harnesses/claude/runtime/timeoutResolution.ts
800
+ function resolveAdapterTimeoutMs(interaction, fallbackMs) {
801
+ const ms = interaction.defaultTimeoutMs;
802
+ if (ms === null) return null;
803
+ return ms ?? fallbackMs;
804
+ }
805
+
757
806
  // src/harnesses/claude/runtime/streamJsonToolParser.ts
758
807
  function extractResultText(content) {
759
808
  if (typeof content === "string") return content;
@@ -943,7 +992,7 @@ function createServer2(opts) {
943
992
  return startPromise;
944
993
  }
945
994
  socketPath = resolveHookSocketPath(instanceId);
946
- const socketDir = path2.dirname(socketPath);
995
+ const socketDir = path3.dirname(socketPath);
947
996
  lastError = null;
948
997
  const simulatedFailure = getSimulatedStartupFailure();
949
998
  if (simulatedFailure) {
@@ -969,7 +1018,7 @@ function createServer2(opts) {
969
1018
  return Promise.resolve();
970
1019
  }
971
1020
  try {
972
- fs2.mkdirSync(socketDir, { recursive: true });
1021
+ fs3.mkdirSync(socketDir, { recursive: true });
973
1022
  } catch (error) {
974
1023
  status = "stopped";
975
1024
  lastError = makeStartupError(
@@ -983,7 +1032,7 @@ function createServer2(opts) {
983
1032
  }
984
1033
  cleanupStaleSockets(socketDir);
985
1034
  try {
986
- fs2.unlinkSync(socketPath);
1035
+ fs3.unlinkSync(socketPath);
987
1036
  } catch {
988
1037
  }
989
1038
  server = net.createServer((socket) => {
@@ -1002,8 +1051,11 @@ function createServer2(opts) {
1002
1051
  sessionId = envelope.session_id;
1003
1052
  }
1004
1053
  const runtimeEvent = mapEnvelopeToRuntimeEvent(envelope);
1005
- const timeoutMs = runtimeEvent.interaction.defaultTimeoutMs ?? PENDING_TTL_MS;
1006
- const timer = setTimeout(() => {
1054
+ const timeoutMs = resolveAdapterTimeoutMs(
1055
+ runtimeEvent.interaction,
1056
+ PENDING_TTL_MS
1057
+ );
1058
+ const timer = timeoutMs === null ? void 0 : setTimeout(() => {
1007
1059
  const timeoutDecision = {
1008
1060
  type: "passthrough",
1009
1061
  source: "timeout"
@@ -1068,7 +1120,7 @@ function createServer2(opts) {
1068
1120
  try {
1069
1121
  currentServer.listen(socketPath, () => {
1070
1122
  try {
1071
- fs2.chmodSync(socketPath, 384);
1123
+ fs3.chmodSync(socketPath, 384);
1072
1124
  } catch {
1073
1125
  }
1074
1126
  });
@@ -1099,7 +1151,7 @@ function createServer2(opts) {
1099
1151
  status = "stopped";
1100
1152
  lastError = null;
1101
1153
  try {
1102
- fs2.unlinkSync(socketPath);
1154
+ fs3.unlinkSync(socketPath);
1103
1155
  } catch {
1104
1156
  }
1105
1157
  },
@@ -1151,20 +1203,20 @@ function createClaudeHookRuntime(opts) {
1151
1203
  }
1152
1204
 
1153
1205
  // src/harnesses/claude/config/readSettingsModel.ts
1154
- import fs3 from "fs";
1155
- import os from "os";
1156
- import path3 from "path";
1206
+ import fs4 from "fs";
1207
+ import os2 from "os";
1208
+ import path4 from "path";
1157
1209
  function readClaudeSettingsModel(projectDir) {
1158
- const home = os.homedir();
1210
+ const home = os2.homedir();
1159
1211
  const paths = [
1160
- path3.join(projectDir, ".claude", "settings.local.json"),
1161
- path3.join(projectDir, ".claude", "settings.json"),
1162
- path3.join(home, ".claude", "settings.local.json"),
1163
- path3.join(home, ".claude", "settings.json")
1212
+ path4.join(projectDir, ".claude", "settings.local.json"),
1213
+ path4.join(projectDir, ".claude", "settings.json"),
1214
+ path4.join(home, ".claude", "settings.local.json"),
1215
+ path4.join(home, ".claude", "settings.json")
1164
1216
  ];
1165
1217
  for (const p of paths) {
1166
1218
  try {
1167
- const raw = JSON.parse(fs3.readFileSync(p, "utf-8"));
1219
+ const raw = JSON.parse(fs4.readFileSync(p, "utf-8"));
1168
1220
  if (raw.model) return raw.model;
1169
1221
  } catch {
1170
1222
  }
@@ -1186,16 +1238,16 @@ function resolveClaudeModel({
1186
1238
 
1187
1239
  // src/harnesses/claude/system/verifyHarness.ts
1188
1240
  import { execFileSync as execFileSync2 } from "child_process";
1189
- import fs9 from "fs";
1190
- import path8 from "path";
1241
+ import fs10 from "fs";
1242
+ import path9 from "path";
1191
1243
 
1192
1244
  // src/harnesses/claude/system/detectVersion.ts
1193
1245
  import { execFileSync } from "child_process";
1194
1246
 
1195
1247
  // src/harnesses/claude/system/resolveBinary.ts
1196
- import * as fs4 from "fs";
1197
- import * as os2 from "os";
1198
- import * as path4 from "path";
1248
+ import * as fs5 from "fs";
1249
+ import * as os3 from "os";
1250
+ import * as path5 from "path";
1199
1251
  var CLAUDE_BINARY_ENV_VARS = [
1200
1252
  "ATHENA_CLAUDE_PATH",
1201
1253
  "CLAUDE_BINARY",
@@ -1203,7 +1255,7 @@ var CLAUDE_BINARY_ENV_VARS = [
1203
1255
  ];
1204
1256
  function isExecutablePath(candidatePath) {
1205
1257
  try {
1206
- fs4.accessSync(candidatePath, fs4.constants.X_OK);
1258
+ fs5.accessSync(candidatePath, fs5.constants.X_OK);
1207
1259
  return true;
1208
1260
  } catch {
1209
1261
  return false;
@@ -1211,7 +1263,7 @@ function isExecutablePath(candidatePath) {
1211
1263
  }
1212
1264
  function macosFallbacks(homeDir) {
1213
1265
  return [
1214
- path4.join(homeDir, ".local", "bin", "claude"),
1266
+ path5.join(homeDir, ".local", "bin", "claude"),
1215
1267
  "/opt/homebrew/bin/claude",
1216
1268
  "/usr/local/bin/claude",
1217
1269
  "/usr/bin/claude"
@@ -1219,16 +1271,16 @@ function macosFallbacks(homeDir) {
1219
1271
  }
1220
1272
  function linuxFallbacks(homeDir) {
1221
1273
  return [
1222
- path4.join(homeDir, ".local", "bin", "claude"),
1274
+ path5.join(homeDir, ".local", "bin", "claude"),
1223
1275
  "/usr/local/bin/claude",
1224
1276
  "/usr/bin/claude"
1225
1277
  ];
1226
1278
  }
1227
1279
  function collectCandidatePaths(pathValue, platform, homeDir) {
1228
1280
  const candidates = [];
1229
- for (const entry of (pathValue ?? "").split(path4.delimiter)) {
1281
+ for (const entry of (pathValue ?? "").split(path5.delimiter)) {
1230
1282
  if (!entry) continue;
1231
- candidates.push(path4.join(entry, "claude"));
1283
+ candidates.push(path5.join(entry, "claude"));
1232
1284
  }
1233
1285
  if (platform === "darwin") {
1234
1286
  candidates.push(...macosFallbacks(homeDir));
@@ -1240,7 +1292,7 @@ function collectCandidatePaths(pathValue, platform, homeDir) {
1240
1292
  function resolveClaudeBinary(options = {}) {
1241
1293
  const env = options.env ?? process.env;
1242
1294
  const platform = options.platform ?? process.platform;
1243
- const homeDir = options.homeDir ?? os2.homedir();
1295
+ const homeDir = options.homeDir ?? os3.homedir();
1244
1296
  const pathValue = options.pathValue ?? env["PATH"];
1245
1297
  const isExecutable = options.isExecutable ?? isExecutablePath;
1246
1298
  for (const envVar of CLAUDE_BINARY_ENV_VARS) {
@@ -1278,9 +1330,9 @@ function detectClaudeVersion() {
1278
1330
  }
1279
1331
 
1280
1332
  // src/harnesses/claude/hooks/generateHookSettings.ts
1281
- import * as fs5 from "fs";
1282
- import * as path5 from "path";
1283
- import * as os3 from "os";
1333
+ import * as fs6 from "fs";
1334
+ import * as path6 from "path";
1335
+ import * as os4 from "os";
1284
1336
  import { fileURLToPath } from "url";
1285
1337
  var TOOL_HOOK_EVENTS = [
1286
1338
  "PreToolUse",
@@ -1319,17 +1371,17 @@ function formatHookForwarderCommand(nodePath, scriptPath) {
1319
1371
  return `${quoteShellArg(nodePath)} ${quoteShellArg(scriptPath)}`;
1320
1372
  }
1321
1373
  function resolveHookForwarderPath(entryUrl) {
1322
- let currentDir = path5.dirname(fileURLToPath(entryUrl));
1323
- const siblingPath = path5.join(currentDir, "hook-forwarder.js");
1324
- if (fs5.existsSync(siblingPath)) {
1374
+ let currentDir = path6.dirname(fileURLToPath(entryUrl));
1375
+ const siblingPath = path6.join(currentDir, "hook-forwarder.js");
1376
+ if (fs6.existsSync(siblingPath)) {
1325
1377
  return siblingPath;
1326
1378
  }
1327
1379
  for (; ; ) {
1328
- const candidatePath = path5.join(currentDir, "dist", "hook-forwarder.js");
1329
- if (fs5.existsSync(candidatePath)) {
1380
+ const candidatePath = path6.join(currentDir, "dist", "hook-forwarder.js");
1381
+ if (fs6.existsSync(candidatePath)) {
1330
1382
  return candidatePath;
1331
1383
  }
1332
- const parentDir = path5.dirname(currentDir);
1384
+ const parentDir = path6.dirname(currentDir);
1333
1385
  if (parentDir === currentDir) {
1334
1386
  return null;
1335
1387
  }
@@ -1386,10 +1438,10 @@ function generateHookSettings(tempDir, authOverlay) {
1386
1438
  if (authOverlay?.apiKeyHelper) {
1387
1439
  settings.apiKeyHelper = authOverlay.apiKeyHelper;
1388
1440
  }
1389
- const dir = tempDir ?? os3.tmpdir();
1441
+ const dir = tempDir ?? os4.tmpdir();
1390
1442
  const filename = `athena-hooks-${process.pid}-${Date.now()}.json`;
1391
- const settingsPath = path5.join(dir, filename);
1392
- fs5.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), {
1443
+ const settingsPath = path6.join(dir, filename);
1444
+ fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), {
1393
1445
  encoding: "utf8",
1394
1446
  mode: 384
1395
1447
  });
@@ -1404,8 +1456,8 @@ function generateHookSettings(tempDir, authOverlay) {
1404
1456
  settingsPath,
1405
1457
  cleanup: () => {
1406
1458
  try {
1407
- if (fs5.existsSync(settingsPath)) {
1408
- fs5.unlinkSync(settingsPath);
1459
+ if (fs6.existsSync(settingsPath)) {
1460
+ fs6.unlinkSync(settingsPath);
1409
1461
  }
1410
1462
  } catch {
1411
1463
  }
@@ -1429,13 +1481,13 @@ function registerCleanupOnExit(cleanup) {
1429
1481
  }
1430
1482
 
1431
1483
  // src/harnesses/claude/auth/runtimeAuth.ts
1432
- import os6 from "os";
1484
+ import os7 from "os";
1433
1485
 
1434
1486
  // src/harnesses/claude/system/doctorProbes.ts
1435
1487
  import { spawn, spawnSync } from "child_process";
1436
- import * as fs6 from "fs";
1437
- import * as os4 from "os";
1438
- import * as path6 from "path";
1488
+ import * as fs7 from "fs";
1489
+ import * as os5 from "os";
1490
+ import * as path7 from "path";
1439
1491
  var DOCTOR_PROMPT = "Reply with exactly: ATHENA_DOCTOR_OK";
1440
1492
  var DOCTOR_EXPECTED = "ATHENA_DOCTOR_OK";
1441
1493
  var DOCTOR_TIMEOUT_MS = 3e4;
@@ -1553,15 +1605,15 @@ function scanSettingsFiles(homeDir, cwd, env, platform, readFileFn) {
1553
1605
  } else if (platform === "win32") {
1554
1606
  const programFiles = env["PROGRAMFILES"] ?? "C:\\Program Files";
1555
1607
  candidates.push(
1556
- path6.join(programFiles, "ClaudeCode", "managed-settings.json")
1608
+ path7.join(programFiles, "ClaudeCode", "managed-settings.json")
1557
1609
  );
1558
1610
  } else {
1559
1611
  candidates.push("/etc/claude-code/managed-settings.json");
1560
1612
  }
1561
- const configDir = env["CLAUDE_CONFIG_DIR"] ?? path6.join(homeDir, ".claude");
1562
- candidates.push(path6.join(configDir, "settings.json"));
1563
- candidates.push(path6.join(cwd, ".claude", "settings.json"));
1564
- candidates.push(path6.join(cwd, ".claude", "settings.local.json"));
1613
+ const configDir = env["CLAUDE_CONFIG_DIR"] ?? path7.join(homeDir, ".claude");
1614
+ candidates.push(path7.join(configDir, "settings.json"));
1615
+ candidates.push(path7.join(cwd, ".claude", "settings.json"));
1616
+ candidates.push(path7.join(cwd, ".claude", "settings.local.json"));
1565
1617
  const scan = {
1566
1618
  apiKeyHelper: null,
1567
1619
  envApiKey: null,
@@ -1642,9 +1694,9 @@ function lookupCredential(options = {}) {
1642
1694
  function lookupAllCredentials(options = {}) {
1643
1695
  const env = options.env ?? process.env;
1644
1696
  const platform = options.platform ?? process.platform;
1645
- const homeDir = options.homeDir ?? os4.homedir();
1697
+ const homeDir = options.homeDir ?? os5.homedir();
1646
1698
  const cwd = options.cwd ?? process.cwd();
1647
- const readFileFn = options.readFileFn ?? ((p) => fs6.readFileSync(p, "utf8"));
1699
+ const readFileFn = options.readFileFn ?? ((p) => fs7.readFileSync(p, "utf8"));
1648
1700
  const found = [];
1649
1701
  const seen = /* @__PURE__ */ new Set();
1650
1702
  const push = (cred) => {
@@ -1713,9 +1765,9 @@ function lookupAllCredentials(options = {}) {
1713
1765
  }
1714
1766
  }
1715
1767
  const dotenvCandidates = [
1716
- { source: "dotenv:.env", path: path6.join(cwd, ".env") },
1717
- { source: "dotenv:.env.local", path: path6.join(cwd, ".env.local") },
1718
- { source: "dotenv:~/.env", path: path6.join(homeDir, ".env") }
1768
+ { source: "dotenv:.env", path: path7.join(cwd, ".env") },
1769
+ { source: "dotenv:.env.local", path: path7.join(cwd, ".env.local") },
1770
+ { source: "dotenv:~/.env", path: path7.join(homeDir, ".env") }
1719
1771
  ];
1720
1772
  for (const candidate of dotenvCandidates) {
1721
1773
  try {
@@ -1788,8 +1840,8 @@ function lookupAllCredentials(options = {}) {
1788
1840
  const raw = lookup("Claude Code-credentials");
1789
1841
  if (raw) pushBlobCredentials("keychain:Claude Code-credentials", raw);
1790
1842
  } else {
1791
- const credentialsPath = path6.join(
1792
- env["CLAUDE_CONFIG_DIR"] ?? path6.join(homeDir, ".claude"),
1843
+ const credentialsPath = path7.join(
1844
+ env["CLAUDE_CONFIG_DIR"] ?? path7.join(homeDir, ".claude"),
1793
1845
  ".credentials.json"
1794
1846
  );
1795
1847
  try {
@@ -1802,7 +1854,7 @@ function lookupAllCredentials(options = {}) {
1802
1854
  }
1803
1855
  const configHome = env["CLAUDE_CONFIG_DIR"] ?? homeDir;
1804
1856
  try {
1805
- const raw = readFileFn(path6.join(configHome, ".claude.json"));
1857
+ const raw = readFileFn(path7.join(configHome, ".claude.json"));
1806
1858
  if (raw.includes(SK_ANT_API_PREFIX)) {
1807
1859
  const apiKey2 = extractApiKeyFromParsed(safeJsonParse(raw));
1808
1860
  if (apiKey2) {
@@ -1816,19 +1868,19 @@ function lookupAllCredentials(options = {}) {
1816
1868
  function shellQuoteSingle(value) {
1817
1869
  return `'${value.replace(/'/g, `'\\''`)}'`;
1818
1870
  }
1819
- function buildApiKeyHelperSettings(value, tempDir = os4.tmpdir()) {
1871
+ function buildApiKeyHelperSettings(value, tempDir = os5.tmpdir()) {
1820
1872
  const filename = `athena-doctor-helper-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`;
1821
- const settingsPath = path6.join(tempDir, filename);
1873
+ const settingsPath = path7.join(tempDir, filename);
1822
1874
  const settings = {
1823
1875
  apiKeyHelper: `printf %s ${shellQuoteSingle(value)}`
1824
1876
  };
1825
- fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
1877
+ fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf8");
1826
1878
  return {
1827
1879
  settingsPath,
1828
1880
  cleanup: () => {
1829
1881
  try {
1830
- if (fs6.existsSync(settingsPath)) {
1831
- fs6.unlinkSync(settingsPath);
1882
+ if (fs7.existsSync(settingsPath)) {
1883
+ fs7.unlinkSync(settingsPath);
1832
1884
  }
1833
1885
  } catch {
1834
1886
  }
@@ -1851,7 +1903,7 @@ function formatProbeCommand(probe, claudeBinary, aliases) {
1851
1903
  parts.push(`${key}=${shellQuoteArg(maskCredentialValue(value))}`);
1852
1904
  }
1853
1905
  }
1854
- parts.push(path6.basename(claudeBinary));
1906
+ parts.push(path7.basename(claudeBinary));
1855
1907
  const aliasFor = (arg) => {
1856
1908
  if (!aliases) return null;
1857
1909
  for (const [name, fullPath] of aliases) {
@@ -2206,12 +2258,12 @@ function probeSkipReason(probe, opts) {
2206
2258
  }
2207
2259
 
2208
2260
  // src/harnesses/claude/auth/portableAuth.ts
2209
- import fs8 from "fs";
2210
- import os5 from "os";
2261
+ import fs9 from "fs";
2262
+ import os6 from "os";
2211
2263
 
2212
2264
  // src/harnesses/claude/auth/settingsSurfaces.ts
2213
- import fs7 from "fs";
2214
- import path7 from "path";
2265
+ import fs8 from "fs";
2266
+ import path8 from "path";
2215
2267
  var PORTABLE_PROVIDER_ENV_VARS = [
2216
2268
  "ANTHROPIC_API_KEY",
2217
2269
  "ANTHROPIC_AUTH_TOKEN",
@@ -2230,7 +2282,7 @@ var PORTABLE_PROVIDER_ENV_VARS = [
2230
2282
  "CLAUDE_CODE_SKIP_FOUNDRY_AUTH"
2231
2283
  ];
2232
2284
  function resolveClaudeSettingsDir(homeDir, env) {
2233
- return env["CLAUDE_CONFIG_DIR"] ?? path7.join(homeDir, ".claude");
2285
+ return env["CLAUDE_CONFIG_DIR"] ?? path8.join(homeDir, ".claude");
2234
2286
  }
2235
2287
  function managedSettingsDir(platform, env) {
2236
2288
  if (platform === "darwin") {
@@ -2238,27 +2290,27 @@ function managedSettingsDir(platform, env) {
2238
2290
  }
2239
2291
  if (platform === "win32") {
2240
2292
  const programFiles = env["PROGRAMFILES"] ?? "C:\\Program Files";
2241
- return path7.join(programFiles, "ClaudeCode");
2293
+ return path8.join(programFiles, "ClaudeCode");
2242
2294
  }
2243
2295
  return "/etc/claude-code";
2244
2296
  }
2245
- function listManagedSettingsPaths(platform, env, readdirSync2 = fs7.readdirSync) {
2297
+ function listManagedSettingsPaths(platform, env, readdirSync2 = fs8.readdirSync) {
2246
2298
  const baseDir = managedSettingsDir(platform, env);
2247
- const paths = [path7.join(baseDir, "managed-settings.json")];
2248
- const dropInDir = path7.join(baseDir, "managed-settings.d");
2299
+ const paths = [path8.join(baseDir, "managed-settings.json")];
2300
+ const dropInDir = path8.join(baseDir, "managed-settings.d");
2249
2301
  try {
2250
- const dropIns = readdirSync2(dropInDir).filter((name) => name.endsWith(".json")).sort().map((name) => path7.join(dropInDir, name));
2302
+ const dropIns = readdirSync2(dropInDir).filter((name) => name.endsWith(".json")).sort().map((name) => path8.join(dropInDir, name));
2251
2303
  paths.push(...dropIns);
2252
2304
  } catch {
2253
2305
  }
2254
2306
  return paths;
2255
2307
  }
2256
- function resolveClaudeSettingsSurfacePaths(homeDir, cwd, platform, env, readdirSync2 = fs7.readdirSync) {
2308
+ function resolveClaudeSettingsSurfacePaths(homeDir, cwd, platform, env, readdirSync2 = fs8.readdirSync) {
2257
2309
  return {
2258
2310
  managed: listManagedSettingsPaths(platform, env, readdirSync2),
2259
- user: path7.join(resolveClaudeSettingsDir(homeDir, env), "settings.json"),
2260
- project: path7.join(cwd, ".claude", "settings.json"),
2261
- local: path7.join(cwd, ".claude", "settings.local.json")
2311
+ user: path8.join(resolveClaudeSettingsDir(homeDir, env), "settings.json"),
2312
+ project: path8.join(cwd, ".claude", "settings.json"),
2313
+ local: path8.join(cwd, ".claude", "settings.local.json")
2262
2314
  };
2263
2315
  }
2264
2316
 
@@ -2272,10 +2324,10 @@ function readJsonObject(filePath, readFileFn) {
2272
2324
  }
2273
2325
  function resolvePortableAuthSettings(options = {}) {
2274
2326
  const cwd = options.cwd ?? process.cwd();
2275
- const homeDir = options.homeDir ?? os5.homedir();
2327
+ const homeDir = options.homeDir ?? os6.homedir();
2276
2328
  const platform = options.platform ?? process.platform;
2277
2329
  const env = options.env ?? process.env;
2278
- const readFileFn = options.readFileFn ?? ((filePath) => fs8.readFileSync(filePath, "utf8"));
2330
+ const readFileFn = options.readFileFn ?? ((filePath) => fs9.readFileSync(filePath, "utf8"));
2279
2331
  const settingsPaths = resolveClaudeSettingsSurfacePaths(
2280
2332
  homeDir,
2281
2333
  cwd,
@@ -2335,7 +2387,7 @@ function resolveRuntimeAuthOverlay(options = {}) {
2335
2387
  const credential = lookupCredential({
2336
2388
  ...options,
2337
2389
  cwd: options.cwd ?? process.cwd(),
2338
- homeDir: options.homeDir ?? os6.homedir(),
2390
+ homeDir: options.homeDir ?? os7.homedir(),
2339
2391
  platform: options.platform ?? process.platform,
2340
2392
  env: options.env ?? process.env
2341
2393
  });
@@ -2479,16 +2531,16 @@ function runClaudeAuthStatus(claudeBinary) {
2479
2531
  }
2480
2532
  function canExecute(candidatePath) {
2481
2533
  try {
2482
- fs9.accessSync(candidatePath, fs9.constants.X_OK);
2534
+ fs10.accessSync(candidatePath, fs10.constants.X_OK);
2483
2535
  return true;
2484
2536
  } catch {
2485
2537
  return false;
2486
2538
  }
2487
2539
  }
2488
2540
  function resolveExecutableOnPath(commandName, pathValue, fileExists) {
2489
- for (const entry of pathValue.split(path8.delimiter)) {
2541
+ for (const entry of pathValue.split(path9.delimiter)) {
2490
2542
  if (!entry) continue;
2491
- const candidate = path8.join(entry, commandName);
2543
+ const candidate = path9.join(entry, commandName);
2492
2544
  if (fileExists(candidate)) {
2493
2545
  return candidate;
2494
2546
  }
@@ -2565,7 +2617,7 @@ function verifyClaudeHarness(options = {}) {
2565
2617
  }
2566
2618
  if (hookForwarder.source === "bundled") {
2567
2619
  const nodeOk = fileExists(hookForwarder.executable);
2568
- const scriptOk = !!hookForwarder.scriptPath && fs9.existsSync(hookForwarder.scriptPath);
2620
+ const scriptOk = !!hookForwarder.scriptPath && fs10.existsSync(hookForwarder.scriptPath);
2569
2621
  checks.push({
2570
2622
  label: "Hook forwarder",
2571
2623
  status: nodeOk && scriptOk ? "pass" : "fail",
@@ -2730,8 +2782,8 @@ import { useCallback, useEffect, useRef, useState } from "react";
2730
2782
 
2731
2783
  // src/harnesses/claude/process/spawn.ts
2732
2784
  import { spawn as spawn2 } from "child_process";
2733
- import fs10 from "fs";
2734
- import path9 from "path";
2785
+ import fs11 from "fs";
2786
+ import path10 from "path";
2735
2787
 
2736
2788
  // src/harnesses/claude/config/isolation.ts
2737
2789
  var DISALLOWED_FIRST_PARTY_MCPS = [
@@ -2979,11 +3031,11 @@ function makePreflightError(failureCode, message) {
2979
3031
  }
2980
3032
  function resolveExecutableOnPath2(commandName) {
2981
3033
  const pathValue = process.env["PATH"] ?? "";
2982
- for (const entry of pathValue.split(path9.delimiter)) {
3034
+ for (const entry of pathValue.split(path10.delimiter)) {
2983
3035
  if (!entry) continue;
2984
- const candidate = path9.join(entry, commandName);
3036
+ const candidate = path10.join(entry, commandName);
2985
3037
  try {
2986
- fs10.accessSync(candidate, fs10.constants.X_OK);
3038
+ fs11.accessSync(candidate, fs11.constants.X_OK);
2987
3039
  return candidate;
2988
3040
  } catch {
2989
3041
  }
@@ -3008,7 +3060,7 @@ function runSpawnPreflight(hookSocketPath) {
3008
3060
  );
3009
3061
  }
3010
3062
  const hookForwarder = resolveHookForwarderCommand();
3011
- if (hookForwarder.source === "bundled" && (!fs10.existsSync(hookForwarder.executable) || !hookForwarder.scriptPath || !fs10.existsSync(hookForwarder.scriptPath))) {
3063
+ if (hookForwarder.source === "bundled" && (!fs11.existsSync(hookForwarder.executable) || !hookForwarder.scriptPath || !fs11.existsSync(hookForwarder.scriptPath))) {
3012
3064
  throw makePreflightError(
3013
3065
  "hook_forwarder_missing",
3014
3066
  "Athena hook forwarder bundle is missing. Rebuild or reinstall Athena before retrying."
@@ -5518,9 +5570,9 @@ function buildCodexPluginInstallMessage(plugins) {
5518
5570
  }
5519
5571
 
5520
5572
  // src/harnesses/codex/runtime/agentConfig.ts
5521
- import fs11 from "fs";
5522
- import path10 from "path";
5523
- import os7 from "os";
5573
+ import fs12 from "fs";
5574
+ import path11 from "path";
5575
+ import os8 from "os";
5524
5576
 
5525
5577
  // src/shared/utils/yamlFrontmatter.ts
5526
5578
  function parseSimpleYaml(lines) {
@@ -5679,7 +5731,7 @@ function discoverAgents(agentRoots) {
5679
5731
  for (const root of agentRoots) {
5680
5732
  let entries;
5681
5733
  try {
5682
- entries = fs11.readdirSync(root, { withFileTypes: true });
5734
+ entries = fs12.readdirSync(root, { withFileTypes: true });
5683
5735
  } catch {
5684
5736
  continue;
5685
5737
  }
@@ -5687,9 +5739,9 @@ function discoverAgents(agentRoots) {
5687
5739
  if (!entry.isFile() || !entry.name.endsWith(".md")) {
5688
5740
  continue;
5689
5741
  }
5690
- const filePath = path10.join(root, entry.name);
5742
+ const filePath = path11.join(root, entry.name);
5691
5743
  try {
5692
- const content = fs11.readFileSync(filePath, "utf-8");
5744
+ const content = fs12.readFileSync(filePath, "utf-8");
5693
5745
  const agent = parseAgentMd(filePath, content);
5694
5746
  agents.push({ filePath, agent });
5695
5747
  } catch (err) {
@@ -5745,8 +5797,8 @@ function resolveCodexAgentConfig(input) {
5745
5797
  errors: allErrors
5746
5798
  };
5747
5799
  }
5748
- const tempDir = path10.join(os7.tmpdir(), `athena-agents-${sessionId}`);
5749
- fs11.mkdirSync(tempDir, { recursive: true });
5800
+ const tempDir = path11.join(os8.tmpdir(), `athena-agents-${sessionId}`);
5801
+ fs12.mkdirSync(tempDir, { recursive: true });
5750
5802
  const edits = [
5751
5803
  {
5752
5804
  keyPath: "features.multi_agent",
@@ -5767,8 +5819,8 @@ function resolveCodexAgentConfig(input) {
5767
5819
  const agentNames = [];
5768
5820
  for (const { agent } of uniqueAgents) {
5769
5821
  const toml = generateAgentToml(agent);
5770
- const tomlPath = path10.join(tempDir, `${agent.name}.toml`);
5771
- fs11.writeFileSync(tomlPath, toml, "utf-8");
5822
+ const tomlPath = path11.join(tempDir, `${agent.name}.toml`);
5823
+ fs12.writeFileSync(tomlPath, toml, "utf-8");
5772
5824
  edits.push({
5773
5825
  keyPath: `agents.${agent.name}`,
5774
5826
  value: {
@@ -5807,7 +5859,7 @@ function cleanupAgentConfig(tempDir) {
5807
5859
  return;
5808
5860
  }
5809
5861
  try {
5810
- fs11.rmSync(tempDir, { recursive: true, force: true });
5862
+ fs12.rmSync(tempDir, { recursive: true, force: true });
5811
5863
  } catch {
5812
5864
  }
5813
5865
  }
@@ -6422,7 +6474,7 @@ function createCodexRuntime(opts) {
6422
6474
  }
6423
6475
 
6424
6476
  // src/harnesses/codex/session/sessionAssets.ts
6425
- import fs12 from "fs";
6477
+ import fs13 from "fs";
6426
6478
  function asRecord4(value) {
6427
6479
  if (typeof value === "object" && value !== null) {
6428
6480
  return value;
@@ -6491,7 +6543,7 @@ function readMcpServers(configPath) {
6491
6543
  }
6492
6544
  let raw;
6493
6545
  try {
6494
- raw = fs12.readFileSync(configPath, "utf-8");
6546
+ raw = fs13.readFileSync(configPath, "utf-8");
6495
6547
  } catch {
6496
6548
  return {};
6497
6549
  }
@@ -6985,9 +7037,9 @@ function listHarnessCapabilities() {
6985
7037
  }
6986
7038
 
6987
7039
  // src/infra/plugins/register.ts
6988
- import fs14 from "fs";
6989
- import os8 from "os";
6990
- import path12 from "path";
7040
+ import fs15 from "fs";
7041
+ import os9 from "os";
7042
+ import path13 from "path";
6991
7043
 
6992
7044
  // src/app/commands/registry.ts
6993
7045
  var commands = /* @__PURE__ */ new Map();
@@ -7018,8 +7070,8 @@ function getAll() {
7018
7070
  }
7019
7071
 
7020
7072
  // src/infra/plugins/loader.ts
7021
- import fs13 from "fs";
7022
- import path11 from "path";
7073
+ import fs14 from "fs";
7074
+ import path12 from "path";
7023
7075
 
7024
7076
  // src/infra/plugins/frontmatter.ts
7025
7077
  function parseFrontmatter(content) {
@@ -7036,27 +7088,27 @@ function parseFrontmatter(content) {
7036
7088
 
7037
7089
  // src/infra/plugins/loader.ts
7038
7090
  function loadPlugin(pluginDir) {
7039
- if (!fs13.existsSync(pluginDir)) {
7091
+ if (!fs14.existsSync(pluginDir)) {
7040
7092
  throw new Error(`Plugin directory not found: ${pluginDir}`);
7041
7093
  }
7042
- const manifestPath = path11.join(pluginDir, ".claude-plugin", "plugin.json");
7043
- if (!fs13.existsSync(manifestPath)) {
7094
+ const manifestPath = path12.join(pluginDir, ".claude-plugin", "plugin.json");
7095
+ if (!fs14.existsSync(manifestPath)) {
7044
7096
  throw new Error(`Plugin manifest not found: ${manifestPath}`);
7045
7097
  }
7046
- JSON.parse(fs13.readFileSync(manifestPath, "utf-8"));
7047
- const skillsDir = path11.join(pluginDir, "skills");
7048
- if (!fs13.existsSync(skillsDir)) {
7098
+ JSON.parse(fs14.readFileSync(manifestPath, "utf-8"));
7099
+ const skillsDir = path12.join(pluginDir, "skills");
7100
+ if (!fs14.existsSync(skillsDir)) {
7049
7101
  return [];
7050
7102
  }
7051
- const mcpConfigPath = path11.join(pluginDir, ".mcp.json");
7052
- const hasMcpConfig = fs13.existsSync(mcpConfigPath);
7053
- const entries = fs13.readdirSync(skillsDir, { withFileTypes: true });
7103
+ const mcpConfigPath = path12.join(pluginDir, ".mcp.json");
7104
+ const hasMcpConfig = fs14.existsSync(mcpConfigPath);
7105
+ const entries = fs14.readdirSync(skillsDir, { withFileTypes: true });
7054
7106
  const commands2 = [];
7055
7107
  for (const entry of entries) {
7056
7108
  if (!entry.isDirectory()) continue;
7057
- const skillPath = path11.join(skillsDir, entry.name, "SKILL.md");
7058
- if (!fs13.existsSync(skillPath)) continue;
7059
- const content = fs13.readFileSync(skillPath, "utf-8");
7109
+ const skillPath = path12.join(skillsDir, entry.name, "SKILL.md");
7110
+ if (!fs14.existsSync(skillPath)) continue;
7111
+ const content = fs14.readFileSync(skillPath, "utf-8");
7060
7112
  const parsed = parseFrontmatter(content);
7061
7113
  if (!parsed.frontmatter["user-invocable"]) continue;
7062
7114
  commands2.push(
@@ -7095,11 +7147,11 @@ function skillToCommand(frontmatter, body, mcpConfigPath) {
7095
7147
  function buildPluginMcpConfig(pluginDirs, mcpServerOptions) {
7096
7148
  const mergedServers = {};
7097
7149
  for (const dir of pluginDirs) {
7098
- const mcpPath = path12.join(dir, ".mcp.json");
7099
- if (!fs14.existsSync(mcpPath)) {
7150
+ const mcpPath = path13.join(dir, ".mcp.json");
7151
+ if (!fs15.existsSync(mcpPath)) {
7100
7152
  continue;
7101
7153
  }
7102
- const config = JSON.parse(fs14.readFileSync(mcpPath, "utf-8"));
7154
+ const config = JSON.parse(fs15.readFileSync(mcpPath, "utf-8"));
7103
7155
  for (const [serverName, serverConfig] of Object.entries(
7104
7156
  config.mcpServers ?? {}
7105
7157
  )) {
@@ -7122,8 +7174,8 @@ function buildPluginMcpConfig(pluginDirs, mcpServerOptions) {
7122
7174
  if (Object.keys(mergedServers).length === 0) {
7123
7175
  return void 0;
7124
7176
  }
7125
- const mcpConfig = path12.join(os8.tmpdir(), `athena-mcp-${process.pid}.json`);
7126
- fs14.writeFileSync(mcpConfig, JSON.stringify({ mcpServers: mergedServers }));
7177
+ const mcpConfig = path13.join(os9.tmpdir(), `athena-mcp-${process.pid}.json`);
7178
+ fs15.writeFileSync(mcpConfig, JSON.stringify({ mcpServers: mergedServers }));
7127
7179
  return mcpConfig;
7128
7180
  }
7129
7181
  function registerPlugins(pluginDirs, mcpServerOptions, includeMcpConfig = true) {
@@ -7295,7 +7347,7 @@ var EXEC_EXIT_CODE = {
7295
7347
 
7296
7348
  // src/app/exec/runner.ts
7297
7349
  import crypto2 from "crypto";
7298
- import path17 from "path";
7350
+ import path18 from "path";
7299
7351
 
7300
7352
  // src/core/feed/todo.ts
7301
7353
  function isSubagentTool(toolName) {
@@ -7657,27 +7709,27 @@ function generateNeutralTitle(event, g) {
7657
7709
  }
7658
7710
 
7659
7711
  // src/core/feed/transcript.ts
7660
- import * as fs15 from "fs";
7712
+ import * as fs16 from "fs";
7661
7713
  function createTranscriptReader() {
7662
7714
  const offsets = /* @__PURE__ */ new Map();
7663
7715
  function readNewAssistantMessages(transcriptPath) {
7664
7716
  const offset = offsets.get(transcriptPath) ?? 0;
7665
7717
  let fileSize;
7666
7718
  try {
7667
- fileSize = fs15.statSync(transcriptPath).size;
7719
+ fileSize = fs16.statSync(transcriptPath).size;
7668
7720
  if (fileSize <= offset) return [];
7669
7721
  } catch {
7670
7722
  return [];
7671
7723
  }
7672
7724
  let fd;
7673
7725
  try {
7674
- fd = fs15.openSync(transcriptPath, "r");
7726
+ fd = fs16.openSync(transcriptPath, "r");
7675
7727
  } catch {
7676
7728
  return [];
7677
7729
  }
7678
7730
  try {
7679
7731
  const buf = Buffer.alloc(fileSize - offset);
7680
- const bytesRead = fs15.readSync(fd, buf, 0, buf.length, offset);
7732
+ const bytesRead = fs16.readSync(fd, buf, 0, buf.length, offset);
7681
7733
  let lastNewline = -1;
7682
7734
  for (let i = bytesRead - 1; i >= 0; i--) {
7683
7735
  if (buf[i] === 10) {
@@ -7721,7 +7773,7 @@ function createTranscriptReader() {
7721
7773
  }
7722
7774
  return messages;
7723
7775
  } finally {
7724
- fs15.closeSync(fd);
7776
+ fs16.closeSync(fd);
7725
7777
  }
7726
7778
  }
7727
7779
  function getOffset(transcriptPath) {
@@ -9461,8 +9513,8 @@ function persistOrDegrade(store, action, label, onFailure) {
9461
9513
  }
9462
9514
 
9463
9515
  // src/infra/sessions/store.ts
9464
- import fs16 from "fs";
9465
- import path13 from "path";
9516
+ import fs17 from "fs";
9517
+ import path14 from "path";
9466
9518
  import Database from "better-sqlite3";
9467
9519
 
9468
9520
  // src/infra/sessions/schema.ts
@@ -9731,7 +9783,7 @@ function rowToAthenaSession(row, adapterSessionIds, firstPrompt) {
9731
9783
  // src/infra/sessions/store.ts
9732
9784
  function createSessionStore(opts) {
9733
9785
  if (opts.dbPath !== ":memory:") {
9734
- fs16.mkdirSync(path13.dirname(opts.dbPath), { recursive: true });
9786
+ fs17.mkdirSync(path14.dirname(opts.dbPath), { recursive: true });
9735
9787
  }
9736
9788
  const db = new Database(opts.dbPath);
9737
9789
  initSchema(db);
@@ -10006,18 +10058,18 @@ function createSessionStore(opts) {
10006
10058
  }
10007
10059
 
10008
10060
  // src/infra/sessions/registry.ts
10009
- import fs17 from "fs";
10010
- import os9 from "os";
10011
- import path14 from "path";
10061
+ import fs18 from "fs";
10062
+ import os10 from "os";
10063
+ import path15 from "path";
10012
10064
  import Database2 from "better-sqlite3";
10013
10065
  function sessionsDir() {
10014
- return path14.join(os9.homedir(), ".config", "athena", "sessions");
10066
+ return path15.join(os10.homedir(), ".config", "athena", "sessions");
10015
10067
  }
10016
10068
  function sessionDbPath(sessionId) {
10017
- return path14.join(sessionsDir(), sessionId, "session.db");
10069
+ return path15.join(sessionsDir(), sessionId, "session.db");
10018
10070
  }
10019
10071
  function readSessionFromDb(dbPath) {
10020
- if (!fs17.existsSync(dbPath)) return null;
10072
+ if (!fs18.existsSync(dbPath)) return null;
10021
10073
  let db;
10022
10074
  try {
10023
10075
  db = new Database2(dbPath, { readonly: true });
@@ -10050,12 +10102,12 @@ function readSessionFromDb(dbPath) {
10050
10102
  }
10051
10103
  function listSessions(projectDir) {
10052
10104
  const dir = sessionsDir();
10053
- if (!fs17.existsSync(dir)) return [];
10054
- const entries = fs17.readdirSync(dir, { withFileTypes: true });
10105
+ if (!fs18.existsSync(dir)) return [];
10106
+ const entries = fs18.readdirSync(dir, { withFileTypes: true });
10055
10107
  const sessions = [];
10056
10108
  for (const entry of entries) {
10057
10109
  if (!entry.isDirectory()) continue;
10058
- const dbPath = path14.join(dir, entry.name, "session.db");
10110
+ const dbPath = path15.join(dir, entry.name, "session.db");
10059
10111
  const session = readSessionFromDb(dbPath);
10060
10112
  if (session && (session.eventCount ?? 0) > 0) {
10061
10113
  if (!projectDir || session.projectDir === projectDir) {
@@ -10502,9 +10554,9 @@ function defaultLoadToken(tokenPath) {
10502
10554
  }
10503
10555
 
10504
10556
  // src/infra/config/gatewayClient.ts
10505
- import fs18 from "fs";
10506
- import os10 from "os";
10507
- import path15 from "path";
10557
+ import fs19 from "fs";
10558
+ import os11 from "os";
10559
+ import path16 from "path";
10508
10560
 
10509
10561
  // src/shared/gateway-protocol/endpoint.ts
10510
10562
  function parseRuntimeEndpoint(value) {
@@ -10559,17 +10611,17 @@ function isRecord(value) {
10559
10611
 
10560
10612
  // src/infra/config/gatewayClient.ts
10561
10613
  function resolveGatewayClientConfigPath(env = process.env) {
10562
- const home = env["HOME"] ?? os10.homedir();
10563
- return path15.join(home, ".config", "athena", "gateway.json");
10614
+ const home = env["HOME"] ?? os11.homedir();
10615
+ return path16.join(home, ".config", "athena", "gateway.json");
10564
10616
  }
10565
10617
  function readGatewayClientConfig(env = process.env) {
10566
10618
  const configPath = resolveGatewayClientConfigPath(env);
10567
- if (!fs18.existsSync(configPath)) {
10619
+ if (!fs19.existsSync(configPath)) {
10568
10620
  return { mode: "local" };
10569
10621
  }
10570
10622
  let parsed;
10571
10623
  try {
10572
- parsed = JSON.parse(fs18.readFileSync(configPath, "utf-8"));
10624
+ parsed = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
10573
10625
  } catch (err) {
10574
10626
  throw new Error(
10575
10627
  `gateway client config ${configPath} is invalid JSON: ${err instanceof Error ? err.message : String(err)}`
@@ -10586,16 +10638,16 @@ function readGatewayClientConfig(env = process.env) {
10586
10638
  function writeGatewayClientConfig(config, env = process.env) {
10587
10639
  const parsed = parseRuntimeEndpoint(config);
10588
10640
  const configPath = resolveGatewayClientConfigPath(env);
10589
- const dir = path15.dirname(configPath);
10590
- fs18.mkdirSync(dir, { recursive: true, mode: 448 });
10591
- fs18.writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n", {
10641
+ const dir = path16.dirname(configPath);
10642
+ fs19.mkdirSync(dir, { recursive: true, mode: 448 });
10643
+ fs19.writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n", {
10592
10644
  encoding: "utf-8",
10593
10645
  mode: 384
10594
10646
  });
10595
10647
  if (process.platform !== "win32") {
10596
10648
  try {
10597
- fs18.chmodSync(dir, 448);
10598
- fs18.chmodSync(configPath, 384);
10649
+ fs19.chmodSync(dir, 448);
10650
+ fs19.chmodSync(configPath, 384);
10599
10651
  } catch {
10600
10652
  }
10601
10653
  }
@@ -10731,6 +10783,19 @@ var SessionBridge = class {
10731
10783
  );
10732
10784
  return res;
10733
10785
  }
10786
+ async sendRunEvent(event) {
10787
+ const client = await this.requireConnectedClient();
10788
+ const req = {
10789
+ runtimeId: this.opts.runtimeId,
10790
+ location: event.location,
10791
+ runId: event.runId,
10792
+ seq: event.seq,
10793
+ ts: event.ts,
10794
+ kind: event.kind,
10795
+ ...event.payload !== void 0 ? { payload: event.payload } : {}
10796
+ };
10797
+ return client.request("session.run.event", req);
10798
+ }
10734
10799
  async relayPermission(req) {
10735
10800
  writeGatewayTrace(
10736
10801
  `sessionBridge relayPermission tool=${req.toolName} runtimeId=${this.opts.runtimeId}`
@@ -10743,7 +10808,7 @@ var SessionBridge = class {
10743
10808
  inputPreview: req.inputPreview,
10744
10809
  ...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {}
10745
10810
  };
10746
- const overallTimeoutMs = req.ttlMs ?? RELAY_REQUEST_TIMEOUT_MS;
10811
+ const overallTimeoutMs = req.ttlMs === null ? null : req.ttlMs ?? RELAY_REQUEST_TIMEOUT_MS;
10747
10812
  return this.requestWithReconnect("relay.permission.request", payload, overallTimeoutMs);
10748
10813
  }
10749
10814
  async relayQuestion(req) {
@@ -10754,7 +10819,7 @@ var SessionBridge = class {
10754
10819
  questions: req.questions,
10755
10820
  ...req.ttlMs !== void 0 ? { ttlMs: req.ttlMs } : {}
10756
10821
  };
10757
- const overallTimeoutMs = req.ttlMs ?? RELAY_REQUEST_TIMEOUT_MS;
10822
+ const overallTimeoutMs = req.ttlMs === null ? null : req.ttlMs ?? RELAY_REQUEST_TIMEOUT_MS;
10758
10823
  return this.requestWithReconnect("relay.question.request", payload, overallTimeoutMs);
10759
10824
  }
10760
10825
  /**
@@ -10766,19 +10831,21 @@ var SessionBridge = class {
10766
10831
  * caller-provided overall timeout so a long outage still surfaces.
10767
10832
  */
10768
10833
  async requestWithReconnect(kind, payload, overallTimeoutMs) {
10769
- const deadline = Date.now() + overallTimeoutMs;
10834
+ const deadline = overallTimeoutMs === null ? null : Date.now() + overallTimeoutMs;
10770
10835
  while (true) {
10771
10836
  const client = await this.requireConnectedClient();
10772
- const remaining = deadline - Date.now();
10773
- if (remaining <= 0) {
10837
+ const remaining = deadline === null ? null : deadline - Date.now();
10838
+ if (remaining !== null && remaining <= 0) {
10774
10839
  throw new GatewayProtocolError(`request ${kind} timed out`);
10775
10840
  }
10776
10841
  try {
10777
10842
  return await client.request(kind, payload, {
10778
- timeoutMs: remaining
10843
+ // When unbounded, pass the largest safe setTimeout value so the
10844
+ // inner request never self-cancels in any human time scale.
10845
+ timeoutMs: remaining ?? 2147483647
10779
10846
  });
10780
10847
  } catch (err) {
10781
- if (err instanceof GatewayProtocolError && err.code === "connection_closed" && !this.stopped && Date.now() < deadline) {
10848
+ if (err instanceof GatewayProtocolError && err.code === "connection_closed" && !this.stopped && (deadline === null || Date.now() < deadline)) {
10782
10849
  writeGatewayTrace(
10783
10850
  `sessionBridge relay retry kind=${kind} runtimeId=${this.opts.runtimeId}`
10784
10851
  );
@@ -10913,7 +10980,8 @@ var SessionBridge = class {
10913
10980
  const req = {
10914
10981
  runtimeId: this.opts.runtimeId,
10915
10982
  defaultAgentId: this.opts.defaultAgentId,
10916
- pid: this.opts.pid ?? process.pid
10983
+ pid: this.opts.pid ?? process.pid,
10984
+ ...this.opts.attachmentId !== void 0 ? { attachmentId: this.opts.attachmentId } : {}
10917
10985
  };
10918
10986
  try {
10919
10987
  return await client.request("session.register", req);
@@ -10967,7 +11035,8 @@ async function startSessionBridge(opts) {
10967
11035
  if (signal?.aborted) return null;
10968
11036
  const bridge = new SessionBridge({
10969
11037
  runtimeId: opts.runtimeId,
10970
- defaultAgentId: opts.defaultAgentId
11038
+ defaultAgentId: opts.defaultAgentId,
11039
+ ...opts.attachmentId !== void 0 ? { attachmentId: opts.attachmentId } : {}
10971
11040
  });
10972
11041
  writeGatewayTrace(`startSessionBridge attempt runtimeId=${opts.runtimeId}`);
10973
11042
  try {
@@ -11007,6 +11076,194 @@ function sleepOrAbort(ms, signal) {
11007
11076
  });
11008
11077
  }
11009
11078
 
11079
+ // src/app/dashboard/dashboardFeedPublisher.ts
11080
+ import Database3 from "better-sqlite3";
11081
+ function dashboardFeedOutboxPath() {
11082
+ return `${ensureDaemonStateDir().dir}/dashboard-feed-outbox.db`;
11083
+ }
11084
+ function initOutboxSchema(db) {
11085
+ db.exec(`
11086
+ PRAGMA journal_mode = WAL;
11087
+
11088
+ CREATE TABLE IF NOT EXISTS dashboard_feed_outbox (
11089
+ delivery_seq INTEGER PRIMARY KEY AUTOINCREMENT,
11090
+ instance_id TEXT NOT NULL,
11091
+ athena_session_id TEXT NOT NULL,
11092
+ run_id TEXT NOT NULL,
11093
+ origin TEXT NOT NULL CHECK(origin IN ('local', 'dashboard')),
11094
+ event_id TEXT NOT NULL,
11095
+ emitted_at INTEGER NOT NULL,
11096
+ feed_event_json TEXT NOT NULL,
11097
+ attempt INTEGER NOT NULL DEFAULT 0,
11098
+ next_attempt_at INTEGER NOT NULL,
11099
+ last_error TEXT,
11100
+ acked_at INTEGER,
11101
+ UNIQUE(instance_id, event_id)
11102
+ );
11103
+
11104
+ CREATE INDEX IF NOT EXISTS idx_dashboard_feed_outbox_pending
11105
+ ON dashboard_feed_outbox(acked_at, next_attempt_at, delivery_seq);
11106
+ `);
11107
+ }
11108
+ function makeEnvelope(input) {
11109
+ return {
11110
+ instanceId: input.instanceId,
11111
+ athenaSessionId: input.athenaSessionId,
11112
+ runId: input.feedEvent.run_id,
11113
+ origin: input.origin,
11114
+ eventId: `${input.athenaSessionId}:${input.feedEvent.event_id}`,
11115
+ feedSeq: input.deliverySeq,
11116
+ emittedAt: input.emittedAt,
11117
+ feedEvent: input.feedEvent
11118
+ };
11119
+ }
11120
+ function createDashboardFeedOutbox(options = {}) {
11121
+ const db = new Database3(options.dbPath ?? dashboardFeedOutboxPath());
11122
+ initOutboxSchema(db);
11123
+ const insert = db.prepare(`
11124
+ INSERT OR IGNORE INTO dashboard_feed_outbox (
11125
+ instance_id,
11126
+ athena_session_id,
11127
+ run_id,
11128
+ origin,
11129
+ event_id,
11130
+ emitted_at,
11131
+ feed_event_json,
11132
+ next_attempt_at
11133
+ )
11134
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
11135
+ `);
11136
+ const selectPending = db.prepare(`
11137
+ SELECT
11138
+ delivery_seq,
11139
+ instance_id,
11140
+ athena_session_id,
11141
+ run_id,
11142
+ origin,
11143
+ event_id,
11144
+ emitted_at,
11145
+ feed_event_json,
11146
+ attempt,
11147
+ next_attempt_at,
11148
+ last_error
11149
+ FROM dashboard_feed_outbox
11150
+ WHERE acked_at IS NULL AND next_attempt_at <= ?
11151
+ ORDER BY delivery_seq ASC
11152
+ LIMIT ?
11153
+ `);
11154
+ const updateAttempt = db.prepare(`
11155
+ UPDATE dashboard_feed_outbox
11156
+ SET attempt = attempt + 1,
11157
+ next_attempt_at = ?,
11158
+ last_error = ?
11159
+ WHERE delivery_seq = ?
11160
+ `);
11161
+ const ackBySeq = db.prepare(`
11162
+ UPDATE dashboard_feed_outbox
11163
+ SET acked_at = ?
11164
+ WHERE delivery_seq = ?
11165
+ `);
11166
+ const ackByEventId = db.prepare(`
11167
+ UPDATE dashboard_feed_outbox
11168
+ SET acked_at = ?
11169
+ WHERE event_id = ?
11170
+ `);
11171
+ const enqueueTx = db.transaction(
11172
+ (input) => {
11173
+ for (const feedEvent of input.feedEvents) {
11174
+ const eventId = `${input.athenaSessionId}:${feedEvent.event_id}`;
11175
+ insert.run(
11176
+ input.instanceId,
11177
+ input.athenaSessionId,
11178
+ feedEvent.run_id,
11179
+ input.origin,
11180
+ eventId,
11181
+ input.emittedAt,
11182
+ JSON.stringify(feedEvent),
11183
+ input.emittedAt
11184
+ );
11185
+ }
11186
+ }
11187
+ );
11188
+ return {
11189
+ enqueue(input) {
11190
+ if (input.feedEvents.length === 0) return;
11191
+ enqueueTx(input);
11192
+ },
11193
+ pendingBatch(input) {
11194
+ const rows = selectPending.all(input.now, input.limit);
11195
+ return rows.map((row) => {
11196
+ const feedEvent = JSON.parse(row.feed_event_json);
11197
+ return {
11198
+ deliverySeq: row.delivery_seq,
11199
+ envelope: makeEnvelope({
11200
+ instanceId: row.instance_id,
11201
+ athenaSessionId: row.athena_session_id,
11202
+ origin: row.origin,
11203
+ feedEvent,
11204
+ deliverySeq: row.delivery_seq,
11205
+ emittedAt: row.emitted_at
11206
+ }),
11207
+ attempt: row.attempt,
11208
+ nextAttemptAt: row.next_attempt_at,
11209
+ ...row.last_error ? { lastError: row.last_error } : {}
11210
+ };
11211
+ });
11212
+ },
11213
+ markAttempted(input) {
11214
+ updateAttempt.run(
11215
+ input.nextAttemptAt,
11216
+ input.lastError ?? null,
11217
+ input.deliverySeq
11218
+ );
11219
+ },
11220
+ markAcked(input) {
11221
+ const now = Date.now();
11222
+ if (typeof input.deliverySeq === "number") {
11223
+ ackBySeq.run(now, input.deliverySeq);
11224
+ }
11225
+ if (input.eventId) {
11226
+ ackByEventId.run(now, input.eventId);
11227
+ }
11228
+ },
11229
+ close() {
11230
+ db.close();
11231
+ }
11232
+ };
11233
+ }
11234
+ function createDashboardFeedPublisher(options = {}) {
11235
+ const readConfig2 = options.readConfig ?? (() => readDashboardClientConfig());
11236
+ const now = options.now ?? (() => Date.now());
11237
+ const onError = options.onError ?? (() => {
11238
+ });
11239
+ let ownedOutbox = null;
11240
+ function getOutbox() {
11241
+ if (options.outbox) return options.outbox;
11242
+ ownedOutbox ??= createDashboardFeedOutbox();
11243
+ return ownedOutbox;
11244
+ }
11245
+ return {
11246
+ publish(input) {
11247
+ if (input.feedEvents.length === 0) return;
11248
+ try {
11249
+ const config = readConfig2();
11250
+ if (!config) return;
11251
+ getOutbox().enqueue({
11252
+ instanceId: config.instanceId,
11253
+ athenaSessionId: input.athenaSessionId,
11254
+ origin: input.origin,
11255
+ feedEvents: input.feedEvents,
11256
+ emittedAt: now()
11257
+ });
11258
+ } catch (err) {
11259
+ onError(
11260
+ `dashboard feed publish failed: ${err instanceof Error ? err.message : String(err)}`
11261
+ );
11262
+ }
11263
+ }
11264
+ };
11265
+ }
11266
+
11010
11267
  // src/app/exec/finalMessage.ts
11011
11268
  function findLastMappedAgentMessage(feedEvents) {
11012
11269
  for (let i = feedEvents.length - 1; i >= 0; i--) {
@@ -11057,8 +11314,8 @@ function exitCodeFromFailure(failure) {
11057
11314
  }
11058
11315
 
11059
11316
  // src/app/exec/output.ts
11060
- import fs19 from "fs/promises";
11061
- import path16 from "path";
11317
+ import fs20 from "fs/promises";
11318
+ import path17 from "path";
11062
11319
 
11063
11320
  // src/app/exec/jsonl.ts
11064
11321
  function createExecJsonlEvent(type, data, ts = Date.now()) {
@@ -11097,9 +11354,9 @@ function createExecOutputWriter(options) {
11097
11354
  writeLine(options.stdout, message);
11098
11355
  },
11099
11356
  async writeLastMessage(filePath, message) {
11100
- const absPath = path16.resolve(filePath);
11101
- await fs19.mkdir(path16.dirname(absPath), { recursive: true });
11102
- await fs19.writeFile(absPath, message, "utf-8");
11357
+ const absPath = path17.resolve(filePath);
11358
+ await fs20.mkdir(path17.dirname(absPath), { recursive: true });
11359
+ await fs20.writeFile(absPath, message, "utf-8");
11103
11360
  }
11104
11361
  };
11105
11362
  }
@@ -11155,6 +11412,8 @@ async function runExec(options) {
11155
11412
  const runtimeFactory = options.runtimeFactory ?? createRuntime;
11156
11413
  const sessionStoreFactory = options.sessionStoreFactory ?? createSessionStore;
11157
11414
  const athenaSessionId = options.athenaSessionId ?? crypto2.randomUUID();
11415
+ const dashboardFeedPublisher = options.dashboardFeedPublisher ?? createDashboardFeedPublisher();
11416
+ const dashboardOrigin = options.dashboardOrigin ?? "local";
11158
11417
  const output = createExecOutputWriter({
11159
11418
  json,
11160
11419
  verbose,
@@ -11174,7 +11433,7 @@ async function runExec(options) {
11174
11433
  store = sessionStoreFactory({
11175
11434
  sessionId: athenaSessionId,
11176
11435
  projectDir: options.projectDir,
11177
- dbPath: options.ephemeral ? ":memory:" : path17.join(sessionsDir(), athenaSessionId, "session.db")
11436
+ dbPath: options.ephemeral ? ":memory:" : path18.join(sessionsDir(), athenaSessionId, "session.db")
11178
11437
  });
11179
11438
  } catch (error) {
11180
11439
  const message = `Failed to initialize session store: ${error instanceof Error ? error.message : String(error)}`;
@@ -11264,7 +11523,33 @@ async function runExec(options) {
11264
11523
  } : {},
11265
11524
  ...options.signal ? { signal: options.signal } : {}
11266
11525
  };
11526
+ const dashboardDecisionInbox = options.dashboardDecisionInbox;
11527
+ const applyPendingDashboardDecisions = () => {
11528
+ if (!dashboardDecisionInbox) return;
11529
+ const rows = dashboardDecisionInbox.pendingForSession({
11530
+ athenaSessionId,
11531
+ limit: 25
11532
+ });
11533
+ for (const row of rows) {
11534
+ try {
11535
+ runtime.sendDecision(row.requestId, row.decision);
11536
+ dashboardDecisionInbox.markConsumed({ id: row.id });
11537
+ } catch (error) {
11538
+ output.warn(
11539
+ `dashboard decision failed: ${error instanceof Error ? error.message : String(error)}`
11540
+ );
11541
+ }
11542
+ }
11543
+ };
11267
11544
  const linkedAdapterSessions = /* @__PURE__ */ new Set();
11545
+ function publishFeedEvents(feedEvents) {
11546
+ if (feedEvents.length === 0) return;
11547
+ dashboardFeedPublisher.publish({
11548
+ origin: dashboardOrigin,
11549
+ athenaSessionId,
11550
+ feedEvents
11551
+ });
11552
+ }
11268
11553
  const unsubscribeEvent = runtime.onEvent((runtimeEvent) => {
11269
11554
  adapterSessionId = runtimeEvent.sessionId;
11270
11555
  if (runtimeEvent.sessionId && activeRunId && !linkedAdapterSessions.has(runtimeEvent.sessionId)) {
@@ -11299,6 +11584,7 @@ async function runExec(options) {
11299
11584
  mappedFinalMessage = event.data.message;
11300
11585
  }
11301
11586
  }
11587
+ publishFeedEvents(feedEvents);
11302
11588
  });
11303
11589
  const unsubscribeDecision = runtime.onDecision(
11304
11590
  (eventId, decision) => {
@@ -11306,14 +11592,18 @@ async function runExec(options) {
11306
11592
  eventId,
11307
11593
  decision
11308
11594
  });
11309
- ingestRuntimeDecision(eventId, decision, {
11595
+ const feedEvent = ingestRuntimeDecision(eventId, decision, {
11310
11596
  mapper,
11311
11597
  store,
11312
11598
  onPersistFailure: (message) => output.warn(message)
11313
11599
  });
11600
+ if (feedEvent) {
11601
+ publishFeedEvents([feedEvent]);
11602
+ }
11314
11603
  }
11315
11604
  );
11316
11605
  let timeoutTimer;
11606
+ let dashboardDecisionTimer;
11317
11607
  if (typeof options.timeoutMs === "number" && options.timeoutMs > 0) {
11318
11608
  timeoutTimer = setTimeout(() => {
11319
11609
  latch.register({
@@ -11333,6 +11623,14 @@ async function runExec(options) {
11333
11623
  output.emitJsonEvent("runtime.started", {
11334
11624
  status: runtime.getStatus()
11335
11625
  });
11626
+ if (dashboardDecisionInbox) {
11627
+ applyPendingDashboardDecisions();
11628
+ dashboardDecisionTimer = setInterval(
11629
+ applyPendingDashboardDecisions,
11630
+ options.dashboardDecisionPollIntervalMs ?? 1e3
11631
+ );
11632
+ dashboardDecisionTimer.unref();
11633
+ }
11336
11634
  const workflow = options.workflow;
11337
11635
  output.emitJsonEvent("run.started", {
11338
11636
  workflow: workflow?.name ?? null,
@@ -11418,6 +11716,9 @@ async function runExec(options) {
11418
11716
  if (timeoutTimer) {
11419
11717
  clearTimeout(timeoutTimer);
11420
11718
  }
11719
+ if (dashboardDecisionTimer) {
11720
+ clearInterval(dashboardDecisionTimer);
11721
+ }
11421
11722
  await sessionController.kill();
11422
11723
  unsubscribeEvent();
11423
11724
  unsubscribeDecision();
@@ -11480,254 +11781,92 @@ async function runExec(options) {
11480
11781
  return result;
11481
11782
  }
11482
11783
 
11483
- // src/app/dashboard/instanceSocketClient.ts
11784
+ // src/app/bootstrap/harnessOverride.ts
11785
+ function normalizeHarnessOverride(value) {
11786
+ switch (value) {
11787
+ case "claude":
11788
+ case "claude-code":
11789
+ return "claude-code";
11790
+ case "codex":
11791
+ case "openai-codex":
11792
+ return "openai-codex";
11793
+ case "opencode":
11794
+ return "opencode";
11795
+ default:
11796
+ return void 0;
11797
+ }
11798
+ }
11799
+
11800
+ // src/app/dashboard/runStreamClient.ts
11484
11801
  import { WebSocket as WebSocket2 } from "ws";
11802
+ var DEFAULT_RECONNECT_DELAYS_MS = [250, 1e3, 2e3, 5e3, 15e3];
11485
11803
  var DEFAULT_HEARTBEAT_MS = 3e4;
11486
- var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
11487
- function instanceSocketUrl(dashboardUrl, instanceId) {
11488
- const url = new URL(dashboardUrl);
11489
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
11490
- url.pathname = `/api/instances/${encodeURIComponent(instanceId)}/socket`;
11491
- url.search = "";
11492
- url.hash = "";
11493
- return url.toString();
11494
- }
11495
- function createInstanceSocketClient(opts) {
11496
- const heartbeatMs = opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
11497
- const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
11804
+ var DEFAULT_WATCHDOG_MS = 9e4;
11805
+ var DEFAULT_MAX_QUEUE_SIZE = 5e3;
11806
+ function createRunStreamClient(opts) {
11498
11807
  const log = opts.log ?? (() => {
11499
11808
  });
11500
11809
  const now = opts.now ?? (() => Date.now());
11501
- const makeWebSocket = opts.makeWebSocket ?? ((url, accessToken) => new WebSocket2(url, [accessToken]));
11502
- const frameHandlers = /* @__PURE__ */ new Set();
11503
- const closeHandlers = /* @__PURE__ */ new Set();
11810
+ const reconnectDelays = opts.reconnectDelaysMs ?? DEFAULT_RECONNECT_DELAYS_MS;
11811
+ const heartbeatMs = opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
11812
+ const watchdogMs = opts.watchdogTimeoutMs ?? DEFAULT_WATCHDOG_MS;
11813
+ const maxQueueSize = opts.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
11814
+ const makeWebSocket = opts.makeWebSocket ?? ((url) => new WebSocket2(url));
11815
+ const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
11816
+ const clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
11817
+ let nextSeq = 1;
11818
+ const queue = [];
11504
11819
  let ws = null;
11505
- let heartbeat = null;
11506
- let droppedSinceClose = 0;
11507
- function send(frame) {
11508
- if (!ws || ws.readyState !== ws.OPEN) {
11509
- droppedSinceClose += 1;
11510
- if (droppedSinceClose === 1) {
11511
- log(
11512
- "warn",
11513
- `instance socket dropped frame (socket not open): type=${frame.type}`
11514
- );
11515
- }
11516
- return;
11517
- }
11518
- droppedSinceClose = 0;
11519
- try {
11520
- ws.send(JSON.stringify(frame));
11521
- } catch (err) {
11522
- log(
11523
- "warn",
11524
- `instance socket send failed: ${err instanceof Error ? err.message : String(err)}`
11525
- );
11820
+ let connectAttempt = 0;
11821
+ let stopped = false;
11822
+ let serverTerminated = false;
11823
+ let heartbeatTimer = null;
11824
+ let watchdogTimer = null;
11825
+ let reconnectTimer = null;
11826
+ let resumeResolved = false;
11827
+ let firstConnect = null;
11828
+ const terminationWaiters = [];
11829
+ function trimQueueUpTo(lastAckedSeq) {
11830
+ while (queue.length > 0 && queue[0].seq <= lastAckedSeq) {
11831
+ queue.shift();
11526
11832
  }
11527
11833
  }
11528
- function startHeartbeat() {
11529
- stopHeartbeat();
11530
- const interval = setInterval(() => {
11531
- send({ type: "ping", ts: now() });
11532
- }, heartbeatMs);
11533
- interval.unref();
11534
- heartbeat = interval;
11535
- }
11536
- function stopHeartbeat() {
11537
- if (heartbeat) {
11538
- clearInterval(heartbeat);
11539
- heartbeat = null;
11540
- }
11541
- }
11542
- function emitClose(reason) {
11543
- stopHeartbeat();
11544
- for (const handler of [...closeHandlers]) {
11545
- try {
11546
- handler(reason);
11547
- } catch {
11548
- }
11834
+ function trimQueueUpToExclusive(expectedSeq) {
11835
+ while (queue.length > 0 && queue[0].seq < expectedSeq) {
11836
+ queue.shift();
11549
11837
  }
11550
11838
  }
11551
- function handleFrame(parsed) {
11552
- if (parsed.type === "job_assignment") {
11553
- send({ type: "assignment_accepted", runId: parsed.runId });
11554
- log("info", `instance socket: assignment accepted runId=${parsed.runId}`);
11839
+ function clearTimers() {
11840
+ if (heartbeatTimer) {
11841
+ clearTimer(heartbeatTimer);
11842
+ heartbeatTimer = null;
11843
+ }
11844
+ if (watchdogTimer) {
11845
+ clearTimer(watchdogTimer);
11846
+ watchdogTimer = null;
11847
+ }
11848
+ if (reconnectTimer) {
11849
+ clearTimer(reconnectTimer);
11850
+ reconnectTimer = null;
11555
11851
  }
11556
- for (const handler of [...frameHandlers]) {
11852
+ }
11853
+ function startHeartbeat() {
11854
+ if (heartbeatMs <= 0) return;
11855
+ const tick = () => {
11856
+ if (!ws || ws.readyState !== ws.OPEN) return;
11557
11857
  try {
11558
- handler(parsed);
11858
+ ws.send(JSON.stringify({ type: "ping", ts: now() }));
11559
11859
  } catch (err) {
11560
11860
  log(
11561
11861
  "warn",
11562
- `instance socket frame handler threw: ${err instanceof Error ? err.message : String(err)}`
11862
+ `run-stream ping failed: ${err instanceof Error ? err.message : String(err)}`
11563
11863
  );
11564
11864
  }
11565
- }
11566
- }
11567
- async function connect2() {
11568
- if (ws) throw new Error("instance socket already connected");
11569
- const url = instanceSocketUrl(opts.dashboardUrl, opts.instanceId);
11570
- const next = makeWebSocket(url, opts.accessToken);
11571
- try {
11572
- await new Promise((resolve, reject) => {
11573
- let settled = false;
11574
- const cleanup = () => {
11575
- next.off("open", onOpen);
11576
- next.off("error", onError);
11577
- clearTimeout(timer);
11578
- };
11579
- const onOpen = () => {
11580
- if (settled) return;
11581
- settled = true;
11582
- cleanup();
11583
- resolve();
11584
- };
11585
- const onError = (err) => {
11586
- if (settled) return;
11587
- settled = true;
11588
- cleanup();
11589
- reject(new Error(`instance socket connect failed: ${err.message}`));
11590
- };
11591
- const timer = setTimeout(() => {
11592
- if (settled) return;
11593
- settled = true;
11594
- cleanup();
11595
- reject(
11596
- new Error(
11597
- `instance socket connect failed: timed out after ${connectTimeoutMs}ms`
11598
- )
11599
- );
11600
- }, connectTimeoutMs);
11601
- next.once("open", onOpen);
11602
- next.once("error", onError);
11603
- });
11604
- } catch (err) {
11605
- next.on("error", () => {
11606
- });
11607
- try {
11608
- next.terminate();
11609
- } catch {
11610
- }
11611
- throw err;
11612
- }
11613
- ws = next;
11614
- startHeartbeat();
11615
- next.on("message", (data) => {
11616
- let parsed;
11617
- try {
11618
- parsed = JSON.parse(String(data));
11619
- } catch (err) {
11620
- log(
11621
- "warn",
11622
- `instance socket frame parse failed: ${err instanceof Error ? err.message : String(err)}`
11623
- );
11624
- return;
11625
- }
11626
- handleFrame(parsed);
11627
- });
11628
- next.on("close", (_code, reasonBuf) => {
11629
- if (next !== ws) return;
11630
- ws = null;
11631
- const reason = reasonBuf.toString() || "closed";
11632
- emitClose(reason);
11633
- });
11634
- next.on("error", (err) => {
11635
- log("warn", `instance socket error: ${err.message}`);
11636
- });
11637
- }
11638
- function close(reason) {
11639
- stopHeartbeat();
11640
- if (ws) {
11641
- try {
11642
- ws.close(1e3, reason ?? "client closed");
11643
- } catch {
11644
- ws.terminate();
11645
- }
11646
- }
11647
- ws = null;
11648
- }
11649
- function onFrame(handler) {
11650
- frameHandlers.add(handler);
11651
- }
11652
- function onClose(handler) {
11653
- closeHandlers.add(handler);
11654
- }
11655
- function sendRunEvent(event) {
11656
- send({ type: "run_event", ...event });
11657
- }
11658
- return { connect: connect2, close, onFrame, onClose, sendRunEvent };
11659
- }
11660
-
11661
- // src/app/dashboard/runStreamClient.ts
11662
- import { WebSocket as WebSocket3 } from "ws";
11663
- var DEFAULT_RECONNECT_DELAYS_MS = [250, 1e3, 2e3, 5e3, 15e3];
11664
- var DEFAULT_HEARTBEAT_MS2 = 3e4;
11665
- var DEFAULT_WATCHDOG_MS = 9e4;
11666
- var DEFAULT_MAX_QUEUE_SIZE = 5e3;
11667
- function createRunStreamClient(opts) {
11668
- const log = opts.log ?? (() => {
11669
- });
11670
- const now = opts.now ?? (() => Date.now());
11671
- const reconnectDelays = opts.reconnectDelaysMs ?? DEFAULT_RECONNECT_DELAYS_MS;
11672
- const heartbeatMs = opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS2;
11673
- const watchdogMs = opts.watchdogTimeoutMs ?? DEFAULT_WATCHDOG_MS;
11674
- const maxQueueSize = opts.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
11675
- const makeWebSocket = opts.makeWebSocket ?? ((url) => new WebSocket3(url));
11676
- const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
11677
- const clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
11678
- let nextSeq = 1;
11679
- const queue = [];
11680
- let ws = null;
11681
- let connectAttempt = 0;
11682
- let stopped = false;
11683
- let serverTerminated = false;
11684
- let heartbeatTimer = null;
11685
- let watchdogTimer = null;
11686
- let reconnectTimer = null;
11687
- let resumeResolved = false;
11688
- let firstConnect = null;
11689
- const terminationWaiters = [];
11690
- function trimQueueUpTo(lastAckedSeq) {
11691
- while (queue.length > 0 && queue[0].seq <= lastAckedSeq) {
11692
- queue.shift();
11693
- }
11694
- }
11695
- function trimQueueUpToExclusive(expectedSeq) {
11696
- while (queue.length > 0 && queue[0].seq < expectedSeq) {
11697
- queue.shift();
11698
- }
11699
- }
11700
- function clearTimers() {
11701
- if (heartbeatTimer) {
11702
- clearTimer(heartbeatTimer);
11703
- heartbeatTimer = null;
11704
- }
11705
- if (watchdogTimer) {
11706
- clearTimer(watchdogTimer);
11707
- watchdogTimer = null;
11708
- }
11709
- if (reconnectTimer) {
11710
- clearTimer(reconnectTimer);
11711
- reconnectTimer = null;
11712
- }
11713
- }
11714
- function startHeartbeat() {
11715
- if (heartbeatMs <= 0) return;
11716
- const tick = () => {
11717
- if (!ws || ws.readyState !== ws.OPEN) return;
11718
- try {
11719
- ws.send(JSON.stringify({ type: "ping", ts: now() }));
11720
- } catch (err) {
11721
- log(
11722
- "warn",
11723
- `run-stream ping failed: ${err instanceof Error ? err.message : String(err)}`
11724
- );
11725
- }
11726
- heartbeatTimer = setTimer(tick, heartbeatMs);
11727
- heartbeatTimer.unref?.();
11728
- };
11729
- heartbeatTimer = setTimer(tick, heartbeatMs);
11730
- heartbeatTimer.unref?.();
11865
+ heartbeatTimer = setTimer(tick, heartbeatMs);
11866
+ heartbeatTimer.unref();
11867
+ };
11868
+ heartbeatTimer = setTimer(tick, heartbeatMs);
11869
+ heartbeatTimer.unref();
11731
11870
  }
11732
11871
  function bumpWatchdog() {
11733
11872
  if (watchdogMs <= 0) return;
@@ -11742,7 +11881,7 @@ function createRunStreamClient(opts) {
11742
11881
  } catch {
11743
11882
  }
11744
11883
  }, watchdogMs);
11745
- watchdogTimer.unref?.();
11884
+ watchdogTimer.unref();
11746
11885
  }
11747
11886
  function flushQueue() {
11748
11887
  if (!ws || ws.readyState !== ws.OPEN || !resumeResolved) return;
@@ -11829,7 +11968,7 @@ function createRunStreamClient(opts) {
11829
11968
  reconnectTimer = null;
11830
11969
  void openSocket();
11831
11970
  }, delayMs);
11832
- reconnectTimer.unref?.();
11971
+ reconnectTimer.unref();
11833
11972
  }
11834
11973
  async function openSocket() {
11835
11974
  if (stopped || serverTerminated) return;
@@ -11855,7 +11994,7 @@ function createRunStreamClient(opts) {
11855
11994
  if (next !== ws) return;
11856
11995
  ws = null;
11857
11996
  clearTimers();
11858
- const reason = reasonBuf?.toString?.() || "closed";
11997
+ const reason = reasonBuf.toString() || "closed";
11859
11998
  if (code === 1e3 && reason === "run_terminated") {
11860
11999
  notifyTerminated();
11861
12000
  return;
@@ -11940,6 +12079,7 @@ function createRunStreamClient(opts) {
11940
12079
  }
11941
12080
 
11942
12081
  // src/app/dashboard/remoteRunExecutor.ts
12082
+ var DEFAULT_MARKETPLACE_SLUG = "lespaceman/athena-workflow-marketplace";
11943
12083
  function parseRunSpec(value) {
11944
12084
  if (typeof value !== "object" || value === null) return null;
11945
12085
  const obj = value;
@@ -11949,11 +12089,19 @@ function parseRunSpec(value) {
11949
12089
  const workflow = obj["workflow"];
11950
12090
  const callbackWsUrl = obj["callbackWsUrl"];
11951
12091
  const callbackToken = obj["callbackToken"];
12092
+ const workflowObj = typeof workflow === "object" && workflow !== null ? workflow : null;
11952
12093
  return {
11953
12094
  prompt,
12095
+ athenaSessionId: typeof obj["athenaSessionId"] === "string" && obj["athenaSessionId"].length > 0 ? obj["athenaSessionId"] : void 0,
12096
+ adapterResumeSessionId: typeof obj["adapterResumeSessionId"] === "string" && obj["adapterResumeSessionId"].length > 0 ? obj["adapterResumeSessionId"] : void 0,
11954
12097
  sessionId: typeof obj["sessionId"] === "string" && obj["sessionId"].length > 0 ? obj["sessionId"] : void 0,
11955
12098
  projectDir: typeof obj["projectDir"] === "string" && obj["projectDir"].length > 0 ? obj["projectDir"] : void 0,
11956
- workflow: typeof workflow === "object" && workflow !== null && typeof workflow["ref"] === "string" ? { ref: workflow["ref"] } : void 0,
12099
+ workflow: workflowObj && typeof workflowObj["ref"] === "string" ? {
12100
+ ref: workflowObj["ref"],
12101
+ ...typeof workflowObj["source"] === "string" ? { source: workflowObj["source"] } : {},
12102
+ ...typeof workflowObj["version"] === "string" ? { version: workflowObj["version"] } : {}
12103
+ } : void 0,
12104
+ harness: normalizeHarnessOverride(obj["harness"]),
11957
12105
  env: typeof env === "object" && env !== null ? Object.fromEntries(
11958
12106
  Object.entries(env).filter(
11959
12107
  (entry) => typeof entry[1] === "string"
@@ -11969,6 +12117,47 @@ function workflowNameFromRef(ref) {
11969
12117
  const [name] = ref.split("@", 1);
11970
12118
  return name && name.length > 0 ? name : void 0;
11971
12119
  }
12120
+ function isMissingWorkflowError(err, workflowName) {
12121
+ return err instanceof Error && err.message.includes(`Workflow "${workflowName}" not found`);
12122
+ }
12123
+ function configuredWorkflowSources(readGlobalConfigFn) {
12124
+ const sources = readGlobalConfigFn().workflowMarketplaceSources;
12125
+ return sources && sources.length > 0 ? sources : [DEFAULT_MARKETPLACE_SLUG];
12126
+ }
12127
+ function workflowInstallRef(spec) {
12128
+ const ref = spec.workflow?.ref;
12129
+ if (!ref) return void 0;
12130
+ const version = spec.workflow?.version;
12131
+ if (version && !ref.includes("@")) {
12132
+ return `${ref}@${version}`;
12133
+ }
12134
+ return ref;
12135
+ }
12136
+ function workflowInstallSources(spec, readGlobalConfigFn) {
12137
+ const source = spec.workflow?.source?.trim();
12138
+ if (source && source !== "marketplace") {
12139
+ return [source];
12140
+ }
12141
+ return configuredWorkflowSources(readGlobalConfigFn);
12142
+ }
12143
+ function ensureRemoteWorkflowInstalled(input) {
12144
+ const ref = workflowInstallRef(input.spec);
12145
+ const workflowName = workflowNameFromRef(ref);
12146
+ if (!workflowName) return void 0;
12147
+ try {
12148
+ input.resolveWorkflowFn(workflowName);
12149
+ return workflowName;
12150
+ } catch (err) {
12151
+ if (!isMissingWorkflowError(err, workflowName)) {
12152
+ throw err;
12153
+ }
12154
+ }
12155
+ const resolved = input.resolveWorkflowInstallFn(
12156
+ ref,
12157
+ workflowInstallSources(input.spec, input.readGlobalConfigFn)
12158
+ );
12159
+ return input.installWorkflowFromSourceFn(resolved);
12160
+ }
11972
12161
  function eventKind(event) {
11973
12162
  if (event.type === "exec.completed") {
11974
12163
  const data = event.data;
@@ -11988,22 +12177,18 @@ function eventPayload(event) {
11988
12177
  }
11989
12178
  return event.data ?? null;
11990
12179
  }
11991
- function withEnv(env, fn) {
11992
- if (!env || Object.keys(env).length === 0) return fn();
11993
- const previous = /* @__PURE__ */ new Map();
11994
- for (const [key, value] of Object.entries(env)) {
11995
- previous.set(key, process.env[key]);
11996
- process.env[key] = value;
11997
- }
11998
- return fn().finally(() => {
11999
- for (const [key, value] of previous) {
12000
- if (value === void 0) {
12001
- delete process.env[key];
12002
- } else {
12003
- process.env[key] = value;
12004
- }
12005
- }
12006
- });
12180
+ function mergeRunSpecEnvIntoWorkflow(workflow, env) {
12181
+ if (!env || Object.keys(env).length === 0) return workflow;
12182
+ const mergedEnv = { ...workflow?.env ?? {}, ...env };
12183
+ if (workflow) {
12184
+ return { ...workflow, env: mergedEnv };
12185
+ }
12186
+ return {
12187
+ name: "dashboard-remote",
12188
+ plugins: [],
12189
+ promptTemplate: "{input}",
12190
+ env: mergedEnv
12191
+ };
12007
12192
  }
12008
12193
  async function executeRemoteAssignment({
12009
12194
  frame,
@@ -12012,14 +12197,21 @@ async function executeRemoteAssignment({
12012
12197
  log = () => {
12013
12198
  },
12014
12199
  runExecFn = runExec,
12200
+ decisionInbox,
12015
12201
  bootstrapRuntimeConfigFn = bootstrapRuntimeConfig,
12016
12202
  now = Date.now,
12017
12203
  abortSignal,
12018
12204
  createRunStreamClientFn = createRunStreamClient,
12205
+ resolveWorkflowFn = resolveWorkflow,
12206
+ resolveWorkflowInstallFn = resolveWorkflowInstall,
12207
+ installWorkflowFromSourceFn = installWorkflowFromSource,
12208
+ readGlobalConfigFn = readGlobalConfig,
12019
12209
  runStreamConnectTimeoutMs = 5e3
12020
12210
  }) {
12021
- let lastTerminalFailureMessage = null;
12022
- let deferredFailedCompletion = null;
12211
+ const lastTerminalFailureMessage = { current: null };
12212
+ const deferredFailedCompletion = {
12213
+ current: null
12214
+ };
12023
12215
  const spec = parseRunSpec(frame.runSpec);
12024
12216
  let runStream = null;
12025
12217
  if (spec?.callbackWsUrl && spec.callbackToken) {
@@ -12031,7 +12223,7 @@ async function executeRemoteAssignment({
12031
12223
  });
12032
12224
  const timeoutPromise = new Promise((resolve) => {
12033
12225
  const t = setTimeout(() => resolve("timeout"), runStreamConnectTimeoutMs);
12034
- t.unref?.();
12226
+ t.unref();
12035
12227
  });
12036
12228
  try {
12037
12229
  const result = await Promise.race([
@@ -12057,7 +12249,7 @@ async function executeRemoteAssignment({
12057
12249
  let legacySeq = 0;
12058
12250
  const send = (kind, payload, ts = now()) => {
12059
12251
  if (kind === "error" && typeof payload === "object" && payload !== null && typeof payload.message === "string") {
12060
- lastTerminalFailureMessage = payload.message;
12252
+ lastTerminalFailureMessage.current = payload.message;
12061
12253
  }
12062
12254
  if (runStream) {
12063
12255
  runStream.sendEvent({ ts, kind, payload });
@@ -12081,11 +12273,19 @@ async function executeRemoteAssignment({
12081
12273
  const projectDir = spec.projectDir ?? fallbackProjectDir;
12082
12274
  let runtimeConfig;
12083
12275
  try {
12276
+ const workflowOverride = ensureRemoteWorkflowInstalled({
12277
+ spec,
12278
+ resolveWorkflowFn,
12279
+ resolveWorkflowInstallFn,
12280
+ installWorkflowFromSourceFn,
12281
+ readGlobalConfigFn
12282
+ });
12084
12283
  runtimeConfig = bootstrapRuntimeConfigFn({
12085
12284
  projectDir,
12086
12285
  showSetup: false,
12087
12286
  isolationPreset: "minimal",
12088
- workflowOverride: workflowNameFromRef(spec.workflow?.ref)
12287
+ harnessOverride: spec.harness,
12288
+ workflowOverride
12089
12289
  });
12090
12290
  } catch (err) {
12091
12291
  send("error", {
@@ -12109,7 +12309,7 @@ async function executeRemoteAssignment({
12109
12309
  const event = JSON.parse(line);
12110
12310
  const data = event.data;
12111
12311
  if (event.type === "exec.completed" && data?.success === false) {
12112
- deferredFailedCompletion = event;
12312
+ deferredFailedCompletion.current = event;
12113
12313
  continue;
12114
12314
  }
12115
12315
  send(eventKind(event), eventPayload(event), now());
@@ -12134,45 +12334,37 @@ async function executeRemoteAssignment({
12134
12334
  }
12135
12335
  };
12136
12336
  try {
12137
- await withEnv(spec.env, async () => {
12138
- const result = await runExecFn({
12139
- prompt: spec.prompt,
12140
- projectDir,
12141
- harness: runtimeConfig.harness,
12142
- athenaSessionId: spec.sessionId ?? `athena-${frame.runId}`,
12143
- isolationConfig: runtimeConfig.isolationConfig,
12144
- pluginMcpConfig: runtimeConfig.pluginMcpConfig,
12145
- workflow: runtimeConfig.workflow,
12146
- workflowPlan: runtimeConfig.workflowPlan,
12147
- json: true,
12148
- verbose: false,
12149
- ephemeral: false,
12150
- timeoutMs: spec.timeoutSec ? spec.timeoutSec * 1e3 : void 0,
12151
- signal: abortSignal,
12152
- stdout,
12153
- stderr
12154
- });
12155
- if (deferredFailedCompletion) {
12156
- const data = typeof deferredFailedCompletion.data === "object" && deferredFailedCompletion.data !== null ? deferredFailedCompletion.data : {};
12157
- send(
12158
- "error",
12159
- {
12160
- ...data,
12161
- success: result.success,
12162
- exitCode: result.exitCode,
12163
- athenaSessionId: result.athenaSessionId,
12164
- adapterSessionId: result.adapterSessionId,
12165
- finalMessage: result.finalMessage,
12166
- tokens: result.tokens,
12167
- durationMs: result.durationMs,
12168
- message: result.failure?.message ?? eventPayload(deferredFailedCompletion).message ?? "remote execution failed"
12169
- },
12170
- typeof deferredFailedCompletion.ts === "number" ? deferredFailedCompletion.ts : now()
12171
- );
12172
- return;
12173
- }
12174
- if (result.failure && result.failure.message !== lastTerminalFailureMessage) {
12175
- send("error", {
12337
+ const workflow = mergeRunSpecEnvIntoWorkflow(
12338
+ runtimeConfig.workflow,
12339
+ spec.env
12340
+ );
12341
+ const result = await runExecFn({
12342
+ prompt: spec.prompt,
12343
+ projectDir,
12344
+ harness: runtimeConfig.harness,
12345
+ athenaSessionId: spec.athenaSessionId ?? spec.sessionId ?? `athena-${frame.runId}`,
12346
+ adapterResumeSessionId: spec.adapterResumeSessionId,
12347
+ isolationConfig: runtimeConfig.isolationConfig,
12348
+ pluginMcpConfig: runtimeConfig.pluginMcpConfig,
12349
+ workflow,
12350
+ workflowPlan: runtimeConfig.workflowPlan,
12351
+ dashboardOrigin: "dashboard",
12352
+ json: true,
12353
+ verbose: false,
12354
+ ephemeral: false,
12355
+ timeoutMs: spec.timeoutSec ? spec.timeoutSec * 1e3 : void 0,
12356
+ signal: abortSignal,
12357
+ stdout,
12358
+ stderr,
12359
+ ...decisionInbox ? { dashboardDecisionInbox: decisionInbox } : {}
12360
+ });
12361
+ const failedCompletion = deferredFailedCompletion.current;
12362
+ if (failedCompletion) {
12363
+ const data = typeof failedCompletion.data === "object" && failedCompletion.data !== null ? failedCompletion.data : {};
12364
+ send(
12365
+ "error",
12366
+ {
12367
+ ...data,
12176
12368
  success: result.success,
12177
12369
  exitCode: result.exitCode,
12178
12370
  athenaSessionId: result.athenaSessionId,
@@ -12180,10 +12372,24 @@ async function executeRemoteAssignment({
12180
12372
  finalMessage: result.finalMessage,
12181
12373
  tokens: result.tokens,
12182
12374
  durationMs: result.durationMs,
12183
- message: result.failure.message
12184
- });
12185
- }
12186
- });
12375
+ message: result.failure?.message ?? eventPayload(failedCompletion).message ?? "remote execution failed"
12376
+ },
12377
+ typeof failedCompletion.ts === "number" ? failedCompletion.ts : now()
12378
+ );
12379
+ return;
12380
+ }
12381
+ if (result.failure && result.failure.message !== lastTerminalFailureMessage.current) {
12382
+ send("error", {
12383
+ success: result.success,
12384
+ exitCode: result.exitCode,
12385
+ athenaSessionId: result.athenaSessionId,
12386
+ adapterSessionId: result.adapterSessionId,
12387
+ finalMessage: result.finalMessage,
12388
+ tokens: result.tokens,
12389
+ durationMs: result.durationMs,
12390
+ message: result.failure.message
12391
+ });
12392
+ }
12187
12393
  } catch (err) {
12188
12394
  send("error", {
12189
12395
  message: err instanceof Error ? err.message : String(err)
@@ -12193,7 +12399,7 @@ async function executeRemoteAssignment({
12193
12399
  if (runStream) {
12194
12400
  const drainTimeout = new Promise((resolve) => {
12195
12401
  const t = setTimeout(() => resolve(), 1e4);
12196
- t.unref?.();
12402
+ t.unref();
12197
12403
  });
12198
12404
  await Promise.race([runStream.whenTerminated(), drainTimeout]);
12199
12405
  await runStream.close("done");
@@ -12201,131 +12407,304 @@ async function executeRemoteAssignment({
12201
12407
  }
12202
12408
  }
12203
12409
 
12204
- // src/infra/config/attachmentMirror.ts
12205
- import crypto3 from "crypto";
12206
- import fs20 from "fs";
12207
- import os11 from "os";
12208
- import path18 from "path";
12209
- function attachmentMirrorPath(env = process.env) {
12210
- const home = env["HOME"] ?? os11.homedir();
12211
- return path18.join(home, ".config", "athena", "attachments.json");
12410
+ // src/app/dashboard/dashboardDecisionInbox.ts
12411
+ import Database4 from "better-sqlite3";
12412
+ function dashboardDecisionInboxPath() {
12413
+ return `${ensureDaemonStateDir().dir}/dashboard-decision-inbox.db`;
12212
12414
  }
12213
- function readAttachmentMirror(env = process.env) {
12214
- const file = attachmentMirrorPath(env);
12215
- let raw;
12216
- try {
12217
- raw = fs20.readFileSync(file, "utf-8");
12218
- } catch (err) {
12219
- if (err.code === "ENOENT") return null;
12220
- throw err;
12221
- }
12222
- let parsed;
12223
- try {
12224
- parsed = JSON.parse(raw);
12225
- } catch (err) {
12226
- throw new Error(
12227
- `attachment mirror ${file} is invalid JSON: ${err instanceof Error ? err.message : String(err)}`
12228
- );
12229
- }
12230
- try {
12231
- return parseAttachmentMirror(parsed);
12232
- } catch (err) {
12233
- throw new Error(
12234
- `attachment mirror ${file} is invalid: ${err instanceof Error ? err.message : String(err)}`
12235
- );
12236
- }
12415
+ function hasLegacyUniqueConstraint(db) {
12416
+ const rows = db.prepare(`PRAGMA index_list('dashboard_decision_inbox')`).all();
12417
+ return rows.some((row) => row.unique === 1 && row.origin === "u");
12237
12418
  }
12238
- function writeAttachmentMirror(mirror, env = process.env) {
12239
- const validated = parseAttachmentMirror(mirror);
12240
- const file = attachmentMirrorPath(env);
12241
- const dir = path18.dirname(file);
12242
- fs20.mkdirSync(dir, { recursive: true, mode: 448 });
12243
- const tmp = `${file}.${process.pid}.${crypto3.randomBytes(4).toString("hex")}.tmp`;
12244
- const fd = fs20.openSync(tmp, "w", 384);
12245
- try {
12246
- fs20.writeSync(fd, JSON.stringify(validated, null, 2) + "\n");
12247
- fs20.fsyncSync(fd);
12248
- } finally {
12249
- fs20.closeSync(fd);
12250
- }
12251
- try {
12252
- fs20.renameSync(tmp, file);
12253
- } catch (err) {
12254
- try {
12255
- fs20.unlinkSync(tmp);
12256
- } catch {
12419
+ function migrateLegacyUniqueConstraint(db) {
12420
+ if (!hasLegacyUniqueConstraint(db)) return;
12421
+ db.exec(`
12422
+ ALTER TABLE dashboard_decision_inbox
12423
+ RENAME TO dashboard_decision_inbox_legacy;
12424
+
12425
+ CREATE TABLE dashboard_decision_inbox (
12426
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
12427
+ athena_session_id TEXT NOT NULL,
12428
+ request_id TEXT NOT NULL,
12429
+ decision_json TEXT NOT NULL,
12430
+ received_at INTEGER NOT NULL,
12431
+ consumed_at INTEGER
12432
+ );
12433
+
12434
+ INSERT INTO dashboard_decision_inbox (
12435
+ id,
12436
+ athena_session_id,
12437
+ request_id,
12438
+ decision_json,
12439
+ received_at,
12440
+ consumed_at
12441
+ )
12442
+ SELECT
12443
+ id,
12444
+ athena_session_id,
12445
+ request_id,
12446
+ decision_json,
12447
+ received_at,
12448
+ consumed_at
12449
+ FROM dashboard_decision_inbox_legacy;
12450
+
12451
+ DROP TABLE dashboard_decision_inbox_legacy;
12452
+ `);
12453
+ }
12454
+ function initInboxSchema(db) {
12455
+ db.exec(`
12456
+ PRAGMA journal_mode = WAL;
12457
+
12458
+ CREATE TABLE IF NOT EXISTS dashboard_decision_inbox (
12459
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
12460
+ athena_session_id TEXT NOT NULL,
12461
+ request_id TEXT NOT NULL,
12462
+ decision_json TEXT NOT NULL,
12463
+ received_at INTEGER NOT NULL,
12464
+ consumed_at INTEGER
12465
+ );
12466
+ `);
12467
+ migrateLegacyUniqueConstraint(db);
12468
+ db.exec(`
12469
+
12470
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_dashboard_decision_unconsumed
12471
+ ON dashboard_decision_inbox(athena_session_id, request_id)
12472
+ WHERE consumed_at IS NULL;
12473
+
12474
+ CREATE INDEX IF NOT EXISTS idx_dashboard_decision_pending
12475
+ ON dashboard_decision_inbox(athena_session_id, consumed_at, id);
12476
+ `);
12477
+ }
12478
+ function createDashboardDecisionInbox(options = {}) {
12479
+ const db = new Database4(options.dbPath ?? dashboardDecisionInboxPath());
12480
+ initInboxSchema(db);
12481
+ const upsertUnconsumed = db.prepare(`
12482
+ INSERT INTO dashboard_decision_inbox (
12483
+ athena_session_id,
12484
+ request_id,
12485
+ decision_json,
12486
+ received_at
12487
+ )
12488
+ VALUES (?, ?, ?, ?)
12489
+ ON CONFLICT(athena_session_id, request_id) WHERE consumed_at IS NULL
12490
+ DO UPDATE SET
12491
+ decision_json = excluded.decision_json,
12492
+ received_at = excluded.received_at
12493
+ `);
12494
+ const selectPending = db.prepare(`
12495
+ SELECT id, athena_session_id, request_id, decision_json, received_at
12496
+ FROM dashboard_decision_inbox
12497
+ WHERE athena_session_id = ? AND consumed_at IS NULL
12498
+ ORDER BY id ASC
12499
+ LIMIT ?
12500
+ `);
12501
+ const consume = db.prepare(`
12502
+ UPDATE dashboard_decision_inbox
12503
+ SET consumed_at = ?
12504
+ WHERE id = ?
12505
+ `);
12506
+ return {
12507
+ enqueue(input) {
12508
+ upsertUnconsumed.run(
12509
+ input.athenaSessionId,
12510
+ input.requestId,
12511
+ JSON.stringify(input.decision),
12512
+ input.receivedAt
12513
+ );
12514
+ },
12515
+ pendingForSession(input) {
12516
+ const rows = selectPending.all(
12517
+ input.athenaSessionId,
12518
+ input.limit
12519
+ );
12520
+ return rows.map((row) => ({
12521
+ id: row.id,
12522
+ athenaSessionId: row.athena_session_id,
12523
+ requestId: row.request_id,
12524
+ decision: JSON.parse(row.decision_json),
12525
+ receivedAt: row.received_at
12526
+ }));
12527
+ },
12528
+ markConsumed(input) {
12529
+ consume.run(Date.now(), input.id);
12530
+ },
12531
+ close() {
12532
+ db.close();
12533
+ }
12534
+ };
12535
+ }
12536
+
12537
+ // src/app/dashboard/dashboardPairedExecution.ts
12538
+ var DEFAULT_MAX_CONCURRENT_RUNS = 1;
12539
+ var DEFAULT_RUN_HISTORY_LIMIT = 100;
12540
+ function createDashboardPairedExecution(options) {
12541
+ const client = options.client;
12542
+ const executor = options.executor;
12543
+ const projectDir = options.projectDir;
12544
+ const decisionInbox = options.decisionInbox;
12545
+ const log = options.log ?? (() => {
12546
+ });
12547
+ const maxConcurrentRuns = options.maxConcurrentRuns ?? DEFAULT_MAX_CONCURRENT_RUNS;
12548
+ const runHistoryLimit = options.runHistoryLimit ?? DEFAULT_RUN_HISTORY_LIMIT;
12549
+ const now = options.now ?? (() => Date.now());
12550
+ let completedRuns = 0;
12551
+ const active = /* @__PURE__ */ new Map();
12552
+ const activeByRunner = /* @__PURE__ */ new Map();
12553
+ const runHistory = [];
12554
+ function recordRun(record) {
12555
+ runHistory.push(record);
12556
+ while (runHistory.length > runHistoryLimit) {
12557
+ runHistory.shift();
12257
12558
  }
12258
- throw err;
12259
12559
  }
12260
- if (process.platform !== "win32") {
12560
+ function rejectAssignment(runId, reason) {
12261
12561
  try {
12262
- fs20.chmodSync(dir, 448);
12263
- fs20.chmodSync(file, 384);
12264
- } catch {
12562
+ client.sendRunEvent({
12563
+ runId,
12564
+ seq: 0,
12565
+ ts: now(),
12566
+ kind: "rejected",
12567
+ payload: { reason }
12568
+ });
12569
+ } catch (err) {
12570
+ log(
12571
+ "warn",
12572
+ `runtime daemon: failed to send rejected for ${runId}: ${err instanceof Error ? err.message : String(err)}`
12573
+ );
12265
12574
  }
12575
+ recordRun({
12576
+ runId,
12577
+ startedAt: now(),
12578
+ endedAt: now(),
12579
+ status: "rejected",
12580
+ error: reason
12581
+ });
12582
+ log("warn", `run ${runId} rejected: ${reason}`);
12266
12583
  }
12267
- }
12268
- function removeAttachmentMirror(env = process.env) {
12269
- const file = attachmentMirrorPath(env);
12270
- try {
12271
- fs20.unlinkSync(file);
12272
- } catch (err) {
12273
- if (err.code !== "ENOENT") throw err;
12274
- }
12275
- }
12276
- function parseAttachmentMirror(raw) {
12277
- if (typeof raw !== "object" || raw === null) {
12278
- throw new Error("root must be an object");
12279
- }
12280
- const obj = raw;
12281
- if (typeof obj["instanceId"] !== "string" || obj["instanceId"].length === 0) {
12282
- throw new Error("instanceId must be a non-empty string");
12283
- }
12284
- if (typeof obj["fetchedAt"] !== "number") {
12285
- throw new Error("fetchedAt must be a number");
12286
- }
12287
- if (!Array.isArray(obj["attachments"])) {
12288
- throw new Error("attachments must be an array");
12584
+ function handleDecision(frame) {
12585
+ decisionInbox.enqueue({
12586
+ athenaSessionId: frame.athenaSessionId,
12587
+ requestId: frame.requestId,
12588
+ decision: frame.decision,
12589
+ receivedAt: now()
12590
+ });
12289
12591
  }
12290
- const attachments = obj["attachments"].map((entry, idx) => {
12291
- if (typeof entry !== "object" || entry === null) {
12292
- throw new Error(`attachments[${idx}] must be an object`);
12293
- }
12294
- const e = entry;
12295
- if (typeof e["runnerId"] !== "string" || e["runnerId"].length === 0) {
12296
- throw new Error(
12297
- `attachments[${idx}].runnerId must be a non-empty string`
12592
+ function handleCancel(frame) {
12593
+ const entry = active.get(frame.runId);
12594
+ if (!entry) return;
12595
+ entry.record.status = "cancelled";
12596
+ entry.controller.abort();
12597
+ }
12598
+ function handleAssignment(frame) {
12599
+ if (active.has(frame.runId)) {
12600
+ rejectAssignment(
12601
+ frame.runId,
12602
+ `duplicate active assignment ${frame.runId}`
12298
12603
  );
12604
+ return;
12299
12605
  }
12300
- const out = { runnerId: e["runnerId"] };
12301
- if (typeof e["name"] === "string") out.name = e["name"];
12302
- if (typeof e["executionTarget"] === "string") {
12303
- out.executionTarget = e["executionTarget"];
12304
- }
12305
- if (typeof e["remoteInstanceId"] === "string") {
12306
- out.remoteInstanceId = e["remoteInstanceId"];
12606
+ const runnerKey = frame.runnerId;
12607
+ const bucket = activeByRunner.get(runnerKey) ?? /* @__PURE__ */ new Set();
12608
+ if (bucket.size >= maxConcurrentRuns) {
12609
+ rejectAssignment(
12610
+ frame.runId,
12611
+ `runtime daemon at concurrency cap (${maxConcurrentRuns}) for runner ${runnerKey ?? "<legacy>"}`
12612
+ );
12613
+ return;
12307
12614
  }
12308
- return out;
12309
- });
12615
+ const controller = new AbortController();
12616
+ const record = {
12617
+ runId: frame.runId,
12618
+ startedAt: now(),
12619
+ status: "running"
12620
+ };
12621
+ recordRun(record);
12622
+ bucket.add(frame.runId);
12623
+ activeByRunner.set(runnerKey, bucket);
12624
+ const promise = executor({
12625
+ frame,
12626
+ client,
12627
+ projectDir,
12628
+ log,
12629
+ abortSignal: controller.signal,
12630
+ decisionInbox
12631
+ }).then(() => {
12632
+ if (record.status === "running") record.status = "completed";
12633
+ }).catch((err) => {
12634
+ if (record.status === "running") {
12635
+ record.status = "failed";
12636
+ }
12637
+ record.error = err instanceof Error ? err.message : String(err);
12638
+ log(
12639
+ "error",
12640
+ `run ${frame.runId} failed: ${err instanceof Error ? err.message : String(err)}`
12641
+ );
12642
+ }).finally(() => {
12643
+ record.endedAt = now();
12644
+ completedRuns += 1;
12645
+ active.delete(frame.runId);
12646
+ const remaining = activeByRunner.get(runnerKey);
12647
+ if (remaining) {
12648
+ remaining.delete(frame.runId);
12649
+ if (remaining.size === 0) activeByRunner.delete(runnerKey);
12650
+ }
12651
+ });
12652
+ active.set(frame.runId, { controller, promise, record, runnerKey });
12653
+ }
12310
12654
  return {
12311
- instanceId: obj["instanceId"],
12312
- fetchedAt: obj["fetchedAt"],
12313
- attachments
12655
+ handleFrame(frame) {
12656
+ if (frame.type === "dashboard_decision") {
12657
+ handleDecision(frame);
12658
+ return true;
12659
+ }
12660
+ if (frame.type === "cancel") {
12661
+ handleCancel(frame);
12662
+ return true;
12663
+ }
12664
+ if (frame.type === "job_assignment") {
12665
+ handleAssignment(frame);
12666
+ return true;
12667
+ }
12668
+ return false;
12669
+ },
12670
+ snapshot() {
12671
+ return {
12672
+ activeRuns: active.size,
12673
+ completedRuns
12674
+ };
12675
+ },
12676
+ listRuns(opts = {}) {
12677
+ let out = runHistory.slice();
12678
+ if (typeof opts.limit === "number" && opts.limit > 0) {
12679
+ out = out.slice(-opts.limit);
12680
+ }
12681
+ if (opts.active) {
12682
+ out = out.filter((r) => r.status === "running");
12683
+ }
12684
+ return out;
12685
+ },
12686
+ async stop() {
12687
+ for (const run of active.values()) {
12688
+ run.controller.abort();
12689
+ }
12690
+ await Promise.allSettled([...active.values()].map((run) => run.promise));
12691
+ }
12314
12692
  };
12315
12693
  }
12316
12694
 
12317
12695
  // src/app/dashboard/runtimeDaemon.ts
12318
12696
  var DEFAULT_RECONNECT_DELAYS_MS2 = [1e3, 2e3, 5e3, 1e4, 3e4];
12319
- var DEFAULT_MAX_CONCURRENT_RUNS = 1;
12697
+ var DEFAULT_MAX_CONCURRENT_RUNS2 = 1;
12320
12698
  var DEFAULT_REFRESH_LEAD_SEC = 60;
12321
12699
  var DEFAULT_REFRESH_FAILURE_LIMIT = 5;
12322
12700
  var DEFAULT_REFRESH_FAILURE_WINDOW_MS = 5 * 6e4;
12323
12701
  var DEFAULT_REFRESH_COOLDOWN_MS = 5 * 6e4;
12324
- var DEFAULT_RUN_HISTORY_LIMIT = 100;
12702
+ var DEFAULT_RUN_HISTORY_LIMIT2 = 100;
12703
+ var DEFAULT_FEED_DRAIN_INTERVAL_MS = 1e3;
12325
12704
  function delay(ms) {
12326
12705
  return new Promise((resolve) => {
12327
12706
  const timer = setTimeout(resolve, ms);
12328
- timer.unref?.();
12707
+ timer.unref();
12329
12708
  });
12330
12709
  }
12331
12710
  async function runDashboardRuntimeDaemon(options = {}) {
@@ -12342,33 +12721,52 @@ async function runDashboardRuntimeDaemon(options = {}) {
12342
12721
  const log = options.log ?? (() => {
12343
12722
  });
12344
12723
  const reconnectDelays = options.reconnectDelaysMs ?? DEFAULT_RECONNECT_DELAYS_MS2;
12345
- const maxConcurrentRuns = options.maxConcurrentRuns ?? DEFAULT_MAX_CONCURRENT_RUNS;
12724
+ const maxConcurrentRuns = options.maxConcurrentRuns ?? DEFAULT_MAX_CONCURRENT_RUNS2;
12346
12725
  const refreshLeadSec = options.refreshLeadSec ?? DEFAULT_REFRESH_LEAD_SEC;
12347
12726
  const refreshFailureLimit = options.refreshFailureLimit ?? DEFAULT_REFRESH_FAILURE_LIMIT;
12348
12727
  const refreshFailureWindowMs = options.refreshFailureWindowMs ?? DEFAULT_REFRESH_FAILURE_WINDOW_MS;
12349
12728
  const refreshCooldownMs = options.refreshCooldownMs ?? DEFAULT_REFRESH_COOLDOWN_MS;
12350
- const runHistoryLimit = options.runHistoryLimit ?? DEFAULT_RUN_HISTORY_LIMIT;
12729
+ const runHistoryLimit = options.runHistoryLimit ?? DEFAULT_RUN_HISTORY_LIMIT2;
12351
12730
  const now = options.now ?? (() => Date.now());
12352
12731
  const writeMirror = options.writeMirror ?? writeAttachmentMirror;
12732
+ const feedOutbox = options.feedOutbox ?? createDashboardFeedOutbox();
12733
+ const decisionInbox = options.decisionInbox ?? createDashboardDecisionInbox();
12734
+ const feedDrainIntervalMs = options.feedDrainIntervalMs ?? DEFAULT_FEED_DRAIN_INTERVAL_MS;
12353
12735
  const startedAt = now();
12354
12736
  let stopped = false;
12355
12737
  let reconnectAttempt = 0;
12356
12738
  let client = null;
12739
+ let lastSocketClient = null;
12357
12740
  let currentInstanceId;
12358
12741
  let currentDashboardUrl;
12359
12742
  let lastFrameAt;
12360
- let completedRuns = 0;
12361
12743
  let refreshTimer = null;
12744
+ let feedDrainTimer = null;
12362
12745
  const refreshFailures = [];
12363
12746
  let cooldownUntil = 0;
12364
- const active = /* @__PURE__ */ new Map();
12365
- const runHistory = [];
12366
- function recordRun(record) {
12367
- runHistory.push(record);
12368
- while (runHistory.length > runHistoryLimit) {
12369
- runHistory.shift();
12747
+ const executionClient = {
12748
+ sendRunEvent(event) {
12749
+ const current = client ?? lastSocketClient;
12750
+ if (!current) {
12751
+ log(
12752
+ "warn",
12753
+ `instance socket dropped run_event (socket not connected): runId=${event.runId} kind=${event.kind}`
12754
+ );
12755
+ return;
12756
+ }
12757
+ current.sendRunEvent(event);
12370
12758
  }
12371
- }
12759
+ };
12760
+ const pairedExecution = createDashboardPairedExecution({
12761
+ client: executionClient,
12762
+ executor,
12763
+ projectDir,
12764
+ decisionInbox,
12765
+ log,
12766
+ maxConcurrentRuns,
12767
+ now,
12768
+ runHistoryLimit
12769
+ });
12372
12770
  function nextReconnectDelay() {
12373
12771
  if (reconnectDelays.length === 0) return 0;
12374
12772
  const delayMs = reconnectDelays[Math.min(reconnectAttempt, reconnectDelays.length - 1)] ?? 0;
@@ -12381,6 +12779,35 @@ async function runDashboardRuntimeDaemon(options = {}) {
12381
12779
  refreshTimer = null;
12382
12780
  }
12383
12781
  }
12782
+ function clearFeedDrainTimer() {
12783
+ if (feedDrainTimer) {
12784
+ clearInterval(feedDrainTimer);
12785
+ feedDrainTimer = null;
12786
+ }
12787
+ }
12788
+ function drainFeedOutbox() {
12789
+ const current = client;
12790
+ if (!current) return;
12791
+ const rows = feedOutbox.pendingBatch({ limit: 100, now: now() });
12792
+ for (const row of rows) {
12793
+ current.sendFeedEvent({
12794
+ deliverySeq: row.deliverySeq,
12795
+ envelope: row.envelope
12796
+ });
12797
+ const retryDelayMs = Math.min(3e4, (row.attempt + 1) * 1e3);
12798
+ feedOutbox.markAttempted({
12799
+ deliverySeq: row.deliverySeq,
12800
+ nextAttemptAt: now() + retryDelayMs
12801
+ });
12802
+ }
12803
+ }
12804
+ function startFeedDrainTimer() {
12805
+ clearFeedDrainTimer();
12806
+ const timer = setInterval(drainFeedOutbox, feedDrainIntervalMs);
12807
+ timer.unref();
12808
+ feedDrainTimer = timer;
12809
+ drainFeedOutbox();
12810
+ }
12384
12811
  function scheduleRefresh(expiresInSec) {
12385
12812
  clearRefreshTimer();
12386
12813
  if (!Number.isFinite(expiresInSec) || expiresInSec <= refreshLeadSec) {
@@ -12390,7 +12817,7 @@ async function runDashboardRuntimeDaemon(options = {}) {
12390
12817
  const timer = setTimeout(() => {
12391
12818
  void proactiveRefresh();
12392
12819
  }, ms);
12393
- timer.unref?.();
12820
+ timer.unref();
12394
12821
  refreshTimer = timer;
12395
12822
  }
12396
12823
  async function proactiveRefresh() {
@@ -12425,30 +12852,6 @@ async function runDashboardRuntimeDaemon(options = {}) {
12425
12852
  );
12426
12853
  }
12427
12854
  }
12428
- function rejectAssignment(client_, runId, reason) {
12429
- try {
12430
- client_.sendRunEvent({
12431
- runId,
12432
- seq: 0,
12433
- ts: now(),
12434
- kind: "rejected",
12435
- payload: { reason }
12436
- });
12437
- } catch (err) {
12438
- log(
12439
- "warn",
12440
- `runtime daemon: failed to send rejected for ${runId}: ${err instanceof Error ? err.message : String(err)}`
12441
- );
12442
- }
12443
- recordRun({
12444
- runId,
12445
- startedAt: now(),
12446
- endedAt: now(),
12447
- status: "rejected",
12448
- error: reason
12449
- });
12450
- log("warn", `run ${runId} rejected: ${reason}`);
12451
- }
12452
12855
  async function connectOnce() {
12453
12856
  const config = readConfig2();
12454
12857
  if (!config) {
@@ -12503,54 +12906,14 @@ async function runDashboardRuntimeDaemon(options = {}) {
12503
12906
  }
12504
12907
  return;
12505
12908
  }
12506
- if (frame.type === "cancel") {
12507
- const entry = active.get(frame.runId);
12508
- if (entry) {
12509
- entry.record.status = "cancelled";
12510
- entry.controller.abort();
12511
- }
12512
- return;
12513
- }
12514
- if (frame.type !== "job_assignment") return;
12515
- if (active.has(frame.runId)) return;
12516
- if (active.size >= maxConcurrentRuns) {
12517
- rejectAssignment(
12518
- next,
12519
- frame.runId,
12520
- `runtime daemon at concurrency cap (${maxConcurrentRuns})`
12521
- );
12909
+ if (frame.type === "feed_ack") {
12910
+ feedOutbox.markAcked({
12911
+ ...typeof frame.deliverySeq === "number" ? { deliverySeq: frame.deliverySeq } : {},
12912
+ ...typeof frame.eventId === "string" ? { eventId: frame.eventId } : {}
12913
+ });
12522
12914
  return;
12523
12915
  }
12524
- const controller = new AbortController();
12525
- const record = {
12526
- runId: frame.runId,
12527
- startedAt: now(),
12528
- status: "running"
12529
- };
12530
- recordRun(record);
12531
- const promise = executor({
12532
- frame,
12533
- client: next,
12534
- projectDir,
12535
- log,
12536
- abortSignal: controller.signal
12537
- }).then(() => {
12538
- if (record.status === "running") record.status = "completed";
12539
- }).catch((err) => {
12540
- if (record.status === "running") {
12541
- record.status = "failed";
12542
- }
12543
- record.error = err instanceof Error ? err.message : String(err);
12544
- log(
12545
- "error",
12546
- `run ${frame.runId} failed: ${err instanceof Error ? err.message : String(err)}`
12547
- );
12548
- }).finally(() => {
12549
- record.endedAt = now();
12550
- completedRuns += 1;
12551
- active.delete(frame.runId);
12552
- });
12553
- active.set(frame.runId, { controller, promise, record });
12916
+ pairedExecution.handleFrame(frame);
12554
12917
  });
12555
12918
  next.onClose((reason) => {
12556
12919
  if (stopped || client !== next) return;
@@ -12558,14 +12921,17 @@ async function runDashboardRuntimeDaemon(options = {}) {
12558
12921
  client = null;
12559
12922
  currentInstanceId = void 0;
12560
12923
  clearRefreshTimer();
12924
+ clearFeedDrainTimer();
12561
12925
  void reconnectLoop();
12562
12926
  });
12563
12927
  await next.connect();
12564
12928
  client = next;
12929
+ lastSocketClient = next;
12565
12930
  currentInstanceId = token.instanceId;
12566
12931
  currentDashboardUrl = config.dashboardUrl;
12567
12932
  reconnectAttempt = 0;
12568
12933
  scheduleRefresh(token.expiresInSec);
12934
+ startFeedDrainTimer();
12569
12935
  log("info", `dashboard runtime daemon connected as ${token.instanceId}`);
12570
12936
  }
12571
12937
  async function reconnectLoop() {
@@ -12587,6 +12953,7 @@ async function runDashboardRuntimeDaemon(options = {}) {
12587
12953
  await connectOnce();
12588
12954
  return {
12589
12955
  snapshot() {
12956
+ const executionSnapshot = pairedExecution.snapshot();
12590
12957
  const refreshState = refreshFailures.length > 0 || cooldownUntil > now() ? {
12591
12958
  recentFailures: refreshFailures.length,
12592
12959
  ...cooldownUntil > now() ? { cooldownUntilMs: cooldownUntil } : {}
@@ -12595,158 +12962,30 @@ async function runDashboardRuntimeDaemon(options = {}) {
12595
12962
  startedAt,
12596
12963
  socketConnected: client !== null,
12597
12964
  ...lastFrameAt !== void 0 ? { lastFrameAt } : {},
12598
- activeRuns: active.size,
12599
- completedRuns,
12965
+ activeRuns: executionSnapshot.activeRuns,
12966
+ completedRuns: executionSnapshot.completedRuns,
12600
12967
  ...currentInstanceId ? { instanceId: currentInstanceId } : {},
12601
12968
  ...currentDashboardUrl ? { dashboardUrl: currentDashboardUrl } : {},
12602
12969
  ...refreshState ? { refreshState } : {}
12603
12970
  };
12604
12971
  },
12605
12972
  listRuns(opts = {}) {
12606
- let out = runHistory.slice();
12607
- if (typeof opts.limit === "number" && opts.limit > 0) {
12608
- out = out.slice(-opts.limit);
12609
- }
12610
- if (opts.active) {
12611
- out = out.filter((r) => r.status === "running");
12612
- }
12613
- return out;
12973
+ return pairedExecution.listRuns(opts);
12614
12974
  },
12615
12975
  async stop(reason = "stopped") {
12616
12976
  stopped = true;
12617
12977
  clearRefreshTimer();
12618
- for (const run of active.values()) {
12619
- run.controller.abort();
12620
- }
12978
+ clearFeedDrainTimer();
12621
12979
  const current = client;
12622
12980
  client = null;
12623
12981
  current?.close(reason);
12624
- await Promise.allSettled([...active.values()].map((run) => run.promise));
12982
+ await pairedExecution.stop();
12625
12983
  }
12626
12984
  };
12627
12985
  }
12628
12986
 
12629
- // src/infra/daemon/stateDir.ts
12630
- import fs21 from "fs";
12631
- import os12 from "os";
12632
- import path19 from "path";
12633
- function daemonStatePaths(env = process.env) {
12634
- const xdg = env["XDG_STATE_HOME"];
12635
- const home = env["HOME"] ?? os12.homedir();
12636
- const base = xdg && xdg.length > 0 ? xdg : path19.join(home, ".local", "state");
12637
- const dir = path19.join(base, "drisp");
12638
- return {
12639
- dir,
12640
- pidPath: path19.join(dir, "dashboard-daemon.pid"),
12641
- logPath: path19.join(dir, "dashboard-daemon.log"),
12642
- socketPath: path19.join(dir, "dashboard-daemon.sock")
12643
- };
12644
- }
12645
- function ensureDaemonStateDir(env = process.env) {
12646
- const paths = daemonStatePaths(env);
12647
- fs21.mkdirSync(paths.dir, { recursive: true, mode: 448 });
12648
- if (process.platform !== "win32") {
12649
- try {
12650
- fs21.chmodSync(paths.dir, 448);
12651
- } catch {
12652
- }
12653
- }
12654
- return paths;
12655
- }
12656
-
12657
- // src/infra/daemon/pidLock.ts
12658
- import fs22 from "fs";
12659
- function acquirePidLock(pidPath) {
12660
- const ownPid = process.pid;
12661
- for (let attempt = 0; attempt < 2; attempt += 1) {
12662
- try {
12663
- const fd = fs22.openSync(pidPath, "wx", 384);
12664
- try {
12665
- fs22.writeSync(fd, `${ownPid}
12666
- `);
12667
- fs22.fsyncSync(fd);
12668
- } finally {
12669
- fs22.closeSync(fd);
12670
- }
12671
- return makeHandle(pidPath, ownPid);
12672
- } catch (err) {
12673
- if (err.code !== "EEXIST") throw err;
12674
- }
12675
- const existing = readPidLock(pidPath);
12676
- if (existing.state === "held") {
12677
- throw new Error(
12678
- `dashboard daemon is already running as pid ${existing.pid} (lock at ${pidPath}). Use "drisp dashboard daemon stop" to terminate it.`
12679
- );
12680
- }
12681
- if (existing.state === "stale") {
12682
- try {
12683
- fs22.unlinkSync(pidPath);
12684
- } catch (err) {
12685
- if (err.code !== "ENOENT") throw err;
12686
- }
12687
- continue;
12688
- }
12689
- }
12690
- throw new Error(
12691
- `dashboard daemon: failed to acquire pid lock at ${pidPath} after retry`
12692
- );
12693
- }
12694
- function readPidLock(pidPath) {
12695
- let raw;
12696
- try {
12697
- raw = fs22.readFileSync(pidPath, "utf-8");
12698
- } catch (err) {
12699
- if (err.code === "ENOENT") {
12700
- return { state: "absent" };
12701
- }
12702
- throw err;
12703
- }
12704
- const pid = Number.parseInt(raw.trim(), 10);
12705
- if (!Number.isFinite(pid) || pid <= 0) {
12706
- return { state: "stale", pid: 0 };
12707
- }
12708
- if (!isProcessAlive(pid)) {
12709
- return { state: "stale", pid };
12710
- }
12711
- return { state: "held", pid };
12712
- }
12713
- function makeHandle(pidPath, pid) {
12714
- let released = false;
12715
- return {
12716
- pid,
12717
- release() {
12718
- if (released) return;
12719
- released = true;
12720
- try {
12721
- const raw = fs22.readFileSync(pidPath, "utf-8").trim();
12722
- if (raw === String(pid)) {
12723
- fs22.unlinkSync(pidPath);
12724
- }
12725
- } catch (err) {
12726
- if (err.code !== "ENOENT") {
12727
- }
12728
- }
12729
- }
12730
- };
12731
- }
12732
- function isProcessAlive(pid) {
12733
- if (process.platform === "win32") {
12734
- return true;
12735
- }
12736
- try {
12737
- process.kill(pid, 0);
12738
- return true;
12739
- } catch (err) {
12740
- const code = err.code;
12741
- if (code === "EPERM") {
12742
- return true;
12743
- }
12744
- return false;
12745
- }
12746
- }
12747
-
12748
12987
  // src/infra/daemon/udsIpc.ts
12749
- import fs23 from "fs";
12988
+ import fs21 from "fs";
12750
12989
  import net2 from "net";
12751
12990
 
12752
12991
  // src/infra/daemon/udsFrameCodec.ts
@@ -12791,7 +13030,7 @@ async function startUdsServer(socketPath, handler, log) {
12791
13030
  });
12792
13031
  if (process.platform !== "win32") {
12793
13032
  try {
12794
- fs23.chmodSync(socketPath, 384);
13033
+ fs21.chmodSync(socketPath, 384);
12795
13034
  } catch {
12796
13035
  }
12797
13036
  }
@@ -12801,7 +13040,7 @@ async function startUdsServer(socketPath, handler, log) {
12801
13040
  server.close(() => resolve());
12802
13041
  });
12803
13042
  try {
12804
- fs23.unlinkSync(socketPath);
13043
+ fs21.unlinkSync(socketPath);
12805
13044
  } catch (err) {
12806
13045
  if (err.code !== "ENOENT") {
12807
13046
  }
@@ -12867,7 +13106,7 @@ async function sendUdsRequest(socketPath, request, options = {}) {
12867
13106
  socket.destroy();
12868
13107
  reject(new Error(`uds request timed out after ${timeoutMs}ms`));
12869
13108
  }, timeoutMs);
12870
- timer.unref?.();
13109
+ timer.unref();
12871
13110
  const finish = (action) => {
12872
13111
  if (settled) return;
12873
13112
  settled = true;
@@ -12919,7 +13158,7 @@ async function sendUdsRequest(socketPath, request, options = {}) {
12919
13158
  async function unlinkStaleSocket(socketPath) {
12920
13159
  let stat;
12921
13160
  try {
12922
- stat = fs23.statSync(socketPath);
13161
+ stat = fs21.statSync(socketPath);
12923
13162
  } catch (err) {
12924
13163
  if (err.code === "ENOENT") return;
12925
13164
  throw err;
@@ -12946,7 +13185,7 @@ async function unlinkStaleSocket(socketPath) {
12946
13185
  `uds path ${socketPath} is in use by another process; aborting`
12947
13186
  );
12948
13187
  }
12949
- fs23.unlinkSync(socketPath);
13188
+ fs21.unlinkSync(socketPath);
12950
13189
  }
12951
13190
 
12952
13191
  export {
@@ -12964,6 +13203,10 @@ export {
12964
13203
  ingestRuntimeEvent,
12965
13204
  ingestRuntimeDecision,
12966
13205
  generateId,
13206
+ daemonStatePaths,
13207
+ ensureDaemonStateDir,
13208
+ createDashboardFeedPublisher,
13209
+ createDashboardDecisionInbox,
12967
13210
  createSessionStore,
12968
13211
  sessionsDir,
12969
13212
  listSessions,
@@ -13011,15 +13254,9 @@ export {
13011
13254
  bootstrapRuntimeConfig,
13012
13255
  EXEC_EXIT_CODE,
13013
13256
  runExec,
13014
- readAttachmentMirror,
13015
- writeAttachmentMirror,
13016
- removeAttachmentMirror,
13257
+ normalizeHarnessOverride,
13017
13258
  runDashboardRuntimeDaemon,
13018
- daemonStatePaths,
13019
- ensureDaemonStateDir,
13020
- acquirePidLock,
13021
- readPidLock,
13022
13259
  startUdsServer,
13023
13260
  sendUdsRequest
13024
13261
  };
13025
- //# sourceMappingURL=chunk-PJUDHH4R.js.map
13262
+ //# sourceMappingURL=chunk-K53YMYTG.js.map