replicas-engine 0.1.230 → 0.1.232

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.
Files changed (2) hide show
  1. package/dist/src/index.js +380 -131
  2. package/package.json +1 -1
package/dist/src/index.js CHANGED
@@ -1652,6 +1652,22 @@ function parseGoalCommand(message) {
1652
1652
  // ../shared/src/replicas-config.ts
1653
1653
  import { parse as parseYaml } from "yaml";
1654
1654
 
1655
+ // ../shared/src/hook-config.ts
1656
+ function resolveHookConfig(value) {
1657
+ if (value === void 0) {
1658
+ return null;
1659
+ }
1660
+ const commands = typeof value === "string" ? [value.trim()].filter(Boolean) : value.commands.map((command) => command.trim()).filter(Boolean);
1661
+ if (commands.length === 0) {
1662
+ return null;
1663
+ }
1664
+ return {
1665
+ commands,
1666
+ timeoutMs: typeof value === "string" ? void 0 : value.timeout,
1667
+ separate: typeof value === "string" ? void 0 : value.separate
1668
+ };
1669
+ }
1670
+
1655
1671
  // ../shared/src/warm-hooks.ts
1656
1672
  var DEFAULT_WARM_HOOK_TIMEOUT_MS = 60 * 60 * 1e3;
1657
1673
  var MAX_WARM_HOOK_TIMEOUT_MS = 120 * 60 * 1e3;
@@ -1667,17 +1683,25 @@ function clampWarmHookTimeoutMs(timeoutMs) {
1667
1683
  }
1668
1684
  return Math.min(timeoutMs, MAX_WARM_HOOK_TIMEOUT_MS);
1669
1685
  }
1670
- function buildHookOutputPreview(text, maxChars = DEFAULT_HOOK_OUTPUT_PREVIEW_CHARS) {
1686
+ function buildOutputPreview(text, {
1687
+ maxChars = DEFAULT_HOOK_OUTPUT_PREVIEW_CHARS,
1688
+ truncateFrom = "head",
1689
+ marker = "\n...[truncated \u2014 download the full log to see the rest]"
1690
+ } = {}) {
1671
1691
  if (text.length <= maxChars) {
1672
1692
  return { output: text, outputTruncated: false, outputTotalChars: text.length };
1673
1693
  }
1694
+ const previewChars = Math.max(0, maxChars - marker.length);
1695
+ const output = previewChars === 0 ? marker.slice(0, maxChars) : truncateFrom === "tail" ? `${marker}${text.slice(-previewChars)}` : `${text.slice(0, previewChars)}${marker}`;
1674
1696
  return {
1675
- output: `${text.slice(0, maxChars)}
1676
- ...[truncated \u2014 download the full log to see the rest]`,
1697
+ output,
1677
1698
  outputTruncated: true,
1678
1699
  outputTotalChars: text.length
1679
1700
  };
1680
1701
  }
1702
+ function buildHookOutputPreview(text, maxChars = DEFAULT_HOOK_OUTPUT_PREVIEW_CHARS) {
1703
+ return buildOutputPreview(text, { maxChars });
1704
+ }
1681
1705
  function parseWarmHookConfig(value, filename = "replicas.json") {
1682
1706
  if (typeof value === "string") {
1683
1707
  return value;
@@ -1704,16 +1728,7 @@ function resolveWarmHookConfig(value) {
1704
1728
  if (value === void 0) {
1705
1729
  return null;
1706
1730
  }
1707
- const parsed = parseWarmHookConfig(value);
1708
- const commands = typeof parsed === "string" ? [parsed.trim()].filter(Boolean) : parsed.commands.map((command) => command.trim()).filter(Boolean);
1709
- if (commands.length === 0) {
1710
- return null;
1711
- }
1712
- return {
1713
- commands,
1714
- timeoutMs: typeof parsed === "string" ? void 0 : parsed.timeout,
1715
- separate: typeof parsed === "string" ? void 0 : parsed.separate
1716
- };
1731
+ return resolveHookConfig(parseWarmHookConfig(value));
1717
1732
  }
1718
1733
 
1719
1734
  // ../shared/src/replicas-config.ts
@@ -1777,7 +1792,7 @@ function isClaudeAuthErrorText(text) {
1777
1792
  }
1778
1793
 
1779
1794
  // ../shared/src/engine/environment.ts
1780
- var DAYTONA_SNAPSHOT_ID = "29-05-2026-royal-york-v1";
1795
+ var DAYTONA_SNAPSHOT_ID = "29-05-2026-royal-york-v3";
1781
1796
 
1782
1797
  // ../shared/src/engine/types.ts
1783
1798
  var DEFAULT_CHAT_TITLES = {
@@ -2023,7 +2038,9 @@ function loadEngineEnv() {
2023
2038
  AWS_REGION: readEnv("AWS_REGION"),
2024
2039
  REPLICAS_CLAUDE_AUTH_METHOD: parseClaudeAuthMethod(readEnv("REPLICAS_CLAUDE_AUTH_METHOD")),
2025
2040
  REPLICAS_CODEX_AUTH_METHOD: parseCodexAuthMethod(readEnv("REPLICAS_CODEX_AUTH_METHOD")),
2026
- REPLICAS_ENV_SYSTEM_PROMPT: readEnv("REPLICAS_ENV_SYSTEM_PROMPT")
2041
+ REPLICAS_ENV_SYSTEM_PROMPT: readEnv("REPLICAS_ENV_SYSTEM_PROMPT"),
2042
+ REPLICAS_ENV_START_HOOK: readEnv("REPLICAS_ENV_START_HOOK"),
2043
+ REPLICAS_DISABLE_AUTO_START_HOOKS: readEnv("REPLICAS_DISABLE_AUTO_START_HOOKS")?.toLowerCase() === "true"
2027
2044
  };
2028
2045
  if (!IS_WARMING_MODE && !env.WORKSPACE_ID) {
2029
2046
  console.error("WORKSPACE_ID is not set \u2014 this is required in normal (non-warming) mode");
@@ -2426,11 +2443,24 @@ import { execFileSync as execFileSync2, spawnSync } from "child_process";
2426
2443
  import { join as join5 } from "path";
2427
2444
 
2428
2445
  // src/utils/state.ts
2429
- import { readFile, writeFile, mkdir, rename, unlink } from "fs/promises";
2446
+ import { readFile, mkdir } from "fs/promises";
2430
2447
  import { existsSync } from "fs";
2431
2448
  import { join as join3 } from "path";
2432
2449
  import { homedir as homedir3 } from "os";
2433
2450
 
2451
+ // src/utils/file.ts
2452
+ import { rename, unlink, writeFile } from "fs/promises";
2453
+ async function atomicWriteFile(path4, data) {
2454
+ const tmpFile = `${path4}.${process.pid}.${Date.now()}.tmp`;
2455
+ try {
2456
+ await writeFile(tmpFile, data, "utf-8");
2457
+ await rename(tmpFile, path4);
2458
+ } catch (error) {
2459
+ await unlink(tmpFile).catch(() => void 0);
2460
+ throw error;
2461
+ }
2462
+ }
2463
+
2434
2464
  // src/utils/type-guards.ts
2435
2465
  function isRecord4(value) {
2436
2466
  return typeof value === "object" && value !== null;
@@ -2453,14 +2483,7 @@ async function updateEngineState(updater) {
2453
2483
  await mkdir(STATE_DIR, { recursive: true });
2454
2484
  const currentState = await loadEngineState();
2455
2485
  const nextState = updater(currentState);
2456
- const tmpFile = `${STATE_FILE}.${process.pid}.tmp`;
2457
- try {
2458
- await writeFile(tmpFile, JSON.stringify(nextState, null, 2), "utf-8");
2459
- await rename(tmpFile, STATE_FILE);
2460
- } catch (err) {
2461
- await unlink(tmpFile).catch(() => void 0);
2462
- throw err;
2463
- }
2486
+ await atomicWriteFile(STATE_FILE, JSON.stringify(nextState, null, 2));
2464
2487
  });
2465
2488
  }
2466
2489
  function isEngineRepoDiff(value) {
@@ -3050,15 +3073,14 @@ var EngineLogger = class {
3050
3073
  var engineLogger = new EngineLogger();
3051
3074
 
3052
3075
  // src/services/replicas-config-service.ts
3053
- import { readFile as readFile4, appendFile as appendFile2, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
3076
+ import { readFile as readFile4, appendFile as appendFile2, writeFile as writeFile4, mkdir as mkdir5 } from "fs/promises";
3054
3077
  import { existsSync as existsSync4 } from "fs";
3055
3078
  import { join as join9 } from "path";
3056
3079
  import { homedir as homedir7 } from "os";
3057
- import { exec } from "child_process";
3058
- import { promisify as promisify2 } from "util";
3080
+ import { spawn } from "child_process";
3059
3081
 
3060
3082
  // src/services/environment-details-service.ts
3061
- import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
3083
+ import { mkdir as mkdir3, readFile as readFile2 } from "fs/promises";
3062
3084
  import { existsSync as existsSync3 } from "fs";
3063
3085
  import { homedir as homedir5 } from "os";
3064
3086
  import { join as join7 } from "path";
@@ -3115,6 +3137,7 @@ function createDefaultDetails() {
3115
3137
  engineVersion: DAYTONA_SNAPSHOT_ID,
3116
3138
  globalWarmHookCompleted: { status: "n/a", details: null },
3117
3139
  environmentWarmHookCompleted: { status: "n/a", details: null },
3140
+ environmentStartHookCompleted: { status: "n/a", details: null },
3118
3141
  repositories: [],
3119
3142
  filesUploaded: [],
3120
3143
  envVarsSet: [],
@@ -3145,8 +3168,8 @@ async function readDetails() {
3145
3168
  }
3146
3169
  async function writeDetails(details) {
3147
3170
  await mkdir3(REPLICAS_DIR, { recursive: true });
3148
- await writeFile3(DETAILS_FILE, `${JSON.stringify(details, null, 2)}
3149
- `, "utf-8");
3171
+ await atomicWriteFile(DETAILS_FILE, `${JSON.stringify(details, null, 2)}
3172
+ `);
3150
3173
  }
3151
3174
  var EnvironmentDetailsService = class {
3152
3175
  async getDetails() {
@@ -3200,6 +3223,12 @@ var EnvironmentDetailsService = class {
3200
3223
  current.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
3201
3224
  await writeDetails(current);
3202
3225
  }
3226
+ async setEnvironmentStartHook(status, details) {
3227
+ const current = await readDetails();
3228
+ current.environmentStartHookCompleted = { status, details: details ?? null };
3229
+ current.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
3230
+ await writeDetails(current);
3231
+ }
3203
3232
  async setRepositoryWarmHook(repositoryName, status) {
3204
3233
  const current = await readDetails();
3205
3234
  current.repositories = upsertRepositoryStatus(current.repositories, [{
@@ -3225,7 +3254,7 @@ var environmentDetailsService = new EnvironmentDetailsService();
3225
3254
 
3226
3255
  // src/services/start-hook-logs-service.ts
3227
3256
  import { createHash } from "crypto";
3228
- import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4, readdir as readdir2 } from "fs/promises";
3257
+ import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile3, readdir as readdir2 } from "fs/promises";
3229
3258
  import { homedir as homedir6 } from "os";
3230
3259
  import { join as join8 } from "path";
3231
3260
  var LOGS_DIR = join8(homedir6(), ".replicas", "start-hook-logs");
@@ -3248,7 +3277,7 @@ var StartHookLogsService = class {
3248
3277
  async saveRepoLog(repoName, entry) {
3249
3278
  await this.ensureDir();
3250
3279
  const log = { repoName, ...entry };
3251
- await writeFile4(join8(LOGS_DIR, repoFilename(repoName)), `${JSON.stringify(log, null, 2)}
3280
+ await writeFile3(join8(LOGS_DIR, repoFilename(repoName)), `${JSON.stringify(log, null, 2)}
3252
3281
  `, "utf-8");
3253
3282
  }
3254
3283
  async getAllLogs() {
@@ -3295,7 +3324,6 @@ var StartHookLogsService = class {
3295
3324
  var startHookLogsService = new StartHookLogsService();
3296
3325
 
3297
3326
  // src/services/replicas-config-service.ts
3298
- var execAsync = promisify2(exec);
3299
3327
  var START_HOOKS_LOG = join9(homedir7(), ".replicas", "startHooks.log");
3300
3328
  var START_HOOKS_RUNNING_PROMPT = `IMPORTANT - Start Hooks Running:
3301
3329
  Start hooks are shell commands/scripts set by repository owners that run on workspace startup.
@@ -3327,6 +3355,12 @@ var ReplicasConfigService = class {
3327
3355
  */
3328
3356
  async initialize() {
3329
3357
  await this.loadConfigs();
3358
+ if (IS_WARMING_MODE || ENGINE_ENV.REPLICAS_DISABLE_AUTO_START_HOOKS) {
3359
+ this.hooksRunning = false;
3360
+ this.hooksCompleted = false;
3361
+ this.hooksFailed = false;
3362
+ return;
3363
+ }
3330
3364
  void this.executeStartHooks().catch((error) => {
3331
3365
  const errorMessage = error instanceof Error ? error.message : String(error);
3332
3366
  this.hooksFailed = true;
@@ -3377,71 +3411,156 @@ var ReplicasConfigService = class {
3377
3411
  console.error("Failed to write to start hooks log:", error);
3378
3412
  }
3379
3413
  }
3414
+ async executeEnvironmentStartHook(params) {
3415
+ await this.logToFile(`[environment] --- Running env-level start hook ---`);
3416
+ const result = await this.execStartHookCommand({
3417
+ command: params.content,
3418
+ label: "env start hook",
3419
+ repoName: "environment",
3420
+ cwd: homedir7(),
3421
+ timeout: params.timeoutMs ?? DEFAULT_START_HOOK_TIMEOUT_MS,
3422
+ onOutputChunk: params.onOutputChunk
3423
+ });
3424
+ if (result.exitCode !== 0) {
3425
+ this.hooksFailed = true;
3426
+ await environmentDetailsService.setEnvironmentStartHook("no");
3427
+ } else {
3428
+ await environmentDetailsService.setEnvironmentStartHook("yes");
3429
+ }
3430
+ return result;
3431
+ }
3380
3432
  async execStartHookCommand(params) {
3381
3433
  const output = [];
3382
- try {
3383
- await this.logToFile(`[${params.repoName}] --- Running: ${params.label} ---`);
3384
- const { stdout, stderr } = await execAsync(params.command, {
3434
+ let outputBytes = 0;
3435
+ let bufferExceeded = false;
3436
+ const emit = (chunk) => {
3437
+ output.push(chunk);
3438
+ params.onOutputChunk?.(chunk);
3439
+ };
3440
+ await this.logToFile(`[${params.repoName}] --- Running: ${params.label} ---`);
3441
+ emit(`$ ${params.label}
3442
+ `);
3443
+ return new Promise((resolve3) => {
3444
+ const proc = spawn("bash", ["-lc", params.command], {
3385
3445
  cwd: params.cwd,
3386
- timeout: params.timeout,
3387
- maxBuffer: HOOK_EXEC_MAX_BUFFER_BYTES,
3388
- env: process.env
3446
+ env: process.env,
3447
+ stdio: ["pipe", "pipe", "pipe"]
3389
3448
  });
3390
- if (stdout) {
3391
- output.push(stdout);
3392
- await this.logToFile(`[${params.repoName}] [stdout] ${stdout}`);
3393
- }
3394
- if (stderr) {
3395
- output.push(stderr);
3396
- await this.logToFile(`[${params.repoName}] [stderr] ${stderr}`);
3449
+ let timedOut = false;
3450
+ let settled = false;
3451
+ const finish = (result) => {
3452
+ if (settled) return;
3453
+ settled = true;
3454
+ resolve3(result);
3455
+ };
3456
+ const timer = setTimeout(() => {
3457
+ timedOut = true;
3458
+ proc.kill("SIGTERM");
3459
+ }, params.timeout);
3460
+ const handleData = (data, streamName) => {
3461
+ outputBytes += data.length;
3462
+ if (bufferExceeded) return;
3463
+ if (outputBytes > HOOK_EXEC_MAX_BUFFER_BYTES) {
3464
+ bufferExceeded = true;
3465
+ const message = `
3466
+ [output truncated \u2014 exceeded ${HOOK_EXEC_MAX_BUFFER_BYTES} bytes]
3467
+ `;
3468
+ emit(message);
3469
+ void this.logToFile(`[${params.repoName}] [ERROR] ${message.trim()}`);
3470
+ proc.kill("SIGTERM");
3471
+ return;
3472
+ }
3473
+ const text = data.toString();
3474
+ emit(text);
3475
+ void this.logToFile(`[${params.repoName}] [${streamName}] ${text}`);
3476
+ };
3477
+ proc.stdout.on("data", (data) => handleData(data, "stdout"));
3478
+ proc.stderr.on("data", (data) => handleData(data, "stderr"));
3479
+ proc.on("close", (code) => {
3480
+ clearTimeout(timer);
3481
+ const exitCode = bufferExceeded ? 1 : code ?? 1;
3482
+ if (exitCode !== 0) {
3483
+ const reason = timedOut ? "timed out" : `exited with code ${exitCode}`;
3484
+ const message = `[ERROR] ${params.label} failed: ${reason}
3485
+ `;
3486
+ emit(message);
3487
+ void this.logToFile(`[${params.repoName}] ${message.trim()}`);
3488
+ } else {
3489
+ void this.logToFile(`[${params.repoName}] --- Completed: ${params.label} ---`);
3490
+ }
3491
+ finish({ exitCode, timedOut: timedOut && !bufferExceeded, output });
3492
+ });
3493
+ proc.on("error", (error) => {
3494
+ clearTimeout(timer);
3495
+ const message = `[ERROR] ${params.label} failed: ${error.message}
3496
+ `;
3497
+ emit(message);
3498
+ void this.logToFile(`[${params.repoName}] ${message.trim()}`);
3499
+ finish({ exitCode: 1, timedOut: false, output });
3500
+ });
3501
+ proc.stdin.end();
3502
+ });
3503
+ }
3504
+ async executeStartHooks(params = {}) {
3505
+ const onEvent = params.onEvent ?? (() => {
3506
+ });
3507
+ const envHookContent = (params.environmentStartHook ?? ENGINE_ENV.REPLICAS_ENV_START_HOOK)?.trim() ?? "";
3508
+ const hookEntries = this.configs.filter((entry) => entry.config.startHook && entry.config.startHook.commands.length > 0);
3509
+ const outputBlocks = [];
3510
+ let overallExitCode = 0;
3511
+ let overallTimedOut = false;
3512
+ const recordResult = (result) => {
3513
+ const output = result.output.join("");
3514
+ if (output) {
3515
+ outputBlocks.push(output);
3397
3516
  }
3398
- await this.logToFile(`[${params.repoName}] --- Completed: ${params.label} ---`);
3399
- return { exitCode: 0, timedOut: false, output };
3400
- } catch (error) {
3401
- const execError = error;
3402
- if (execError.stdout) {
3403
- output.push(execError.stdout);
3404
- await this.logToFile(`[${params.repoName}] [stdout] ${execError.stdout}`);
3517
+ if (result.exitCode !== 0 && overallExitCode === 0) {
3518
+ overallExitCode = result.exitCode;
3405
3519
  }
3406
- if (execError.stderr) {
3407
- output.push(execError.stderr);
3408
- await this.logToFile(`[${params.repoName}] [stderr] ${execError.stderr}`);
3520
+ if (result.timedOut) {
3521
+ overallTimedOut = true;
3409
3522
  }
3410
- const rawMessage = execError.message ?? "Unknown error";
3411
- const errorMessage = rawMessage.split("\n", 1)[0];
3412
- output.push(`[ERROR] ${params.label} failed: ${errorMessage}`);
3413
- await this.logToFile(`[${params.repoName}] [ERROR] ${params.label} failed: ${errorMessage}`);
3414
- return { exitCode: execError.code ?? 1, timedOut: execError.killed === true, output };
3415
- }
3416
- }
3417
- /**
3418
- * Execute start hooks from all repositories sequentially.
3419
- */
3420
- async executeStartHooks() {
3421
- const hookEntries = this.configs.filter((entry) => entry.config.startHook && entry.config.startHook.commands.length > 0);
3422
- if (hookEntries.length === 0) {
3523
+ };
3524
+ if (!envHookContent && hookEntries.length === 0) {
3423
3525
  this.hooksRunning = false;
3424
3526
  this.hooksCompleted = true;
3425
3527
  this.hooksFailed = false;
3528
+ await environmentDetailsService.setEnvironmentStartHook("n/a");
3426
3529
  const repos = await gitService.listRepositories();
3427
3530
  for (const repo of repos) {
3428
3531
  await environmentDetailsService.setRepositoryStartHook(repo.name, "n/a");
3429
3532
  }
3430
- return;
3533
+ const result = { exitCode: 0, output: "No start hooks configured.", timedOut: false };
3534
+ onEvent({ type: "output", data: `${result.output}
3535
+ `, label: "start-hooks" });
3536
+ onEvent({ type: "complete", exitCode: result.exitCode, timedOut: result.timedOut });
3537
+ return result;
3431
3538
  }
3432
3539
  this.hooksRunning = true;
3433
3540
  this.hooksCompleted = false;
3541
+ this.hooksFailed = false;
3434
3542
  try {
3435
3543
  await mkdir5(join9(homedir7(), ".replicas"), { recursive: true });
3436
- await writeFile5(
3544
+ await writeFile4(
3437
3545
  START_HOOKS_LOG,
3438
3546
  `=== Start Hooks Execution Log ===
3439
3547
  Started: ${(/* @__PURE__ */ new Date()).toISOString()}
3548
+ Environment hook: ${envHookContent ? "yes" : "no"}
3440
3549
  Repositories: ${hookEntries.length}
3441
3550
 
3442
3551
  `,
3443
3552
  "utf-8"
3444
3553
  );
3554
+ if (envHookContent) {
3555
+ const envResult = await this.executeEnvironmentStartHook({
3556
+ content: envHookContent,
3557
+ timeoutMs: params.timeoutMs,
3558
+ onOutputChunk: (chunk) => onEvent({ type: "output", data: chunk, label: "environment" })
3559
+ });
3560
+ recordResult(envResult);
3561
+ } else {
3562
+ await environmentDetailsService.setEnvironmentStartHook("n/a");
3563
+ }
3445
3564
  const repos = await gitService.listRepositories();
3446
3565
  const reposWithHooks = new Set(hookEntries.map((entry) => entry.repoName));
3447
3566
  for (const repo of repos) {
@@ -3455,48 +3574,53 @@ Repositories: ${hookEntries.length}
3455
3574
  continue;
3456
3575
  }
3457
3576
  const persistedRepoState = await loadRepoState(entry.repoName);
3458
- if (persistedRepoState?.startHooksCompleted) {
3577
+ if (!params.ignorePersistedState && persistedRepoState?.startHooksCompleted) {
3459
3578
  await this.logToFile(`[${entry.repoName}] Start hooks already completed in this workspace lifecycle, skipping`);
3460
3579
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, "yes");
3461
3580
  continue;
3462
3581
  }
3463
- const timeout = startHookConfig.timeout ?? DEFAULT_START_HOOK_TIMEOUT_MS;
3582
+ const timeout = params.timeoutMs ?? startHookConfig.timeout ?? DEFAULT_START_HOOK_TIMEOUT_MS;
3464
3583
  let repoFailed = false;
3465
3584
  let lastExitCode = 0;
3466
3585
  let repoTimedOut = false;
3467
3586
  const repoOutput = [];
3587
+ const onRepoOutput = (chunk) => onEvent({ type: "output", data: chunk, label: `repo:${entry.repoName}` });
3468
3588
  await this.logToFile(`[${entry.repoName}] Executing ${startHookConfig.commands.length} hook(s) with timeout ${timeout}ms`);
3469
3589
  if (startHookConfig.separate === false) {
3470
3590
  const combinedScript = `set -e
3471
3591
  ${startHookConfig.commands.join("\n")}`;
3472
- const result = await this.execStartHookCommand({
3592
+ const result2 = await this.execStartHookCommand({
3473
3593
  command: combinedScript,
3474
3594
  label: "combined script",
3475
3595
  repoName: entry.repoName,
3476
3596
  cwd: entry.workingDirectory,
3477
- timeout
3597
+ timeout,
3598
+ onOutputChunk: onRepoOutput
3478
3599
  });
3479
- repoOutput.push(...result.output);
3480
- if (result.exitCode !== 0) {
3481
- lastExitCode = result.exitCode;
3482
- repoTimedOut = result.timedOut;
3600
+ repoOutput.push(...result2.output);
3601
+ recordResult(result2);
3602
+ if (result2.exitCode !== 0) {
3603
+ lastExitCode = result2.exitCode;
3604
+ repoTimedOut = result2.timedOut;
3483
3605
  this.hooksFailed = true;
3484
3606
  repoFailed = true;
3485
3607
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
3486
3608
  }
3487
3609
  } else {
3488
3610
  for (const hook of startHookConfig.commands) {
3489
- const result = await this.execStartHookCommand({
3611
+ const result2 = await this.execStartHookCommand({
3490
3612
  command: hook,
3491
3613
  label: hook,
3492
3614
  repoName: entry.repoName,
3493
3615
  cwd: entry.workingDirectory,
3494
- timeout
3616
+ timeout,
3617
+ onOutputChunk: onRepoOutput
3495
3618
  });
3496
- repoOutput.push(...result.output);
3497
- if (result.exitCode !== 0) {
3498
- lastExitCode = result.exitCode;
3499
- repoTimedOut = result.timedOut;
3619
+ repoOutput.push(...result2.output);
3620
+ recordResult(result2);
3621
+ if (result2.exitCode !== 0) {
3622
+ lastExitCode = result2.exitCode;
3623
+ repoTimedOut = result2.timedOut;
3500
3624
  this.hooksFailed = true;
3501
3625
  repoFailed = true;
3502
3626
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, "no");
@@ -3511,20 +3635,30 @@ ${startHookConfig.commands.join("\n")}`;
3511
3635
  timedOut: repoTimedOut,
3512
3636
  executedAt: (/* @__PURE__ */ new Date()).toISOString()
3513
3637
  });
3514
- const fallbackRepoState = persistedRepoState ?? {
3515
- name: entry.repoName,
3516
- path: entry.workingDirectory,
3517
- defaultBranch: entry.defaultBranch,
3518
- currentBranch: entry.defaultBranch,
3519
- prUrls: [],
3520
- gitDiff: null,
3521
- startHooksCompleted: false
3522
- };
3523
- await saveRepoState(entry.repoName, { startHooksCompleted: true }, fallbackRepoState);
3638
+ if ((params.markRepoCompleted ?? true) && !repoFailed) {
3639
+ const fallbackRepoState = persistedRepoState ?? {
3640
+ name: entry.repoName,
3641
+ path: entry.workingDirectory,
3642
+ defaultBranch: entry.defaultBranch,
3643
+ currentBranch: entry.defaultBranch,
3644
+ prUrls: [],
3645
+ gitDiff: null,
3646
+ startHooksCompleted: false
3647
+ };
3648
+ await saveRepoState(entry.repoName, { startHooksCompleted: true }, fallbackRepoState);
3649
+ }
3524
3650
  await environmentDetailsService.setRepositoryStartHook(entry.repoName, repoFailed ? "no" : "yes");
3525
3651
  }
3526
3652
  this.hooksCompleted = true;
3653
+ this.hooksFailed = overallExitCode !== 0;
3527
3654
  await this.logToFile(`=== All start hooks completed at ${(/* @__PURE__ */ new Date()).toISOString()} ===`);
3655
+ const result = {
3656
+ exitCode: overallExitCode,
3657
+ output: outputBlocks.join("\n\n"),
3658
+ timedOut: overallTimedOut
3659
+ };
3660
+ onEvent({ type: "complete", exitCode: result.exitCode, timedOut: result.timedOut });
3661
+ return result;
3528
3662
  } catch (error) {
3529
3663
  const errorMessage = error instanceof Error ? error.message : String(error);
3530
3664
  this.hooksFailed = true;
@@ -3536,6 +3670,16 @@ ${startHookConfig.commands.join("\n")}`;
3536
3670
  this.hooksRunning = false;
3537
3671
  }
3538
3672
  }
3673
+ async runStartHooksStreaming(params) {
3674
+ await this.loadConfigs();
3675
+ return this.executeStartHooks({
3676
+ environmentStartHook: params.environmentStartHook,
3677
+ timeoutMs: params.timeoutMs,
3678
+ ignorePersistedState: true,
3679
+ markRepoCompleted: false,
3680
+ onEvent: params.onEvent
3681
+ });
3682
+ }
3539
3683
  /**
3540
3684
  * Check if start hooks are currently running.
3541
3685
  */
@@ -3548,14 +3692,18 @@ ${startHookConfig.commands.join("\n")}`;
3548
3692
  didHooksFail() {
3549
3693
  return this.hooksFailed;
3550
3694
  }
3551
- /**
3552
- * Get aggregated start hook metadata.
3553
- */
3554
3695
  getStartHookConfig() {
3555
- const commands = this.configs.flatMap((entry) => {
3696
+ const commands = [];
3697
+ const envHookContent = ENGINE_ENV.REPLICAS_ENV_START_HOOK?.trim();
3698
+ if (envHookContent) {
3699
+ commands.push(`[environment] ${envHookContent}`);
3700
+ }
3701
+ for (const entry of this.configs) {
3556
3702
  const repoCommands = entry.config.startHook?.commands ?? [];
3557
- return repoCommands.map((command) => `[${entry.repoName}] ${command}`);
3558
- });
3703
+ for (const command of repoCommands) {
3704
+ commands.push(`[${entry.repoName}] ${command}`);
3705
+ }
3706
+ }
3559
3707
  if (commands.length === 0) {
3560
3708
  return void 0;
3561
3709
  }
@@ -3619,7 +3767,7 @@ var EventService = class {
3619
3767
  var eventService = new EventService();
3620
3768
 
3621
3769
  // src/services/preview-service.ts
3622
- import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
3770
+ import { mkdir as mkdir7, readFile as readFile5 } from "fs/promises";
3623
3771
  import { existsSync as existsSync5 } from "fs";
3624
3772
  import { randomUUID as randomUUID2 } from "crypto";
3625
3773
  import { homedir as homedir9 } from "os";
@@ -3639,8 +3787,8 @@ async function readPreviewsFile() {
3639
3787
  async function writePreviewsFile(data) {
3640
3788
  const dir = dirname(PREVIEW_PORTS_FILE);
3641
3789
  await mkdir7(dir, { recursive: true });
3642
- await writeFile6(PREVIEW_PORTS_FILE, `${JSON.stringify(data, null, 2)}
3643
- `, "utf-8");
3790
+ await atomicWriteFile(PREVIEW_PORTS_FILE, `${JSON.stringify(data, null, 2)}
3791
+ `);
3644
3792
  }
3645
3793
  var PreviewService = class {
3646
3794
  async initialize() {
@@ -3692,7 +3840,7 @@ var previewService = new PreviewService();
3692
3840
 
3693
3841
  // src/services/chat/chat-service.ts
3694
3842
  import { existsSync as existsSync7 } from "fs";
3695
- import { appendFile as appendFile5, mkdir as mkdir11, readFile as readFile8, rm, writeFile as writeFile9 } from "fs/promises";
3843
+ import { appendFile as appendFile5, copyFile, mkdir as mkdir11, readFile as readFile8, rename as rename2, rm } from "fs/promises";
3696
3844
  import { homedir as homedir13 } from "os";
3697
3845
  import { join as join15 } from "path";
3698
3846
  import { randomUUID as randomUUID5 } from "crypto";
@@ -4245,7 +4393,7 @@ function extractPlanFromCodexAspNotification(notification) {
4245
4393
 
4246
4394
  // src/utils/image-utils.ts
4247
4395
  import { randomUUID as randomUUID3 } from "crypto";
4248
- import { mkdir as mkdir8, unlink as unlink2, writeFile as writeFile7 } from "fs/promises";
4396
+ import { mkdir as mkdir8, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
4249
4397
  import { homedir as homedir10 } from "os";
4250
4398
  import { join as join12 } from "path";
4251
4399
  function isImageMediaType(value) {
@@ -4333,7 +4481,7 @@ async function saveNormalizedImagesToTempFiles(images, tempImageDir = join12(hom
4333
4481
  const ext = image.source.media_type.split("/")[1] || "png";
4334
4482
  const filename = `img_${randomUUID3()}.${ext}`;
4335
4483
  const filepath = join12(tempImageDir, filename);
4336
- await writeFile7(filepath, Buffer.from(image.source.data, "base64"));
4484
+ await writeFile5(filepath, Buffer.from(image.source.data, "base64"));
4337
4485
  tempPaths.push(filepath);
4338
4486
  }
4339
4487
  } catch (error) {
@@ -5359,7 +5507,7 @@ var ClaudeManager = class _ClaudeManager extends CodingAgentManager {
5359
5507
  };
5360
5508
 
5361
5509
  // src/managers/codex-asp/app-server-process.ts
5362
- import { spawn } from "child_process";
5510
+ import { spawn as spawn2 } from "child_process";
5363
5511
  import { EventEmitter as EventEmitter2 } from "events";
5364
5512
 
5365
5513
  // src/managers/codex-asp/asp-client.ts
@@ -5581,7 +5729,7 @@ var AppServerProcess = class {
5581
5729
  return { client: this.client };
5582
5730
  }
5583
5731
  this.shuttingDown = false;
5584
- const child = spawn(this.binary, this.args, {
5732
+ const child = spawn2(this.binary, this.args, {
5585
5733
  cwd: this.cwd,
5586
5734
  env: this.env,
5587
5735
  stdio: ["pipe", "pipe", "pipe"]
@@ -5831,6 +5979,7 @@ var THREAD_GOAL_CLEAR_METHOD = "thread/goal/clear";
5831
5979
  var TURN_START_METHOD = "turn/start";
5832
5980
  var TURN_INTERRUPT_METHOD = "turn/interrupt";
5833
5981
  var ACCOUNT_RATE_LIMITS_READ_METHOD = "account/rateLimits/read";
5982
+ var MAX_CODEX_ASP_TRANSCRIPT_OUTPUT_CHARS = DEFAULT_HOOK_OUTPUT_PREVIEW_CHARS;
5834
5983
  function toReasoningEffort(thinkingLevel) {
5835
5984
  return codexReasoningEffortForThinkingLevel(thinkingLevel);
5836
5985
  }
@@ -5852,7 +6001,7 @@ function timestampFromMilliseconds(value) {
5852
6001
  }
5853
6002
  function stringifyToolOutput(value) {
5854
6003
  if (value === null || value === void 0) return void 0;
5855
- if (typeof value === "string") return value;
6004
+ if (typeof value === "string") return truncateCodexAspTranscriptOutput(value);
5856
6005
  if (typeof value === "object" && "content" in value && Array.isArray(value.content)) {
5857
6006
  const text = value.content.map((item) => {
5858
6007
  if (typeof item === "string") return item;
@@ -5861,14 +6010,24 @@ function stringifyToolOutput(value) {
5861
6010
  }
5862
6011
  return "";
5863
6012
  }).filter(Boolean).join("\n");
5864
- if (text) return text;
6013
+ if (text) return truncateCodexAspTranscriptOutput(text);
5865
6014
  }
5866
6015
  try {
5867
- return JSON.stringify(value);
6016
+ return truncateCodexAspTranscriptOutput(JSON.stringify(value));
5868
6017
  } catch {
5869
- return String(value);
6018
+ return truncateCodexAspTranscriptOutput(String(value));
5870
6019
  }
5871
6020
  }
6021
+ function truncateCodexAspTranscriptOutput(output, maxChars = MAX_CODEX_ASP_TRANSCRIPT_OUTPUT_CHARS) {
6022
+ return buildOutputPreview(output, {
6023
+ maxChars,
6024
+ truncateFrom: "tail",
6025
+ marker: "\n[output truncated: showing tail of output]\n"
6026
+ }).output;
6027
+ }
6028
+ function appendCodexAspTranscriptOutput(current, delta, maxChars = MAX_CODEX_ASP_TRANSCRIPT_OUTPUT_CHARS) {
6029
+ return truncateCodexAspTranscriptOutput(`${current ?? ""}${delta}`, maxChars);
6030
+ }
5872
6031
  function transcriptPatchOperation(change) {
5873
6032
  return {
5874
6033
  action: change.kind.type,
@@ -5915,7 +6074,7 @@ function itemToTranscriptItem(item, timestamp, status) {
5915
6074
  type: "commandExecution",
5916
6075
  id: item.id,
5917
6076
  command: item.command,
5918
- ...item.aggregatedOutput ? { output: item.aggregatedOutput } : {},
6077
+ ...item.aggregatedOutput ? { output: truncateCodexAspTranscriptOutput(item.aggregatedOutput) } : {},
5919
6078
  exitCode,
5920
6079
  timestamp,
5921
6080
  status: normalizeCodexAspTranscriptStatus(item.status, typeof exitCode === "number" && exitCode !== 0)
@@ -5939,7 +6098,7 @@ function itemToTranscriptItem(item, timestamp, status) {
5939
6098
  server: item.server,
5940
6099
  tool: item.tool,
5941
6100
  input: item.arguments,
5942
- output: item.error?.message ?? stringifyToolOutput(item.result),
6101
+ output: item.error?.message ? truncateCodexAspTranscriptOutput(item.error.message) : stringifyToolOutput(item.result),
5943
6102
  timestamp,
5944
6103
  status: normalizeCodexAspTranscriptStatus(item.status, item.status === "failed")
5945
6104
  };
@@ -6953,7 +7112,7 @@ var CodexAspManager = class extends CodingAgentManager {
6953
7112
  if (item.type !== "commandExecution" && item.type !== "fileChange") return;
6954
7113
  turn.items[itemIndex] = {
6955
7114
  ...item,
6956
- output: `${item.output ?? ""}${delta}`,
7115
+ output: appendCodexAspTranscriptOutput(item.output, delta),
6957
7116
  status: "in_progress"
6958
7117
  };
6959
7118
  this.codexAspTranscript.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -7055,7 +7214,7 @@ var CodexAspManager = class extends CodingAgentManager {
7055
7214
 
7056
7215
  // src/managers/codex-manager.ts
7057
7216
  import { Codex } from "@openai/codex-sdk";
7058
- import { readdir as readdir3, stat as stat2, writeFile as writeFile8, mkdir as mkdir10, readFile as readFile7 } from "fs/promises";
7217
+ import { readdir as readdir3, stat as stat2, writeFile as writeFile6, mkdir as mkdir10, readFile as readFile7 } from "fs/promises";
7059
7218
  import { existsSync as existsSync6 } from "fs";
7060
7219
  import { join as join14 } from "path";
7061
7220
  import { homedir as homedir12 } from "os";
@@ -7159,7 +7318,7 @@ var CodexManager = class extends CodingAgentManager {
7159
7318
  delete config.developer_instructions;
7160
7319
  }
7161
7320
  const tomlContent = stringifyToml(config);
7162
- await writeFile8(CODEX_CONFIG_PATH, tomlContent, "utf-8");
7321
+ await writeFile6(CODEX_CONFIG_PATH, tomlContent, "utf-8");
7163
7322
  console.log("[CodexManager] Updated config.toml with developer_instructions");
7164
7323
  } catch (error) {
7165
7324
  console.error("[CodexManager] Failed to update config.toml:", error);
@@ -8070,6 +8229,7 @@ var CLAUDE_HISTORY_DIR = join15(ENGINE_DIR2, "claude-histories");
8070
8229
  var RELAY_HISTORY_DIR = join15(ENGINE_DIR2, "relay-histories");
8071
8230
  var CHAT_SENDERS_DIR = join15(ENGINE_DIR2, "chat-senders");
8072
8231
  var CODEX_AUTH_PATH2 = join15(homedir13(), ".codex", "auth.json");
8232
+ var CHATS_BACKUP_FILE = `${CHATS_FILE}.bak`;
8073
8233
  function isChatMessageSender(value) {
8074
8234
  if (!isRecord4(value)) return false;
8075
8235
  return typeof value.senderUserId === "string" && typeof value.senderEmail === "string" && typeof value.recordedAt === "string";
@@ -8124,6 +8284,16 @@ function normalizePersistedChat(chat) {
8124
8284
  ...chat.provider === "codex" ? { codexBackend: codexBackendForChat(chat) } : {}
8125
8285
  };
8126
8286
  }
8287
+ function parsePersistedChatsContent(content) {
8288
+ const parsed = JSON.parse(content);
8289
+ if (!Array.isArray(parsed)) {
8290
+ return [];
8291
+ }
8292
+ return parsed.filter((entry) => isPersistedChat(entry)).map((entry) => normalizePersistedChat(entry));
8293
+ }
8294
+ function corruptChatsFilePath() {
8295
+ return `${CHATS_FILE}.corrupt-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
8296
+ }
8127
8297
  function createUserMessageEvent(message, messageId) {
8128
8298
  return {
8129
8299
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8615,23 +8785,44 @@ var ChatService = class {
8615
8785
  async loadChats() {
8616
8786
  try {
8617
8787
  const content = await readFile8(CHATS_FILE, "utf-8");
8618
- const parsed = JSON.parse(content);
8619
- if (!Array.isArray(parsed)) {
8620
- return [];
8621
- }
8622
- return parsed.filter((entry) => isPersistedChat(entry)).map((entry) => normalizePersistedChat(entry));
8788
+ return parsePersistedChatsContent(content);
8623
8789
  } catch (error) {
8624
8790
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
8625
8791
  return [];
8626
8792
  }
8627
- throw error;
8793
+ const quarantinePath = corruptChatsFilePath();
8794
+ console.error(`[ChatService] Failed to load ${CHATS_FILE}; quarantining and trying backup:`, error);
8795
+ try {
8796
+ await rename2(CHATS_FILE, quarantinePath);
8797
+ console.error(`[ChatService] Quarantined corrupt chats file at ${quarantinePath}`);
8798
+ } catch (renameError) {
8799
+ console.error("[ChatService] Failed to quarantine corrupt chats file:", renameError);
8800
+ }
8801
+ try {
8802
+ const backupContent = await readFile8(CHATS_BACKUP_FILE, "utf-8");
8803
+ return parsePersistedChatsContent(backupContent);
8804
+ } catch (backupError) {
8805
+ if (backupError && typeof backupError === "object" && "code" in backupError && backupError.code === "ENOENT") {
8806
+ return [];
8807
+ }
8808
+ console.error(`[ChatService] Failed to load backup ${CHATS_BACKUP_FILE}; starting with defaults:`, backupError);
8809
+ return [];
8810
+ }
8628
8811
  }
8629
8812
  }
8630
8813
  async persistAllChats() {
8631
8814
  this.writeChain = this.writeChain.catch(() => {
8632
8815
  }).then(async () => {
8633
8816
  const payload = Array.from(this.chats.values()).map((chat) => chat.persisted);
8634
- await writeFile9(CHATS_FILE, JSON.stringify(payload, null, 2), "utf-8");
8817
+ try {
8818
+ await copyFile(CHATS_FILE, CHATS_BACKUP_FILE);
8819
+ } catch (error) {
8820
+ if (!(error && typeof error === "object" && "code" in error && error.code === "ENOENT")) {
8821
+ console.error("[ChatService] Failed to update chats backup:", error);
8822
+ }
8823
+ }
8824
+ await atomicWriteFile(CHATS_FILE, `${JSON.stringify(payload, null, 2)}
8825
+ `);
8635
8826
  });
8636
8827
  await this.writeChain;
8637
8828
  }
@@ -9011,14 +9202,14 @@ var PlanService = class {
9011
9202
  var planService = new PlanService();
9012
9203
 
9013
9204
  // src/services/warm-hooks-service.ts
9014
- import { spawn as spawn2 } from "child_process";
9205
+ import { spawn as spawn3 } from "child_process";
9015
9206
  import { readFile as readFile12 } from "fs/promises";
9016
9207
  import { existsSync as existsSync8 } from "fs";
9017
9208
  import { join as join19 } from "path";
9018
9209
 
9019
9210
  // src/services/warm-hook-logs-service.ts
9020
9211
  import { createHash as createHash2 } from "crypto";
9021
- import { mkdir as mkdir12, readFile as readFile11, writeFile as writeFile10, readdir as readdir5, appendFile as appendFile6, unlink as unlink3 } from "fs/promises";
9212
+ import { mkdir as mkdir12, readFile as readFile11, writeFile as writeFile7, readdir as readdir5, appendFile as appendFile6, unlink as unlink3 } from "fs/promises";
9022
9213
  import { homedir as homedir15 } from "os";
9023
9214
  import { join as join18 } from "path";
9024
9215
  var LOGS_DIR2 = join18(homedir15(), ".replicas", "warm-hook-logs");
@@ -9052,7 +9243,7 @@ var WarmHookLogsService = class {
9052
9243
  hookName: "organization",
9053
9244
  ...entry
9054
9245
  };
9055
- await writeFile10(join18(LOGS_DIR2, globalFilename()), `${JSON.stringify(log, null, 2)}
9246
+ await writeFile7(join18(LOGS_DIR2, globalFilename()), `${JSON.stringify(log, null, 2)}
9056
9247
  `, "utf-8");
9057
9248
  }
9058
9249
  async saveEnvironmentHookLog(entry) {
@@ -9062,7 +9253,7 @@ var WarmHookLogsService = class {
9062
9253
  hookName: "environment",
9063
9254
  ...entry
9064
9255
  };
9065
- await writeFile10(join18(LOGS_DIR2, environmentFilename()), `${JSON.stringify(log, null, 2)}
9256
+ await writeFile7(join18(LOGS_DIR2, environmentFilename()), `${JSON.stringify(log, null, 2)}
9066
9257
  `, "utf-8");
9067
9258
  }
9068
9259
  async saveRepoHookLog(repoName, entry) {
@@ -9072,7 +9263,7 @@ var WarmHookLogsService = class {
9072
9263
  hookName: repoName,
9073
9264
  ...entry
9074
9265
  };
9075
- await writeFile10(join18(LOGS_DIR2, repoFilename2(repoName)), `${JSON.stringify(log, null, 2)}
9266
+ await writeFile7(join18(LOGS_DIR2, repoFilename2(repoName)), `${JSON.stringify(log, null, 2)}
9076
9267
  `, "utf-8");
9077
9268
  }
9078
9269
  async getAllLogs() {
@@ -9200,7 +9391,7 @@ async function executeHookScriptStreaming(params) {
9200
9391
  params.onChunk(`$ ${params.label}
9201
9392
  `);
9202
9393
  return new Promise((resolve3) => {
9203
- const proc = spawn2("bash", ["-lc", params.content], {
9394
+ const proc = spawn3("bash", ["-lc", params.content], {
9204
9395
  cwd: params.cwd,
9205
9396
  env: process.env,
9206
9397
  stdio: ["pipe", "pipe", "pipe"]
@@ -9855,6 +10046,64 @@ function createV1Routes(deps) {
9855
10046
  );
9856
10047
  }
9857
10048
  });
10049
+ app2.post("/start-hooks/run/stream", async (c) => {
10050
+ try {
10051
+ const body = await c.req.json();
10052
+ const encoder = new TextEncoder();
10053
+ const stream = new ReadableStream({
10054
+ start: (controller) => {
10055
+ let closed = false;
10056
+ const safeEnqueue = (chunk) => {
10057
+ if (closed) return false;
10058
+ try {
10059
+ controller.enqueue(encoder.encode(chunk));
10060
+ return true;
10061
+ } catch {
10062
+ closed = true;
10063
+ return false;
10064
+ }
10065
+ };
10066
+ const heartbeat = setInterval(() => {
10067
+ if (!safeEnqueue(": ping\n\n")) clearInterval(heartbeat);
10068
+ }, 15e3);
10069
+ safeEnqueue(": connected\n\n");
10070
+ replicasConfigService.runStartHooksStreaming({
10071
+ environmentStartHook: body.environmentStartHook,
10072
+ timeoutMs: body.timeoutMs,
10073
+ onEvent: (event) => {
10074
+ safeEnqueue(`data: ${JSON.stringify(event)}
10075
+
10076
+ `);
10077
+ }
10078
+ }).then(() => {
10079
+ clearInterval(heartbeat);
10080
+ if (!closed) {
10081
+ closed = true;
10082
+ controller.close();
10083
+ }
10084
+ }).catch((err) => {
10085
+ const message = err instanceof Error ? err.message : "Unknown error";
10086
+ safeEnqueue(`data: ${JSON.stringify({ type: "error", data: message })}
10087
+
10088
+ `);
10089
+ clearInterval(heartbeat);
10090
+ if (!closed) {
10091
+ closed = true;
10092
+ controller.close();
10093
+ }
10094
+ });
10095
+ }
10096
+ });
10097
+ return new Response(stream, {
10098
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" }
10099
+ });
10100
+ } catch (error) {
10101
+ return c.json(
10102
+ jsonError("Failed to run start hooks", error instanceof Error ? error.message : "Unknown error"),
10103
+ 500
10104
+ );
10105
+ }
10106
+ });
9858
10107
  app2.get("/warm-hooks/logs", async (c) => {
9859
10108
  try {
9860
10109
  const logs = await warmHookLogsService.getAllLogs();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replicas-engine",
3
- "version": "0.1.230",
3
+ "version": "0.1.232",
4
4
  "description": "Lightweight API server for Replicas workspaces",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",