@drisp/cli 0.4.4 → 0.4.7

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-WRHKXH5M.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,276 +11781,97 @@ async function runExec(options) {
11480
11781
  return result;
11481
11782
  }
11482
11783
 
11483
- // src/app/dashboard/instanceSocketClient.ts
11784
+ // src/app/dashboard/runStreamClient.ts
11484
11785
  import { WebSocket as WebSocket2 } from "ws";
11786
+ var DEFAULT_RECONNECT_DELAYS_MS = [250, 1e3, 2e3, 5e3, 15e3];
11485
11787
  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;
11788
+ var DEFAULT_WATCHDOG_MS = 9e4;
11789
+ var DEFAULT_MAX_QUEUE_SIZE = 5e3;
11790
+ function createRunStreamClient(opts) {
11498
11791
  const log = opts.log ?? (() => {
11499
11792
  });
11500
11793
  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();
11794
+ const reconnectDelays = opts.reconnectDelaysMs ?? DEFAULT_RECONNECT_DELAYS_MS;
11795
+ const heartbeatMs = opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
11796
+ const watchdogMs = opts.watchdogTimeoutMs ?? DEFAULT_WATCHDOG_MS;
11797
+ const maxQueueSize = opts.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
11798
+ const makeWebSocket = opts.makeWebSocket ?? ((url) => new WebSocket2(url));
11799
+ const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
11800
+ const clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
11801
+ let nextSeq = 1;
11802
+ const queue = [];
11504
11803
  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) {
11804
+ let connectAttempt = 0;
11805
+ let stopped = false;
11806
+ let serverTerminated = false;
11807
+ let heartbeatTimer = null;
11808
+ let watchdogTimer = null;
11809
+ let reconnectTimer = null;
11810
+ let resumeResolved = false;
11811
+ let firstConnect = null;
11812
+ const terminationWaiters = [];
11813
+ function trimQueueUpTo(lastAckedSeq) {
11814
+ while (queue.length > 0 && queue[0].seq <= lastAckedSeq) {
11815
+ queue.shift();
11816
+ }
11817
+ }
11818
+ function trimQueueUpToExclusive(expectedSeq) {
11819
+ while (queue.length > 0 && queue[0].seq < expectedSeq) {
11820
+ queue.shift();
11821
+ }
11822
+ }
11823
+ function clearTimers() {
11824
+ if (heartbeatTimer) {
11825
+ clearTimer(heartbeatTimer);
11826
+ heartbeatTimer = null;
11827
+ }
11828
+ if (watchdogTimer) {
11829
+ clearTimer(watchdogTimer);
11830
+ watchdogTimer = null;
11831
+ }
11832
+ if (reconnectTimer) {
11833
+ clearTimer(reconnectTimer);
11834
+ reconnectTimer = null;
11835
+ }
11836
+ }
11837
+ function startHeartbeat() {
11838
+ if (heartbeatMs <= 0) return;
11839
+ const tick = () => {
11840
+ if (!ws || ws.readyState !== ws.OPEN) return;
11841
+ try {
11842
+ ws.send(JSON.stringify({ type: "ping", ts: now() }));
11843
+ } catch (err) {
11511
11844
  log(
11512
11845
  "warn",
11513
- `instance socket dropped frame (socket not open): type=${frame.type}`
11846
+ `run-stream ping failed: ${err instanceof Error ? err.message : String(err)}`
11514
11847
  );
11515
11848
  }
11516
- return;
11517
- }
11518
- droppedSinceClose = 0;
11519
- try {
11520
- ws.send(JSON.stringify(frame));
11521
- } catch (err) {
11849
+ heartbeatTimer = setTimer(tick, heartbeatMs);
11850
+ heartbeatTimer.unref();
11851
+ };
11852
+ heartbeatTimer = setTimer(tick, heartbeatMs);
11853
+ heartbeatTimer.unref();
11854
+ }
11855
+ function bumpWatchdog() {
11856
+ if (watchdogMs <= 0) return;
11857
+ if (watchdogTimer) clearTimer(watchdogTimer);
11858
+ watchdogTimer = setTimer(() => {
11522
11859
  log(
11523
11860
  "warn",
11524
- `instance socket send failed: ${err instanceof Error ? err.message : String(err)}`
11861
+ `run-stream watchdog: no server frames for ${watchdogMs}ms \u2014 recycling socket`
11525
11862
  );
11526
- }
11527
- }
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
11863
  try {
11546
- handler(reason);
11864
+ ws?.terminate();
11547
11865
  } catch {
11548
11866
  }
11549
- }
11867
+ }, watchdogMs);
11868
+ watchdogTimer.unref();
11550
11869
  }
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}`);
11555
- }
11556
- for (const handler of [...frameHandlers]) {
11870
+ function flushQueue() {
11871
+ if (!ws || ws.readyState !== ws.OPEN || !resumeResolved) return;
11872
+ for (const frame of queue) {
11557
11873
  try {
11558
- handler(parsed);
11559
- } catch (err) {
11560
- log(
11561
- "warn",
11562
- `instance socket frame handler threw: ${err instanceof Error ? err.message : String(err)}`
11563
- );
11564
- }
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
- var TERMINAL_KINDS = /* @__PURE__ */ new Set(["completion", "error"]);
11668
- function createRunStreamClient(opts) {
11669
- const log = opts.log ?? (() => {
11670
- });
11671
- const now = opts.now ?? (() => Date.now());
11672
- const reconnectDelays = opts.reconnectDelaysMs ?? DEFAULT_RECONNECT_DELAYS_MS;
11673
- const heartbeatMs = opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS2;
11674
- const watchdogMs = opts.watchdogTimeoutMs ?? DEFAULT_WATCHDOG_MS;
11675
- const maxQueueSize = opts.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
11676
- const makeWebSocket = opts.makeWebSocket ?? ((url) => new WebSocket3(url));
11677
- const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
11678
- const clearTimer = opts.clearTimer ?? ((t) => clearTimeout(t));
11679
- let nextSeq = 1;
11680
- const queue = [];
11681
- let ws = null;
11682
- let connectAttempt = 0;
11683
- let stopped = false;
11684
- let serverTerminated = false;
11685
- let heartbeatTimer = null;
11686
- let watchdogTimer = null;
11687
- let reconnectTimer = null;
11688
- let resumeResolved = false;
11689
- let firstConnect = null;
11690
- const terminationWaiters = [];
11691
- function trimQueueUpTo(lastAckedSeq) {
11692
- while (queue.length > 0 && queue[0].seq <= lastAckedSeq) {
11693
- queue.shift();
11694
- }
11695
- }
11696
- function trimQueueUpToExclusive(expectedSeq) {
11697
- while (queue.length > 0 && queue[0].seq < expectedSeq) {
11698
- queue.shift();
11699
- }
11700
- }
11701
- function clearTimers() {
11702
- if (heartbeatTimer) {
11703
- clearTimer(heartbeatTimer);
11704
- heartbeatTimer = null;
11705
- }
11706
- if (watchdogTimer) {
11707
- clearTimer(watchdogTimer);
11708
- watchdogTimer = null;
11709
- }
11710
- if (reconnectTimer) {
11711
- clearTimer(reconnectTimer);
11712
- reconnectTimer = null;
11713
- }
11714
- }
11715
- function startHeartbeat() {
11716
- if (heartbeatMs <= 0) return;
11717
- const tick = () => {
11718
- if (!ws || ws.readyState !== ws.OPEN) return;
11719
- try {
11720
- ws.send(JSON.stringify({ type: "ping", ts: now() }));
11721
- } catch (err) {
11722
- log(
11723
- "warn",
11724
- `run-stream ping failed: ${err instanceof Error ? err.message : String(err)}`
11725
- );
11726
- }
11727
- heartbeatTimer = setTimer(tick, heartbeatMs);
11728
- heartbeatTimer.unref?.();
11729
- };
11730
- heartbeatTimer = setTimer(tick, heartbeatMs);
11731
- heartbeatTimer.unref?.();
11732
- }
11733
- function bumpWatchdog() {
11734
- if (watchdogMs <= 0) return;
11735
- if (watchdogTimer) clearTimer(watchdogTimer);
11736
- watchdogTimer = setTimer(() => {
11737
- log(
11738
- "warn",
11739
- `run-stream watchdog: no server frames for ${watchdogMs}ms \u2014 recycling socket`
11740
- );
11741
- try {
11742
- ws?.terminate();
11743
- } catch {
11744
- }
11745
- }, watchdogMs);
11746
- watchdogTimer.unref?.();
11747
- }
11748
- function flushQueue() {
11749
- if (!ws || ws.readyState !== ws.OPEN || !resumeResolved) return;
11750
- for (const frame of queue) {
11751
- try {
11752
- ws.send(JSON.stringify(frame));
11874
+ ws.send(JSON.stringify(frame));
11753
11875
  } catch (err) {
11754
11876
  log(
11755
11877
  "warn",
@@ -11830,7 +11952,7 @@ function createRunStreamClient(opts) {
11830
11952
  reconnectTimer = null;
11831
11953
  void openSocket();
11832
11954
  }, delayMs);
11833
- reconnectTimer.unref?.();
11955
+ reconnectTimer.unref();
11834
11956
  }
11835
11957
  async function openSocket() {
11836
11958
  if (stopped || serverTerminated) return;
@@ -11856,7 +11978,7 @@ function createRunStreamClient(opts) {
11856
11978
  if (next !== ws) return;
11857
11979
  ws = null;
11858
11980
  clearTimers();
11859
- const reason = reasonBuf?.toString?.() || "closed";
11981
+ const reason = reasonBuf.toString() || "closed";
11860
11982
  if (code === 1e3 && reason === "run_terminated") {
11861
11983
  notifyTerminated();
11862
11984
  return;
@@ -11889,9 +12011,8 @@ function createRunStreamClient(opts) {
11889
12011
  });
11890
12012
  },
11891
12013
  sendEvent(input) {
11892
- const seq = nextSeq++;
11893
12014
  const frame = {
11894
- seq,
12015
+ seq: nextSeq++,
11895
12016
  ts: input.ts,
11896
12017
  kind: input.kind,
11897
12018
  payload: input.payload ?? null
@@ -11914,9 +12035,6 @@ function createRunStreamClient(opts) {
11914
12035
  );
11915
12036
  }
11916
12037
  }
11917
- if (TERMINAL_KINDS.has(frame.kind)) {
11918
- }
11919
- return seq;
11920
12038
  },
11921
12039
  whenTerminated() {
11922
12040
  if (serverTerminated) return Promise.resolve();
@@ -11945,6 +12063,7 @@ function createRunStreamClient(opts) {
11945
12063
  }
11946
12064
 
11947
12065
  // src/app/dashboard/remoteRunExecutor.ts
12066
+ var DEFAULT_MARKETPLACE_SLUG = "lespaceman/athena-workflow-marketplace";
11948
12067
  function parseRunSpec(value) {
11949
12068
  if (typeof value !== "object" || value === null) return null;
11950
12069
  const obj = value;
@@ -11954,11 +12073,18 @@ function parseRunSpec(value) {
11954
12073
  const workflow = obj["workflow"];
11955
12074
  const callbackWsUrl = obj["callbackWsUrl"];
11956
12075
  const callbackToken = obj["callbackToken"];
12076
+ const workflowObj = typeof workflow === "object" && workflow !== null ? workflow : null;
11957
12077
  return {
11958
12078
  prompt,
12079
+ athenaSessionId: typeof obj["athenaSessionId"] === "string" && obj["athenaSessionId"].length > 0 ? obj["athenaSessionId"] : void 0,
12080
+ adapterResumeSessionId: typeof obj["adapterResumeSessionId"] === "string" && obj["adapterResumeSessionId"].length > 0 ? obj["adapterResumeSessionId"] : void 0,
11959
12081
  sessionId: typeof obj["sessionId"] === "string" && obj["sessionId"].length > 0 ? obj["sessionId"] : void 0,
11960
12082
  projectDir: typeof obj["projectDir"] === "string" && obj["projectDir"].length > 0 ? obj["projectDir"] : void 0,
11961
- workflow: typeof workflow === "object" && workflow !== null && typeof workflow["ref"] === "string" ? { ref: workflow["ref"] } : void 0,
12083
+ workflow: workflowObj && typeof workflowObj["ref"] === "string" ? {
12084
+ ref: workflowObj["ref"],
12085
+ ...typeof workflowObj["source"] === "string" ? { source: workflowObj["source"] } : {},
12086
+ ...typeof workflowObj["version"] === "string" ? { version: workflowObj["version"] } : {}
12087
+ } : void 0,
11962
12088
  env: typeof env === "object" && env !== null ? Object.fromEntries(
11963
12089
  Object.entries(env).filter(
11964
12090
  (entry) => typeof entry[1] === "string"
@@ -11974,6 +12100,47 @@ function workflowNameFromRef(ref) {
11974
12100
  const [name] = ref.split("@", 1);
11975
12101
  return name && name.length > 0 ? name : void 0;
11976
12102
  }
12103
+ function isMissingWorkflowError(err, workflowName) {
12104
+ return err instanceof Error && err.message.includes(`Workflow "${workflowName}" not found`);
12105
+ }
12106
+ function configuredWorkflowSources(readGlobalConfigFn) {
12107
+ const sources = readGlobalConfigFn().workflowMarketplaceSources;
12108
+ return sources && sources.length > 0 ? sources : [DEFAULT_MARKETPLACE_SLUG];
12109
+ }
12110
+ function workflowInstallRef(spec) {
12111
+ const ref = spec.workflow?.ref;
12112
+ if (!ref) return void 0;
12113
+ const version = spec.workflow?.version;
12114
+ if (version && !ref.includes("@")) {
12115
+ return `${ref}@${version}`;
12116
+ }
12117
+ return ref;
12118
+ }
12119
+ function workflowInstallSources(spec, readGlobalConfigFn) {
12120
+ const source = spec.workflow?.source?.trim();
12121
+ if (source && source !== "marketplace") {
12122
+ return [source];
12123
+ }
12124
+ return configuredWorkflowSources(readGlobalConfigFn);
12125
+ }
12126
+ function ensureRemoteWorkflowInstalled(input) {
12127
+ const ref = workflowInstallRef(input.spec);
12128
+ const workflowName = workflowNameFromRef(ref);
12129
+ if (!workflowName) return void 0;
12130
+ try {
12131
+ input.resolveWorkflowFn(workflowName);
12132
+ return workflowName;
12133
+ } catch (err) {
12134
+ if (!isMissingWorkflowError(err, workflowName)) {
12135
+ throw err;
12136
+ }
12137
+ }
12138
+ const resolved = input.resolveWorkflowInstallFn(
12139
+ ref,
12140
+ workflowInstallSources(input.spec, input.readGlobalConfigFn)
12141
+ );
12142
+ return input.installWorkflowFromSourceFn(resolved);
12143
+ }
11977
12144
  function eventKind(event) {
11978
12145
  if (event.type === "exec.completed") {
11979
12146
  const data = event.data;
@@ -11993,22 +12160,18 @@ function eventPayload(event) {
11993
12160
  }
11994
12161
  return event.data ?? null;
11995
12162
  }
11996
- function withEnv(env, fn) {
11997
- if (!env || Object.keys(env).length === 0) return fn();
11998
- const previous = /* @__PURE__ */ new Map();
11999
- for (const [key, value] of Object.entries(env)) {
12000
- previous.set(key, process.env[key]);
12001
- process.env[key] = value;
12002
- }
12003
- return fn().finally(() => {
12004
- for (const [key, value] of previous) {
12005
- if (value === void 0) {
12006
- delete process.env[key];
12007
- } else {
12008
- process.env[key] = value;
12009
- }
12010
- }
12011
- });
12163
+ function mergeRunSpecEnvIntoWorkflow(workflow, env) {
12164
+ if (!env || Object.keys(env).length === 0) return workflow;
12165
+ const mergedEnv = { ...workflow?.env ?? {}, ...env };
12166
+ if (workflow) {
12167
+ return { ...workflow, env: mergedEnv };
12168
+ }
12169
+ return {
12170
+ name: "dashboard-remote",
12171
+ plugins: [],
12172
+ promptTemplate: "{input}",
12173
+ env: mergedEnv
12174
+ };
12012
12175
  }
12013
12176
  async function executeRemoteAssignment({
12014
12177
  frame,
@@ -12017,14 +12180,21 @@ async function executeRemoteAssignment({
12017
12180
  log = () => {
12018
12181
  },
12019
12182
  runExecFn = runExec,
12183
+ decisionInbox,
12020
12184
  bootstrapRuntimeConfigFn = bootstrapRuntimeConfig,
12021
12185
  now = Date.now,
12022
12186
  abortSignal,
12023
12187
  createRunStreamClientFn = createRunStreamClient,
12188
+ resolveWorkflowFn = resolveWorkflow,
12189
+ resolveWorkflowInstallFn = resolveWorkflowInstall,
12190
+ installWorkflowFromSourceFn = installWorkflowFromSource,
12191
+ readGlobalConfigFn = readGlobalConfig,
12024
12192
  runStreamConnectTimeoutMs = 5e3
12025
12193
  }) {
12026
- let lastTerminalFailureMessage = null;
12027
- let deferredFailedCompletion = null;
12194
+ const lastTerminalFailureMessage = { current: null };
12195
+ const deferredFailedCompletion = {
12196
+ current: null
12197
+ };
12028
12198
  const spec = parseRunSpec(frame.runSpec);
12029
12199
  let runStream = null;
12030
12200
  if (spec?.callbackWsUrl && spec.callbackToken) {
@@ -12036,7 +12206,7 @@ async function executeRemoteAssignment({
12036
12206
  });
12037
12207
  const timeoutPromise = new Promise((resolve) => {
12038
12208
  const t = setTimeout(() => resolve("timeout"), runStreamConnectTimeoutMs);
12039
- t.unref?.();
12209
+ t.unref();
12040
12210
  });
12041
12211
  try {
12042
12212
  const result = await Promise.race([
@@ -12062,7 +12232,7 @@ async function executeRemoteAssignment({
12062
12232
  let legacySeq = 0;
12063
12233
  const send = (kind, payload, ts = now()) => {
12064
12234
  if (kind === "error" && typeof payload === "object" && payload !== null && typeof payload.message === "string") {
12065
- lastTerminalFailureMessage = payload.message;
12235
+ lastTerminalFailureMessage.current = payload.message;
12066
12236
  }
12067
12237
  if (runStream) {
12068
12238
  runStream.sendEvent({ ts, kind, payload });
@@ -12078,100 +12248,101 @@ async function executeRemoteAssignment({
12078
12248
  });
12079
12249
  };
12080
12250
  send("progress", { message: "assignment received" });
12081
- const closeRunStream = async (reason) => {
12082
- if (!runStream) return;
12083
- const drainTimeout = new Promise((resolve) => {
12084
- const t = setTimeout(() => resolve(), 1e4);
12085
- t.unref?.();
12086
- });
12087
- await Promise.race([runStream.whenTerminated(), drainTimeout]);
12088
- await runStream.close(reason);
12089
- runStream = null;
12090
- };
12091
- if (!spec) {
12092
- send("error", {
12093
- message: "remote assignment missing prompt"
12094
- });
12095
- await closeRunStream("done");
12096
- return;
12097
- }
12098
- const projectDir = spec.projectDir ?? fallbackProjectDir;
12099
- let runtimeConfig;
12100
12251
  try {
12101
- runtimeConfig = bootstrapRuntimeConfigFn({
12102
- projectDir,
12103
- showSetup: false,
12104
- isolationPreset: "minimal",
12105
- workflowOverride: workflowNameFromRef(spec.workflow?.ref)
12106
- });
12107
- } catch (err) {
12108
- send("error", {
12109
- message: err instanceof Error ? err.message : String(err)
12110
- });
12111
- await closeRunStream("done");
12112
- return;
12113
- }
12114
- for (const warning of runtimeConfig.warnings) {
12115
- send("warning", { message: warning });
12116
- }
12117
- let buffered = "";
12118
- const stdout = {
12119
- write(chunk) {
12120
- buffered += chunk;
12121
- let newline = buffered.indexOf("\n");
12122
- while (newline >= 0) {
12123
- const line = buffered.slice(0, newline).trim();
12124
- buffered = buffered.slice(newline + 1);
12125
- if (line.length > 0) {
12126
- try {
12127
- const event = JSON.parse(line);
12128
- const data = event.data;
12129
- if (event.type === "exec.completed" && data?.success === false) {
12130
- deferredFailedCompletion = event;
12131
- continue;
12252
+ if (!spec) {
12253
+ send("error", { message: "remote assignment missing prompt" });
12254
+ return;
12255
+ }
12256
+ const projectDir = spec.projectDir ?? fallbackProjectDir;
12257
+ let runtimeConfig;
12258
+ try {
12259
+ const workflowOverride = ensureRemoteWorkflowInstalled({
12260
+ spec,
12261
+ resolveWorkflowFn,
12262
+ resolveWorkflowInstallFn,
12263
+ installWorkflowFromSourceFn,
12264
+ readGlobalConfigFn
12265
+ });
12266
+ runtimeConfig = bootstrapRuntimeConfigFn({
12267
+ projectDir,
12268
+ showSetup: false,
12269
+ isolationPreset: "minimal",
12270
+ workflowOverride
12271
+ });
12272
+ } catch (err) {
12273
+ send("error", {
12274
+ message: err instanceof Error ? err.message : String(err)
12275
+ });
12276
+ return;
12277
+ }
12278
+ for (const warning of runtimeConfig.warnings) {
12279
+ send("warning", { message: warning });
12280
+ }
12281
+ let buffered = "";
12282
+ const stdout = {
12283
+ write(chunk) {
12284
+ buffered += chunk;
12285
+ let newline = buffered.indexOf("\n");
12286
+ while (newline >= 0) {
12287
+ const line = buffered.slice(0, newline).trim();
12288
+ buffered = buffered.slice(newline + 1);
12289
+ if (line.length > 0) {
12290
+ try {
12291
+ const event = JSON.parse(line);
12292
+ const data = event.data;
12293
+ if (event.type === "exec.completed" && data?.success === false) {
12294
+ deferredFailedCompletion.current = event;
12295
+ continue;
12296
+ }
12297
+ send(eventKind(event), eventPayload(event), now());
12298
+ } catch (err) {
12299
+ send("progress", { line });
12300
+ log(
12301
+ "warn",
12302
+ `remote run emitted malformed JSONL: ${err instanceof Error ? err.message : String(err)}`
12303
+ );
12132
12304
  }
12133
- send(eventKind(event), eventPayload(event), now());
12134
- } catch (err) {
12135
- send("progress", { line });
12136
- log(
12137
- "warn",
12138
- `remote run emitted malformed JSONL: ${err instanceof Error ? err.message : String(err)}`
12139
- );
12140
12305
  }
12306
+ newline = buffered.indexOf("\n");
12141
12307
  }
12142
- newline = buffered.indexOf("\n");
12308
+ return true;
12143
12309
  }
12144
- return true;
12145
- }
12146
- };
12147
- const stderr = {
12148
- write(chunk) {
12149
- const text = chunk.trim();
12150
- if (text.length > 0) send("stderr", { text });
12151
- return true;
12152
- }
12153
- };
12154
- try {
12155
- await withEnv(spec.env, async () => {
12310
+ };
12311
+ const stderr = {
12312
+ write(chunk) {
12313
+ const text = chunk.trim();
12314
+ if (text.length > 0) send("stderr", { text });
12315
+ return true;
12316
+ }
12317
+ };
12318
+ try {
12319
+ const workflow = mergeRunSpecEnvIntoWorkflow(
12320
+ runtimeConfig.workflow,
12321
+ spec.env
12322
+ );
12156
12323
  const result = await runExecFn({
12157
12324
  prompt: spec.prompt,
12158
12325
  projectDir,
12159
12326
  harness: runtimeConfig.harness,
12160
- athenaSessionId: spec.sessionId ?? `athena-${frame.runId}`,
12327
+ athenaSessionId: spec.athenaSessionId ?? spec.sessionId ?? `athena-${frame.runId}`,
12328
+ adapterResumeSessionId: spec.adapterResumeSessionId,
12161
12329
  isolationConfig: runtimeConfig.isolationConfig,
12162
12330
  pluginMcpConfig: runtimeConfig.pluginMcpConfig,
12163
- workflow: runtimeConfig.workflow,
12331
+ workflow,
12164
12332
  workflowPlan: runtimeConfig.workflowPlan,
12333
+ dashboardOrigin: "dashboard",
12165
12334
  json: true,
12166
12335
  verbose: false,
12167
12336
  ephemeral: false,
12168
12337
  timeoutMs: spec.timeoutSec ? spec.timeoutSec * 1e3 : void 0,
12169
12338
  signal: abortSignal,
12170
12339
  stdout,
12171
- stderr
12340
+ stderr,
12341
+ ...decisionInbox ? { dashboardDecisionInbox: decisionInbox } : {}
12172
12342
  });
12173
- if (deferredFailedCompletion) {
12174
- const data = typeof deferredFailedCompletion.data === "object" && deferredFailedCompletion.data !== null ? deferredFailedCompletion.data : {};
12343
+ const failedCompletion = deferredFailedCompletion.current;
12344
+ if (failedCompletion) {
12345
+ const data = typeof failedCompletion.data === "object" && failedCompletion.data !== null ? failedCompletion.data : {};
12175
12346
  send(
12176
12347
  "error",
12177
12348
  {
@@ -12183,13 +12354,13 @@ async function executeRemoteAssignment({
12183
12354
  finalMessage: result.finalMessage,
12184
12355
  tokens: result.tokens,
12185
12356
  durationMs: result.durationMs,
12186
- message: result.failure?.message ?? eventPayload(deferredFailedCompletion).message ?? "remote execution failed"
12357
+ message: result.failure?.message ?? eventPayload(failedCompletion).message ?? "remote execution failed"
12187
12358
  },
12188
- typeof deferredFailedCompletion.ts === "number" ? deferredFailedCompletion.ts : now()
12359
+ typeof failedCompletion.ts === "number" ? failedCompletion.ts : now()
12189
12360
  );
12190
12361
  return;
12191
12362
  }
12192
- if (result.failure && result.failure.message !== lastTerminalFailureMessage) {
12363
+ if (result.failure && result.failure.message !== lastTerminalFailureMessage.current) {
12193
12364
  send("error", {
12194
12365
  success: result.success,
12195
12366
  exitCode: result.exitCode,
@@ -12201,28 +12372,321 @@ async function executeRemoteAssignment({
12201
12372
  message: result.failure.message
12202
12373
  });
12203
12374
  }
12375
+ } catch (err) {
12376
+ send("error", {
12377
+ message: err instanceof Error ? err.message : String(err)
12378
+ });
12379
+ }
12380
+ } finally {
12381
+ if (runStream) {
12382
+ const drainTimeout = new Promise((resolve) => {
12383
+ const t = setTimeout(() => resolve(), 1e4);
12384
+ t.unref();
12385
+ });
12386
+ await Promise.race([runStream.whenTerminated(), drainTimeout]);
12387
+ await runStream.close("done");
12388
+ }
12389
+ }
12390
+ }
12391
+
12392
+ // src/app/dashboard/dashboardDecisionInbox.ts
12393
+ import Database4 from "better-sqlite3";
12394
+ function dashboardDecisionInboxPath() {
12395
+ return `${ensureDaemonStateDir().dir}/dashboard-decision-inbox.db`;
12396
+ }
12397
+ function hasLegacyUniqueConstraint(db) {
12398
+ const rows = db.prepare(`PRAGMA index_list('dashboard_decision_inbox')`).all();
12399
+ return rows.some((row) => row.unique === 1 && row.origin === "u");
12400
+ }
12401
+ function migrateLegacyUniqueConstraint(db) {
12402
+ if (!hasLegacyUniqueConstraint(db)) return;
12403
+ db.exec(`
12404
+ ALTER TABLE dashboard_decision_inbox
12405
+ RENAME TO dashboard_decision_inbox_legacy;
12406
+
12407
+ CREATE TABLE dashboard_decision_inbox (
12408
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
12409
+ athena_session_id TEXT NOT NULL,
12410
+ request_id TEXT NOT NULL,
12411
+ decision_json TEXT NOT NULL,
12412
+ received_at INTEGER NOT NULL,
12413
+ consumed_at INTEGER
12414
+ );
12415
+
12416
+ INSERT INTO dashboard_decision_inbox (
12417
+ id,
12418
+ athena_session_id,
12419
+ request_id,
12420
+ decision_json,
12421
+ received_at,
12422
+ consumed_at
12423
+ )
12424
+ SELECT
12425
+ id,
12426
+ athena_session_id,
12427
+ request_id,
12428
+ decision_json,
12429
+ received_at,
12430
+ consumed_at
12431
+ FROM dashboard_decision_inbox_legacy;
12432
+
12433
+ DROP TABLE dashboard_decision_inbox_legacy;
12434
+ `);
12435
+ }
12436
+ function initInboxSchema(db) {
12437
+ db.exec(`
12438
+ PRAGMA journal_mode = WAL;
12439
+
12440
+ CREATE TABLE IF NOT EXISTS dashboard_decision_inbox (
12441
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
12442
+ athena_session_id TEXT NOT NULL,
12443
+ request_id TEXT NOT NULL,
12444
+ decision_json TEXT NOT NULL,
12445
+ received_at INTEGER NOT NULL,
12446
+ consumed_at INTEGER
12447
+ );
12448
+ `);
12449
+ migrateLegacyUniqueConstraint(db);
12450
+ db.exec(`
12451
+
12452
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_dashboard_decision_unconsumed
12453
+ ON dashboard_decision_inbox(athena_session_id, request_id)
12454
+ WHERE consumed_at IS NULL;
12455
+
12456
+ CREATE INDEX IF NOT EXISTS idx_dashboard_decision_pending
12457
+ ON dashboard_decision_inbox(athena_session_id, consumed_at, id);
12458
+ `);
12459
+ }
12460
+ function createDashboardDecisionInbox(options = {}) {
12461
+ const db = new Database4(options.dbPath ?? dashboardDecisionInboxPath());
12462
+ initInboxSchema(db);
12463
+ const upsertUnconsumed = db.prepare(`
12464
+ INSERT INTO dashboard_decision_inbox (
12465
+ athena_session_id,
12466
+ request_id,
12467
+ decision_json,
12468
+ received_at
12469
+ )
12470
+ VALUES (?, ?, ?, ?)
12471
+ ON CONFLICT(athena_session_id, request_id) WHERE consumed_at IS NULL
12472
+ DO UPDATE SET
12473
+ decision_json = excluded.decision_json,
12474
+ received_at = excluded.received_at
12475
+ `);
12476
+ const selectPending = db.prepare(`
12477
+ SELECT id, athena_session_id, request_id, decision_json, received_at
12478
+ FROM dashboard_decision_inbox
12479
+ WHERE athena_session_id = ? AND consumed_at IS NULL
12480
+ ORDER BY id ASC
12481
+ LIMIT ?
12482
+ `);
12483
+ const consume = db.prepare(`
12484
+ UPDATE dashboard_decision_inbox
12485
+ SET consumed_at = ?
12486
+ WHERE id = ?
12487
+ `);
12488
+ return {
12489
+ enqueue(input) {
12490
+ upsertUnconsumed.run(
12491
+ input.athenaSessionId,
12492
+ input.requestId,
12493
+ JSON.stringify(input.decision),
12494
+ input.receivedAt
12495
+ );
12496
+ },
12497
+ pendingForSession(input) {
12498
+ const rows = selectPending.all(
12499
+ input.athenaSessionId,
12500
+ input.limit
12501
+ );
12502
+ return rows.map((row) => ({
12503
+ id: row.id,
12504
+ athenaSessionId: row.athena_session_id,
12505
+ requestId: row.request_id,
12506
+ decision: JSON.parse(row.decision_json),
12507
+ receivedAt: row.received_at
12508
+ }));
12509
+ },
12510
+ markConsumed(input) {
12511
+ consume.run(Date.now(), input.id);
12512
+ },
12513
+ close() {
12514
+ db.close();
12515
+ }
12516
+ };
12517
+ }
12518
+
12519
+ // src/app/dashboard/dashboardPairedExecution.ts
12520
+ var DEFAULT_MAX_CONCURRENT_RUNS = 1;
12521
+ var DEFAULT_RUN_HISTORY_LIMIT = 100;
12522
+ function createDashboardPairedExecution(options) {
12523
+ const client = options.client;
12524
+ const executor = options.executor;
12525
+ const projectDir = options.projectDir;
12526
+ const decisionInbox = options.decisionInbox;
12527
+ const log = options.log ?? (() => {
12528
+ });
12529
+ const maxConcurrentRuns = options.maxConcurrentRuns ?? DEFAULT_MAX_CONCURRENT_RUNS;
12530
+ const runHistoryLimit = options.runHistoryLimit ?? DEFAULT_RUN_HISTORY_LIMIT;
12531
+ const now = options.now ?? (() => Date.now());
12532
+ let completedRuns = 0;
12533
+ const active = /* @__PURE__ */ new Map();
12534
+ const activeByRunner = /* @__PURE__ */ new Map();
12535
+ const runHistory = [];
12536
+ function recordRun(record) {
12537
+ runHistory.push(record);
12538
+ while (runHistory.length > runHistoryLimit) {
12539
+ runHistory.shift();
12540
+ }
12541
+ }
12542
+ function rejectAssignment(runId, reason) {
12543
+ try {
12544
+ client.sendRunEvent({
12545
+ runId,
12546
+ seq: 0,
12547
+ ts: now(),
12548
+ kind: "rejected",
12549
+ payload: { reason }
12550
+ });
12551
+ } catch (err) {
12552
+ log(
12553
+ "warn",
12554
+ `runtime daemon: failed to send rejected for ${runId}: ${err instanceof Error ? err.message : String(err)}`
12555
+ );
12556
+ }
12557
+ recordRun({
12558
+ runId,
12559
+ startedAt: now(),
12560
+ endedAt: now(),
12561
+ status: "rejected",
12562
+ error: reason
12204
12563
  });
12205
- } catch (err) {
12206
- send("error", {
12207
- message: err instanceof Error ? err.message : String(err)
12564
+ log("warn", `run ${runId} rejected: ${reason}`);
12565
+ }
12566
+ function handleDecision(frame) {
12567
+ decisionInbox.enqueue({
12568
+ athenaSessionId: frame.athenaSessionId,
12569
+ requestId: frame.requestId,
12570
+ decision: frame.decision,
12571
+ receivedAt: now()
12208
12572
  });
12209
- } finally {
12210
- await closeRunStream("done");
12211
12573
  }
12574
+ function handleCancel(frame) {
12575
+ const entry = active.get(frame.runId);
12576
+ if (!entry) return;
12577
+ entry.record.status = "cancelled";
12578
+ entry.controller.abort();
12579
+ }
12580
+ function handleAssignment(frame) {
12581
+ if (active.has(frame.runId)) {
12582
+ rejectAssignment(
12583
+ frame.runId,
12584
+ `duplicate active assignment ${frame.runId}`
12585
+ );
12586
+ return;
12587
+ }
12588
+ const runnerKey = frame.runnerId;
12589
+ const bucket = activeByRunner.get(runnerKey) ?? /* @__PURE__ */ new Set();
12590
+ if (bucket.size >= maxConcurrentRuns) {
12591
+ rejectAssignment(
12592
+ frame.runId,
12593
+ `runtime daemon at concurrency cap (${maxConcurrentRuns}) for runner ${runnerKey ?? "<legacy>"}`
12594
+ );
12595
+ return;
12596
+ }
12597
+ const controller = new AbortController();
12598
+ const record = {
12599
+ runId: frame.runId,
12600
+ startedAt: now(),
12601
+ status: "running"
12602
+ };
12603
+ recordRun(record);
12604
+ bucket.add(frame.runId);
12605
+ activeByRunner.set(runnerKey, bucket);
12606
+ const promise = executor({
12607
+ frame,
12608
+ client,
12609
+ projectDir,
12610
+ log,
12611
+ abortSignal: controller.signal,
12612
+ decisionInbox
12613
+ }).then(() => {
12614
+ if (record.status === "running") record.status = "completed";
12615
+ }).catch((err) => {
12616
+ if (record.status === "running") {
12617
+ record.status = "failed";
12618
+ }
12619
+ record.error = err instanceof Error ? err.message : String(err);
12620
+ log(
12621
+ "error",
12622
+ `run ${frame.runId} failed: ${err instanceof Error ? err.message : String(err)}`
12623
+ );
12624
+ }).finally(() => {
12625
+ record.endedAt = now();
12626
+ completedRuns += 1;
12627
+ active.delete(frame.runId);
12628
+ const remaining = activeByRunner.get(runnerKey);
12629
+ if (remaining) {
12630
+ remaining.delete(frame.runId);
12631
+ if (remaining.size === 0) activeByRunner.delete(runnerKey);
12632
+ }
12633
+ });
12634
+ active.set(frame.runId, { controller, promise, record, runnerKey });
12635
+ }
12636
+ return {
12637
+ handleFrame(frame) {
12638
+ if (frame.type === "dashboard_decision") {
12639
+ handleDecision(frame);
12640
+ return true;
12641
+ }
12642
+ if (frame.type === "cancel") {
12643
+ handleCancel(frame);
12644
+ return true;
12645
+ }
12646
+ if (frame.type === "job_assignment") {
12647
+ handleAssignment(frame);
12648
+ return true;
12649
+ }
12650
+ return false;
12651
+ },
12652
+ snapshot() {
12653
+ return {
12654
+ activeRuns: active.size,
12655
+ completedRuns
12656
+ };
12657
+ },
12658
+ listRuns(opts = {}) {
12659
+ let out = runHistory.slice();
12660
+ if (typeof opts.limit === "number" && opts.limit > 0) {
12661
+ out = out.slice(-opts.limit);
12662
+ }
12663
+ if (opts.active) {
12664
+ out = out.filter((r) => r.status === "running");
12665
+ }
12666
+ return out;
12667
+ },
12668
+ async stop() {
12669
+ for (const run of active.values()) {
12670
+ run.controller.abort();
12671
+ }
12672
+ await Promise.allSettled([...active.values()].map((run) => run.promise));
12673
+ }
12674
+ };
12212
12675
  }
12213
12676
 
12214
12677
  // src/app/dashboard/runtimeDaemon.ts
12215
12678
  var DEFAULT_RECONNECT_DELAYS_MS2 = [1e3, 2e3, 5e3, 1e4, 3e4];
12216
- var DEFAULT_MAX_CONCURRENT_RUNS = 1;
12679
+ var DEFAULT_MAX_CONCURRENT_RUNS2 = 1;
12217
12680
  var DEFAULT_REFRESH_LEAD_SEC = 60;
12218
12681
  var DEFAULT_REFRESH_FAILURE_LIMIT = 5;
12219
12682
  var DEFAULT_REFRESH_FAILURE_WINDOW_MS = 5 * 6e4;
12220
12683
  var DEFAULT_REFRESH_COOLDOWN_MS = 5 * 6e4;
12221
- var DEFAULT_RUN_HISTORY_LIMIT = 100;
12684
+ var DEFAULT_RUN_HISTORY_LIMIT2 = 100;
12685
+ var DEFAULT_FEED_DRAIN_INTERVAL_MS = 1e3;
12222
12686
  function delay(ms) {
12223
12687
  return new Promise((resolve) => {
12224
12688
  const timer = setTimeout(resolve, ms);
12225
- timer.unref?.();
12689
+ timer.unref();
12226
12690
  });
12227
12691
  }
12228
12692
  async function runDashboardRuntimeDaemon(options = {}) {
@@ -12239,32 +12703,52 @@ async function runDashboardRuntimeDaemon(options = {}) {
12239
12703
  const log = options.log ?? (() => {
12240
12704
  });
12241
12705
  const reconnectDelays = options.reconnectDelaysMs ?? DEFAULT_RECONNECT_DELAYS_MS2;
12242
- const maxConcurrentRuns = options.maxConcurrentRuns ?? DEFAULT_MAX_CONCURRENT_RUNS;
12706
+ const maxConcurrentRuns = options.maxConcurrentRuns ?? DEFAULT_MAX_CONCURRENT_RUNS2;
12243
12707
  const refreshLeadSec = options.refreshLeadSec ?? DEFAULT_REFRESH_LEAD_SEC;
12244
12708
  const refreshFailureLimit = options.refreshFailureLimit ?? DEFAULT_REFRESH_FAILURE_LIMIT;
12245
12709
  const refreshFailureWindowMs = options.refreshFailureWindowMs ?? DEFAULT_REFRESH_FAILURE_WINDOW_MS;
12246
12710
  const refreshCooldownMs = options.refreshCooldownMs ?? DEFAULT_REFRESH_COOLDOWN_MS;
12247
- const runHistoryLimit = options.runHistoryLimit ?? DEFAULT_RUN_HISTORY_LIMIT;
12711
+ const runHistoryLimit = options.runHistoryLimit ?? DEFAULT_RUN_HISTORY_LIMIT2;
12248
12712
  const now = options.now ?? (() => Date.now());
12713
+ const writeMirror = options.writeMirror ?? writeAttachmentMirror;
12714
+ const feedOutbox = options.feedOutbox ?? createDashboardFeedOutbox();
12715
+ const decisionInbox = options.decisionInbox ?? createDashboardDecisionInbox();
12716
+ const feedDrainIntervalMs = options.feedDrainIntervalMs ?? DEFAULT_FEED_DRAIN_INTERVAL_MS;
12249
12717
  const startedAt = now();
12250
12718
  let stopped = false;
12251
12719
  let reconnectAttempt = 0;
12252
12720
  let client = null;
12721
+ let lastSocketClient = null;
12253
12722
  let currentInstanceId;
12254
12723
  let currentDashboardUrl;
12255
12724
  let lastFrameAt;
12256
- let completedRuns = 0;
12257
12725
  let refreshTimer = null;
12726
+ let feedDrainTimer = null;
12258
12727
  const refreshFailures = [];
12259
12728
  let cooldownUntil = 0;
12260
- const active = /* @__PURE__ */ new Map();
12261
- const runHistory = [];
12262
- function recordRun(record) {
12263
- runHistory.push(record);
12264
- while (runHistory.length > runHistoryLimit) {
12265
- runHistory.shift();
12729
+ const executionClient = {
12730
+ sendRunEvent(event) {
12731
+ const current = client ?? lastSocketClient;
12732
+ if (!current) {
12733
+ log(
12734
+ "warn",
12735
+ `instance socket dropped run_event (socket not connected): runId=${event.runId} kind=${event.kind}`
12736
+ );
12737
+ return;
12738
+ }
12739
+ current.sendRunEvent(event);
12266
12740
  }
12267
- }
12741
+ };
12742
+ const pairedExecution = createDashboardPairedExecution({
12743
+ client: executionClient,
12744
+ executor,
12745
+ projectDir,
12746
+ decisionInbox,
12747
+ log,
12748
+ maxConcurrentRuns,
12749
+ now,
12750
+ runHistoryLimit
12751
+ });
12268
12752
  function nextReconnectDelay() {
12269
12753
  if (reconnectDelays.length === 0) return 0;
12270
12754
  const delayMs = reconnectDelays[Math.min(reconnectAttempt, reconnectDelays.length - 1)] ?? 0;
@@ -12277,6 +12761,35 @@ async function runDashboardRuntimeDaemon(options = {}) {
12277
12761
  refreshTimer = null;
12278
12762
  }
12279
12763
  }
12764
+ function clearFeedDrainTimer() {
12765
+ if (feedDrainTimer) {
12766
+ clearInterval(feedDrainTimer);
12767
+ feedDrainTimer = null;
12768
+ }
12769
+ }
12770
+ function drainFeedOutbox() {
12771
+ const current = client;
12772
+ if (!current) return;
12773
+ const rows = feedOutbox.pendingBatch({ limit: 100, now: now() });
12774
+ for (const row of rows) {
12775
+ current.sendFeedEvent({
12776
+ deliverySeq: row.deliverySeq,
12777
+ envelope: row.envelope
12778
+ });
12779
+ const retryDelayMs = Math.min(3e4, (row.attempt + 1) * 1e3);
12780
+ feedOutbox.markAttempted({
12781
+ deliverySeq: row.deliverySeq,
12782
+ nextAttemptAt: now() + retryDelayMs
12783
+ });
12784
+ }
12785
+ }
12786
+ function startFeedDrainTimer() {
12787
+ clearFeedDrainTimer();
12788
+ const timer = setInterval(drainFeedOutbox, feedDrainIntervalMs);
12789
+ timer.unref();
12790
+ feedDrainTimer = timer;
12791
+ drainFeedOutbox();
12792
+ }
12280
12793
  function scheduleRefresh(expiresInSec) {
12281
12794
  clearRefreshTimer();
12282
12795
  if (!Number.isFinite(expiresInSec) || expiresInSec <= refreshLeadSec) {
@@ -12286,7 +12799,7 @@ async function runDashboardRuntimeDaemon(options = {}) {
12286
12799
  const timer = setTimeout(() => {
12287
12800
  void proactiveRefresh();
12288
12801
  }, ms);
12289
- timer.unref?.();
12802
+ timer.unref();
12290
12803
  refreshTimer = timer;
12291
12804
  }
12292
12805
  async function proactiveRefresh() {
@@ -12321,30 +12834,6 @@ async function runDashboardRuntimeDaemon(options = {}) {
12321
12834
  );
12322
12835
  }
12323
12836
  }
12324
- function rejectAssignment(client_, runId, reason) {
12325
- try {
12326
- client_.sendRunEvent({
12327
- runId,
12328
- seq: 0,
12329
- ts: now(),
12330
- kind: "rejected",
12331
- payload: { reason }
12332
- });
12333
- } catch (err) {
12334
- log(
12335
- "warn",
12336
- `runtime daemon: failed to send rejected for ${runId}: ${err instanceof Error ? err.message : String(err)}`
12337
- );
12338
- }
12339
- recordRun({
12340
- runId,
12341
- startedAt: now(),
12342
- endedAt: now(),
12343
- status: "rejected",
12344
- error: reason
12345
- });
12346
- log("warn", `run ${runId} rejected: ${reason}`);
12347
- }
12348
12837
  async function connectOnce() {
12349
12838
  const config = readConfig2();
12350
12839
  if (!config) {
@@ -12379,54 +12868,34 @@ async function runDashboardRuntimeDaemon(options = {}) {
12379
12868
  });
12380
12869
  next.onFrame((frame) => {
12381
12870
  lastFrameAt = now();
12382
- if (frame.type === "cancel") {
12383
- const entry = active.get(frame.runId);
12384
- if (entry) {
12385
- entry.record.status = "cancelled";
12386
- entry.controller.abort();
12871
+ if (frame.type === "attachments.changed") {
12872
+ try {
12873
+ writeMirror({
12874
+ instanceId: token.instanceId,
12875
+ fetchedAt: now(),
12876
+ attachments: frame.attachments.map((a) => ({
12877
+ runnerId: a.runnerId,
12878
+ ...a.name !== void 0 ? { name: a.name } : {},
12879
+ ...a.executionTarget !== void 0 ? { executionTarget: a.executionTarget } : {},
12880
+ ...a.remoteInstanceId !== void 0 ? { remoteInstanceId: a.remoteInstanceId } : {}
12881
+ }))
12882
+ });
12883
+ } catch (err) {
12884
+ log(
12885
+ "warn",
12886
+ `runtime daemon: failed to write attachment mirror: ${err instanceof Error ? err.message : String(err)}`
12887
+ );
12387
12888
  }
12388
12889
  return;
12389
12890
  }
12390
- if (frame.type !== "job_assignment") return;
12391
- if (active.has(frame.runId)) return;
12392
- if (active.size >= maxConcurrentRuns) {
12393
- rejectAssignment(
12394
- next,
12395
- frame.runId,
12396
- `runtime daemon at concurrency cap (${maxConcurrentRuns})`
12397
- );
12891
+ if (frame.type === "feed_ack") {
12892
+ feedOutbox.markAcked({
12893
+ ...typeof frame.deliverySeq === "number" ? { deliverySeq: frame.deliverySeq } : {},
12894
+ ...typeof frame.eventId === "string" ? { eventId: frame.eventId } : {}
12895
+ });
12398
12896
  return;
12399
12897
  }
12400
- const controller = new AbortController();
12401
- const record = {
12402
- runId: frame.runId,
12403
- startedAt: now(),
12404
- status: "running"
12405
- };
12406
- recordRun(record);
12407
- const promise = executor({
12408
- frame,
12409
- client: next,
12410
- projectDir,
12411
- log,
12412
- abortSignal: controller.signal
12413
- }).then(() => {
12414
- if (record.status === "running") record.status = "completed";
12415
- }).catch((err) => {
12416
- if (record.status === "running") {
12417
- record.status = "failed";
12418
- }
12419
- record.error = err instanceof Error ? err.message : String(err);
12420
- log(
12421
- "error",
12422
- `run ${frame.runId} failed: ${err instanceof Error ? err.message : String(err)}`
12423
- );
12424
- }).finally(() => {
12425
- record.endedAt = now();
12426
- completedRuns += 1;
12427
- active.delete(frame.runId);
12428
- });
12429
- active.set(frame.runId, { controller, promise, record });
12898
+ pairedExecution.handleFrame(frame);
12430
12899
  });
12431
12900
  next.onClose((reason) => {
12432
12901
  if (stopped || client !== next) return;
@@ -12434,14 +12903,17 @@ async function runDashboardRuntimeDaemon(options = {}) {
12434
12903
  client = null;
12435
12904
  currentInstanceId = void 0;
12436
12905
  clearRefreshTimer();
12906
+ clearFeedDrainTimer();
12437
12907
  void reconnectLoop();
12438
12908
  });
12439
12909
  await next.connect();
12440
12910
  client = next;
12911
+ lastSocketClient = next;
12441
12912
  currentInstanceId = token.instanceId;
12442
12913
  currentDashboardUrl = config.dashboardUrl;
12443
12914
  reconnectAttempt = 0;
12444
12915
  scheduleRefresh(token.expiresInSec);
12916
+ startFeedDrainTimer();
12445
12917
  log("info", `dashboard runtime daemon connected as ${token.instanceId}`);
12446
12918
  }
12447
12919
  async function reconnectLoop() {
@@ -12463,6 +12935,7 @@ async function runDashboardRuntimeDaemon(options = {}) {
12463
12935
  await connectOnce();
12464
12936
  return {
12465
12937
  snapshot() {
12938
+ const executionSnapshot = pairedExecution.snapshot();
12466
12939
  const refreshState = refreshFailures.length > 0 || cooldownUntil > now() ? {
12467
12940
  recentFailures: refreshFailures.length,
12468
12941
  ...cooldownUntil > now() ? { cooldownUntilMs: cooldownUntil } : {}
@@ -12471,158 +12944,30 @@ async function runDashboardRuntimeDaemon(options = {}) {
12471
12944
  startedAt,
12472
12945
  socketConnected: client !== null,
12473
12946
  ...lastFrameAt !== void 0 ? { lastFrameAt } : {},
12474
- activeRuns: active.size,
12475
- completedRuns,
12947
+ activeRuns: executionSnapshot.activeRuns,
12948
+ completedRuns: executionSnapshot.completedRuns,
12476
12949
  ...currentInstanceId ? { instanceId: currentInstanceId } : {},
12477
12950
  ...currentDashboardUrl ? { dashboardUrl: currentDashboardUrl } : {},
12478
12951
  ...refreshState ? { refreshState } : {}
12479
12952
  };
12480
12953
  },
12481
12954
  listRuns(opts = {}) {
12482
- let out = runHistory.slice();
12483
- if (typeof opts.limit === "number" && opts.limit > 0) {
12484
- out = out.slice(-opts.limit);
12485
- }
12486
- if (opts.active) {
12487
- out = out.filter((r) => r.status === "running");
12488
- }
12489
- return out;
12955
+ return pairedExecution.listRuns(opts);
12490
12956
  },
12491
12957
  async stop(reason = "stopped") {
12492
12958
  stopped = true;
12493
12959
  clearRefreshTimer();
12494
- for (const run of active.values()) {
12495
- run.controller.abort();
12496
- }
12960
+ clearFeedDrainTimer();
12497
12961
  const current = client;
12498
12962
  client = null;
12499
12963
  current?.close(reason);
12500
- await Promise.allSettled([...active.values()].map((run) => run.promise));
12501
- }
12502
- };
12503
- }
12504
-
12505
- // src/infra/daemon/stateDir.ts
12506
- import fs20 from "fs";
12507
- import os11 from "os";
12508
- import path18 from "path";
12509
- function daemonStatePaths(env = process.env) {
12510
- const xdg = env["XDG_STATE_HOME"];
12511
- const home = env["HOME"] ?? os11.homedir();
12512
- const base = xdg && xdg.length > 0 ? xdg : path18.join(home, ".local", "state");
12513
- const dir = path18.join(base, "drisp");
12514
- return {
12515
- dir,
12516
- pidPath: path18.join(dir, "dashboard-daemon.pid"),
12517
- logPath: path18.join(dir, "dashboard-daemon.log"),
12518
- socketPath: path18.join(dir, "dashboard-daemon.sock")
12519
- };
12520
- }
12521
- function ensureDaemonStateDir(env = process.env) {
12522
- const paths = daemonStatePaths(env);
12523
- fs20.mkdirSync(paths.dir, { recursive: true, mode: 448 });
12524
- if (process.platform !== "win32") {
12525
- try {
12526
- fs20.chmodSync(paths.dir, 448);
12527
- } catch {
12528
- }
12529
- }
12530
- return paths;
12531
- }
12532
-
12533
- // src/infra/daemon/pidLock.ts
12534
- import fs21 from "fs";
12535
- function acquirePidLock(pidPath) {
12536
- const ownPid = process.pid;
12537
- for (let attempt = 0; attempt < 2; attempt += 1) {
12538
- try {
12539
- const fd = fs21.openSync(pidPath, "wx", 384);
12540
- try {
12541
- fs21.writeSync(fd, `${ownPid}
12542
- `);
12543
- fs21.fsyncSync(fd);
12544
- } finally {
12545
- fs21.closeSync(fd);
12546
- }
12547
- return makeHandle(pidPath, ownPid);
12548
- } catch (err) {
12549
- if (err.code !== "EEXIST") throw err;
12550
- }
12551
- const existing = readPidLock(pidPath);
12552
- if (existing.state === "held") {
12553
- throw new Error(
12554
- `dashboard daemon is already running as pid ${existing.pid} (lock at ${pidPath}). Use "drisp dashboard daemon stop" to terminate it.`
12555
- );
12556
- }
12557
- if (existing.state === "stale") {
12558
- try {
12559
- fs21.unlinkSync(pidPath);
12560
- } catch (err) {
12561
- if (err.code !== "ENOENT") throw err;
12562
- }
12563
- continue;
12564
- }
12565
- }
12566
- throw new Error(
12567
- `dashboard daemon: failed to acquire pid lock at ${pidPath} after retry`
12568
- );
12569
- }
12570
- function readPidLock(pidPath) {
12571
- let raw;
12572
- try {
12573
- raw = fs21.readFileSync(pidPath, "utf-8");
12574
- } catch (err) {
12575
- if (err.code === "ENOENT") {
12576
- return { state: "absent" };
12577
- }
12578
- throw err;
12579
- }
12580
- const pid = Number.parseInt(raw.trim(), 10);
12581
- if (!Number.isFinite(pid) || pid <= 0) {
12582
- return { state: "stale", pid: 0 };
12583
- }
12584
- if (!isProcessAlive(pid)) {
12585
- return { state: "stale", pid };
12586
- }
12587
- return { state: "held", pid };
12588
- }
12589
- function makeHandle(pidPath, pid) {
12590
- let released = false;
12591
- return {
12592
- pid,
12593
- release() {
12594
- if (released) return;
12595
- released = true;
12596
- try {
12597
- const raw = fs21.readFileSync(pidPath, "utf-8").trim();
12598
- if (raw === String(pid)) {
12599
- fs21.unlinkSync(pidPath);
12600
- }
12601
- } catch (err) {
12602
- if (err.code !== "ENOENT") {
12603
- }
12604
- }
12964
+ await pairedExecution.stop();
12605
12965
  }
12606
12966
  };
12607
12967
  }
12608
- function isProcessAlive(pid) {
12609
- if (process.platform === "win32") {
12610
- return true;
12611
- }
12612
- try {
12613
- process.kill(pid, 0);
12614
- return true;
12615
- } catch (err) {
12616
- const code = err.code;
12617
- if (code === "EPERM") {
12618
- return true;
12619
- }
12620
- return false;
12621
- }
12622
- }
12623
12968
 
12624
12969
  // src/infra/daemon/udsIpc.ts
12625
- import fs22 from "fs";
12970
+ import fs21 from "fs";
12626
12971
  import net2 from "net";
12627
12972
 
12628
12973
  // src/infra/daemon/udsFrameCodec.ts
@@ -12667,7 +13012,7 @@ async function startUdsServer(socketPath, handler, log) {
12667
13012
  });
12668
13013
  if (process.platform !== "win32") {
12669
13014
  try {
12670
- fs22.chmodSync(socketPath, 384);
13015
+ fs21.chmodSync(socketPath, 384);
12671
13016
  } catch {
12672
13017
  }
12673
13018
  }
@@ -12677,7 +13022,7 @@ async function startUdsServer(socketPath, handler, log) {
12677
13022
  server.close(() => resolve());
12678
13023
  });
12679
13024
  try {
12680
- fs22.unlinkSync(socketPath);
13025
+ fs21.unlinkSync(socketPath);
12681
13026
  } catch (err) {
12682
13027
  if (err.code !== "ENOENT") {
12683
13028
  }
@@ -12743,7 +13088,7 @@ async function sendUdsRequest(socketPath, request, options = {}) {
12743
13088
  socket.destroy();
12744
13089
  reject(new Error(`uds request timed out after ${timeoutMs}ms`));
12745
13090
  }, timeoutMs);
12746
- timer.unref?.();
13091
+ timer.unref();
12747
13092
  const finish = (action) => {
12748
13093
  if (settled) return;
12749
13094
  settled = true;
@@ -12795,7 +13140,7 @@ async function sendUdsRequest(socketPath, request, options = {}) {
12795
13140
  async function unlinkStaleSocket(socketPath) {
12796
13141
  let stat;
12797
13142
  try {
12798
- stat = fs22.statSync(socketPath);
13143
+ stat = fs21.statSync(socketPath);
12799
13144
  } catch (err) {
12800
13145
  if (err.code === "ENOENT") return;
12801
13146
  throw err;
@@ -12822,7 +13167,7 @@ async function unlinkStaleSocket(socketPath) {
12822
13167
  `uds path ${socketPath} is in use by another process; aborting`
12823
13168
  );
12824
13169
  }
12825
- fs22.unlinkSync(socketPath);
13170
+ fs21.unlinkSync(socketPath);
12826
13171
  }
12827
13172
 
12828
13173
  export {
@@ -12840,6 +13185,10 @@ export {
12840
13185
  ingestRuntimeEvent,
12841
13186
  ingestRuntimeDecision,
12842
13187
  generateId,
13188
+ daemonStatePaths,
13189
+ ensureDaemonStateDir,
13190
+ createDashboardFeedPublisher,
13191
+ createDashboardDecisionInbox,
12843
13192
  createSessionStore,
12844
13193
  sessionsDir,
12845
13194
  listSessions,
@@ -12888,11 +13237,7 @@ export {
12888
13237
  EXEC_EXIT_CODE,
12889
13238
  runExec,
12890
13239
  runDashboardRuntimeDaemon,
12891
- daemonStatePaths,
12892
- ensureDaemonStateDir,
12893
- acquirePidLock,
12894
- readPidLock,
12895
13240
  startUdsServer,
12896
13241
  sendUdsRequest
12897
13242
  };
12898
- //# sourceMappingURL=chunk-JAPBSL7D.js.map
13243
+ //# sourceMappingURL=chunk-TQKNZNDP.js.map