@runfusion/fusion 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/bin.js +949 -138
  2. package/dist/client/assets/AgentDetailView-B1zViykq.js +18 -0
  3. package/dist/client/assets/{AgentsView-DkX0tzrN.js → AgentsView-Bl9JH5C8.js} +3 -3
  4. package/dist/client/assets/{ChatView-CEm2Hw6m.js → ChatView-liNErE53.js} +1 -1
  5. package/dist/client/assets/{DevServerView-Bumvo_ge.js → DevServerView-CV_PpbnZ.js} +1 -1
  6. package/dist/client/assets/{DirectoryPicker-CXN11cBp.js → DirectoryPicker-DPfkGnj5.js} +1 -1
  7. package/dist/client/assets/{DocumentsView-B71IqAxA.js → DocumentsView-CESb6RI7.js} +1 -1
  8. package/dist/client/assets/{InsightsView-Bs4Rldu6.js → InsightsView-BKhvyEyQ.js} +1 -1
  9. package/dist/client/assets/{MemoryView-Bs7b_L2Q.js → MemoryView-DB-l2miV.js} +1 -1
  10. package/dist/client/assets/{NodesView-BvAGTXbO.js → NodesView-DgTXO8mm.js} +1 -1
  11. package/dist/client/assets/{PiExtensionsManager-3Kcc4uhA.js → PiExtensionsManager-C4fTzemh.js} +1 -1
  12. package/dist/client/assets/{PluginManager-Ch-Xynlm.js → PluginManager-C2-dExUL.js} +1 -1
  13. package/dist/client/assets/{ResearchView-Bj6Saqf6.js → ResearchView-CkVwRDVA.js} +1 -1
  14. package/dist/client/assets/{RoadmapsView-9qT8Vwd0.js → RoadmapsView-Cu85_XrQ.js} +1 -1
  15. package/dist/client/assets/{SettingsModal-D4ERGQNQ.js → SettingsModal-BGnSAeqa.js} +1 -1
  16. package/dist/client/assets/SettingsModal-C0DokcId.js +31 -0
  17. package/dist/client/assets/{SetupWizardModal-Dv0rX2_o.js → SetupWizardModal-C_d9clJp.js} +1 -1
  18. package/dist/client/assets/{SkillMultiselect-CSkXQzdv.js → SkillMultiselect-DwGWYZi6.js} +1 -1
  19. package/dist/client/assets/{SkillsView-2srXMOzj.js → SkillsView-C096TB7i.js} +1 -1
  20. package/dist/client/assets/{TodoView-CxPPIvw2.js → TodoView-CUiAt2mR.js} +1 -1
  21. package/dist/client/assets/{folder-open-FA1PwpXV.js → folder-open-CKivQd8c.js} +1 -1
  22. package/dist/client/assets/index-B4StE1qN.js +662 -0
  23. package/dist/client/assets/index-DYJk0WDc.css +1 -0
  24. package/dist/client/assets/{list-checks-6EktkUso.js → list-checks-B3oufblU.js} +1 -1
  25. package/dist/client/assets/{star-B6Th07jw.js → star-damu_EYz.js} +1 -1
  26. package/dist/client/assets/{upload-BJwuErhV.js → upload-uH6CHlEw.js} +1 -1
  27. package/dist/client/assets/{users-BrnPTF8H.js → users-CUySbfji.js} +1 -1
  28. package/dist/client/index.html +2 -2
  29. package/dist/client/version.json +1 -1
  30. package/dist/extension.js +722 -120
  31. package/dist/pi-claude-cli/index.ts +2 -2
  32. package/dist/pi-claude-cli/package.json +1 -1
  33. package/package.json +1 -1
  34. package/dist/client/assets/AgentDetailView-C2Iik3Qf.js +0 -18
  35. package/dist/client/assets/SettingsModal-Zo5qDGOq.js +0 -31
  36. package/dist/client/assets/index-CEavim6l.js +0 -662
  37. package/dist/client/assets/index-D1gTSlYB.css +0 -1
package/dist/extension.js CHANGED
@@ -75,6 +75,7 @@ var init_settings_schema = __esm({
75
75
  favoriteModels: void 0,
76
76
  openrouterModelSync: true,
77
77
  updateCheckEnabled: true,
78
+ fnBinaryCheckEnabled: true,
78
79
  updateCheckFrequency: "daily",
79
80
  showGitHubStarButton: true,
80
81
  modelOnboardingComplete: void 0,
@@ -239,7 +240,8 @@ var init_settings_schema = __esm({
239
240
  maxPostReviewFixes: 1,
240
241
  maxSpawnedAgentsPerParent: 5,
241
242
  maxSpawnedAgentsGlobal: 20,
242
- maintenanceIntervalMs: 9e5,
243
+ // Run maintenance (including WAL checkpointing) every 5 minutes by default.
244
+ maintenanceIntervalMs: 3e5,
243
245
  autoArchiveDoneTasksEnabled: true,
244
246
  autoArchiveDoneAfterMs: 48 * 60 * 60 * 1e3,
245
247
  archiveAgentLogMode: "compact",
@@ -2650,6 +2652,7 @@ var init_sqlite_adapter = __esm({
2650
2652
  // ../core/src/db.ts
2651
2653
  import { isAbsolute, join as join2 } from "node:path";
2652
2654
  import { mkdirSync, existsSync } from "node:fs";
2655
+ import { spawnSync } from "node:child_process";
2653
2656
  function toJson(value) {
2654
2657
  if (value === void 0 || value === null) return "[]";
2655
2658
  if (Array.isArray(value) && value.length === 0) return "[]";
@@ -3281,15 +3284,24 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
3281
3284
  Database = class {
3282
3285
  db;
3283
3286
  dbPath;
3287
+ inMemory;
3288
+ corruptionDetected = false;
3284
3289
  /** Tracks transaction nesting depth for savepoint-based nested transactions. */
3285
3290
  transactionDepth = 0;
3286
3291
  _fts5Available;
3287
3292
  constructor(fusionDir, options) {
3288
3293
  const inMemory = options?.inMemory === true;
3294
+ this.inMemory = inMemory;
3289
3295
  this.dbPath = inMemory ? ":memory:" : join2(fusionDir, "fusion.db");
3290
3296
  if (!inMemory && !isAbsolute(fusionDir)) {
3291
3297
  throw new Error(`[fusion] Database constructor requires an absolute fusionDir path, got: ${fusionDir}`);
3292
3298
  }
3299
+ if (!inMemory && /\.fusion[\\/]\.fusion(?:[\\/]|$)/.test(fusionDir)) {
3300
+ throw new Error(
3301
+ `[fusion] Refusing to open Database at nested .fusion/.fusion path: ${fusionDir}
3302
+ This means a caller passed a .fusion directory where a project root was expected. Audit the call site for an extra \`join(rootDir, '.fusion')\` step.`
3303
+ );
3304
+ }
3293
3305
  if (!inMemory && !existsSync(fusionDir)) {
3294
3306
  mkdirSync(fusionDir, { recursive: true });
3295
3307
  }
@@ -3301,8 +3313,13 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
3301
3313
  }
3302
3314
  if (!inMemory) {
3303
3315
  this.db.exec("PRAGMA journal_mode = WAL");
3316
+ this.db.exec("PRAGMA busy_timeout = 5000");
3317
+ this.db.exec("PRAGMA synchronous = NORMAL");
3318
+ this.db.exec("PRAGMA wal_autocheckpoint = 100");
3319
+ this.db.exec("PRAGMA journal_size_limit = 4194304");
3320
+ } else {
3321
+ this.db.exec("PRAGMA busy_timeout = 5000");
3304
3322
  }
3305
- this.db.exec("PRAGMA busy_timeout = 5000");
3306
3323
  this.db.exec("PRAGMA foreign_keys = ON");
3307
3324
  this._fts5Available = probeFts5(this.db);
3308
3325
  }
@@ -3383,6 +3400,35 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
3383
3400
  return false;
3384
3401
  }
3385
3402
  }
3403
+ integrityCheck() {
3404
+ if (this.inMemory) {
3405
+ return { ok: true };
3406
+ }
3407
+ const rows = this.db.prepare("PRAGMA integrity_check(100)").all();
3408
+ const errors = rows.map((row) => row.integrity_check).filter((value) => typeof value === "string" && value !== "ok");
3409
+ if (errors.length > 0) {
3410
+ return { ok: false, errors };
3411
+ }
3412
+ return { ok: true };
3413
+ }
3414
+ recoverDatabase(outputPath) {
3415
+ if (this.inMemory) {
3416
+ return false;
3417
+ }
3418
+ const recoveredSql = spawnSync("sqlite3", ["-cmd", ".recover main", this.dbPath], {
3419
+ encoding: "utf-8",
3420
+ maxBuffer: 50 * 1024 * 1024
3421
+ });
3422
+ if (recoveredSql.status !== 0 || !recoveredSql.stdout) {
3423
+ return false;
3424
+ }
3425
+ const rebuilt = spawnSync("sqlite3", [outputPath], {
3426
+ input: recoveredSql.stdout,
3427
+ encoding: "utf-8",
3428
+ maxBuffer: 50 * 1024 * 1024
3429
+ });
3430
+ return rebuilt.status === 0;
3431
+ }
3386
3432
  /**
3387
3433
  * Initialize the database: create tables if they don't exist
3388
3434
  * and seed meta values.
@@ -3401,6 +3447,11 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
3401
3447
  this.db.exec(
3402
3448
  `INSERT OR IGNORE INTO config (id, nextId, nextWorkflowStepId, settings, workflowSteps, updatedAt) VALUES (1, 1, 1, '${JSON.stringify(DEFAULT_PROJECT_SETTINGS)}', '[]', '${configNow}')`
3403
3449
  );
3450
+ const integrity = this.integrityCheck();
3451
+ if (!integrity.ok) {
3452
+ this.corruptionDetected = true;
3453
+ console.warn("[fusion:db] Database integrity check FAILED \u2014 corruption detected");
3454
+ }
3404
3455
  }
3405
3456
  /**
3406
3457
  * Run incremental schema migrations based on the stored schema version.
@@ -6016,6 +6067,7 @@ var init_agent_store = __esm({
6016
6067
  };
6017
6068
  const line = JSON.stringify(safeEntry) + "\n";
6018
6069
  await appendFile(this.runLogPath(agentId, runId), line, "utf-8");
6070
+ this.emit("run:log", agentId, runId, safeEntry);
6019
6071
  }
6020
6072
  /**
6021
6073
  * Read all log entries for a given run from its JSONL file.
@@ -19069,7 +19121,7 @@ __export(migration_exports, {
19069
19121
  MigrationCoordinator: () => MigrationCoordinator,
19070
19122
  ProjectRequiredError: () => ProjectRequiredError
19071
19123
  });
19072
- import { existsSync as existsSync9, readFileSync as readFileSync2 } from "node:fs";
19124
+ import { existsSync as existsSync9, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
19073
19125
  import { homedir as homedir2, tmpdir } from "node:os";
19074
19126
  import { isAbsolute as isAbsolute3, join as join12, resolve as resolve5, basename as basename3, dirname as dirname4 } from "node:path";
19075
19127
  function getHomeDir2() {
@@ -19170,12 +19222,30 @@ var init_migration = __esm({
19170
19222
  const projects = [];
19171
19223
  const visited = /* @__PURE__ */ new Set();
19172
19224
  let current = resolve5(startDir);
19173
- const home = getHomeDir2();
19225
+ const home = resolve5(getHomeDir2());
19174
19226
  const root = dirname4(current) === current ? current : "/";
19175
19227
  const systemTmp = resolve5(tmpdir());
19176
- while (current !== home && current !== root && current !== systemTmp) {
19228
+ const normalizePath2 = (path2) => {
19229
+ try {
19230
+ return realpathSync2(path2);
19231
+ } catch {
19232
+ return resolve5(path2);
19233
+ }
19234
+ };
19235
+ const normalizedHome = normalizePath2(home);
19236
+ const normalizedSystemTmp = normalizePath2(systemTmp);
19237
+ const normalizedStartDir = normalizePath2(current);
19238
+ while (true) {
19177
19239
  if (visited.has(current)) break;
19178
19240
  visited.add(current);
19241
+ const normalizedCurrent = normalizePath2(current);
19242
+ const isRootBoundary = current === root;
19243
+ const isHomeBoundary = normalizedCurrent === normalizedHome;
19244
+ const isTmpBoundary = normalizedCurrent === normalizedSystemTmp;
19245
+ const isStopBoundary = isRootBoundary || isHomeBoundary || isTmpBoundary;
19246
+ if (isRootBoundary || isTmpBoundary || isHomeBoundary && normalizedCurrent !== normalizedStartDir) {
19247
+ break;
19248
+ }
19179
19249
  if (this.hasFusionProject(current)) {
19180
19250
  const name = await this.generateProjectName(current);
19181
19251
  projects.push({
@@ -19185,6 +19255,9 @@ var init_migration = __esm({
19185
19255
  });
19186
19256
  break;
19187
19257
  }
19258
+ if (isStopBoundary) {
19259
+ break;
19260
+ }
19188
19261
  const parent = dirname4(current);
19189
19262
  if (parent === current) break;
19190
19263
  current = parent;
@@ -34493,6 +34566,39 @@ ${task.description}
34493
34566
  this.db.bumpLastModified();
34494
34567
  this.emit("agent:log", entry);
34495
34568
  }
34569
+ async appendAgentLogBatch(entries) {
34570
+ if (entries.length === 0) {
34571
+ return;
34572
+ }
34573
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
34574
+ const stmt = this.db.prepare(`
34575
+ INSERT INTO agentLogEntries (taskId, timestamp, text, type, detail, agent)
34576
+ VALUES (?, ?, ?, ?, ?, ?)
34577
+ `);
34578
+ this.db.transaction(() => {
34579
+ for (const entry of entries) {
34580
+ stmt.run(
34581
+ entry.taskId,
34582
+ timestamp,
34583
+ entry.text,
34584
+ entry.type,
34585
+ entry.detail ?? null,
34586
+ entry.agent ?? null
34587
+ );
34588
+ }
34589
+ });
34590
+ this.db.bumpLastModified();
34591
+ for (const entry of entries) {
34592
+ this.emit("agent:log", {
34593
+ timestamp,
34594
+ taskId: entry.taskId,
34595
+ text: entry.text,
34596
+ type: entry.type,
34597
+ ...entry.detail !== void 0 && { detail: entry.detail },
34598
+ ...entry.agent !== void 0 && { agent: entry.agent }
34599
+ });
34600
+ }
34601
+ }
34496
34602
  mapAgentLogRow(row) {
34497
34603
  return {
34498
34604
  timestamp: row.timestamp,
@@ -36342,12 +36448,16 @@ var init_gh_cli = __esm({
36342
36448
 
36343
36449
  // ../core/src/fn-binary.ts
36344
36450
  import { spawn as spawn2 } from "node:child_process";
36345
- import { platform as platform2 } from "node:os";
36451
+ import { platform as platform2, tmpdir as tmpdir2 } from "node:os";
36346
36452
  function runProbe(command, args, timeoutMs) {
36347
36453
  return new Promise((resolve19) => {
36348
36454
  let stdout = "";
36349
36455
  let stderr = "";
36350
- const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"], shell: false });
36456
+ const child = spawn2(command, args, {
36457
+ stdio: ["ignore", "pipe", "pipe"],
36458
+ shell: false,
36459
+ cwd: tmpdir2()
36460
+ });
36351
36461
  const timer = setTimeout(() => {
36352
36462
  try {
36353
36463
  child.kill("SIGKILL");
@@ -37478,6 +37588,20 @@ var init_plugin_loader = __esm({
37478
37588
  }
37479
37589
  return slots;
37480
37590
  }
37591
+ /**
37592
+ * Get all top-level dashboard view definitions from loaded plugins.
37593
+ */
37594
+ getPluginDashboardViews() {
37595
+ const views = [];
37596
+ for (const [pluginId, plugin4] of this.plugins) {
37597
+ if (plugin4.dashboardViews) {
37598
+ for (const view of plugin4.dashboardViews) {
37599
+ views.push({ pluginId, view });
37600
+ }
37601
+ }
37602
+ }
37603
+ return views;
37604
+ }
37481
37605
  /**
37482
37606
  * Get all runtime registrations from loaded plugins.
37483
37607
  * Returns plugin ownership metadata along with the runtime registration.
@@ -37665,7 +37789,7 @@ async function syncBackupAutomation(automationStore, settings) {
37665
37789
  if (!AutomationStore2.isValidCron(schedule)) {
37666
37790
  throw new Error(`Invalid backup schedule: ${schedule}`);
37667
37791
  }
37668
- const command = "npx runfusion.ai backup --create";
37792
+ const command = "fn backup --create";
37669
37793
  if (existingSchedule) {
37670
37794
  return await automationStore.updateSchedule(existingSchedule.id, {
37671
37795
  scheduleType: "custom",
@@ -37698,7 +37822,7 @@ async function syncBackupRoutine(routineStore, settings) {
37698
37822
  if (!RoutineStore2.isValidCron(schedule)) {
37699
37823
  throw new Error(`Invalid backup schedule: ${schedule}`);
37700
37824
  }
37701
- const command = "npx runfusion.ai backup --create";
37825
+ const command = "fn backup --create";
37702
37826
  const input = {
37703
37827
  name: BACKUP_SCHEDULE_NAME,
37704
37828
  description: "Automatic database backup based on project settings",
@@ -49573,7 +49697,7 @@ var require_dist3 = __commonJS({
49573
49697
 
49574
49698
  // ../core/src/agent-companies-parser.ts
49575
49699
  import { existsSync as existsSync17, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync4, rmSync, statSync as statSync4 } from "node:fs";
49576
- import { tmpdir as tmpdir2 } from "node:os";
49700
+ import { tmpdir as tmpdir3 } from "node:os";
49577
49701
  import { join as join20, resolve as resolve9 } from "node:path";
49578
49702
  function slugifyAgentReference(value) {
49579
49703
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
@@ -49896,7 +50020,7 @@ async function extractTarArchive(archivePath, outputDir) {
49896
50020
  }
49897
50021
  async function parseCompanyArchive(archivePath) {
49898
50022
  const resolvedArchivePath = resolve9(archivePath);
49899
- const tempDir = mkdtempSync(join20(tmpdir2(), "agent-companies-"));
50023
+ const tempDir = mkdtempSync(join20(tmpdir3(), "agent-companies-"));
49900
50024
  try {
49901
50025
  if (resolvedArchivePath.endsWith(".tar.gz") || resolvedArchivePath.endsWith(".tgz")) {
49902
50026
  await extractTarArchive(resolvedArchivePath, tempDir);
@@ -51216,18 +51340,21 @@ function summarizeToolArgs(name, args) {
51216
51340
  }
51217
51341
  return void 0;
51218
51342
  }
51219
- var FLUSH_SIZE_BYTES, FLUSH_INTERVAL_MS, AgentLogger;
51343
+ var FLUSH_SIZE_BYTES, FLUSH_INTERVAL_MS, ENTRY_BATCH_SIZE, AgentLogger;
51220
51344
  var init_agent_logger = __esm({
51221
51345
  "../engine/src/agent-logger.ts"() {
51222
51346
  "use strict";
51223
51347
  init_logger2();
51224
51348
  FLUSH_SIZE_BYTES = 1024;
51225
51349
  FLUSH_INTERVAL_MS = 500;
51350
+ ENTRY_BATCH_SIZE = 50;
51226
51351
  AgentLogger = class {
51227
51352
  textBuffer = "";
51228
51353
  thinkingBuffer = "";
51229
51354
  flushTimer = null;
51230
51355
  thinkingFlushTimer = null;
51356
+ entryFlushTimer = null;
51357
+ pendingEntries = [];
51231
51358
  flushSizeBytes;
51232
51359
  flushIntervalMs;
51233
51360
  store;
@@ -51332,8 +51459,13 @@ var init_agent_logger = __esm({
51332
51459
  clearTimeout(this.thinkingFlushTimer);
51333
51460
  this.thinkingFlushTimer = null;
51334
51461
  }
51462
+ if (this.entryFlushTimer) {
51463
+ clearTimeout(this.entryFlushTimer);
51464
+ this.entryFlushTimer = null;
51465
+ }
51335
51466
  await this.flushTextBuffer();
51336
51467
  await this.flushThinkingBuffer();
51468
+ await this.flushPendingEntries();
51337
51469
  }
51338
51470
  // ── Internal helpers ───────────────────────────────────────────────
51339
51471
  /**
@@ -51342,7 +51474,7 @@ var init_agent_logger = __esm({
51342
51474
  * When only `appendLogCb` is set (no store/taskId), only the callback is used.
51343
51475
  * @param storeWarnMsg - Warning message prefix used when the task-store write fails.
51344
51476
  */
51345
- writeEntry(text, type, detail, storeWarnMsg) {
51477
+ writeEntry(text, type, detail, _storeWarnMsg, immediate = false) {
51346
51478
  const entry = {
51347
51479
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
51348
51480
  taskId: this.taskId,
@@ -51351,72 +51483,38 @@ var init_agent_logger = __esm({
51351
51483
  ...detail !== void 0 && { detail },
51352
51484
  ...this.agent !== void 0 && { agent: this.agent }
51353
51485
  };
51354
- if (this.store && this.taskId) {
51355
- this.store.appendAgentLog(this.taskId, text, type, detail, this.agent).catch((err) => {
51356
- this.log.warn(`${storeWarnMsg}: ${err instanceof Error ? err.message : String(err)}`);
51357
- });
51486
+ this.pendingEntries.push(entry);
51487
+ if (immediate || type !== "text" && type !== "thinking") {
51488
+ if (this.entryFlushTimer) {
51489
+ clearTimeout(this.entryFlushTimer);
51490
+ this.entryFlushTimer = null;
51491
+ }
51492
+ void this.flushPendingEntries();
51493
+ return;
51358
51494
  }
51359
- if (this.appendLogCb) {
51360
- this.appendLogCb(entry).catch((err) => {
51361
- this.log.warn(`appendLog callback failed for entry (${type}): ${err instanceof Error ? err.message : String(err)}`);
51362
- });
51495
+ if (this.pendingEntries.length >= ENTRY_BATCH_SIZE) {
51496
+ if (this.entryFlushTimer) {
51497
+ clearTimeout(this.entryFlushTimer);
51498
+ this.entryFlushTimer = null;
51499
+ }
51500
+ void this.flushPendingEntries();
51501
+ return;
51363
51502
  }
51503
+ this.scheduleEntryFlush();
51364
51504
  }
51365
51505
  flushTextBuffer() {
51366
51506
  if (this.textBuffer.length === 0) return Promise.resolve();
51367
51507
  const chunk = this.textBuffer;
51368
51508
  this.textBuffer = "";
51369
- const entry = {
51370
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
51371
- taskId: this.taskId,
51372
- text: chunk,
51373
- type: "text",
51374
- ...this.agent !== void 0 && { agent: this.agent }
51375
- };
51376
- const promises = [];
51377
- if (this.store && this.taskId) {
51378
- promises.push(
51379
- this.store.appendAgentLog(this.taskId, chunk, "text", void 0, this.agent).catch((err) => {
51380
- this.log.warn(`Failed to flush text buffer for ${this.taskId}: ${err instanceof Error ? err.message : String(err)}`);
51381
- })
51382
- );
51383
- }
51384
- if (this.appendLogCb) {
51385
- promises.push(
51386
- this.appendLogCb(entry).catch((err) => {
51387
- this.log.warn(`appendLog callback failed for text flush: ${err instanceof Error ? err.message : String(err)}`);
51388
- })
51389
- );
51390
- }
51391
- return Promise.all(promises).then(() => void 0);
51509
+ this.writeEntry(chunk, "text", void 0, `Failed to flush text buffer for ${this.taskId}`, true);
51510
+ return this.flushPendingEntries();
51392
51511
  }
51393
51512
  flushThinkingBuffer() {
51394
51513
  if (this.thinkingBuffer.length === 0) return Promise.resolve();
51395
51514
  const chunk = this.thinkingBuffer;
51396
51515
  this.thinkingBuffer = "";
51397
- const entry = {
51398
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
51399
- taskId: this.taskId,
51400
- text: chunk,
51401
- type: "thinking",
51402
- ...this.agent !== void 0 && { agent: this.agent }
51403
- };
51404
- const promises = [];
51405
- if (this.store && this.taskId) {
51406
- promises.push(
51407
- this.store.appendAgentLog(this.taskId, chunk, "thinking", void 0, this.agent).catch((err) => {
51408
- this.log.warn(`Failed to flush thinking buffer for ${this.taskId}: ${err instanceof Error ? err.message : String(err)}`);
51409
- })
51410
- );
51411
- }
51412
- if (this.appendLogCb) {
51413
- promises.push(
51414
- this.appendLogCb(entry).catch((err) => {
51415
- this.log.warn(`appendLog callback failed for thinking flush: ${err instanceof Error ? err.message : String(err)}`);
51416
- })
51417
- );
51418
- }
51419
- return Promise.all(promises).then(() => void 0);
51516
+ this.writeEntry(chunk, "thinking", void 0, `Failed to flush thinking buffer for ${this.taskId}`, true);
51517
+ return this.flushPendingEntries();
51420
51518
  }
51421
51519
  scheduleFlush() {
51422
51520
  if (this.flushTimer) return;
@@ -51432,6 +51530,52 @@ var init_agent_logger = __esm({
51432
51530
  this.flushThinkingBuffer();
51433
51531
  }, this.flushIntervalMs);
51434
51532
  }
51533
+ scheduleEntryFlush() {
51534
+ if (this.entryFlushTimer) return;
51535
+ this.entryFlushTimer = setTimeout(() => {
51536
+ this.entryFlushTimer = null;
51537
+ void this.flushPendingEntries();
51538
+ }, this.flushIntervalMs);
51539
+ }
51540
+ async flushPendingEntries() {
51541
+ if (this.pendingEntries.length === 0) {
51542
+ return;
51543
+ }
51544
+ const entries = this.pendingEntries;
51545
+ this.pendingEntries = [];
51546
+ if (this.store && this.taskId) {
51547
+ if (typeof this.store.appendAgentLogBatch === "function") {
51548
+ await this.store.appendAgentLogBatch(
51549
+ entries.map((entry) => ({
51550
+ taskId: entry.taskId,
51551
+ text: entry.text,
51552
+ type: entry.type,
51553
+ detail: entry.detail,
51554
+ agent: entry.agent
51555
+ }))
51556
+ ).catch((err) => {
51557
+ this.log.warn(`Failed to flush agent log batch for ${this.taskId}: ${err instanceof Error ? err.message : String(err)}`);
51558
+ });
51559
+ } else {
51560
+ await Promise.all(
51561
+ entries.map(
51562
+ (entry) => this.store.appendAgentLog(entry.taskId, entry.text, entry.type, entry.detail, entry.agent).catch((err) => {
51563
+ this.log.warn(`Failed to flush agent log entry for ${this.taskId}: ${err instanceof Error ? err.message : String(err)}`);
51564
+ })
51565
+ )
51566
+ );
51567
+ }
51568
+ }
51569
+ if (this.appendLogCb) {
51570
+ await Promise.all(
51571
+ entries.map(
51572
+ (entry) => this.appendLogCb(entry).catch((err) => {
51573
+ this.log.warn(`appendLog callback failed for entry (${entry.type}): ${err instanceof Error ? err.message : String(err)}`);
51574
+ })
51575
+ )
51576
+ );
51577
+ }
51578
+ }
51435
51579
  };
51436
51580
  }
51437
51581
  });
@@ -65008,6 +65152,20 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65008
65152
  );
65009
65153
  this.activeStepExecutors.delete(task.id);
65010
65154
  }
65155
+ if (this.activeWorkflowStepSessions.has(task.id)) {
65156
+ executorLog.log(`${task.id} moved from in-progress to ${to} \u2014 terminating workflow step session`);
65157
+ this.pausedAborted.add(task.id);
65158
+ this.options.stuckTaskDetector?.untrackTask(task.id);
65159
+ const workflowSession = this.activeWorkflowStepSessions.get(task.id);
65160
+ const sessionWithAbort = workflowSession;
65161
+ if (typeof sessionWithAbort.abort === "function") {
65162
+ void sessionWithAbort.abort().catch((err) => {
65163
+ executorLog.warn(`Failed to abort workflow step session for ${task.id}: ${err}`);
65164
+ });
65165
+ }
65166
+ workflowSession.dispose();
65167
+ this.activeWorkflowStepSessions.delete(task.id);
65168
+ }
65011
65169
  this.disposeSubagentsForTask(task.id, `parent moved from in-progress to ${to}`);
65012
65170
  this.loopRecoveryState.delete(task.id);
65013
65171
  this.spawnedAgents.delete(task.id);
@@ -65040,8 +65198,42 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65040
65198
  this.disposeSubagentsForTask(task.id, "task paused");
65041
65199
  return;
65042
65200
  }
65043
- if (!task.paused && task.column === "in-progress" && !this.activeSessions.has(task.id) && !this.activeStepExecutors.has(task.id)) {
65201
+ if (task.paused && this.activeWorkflowStepSessions.has(task.id)) {
65202
+ executorLog.log(`Pausing ${task.id} \u2014 terminating workflow step session`);
65203
+ this.pausedAborted.add(task.id);
65204
+ this.options.stuckTaskDetector?.untrackTask(task.id);
65205
+ const workflowSession = this.activeWorkflowStepSessions.get(task.id);
65206
+ const sessionWithAbort = workflowSession;
65207
+ if (typeof sessionWithAbort.abort === "function") {
65208
+ await sessionWithAbort.abort().catch(
65209
+ (err) => executorLog.warn(`Failed to abort workflow step session for pause ${task.id}: ${err}`)
65210
+ );
65211
+ }
65212
+ workflowSession.dispose();
65213
+ this.activeWorkflowStepSessions.delete(task.id);
65214
+ this.loopRecoveryState.delete(task.id);
65215
+ this.spawnedAgents.delete(task.id);
65216
+ this.stuckAborted.delete(task.id);
65217
+ this.disposeSubagentsForTask(task.id, "task paused");
65218
+ return;
65219
+ }
65220
+ if (!task.paused && task.column === "in-progress" && !this.activeSessions.has(task.id) && !this.activeStepExecutors.has(task.id) && !this.activeWorkflowStepSessions.has(task.id)) {
65044
65221
  if (!this.executing.has(task.id) && !this.resumingUnpaused.has(task.id) && !this.recoveringCompleted.has(task.id)) {
65222
+ const pauseLabel = await this.getExecutionPauseLabel();
65223
+ if (pauseLabel) {
65224
+ executorLog.log(`Skipping unpause resume for ${task.id} \u2014 ${pauseLabel} active`);
65225
+ return;
65226
+ }
65227
+ if (this.isTaskWorkComplete(task) && !task.mergeDetails) {
65228
+ this.recoveringCompleted.add(task.id);
65229
+ executorLog.log(`${task.id} unpaused with completed work and no session \u2014 recovering directly to in-review`);
65230
+ void this.recoverCompletedTask(task).catch(
65231
+ (err) => executorLog.error(`Failed to recover completed unpaused task ${task.id}:`, err)
65232
+ ).finally(() => {
65233
+ this.recoveringCompleted.delete(task.id);
65234
+ });
65235
+ return;
65236
+ }
65045
65237
  this.resumingUnpaused.add(task.id);
65046
65238
  executorLog.log(`Unpaused ${task.id} in-progress with no session \u2014 resuming execution`);
65047
65239
  try {
@@ -65160,6 +65352,22 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65160
65352
  this.spawnedAgents.delete(taskId);
65161
65353
  this.stuckAborted.delete(taskId);
65162
65354
  }
65355
+ for (const [taskId, workflowSession] of this.activeWorkflowStepSessions) {
65356
+ executorLog.log(`Global pause \u2014 terminating workflow step session for ${taskId}`);
65357
+ this.pausedAborted.add(taskId);
65358
+ this.options.stuckTaskDetector?.untrackTask(taskId);
65359
+ const sessionWithAbort = workflowSession;
65360
+ if (typeof sessionWithAbort.abort === "function") {
65361
+ void sessionWithAbort.abort().catch((err) => {
65362
+ executorLog.warn(`Failed to abort workflow step session for ${taskId}: ${err}`);
65363
+ });
65364
+ }
65365
+ workflowSession.dispose();
65366
+ this.activeWorkflowStepSessions.delete(taskId);
65367
+ this.loopRecoveryState.delete(taskId);
65368
+ this.spawnedAgents.delete(taskId);
65369
+ this.stuckAborted.delete(taskId);
65370
+ }
65163
65371
  }
65164
65372
  });
65165
65373
  }
@@ -65177,6 +65385,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65177
65385
  activeSessions = /* @__PURE__ */ new Map();
65178
65386
  /** Active step-session executors per task (mutually exclusive with activeSessions). */
65179
65387
  activeStepExecutors = /* @__PURE__ */ new Map();
65388
+ /** Active pre-merge workflow step sessions per task. */
65389
+ activeWorkflowStepSessions = /* @__PURE__ */ new Map();
65180
65390
  /**
65181
65391
  * Reviewer subagent sessions per task. Reviewers (`reviewer.ts`) create their
65182
65392
  * own AgentSessions that aren't part of `activeSessions`/`activeStepExecutors`,
@@ -65223,6 +65433,69 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65223
65433
  await this.store.mergeTask(taskId);
65224
65434
  return "merged";
65225
65435
  }
65436
+ async getExecutionPauseLabel() {
65437
+ const settings = await this.store.getSettings();
65438
+ if (settings.globalPause) return "global pause";
65439
+ if (settings.enginePaused) return "engine pause";
65440
+ return null;
65441
+ }
65442
+ async shouldDeferCompletionForGlobalPause(taskId, context) {
65443
+ const settings = await this.store.getSettings();
65444
+ if (!settings.globalPause) {
65445
+ return false;
65446
+ }
65447
+ this.clearCompletedTaskWatchdog(taskId);
65448
+ executorLog.log(`${taskId}: completion handoff deferred \u2014 global pause active (${context})`);
65449
+ await this.store.logEntry(
65450
+ taskId,
65451
+ `Completion handoff deferred \u2014 global pause active (${context})`,
65452
+ void 0,
65453
+ this.currentRunContext
65454
+ ).catch(() => void 0);
65455
+ return true;
65456
+ }
65457
+ async shouldDeferWorkflowStepCompletion(taskId, context) {
65458
+ let latestTask = null;
65459
+ try {
65460
+ latestTask = await this.store.getTask(taskId);
65461
+ } catch {
65462
+ latestTask = null;
65463
+ }
65464
+ if (latestTask?.paused || this.pausedAborted.has(taskId)) {
65465
+ this.clearCompletedTaskWatchdog(taskId);
65466
+ executorLog.log(`${taskId}: completion handoff deferred \u2014 task paused (${context})`);
65467
+ await this.store.logEntry(
65468
+ taskId,
65469
+ `Completion handoff deferred \u2014 task paused (${context})`,
65470
+ void 0,
65471
+ this.currentRunContext
65472
+ ).catch(() => void 0);
65473
+ return true;
65474
+ }
65475
+ return this.shouldDeferCompletionForGlobalPause(taskId, context);
65476
+ }
65477
+ async parkTaskAfterWorkflowStepPause(taskId) {
65478
+ let latestTask = null;
65479
+ try {
65480
+ latestTask = await this.store.getTask(taskId);
65481
+ } catch {
65482
+ latestTask = null;
65483
+ }
65484
+ if (!latestTask?.paused) {
65485
+ return false;
65486
+ }
65487
+ executorLog.log(`${taskId}: workflow step interrupted by task pause \u2014 moving to todo`);
65488
+ await this.store.logEntry(
65489
+ taskId,
65490
+ "Execution paused during pre-merge workflow step \u2014 moved to todo",
65491
+ void 0,
65492
+ this.currentRunContext
65493
+ ).catch(() => void 0);
65494
+ if (latestTask.column === "in-progress") {
65495
+ await this.store.moveTask(taskId, "todo", { preserveResumeState: true });
65496
+ }
65497
+ return true;
65498
+ }
65226
65499
  /** Child agent sessions keyed by agent ID. Used for termination. */
65227
65500
  childSessions = /* @__PURE__ */ new Map();
65228
65501
  /** Total count of currently spawned agents (across all parents). */
@@ -65417,11 +65690,15 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65417
65690
  this.clearCompletedTaskWatchdog(taskId);
65418
65691
  const handle = setTimeout(async () => {
65419
65692
  this.completedTaskWatchdogs.delete(taskId);
65420
- if (this.recoveringCompleted.has(taskId) || this.executing.has(taskId) || this.activeSessions.has(taskId) || this.activeStepExecutors.has(taskId) || this.resumingUnpaused.has(taskId)) {
65693
+ if (this.recoveringCompleted.has(taskId) || this.executing.has(taskId) || this.activeSessions.has(taskId) || this.activeStepExecutors.has(taskId) || this.activeWorkflowStepSessions.has(taskId) || this.resumingUnpaused.has(taskId)) {
65421
65694
  return;
65422
65695
  }
65423
65696
  this.recoveringCompleted.add(taskId);
65424
65697
  try {
65698
+ const pauseLabel = await this.getExecutionPauseLabel();
65699
+ if (pauseLabel) {
65700
+ return;
65701
+ }
65425
65702
  let currentTask = null;
65426
65703
  try {
65427
65704
  currentTask = await this.store.getTask(taskId);
@@ -65467,6 +65744,11 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65467
65744
  * stuck.
65468
65745
  */
65469
65746
  async performWorkflowRerunBounce(taskId, worktreePath, preserveResumeState = true) {
65747
+ const pauseLabel = await this.getExecutionPauseLabel();
65748
+ if (pauseLabel) {
65749
+ executorLog.log(`${taskId}: workflow rerun deferred \u2014 ${pauseLabel} active`);
65750
+ return "deferred-paused";
65751
+ }
65470
65752
  if (this.workflowRerunPending.has(taskId)) {
65471
65753
  executorLog.warn(`${taskId}: workflow rerun bounce already in flight \u2014 skipping re-entry`);
65472
65754
  return "skipped-pending";
@@ -65477,6 +65759,10 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65477
65759
  if (!latestTask) {
65478
65760
  throw new Error("task missing during workflow rerun bounce");
65479
65761
  }
65762
+ if (latestTask.paused) {
65763
+ executorLog.log(`${taskId}: workflow rerun deferred \u2014 task is paused`);
65764
+ return "deferred-paused";
65765
+ }
65480
65766
  if (latestTask.column === "in-progress") {
65481
65767
  const originalExecutionStartedAt = latestTask.executionStartedAt;
65482
65768
  if (preserveResumeState) {
@@ -65488,11 +65774,21 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65488
65774
  worktree: worktreePath,
65489
65775
  executionStartedAt: originalExecutionStartedAt ?? null
65490
65776
  });
65777
+ const pauseLabelAfterTodo = await this.getExecutionPauseLabel();
65778
+ if (pauseLabelAfterTodo) {
65779
+ executorLog.log(`${taskId}: workflow rerun parked in todo \u2014 ${pauseLabelAfterTodo} became active during bounce`);
65780
+ return "deferred-paused";
65781
+ }
65491
65782
  await this.store.moveTask(taskId, "in-progress");
65492
65783
  return "bounced";
65493
65784
  }
65494
65785
  if (latestTask.column === "todo") {
65495
65786
  await this.store.updateTask(taskId, { worktree: worktreePath });
65787
+ const pauseLabelBeforeResume = await this.getExecutionPauseLabel();
65788
+ if (pauseLabelBeforeResume) {
65789
+ executorLog.log(`${taskId}: workflow rerun parked in todo \u2014 ${pauseLabelBeforeResume} became active before resume`);
65790
+ return "deferred-paused";
65791
+ }
65496
65792
  await this.store.moveTask(taskId, "in-progress");
65497
65793
  return "bounced";
65498
65794
  }
@@ -65508,8 +65804,10 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65508
65804
  const outcome = await this.performWorkflowRerunBounce(taskId, worktreePath, preserveResumeState);
65509
65805
  if (outcome === "bounced") {
65510
65806
  executorLog.log(successMessage);
65511
- } else {
65807
+ } else if (outcome === "skipped-pending") {
65512
65808
  executorLog.warn(`${taskId}: rerun bounce skipped \u2014 another bounce already in flight`);
65809
+ } else {
65810
+ executorLog.log(`${taskId}: rerun bounce deferred while pause is active`);
65513
65811
  }
65514
65812
  } catch (err) {
65515
65813
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -65518,6 +65816,11 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65518
65816
  }, 0);
65519
65817
  const watchdog = setTimeout(async () => {
65520
65818
  this.workflowRerunWatchdogs.delete(taskId);
65819
+ const pauseLabel = await this.getExecutionPauseLabel();
65820
+ if (pauseLabel) {
65821
+ executorLog.log(`${taskId}: workflow rerun watchdog skipped \u2014 ${pauseLabel} active`);
65822
+ return;
65823
+ }
65521
65824
  let currentTask = null;
65522
65825
  try {
65523
65826
  currentTask = await this.store.getTask(taskId);
@@ -65540,7 +65843,7 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65540
65843
  const outcome = await this.performWorkflowRerunBounce(taskId, worktreePath, preserveResumeState);
65541
65844
  if (outcome === "bounced") {
65542
65845
  executorLog.warn(`${taskId}: workflow rerun watchdog retry succeeded`);
65543
- } else {
65846
+ } else if (outcome === "skipped-pending") {
65544
65847
  executorLog.error(
65545
65848
  `${taskId}: workflow rerun watchdog retry skipped \u2014 original bounce still in flight after ${WORKFLOW_RERUN_WATCHDOG_MS / 1e3}s; task may be stuck`
65546
65849
  );
@@ -65548,6 +65851,8 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65548
65851
  taskId,
65549
65852
  `Workflow rerun watchdog retry skipped \u2014 original bounce still in flight after ${WORKFLOW_RERUN_WATCHDOG_MS / 1e3}s; task may be stuck`
65550
65853
  ).catch(() => void 0);
65854
+ } else {
65855
+ executorLog.log(`${taskId}: workflow rerun watchdog retry deferred while pause is active`);
65551
65856
  }
65552
65857
  } catch (err) {
65553
65858
  const errorMessage = err instanceof Error ? err.message : String(err);
@@ -65670,11 +65975,17 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65670
65975
  */
65671
65976
  async recoverCompletedTask(task) {
65672
65977
  try {
65673
- if (this.executing.has(task.id) || this.activeSessions.has(task.id) || this.activeStepExecutors.has(task.id) || this.resumingUnpaused.has(task.id)) {
65978
+ if (this.executing.has(task.id) || this.activeSessions.has(task.id) || this.activeStepExecutors.has(task.id) || this.activeWorkflowStepSessions.has(task.id) || this.resumingUnpaused.has(task.id)) {
65674
65979
  executorLog.log(`${task.id}: skipping recoverCompletedTask \u2014 task has active execution in flight`);
65675
65980
  return false;
65676
65981
  }
65677
65982
  const settings = await this.store.getSettings();
65983
+ if (settings.globalPause || settings.enginePaused) {
65984
+ executorLog.log(
65985
+ `${task.id}: skipping recoverCompletedTask \u2014 ${settings.globalPause ? "global pause" : "engine pause"} active`
65986
+ );
65987
+ return false;
65988
+ }
65678
65989
  if (task.worktree && existsSync27(task.worktree)) {
65679
65990
  const modifiedFiles = await this.captureModifiedFiles(task.worktree, task.baseCommitSha);
65680
65991
  if (modifiedFiles.length > 0) {
@@ -65682,7 +65993,16 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65682
65993
  executorLog.log(`${task.id}: recovered ${modifiedFiles.length} modified files`);
65683
65994
  }
65684
65995
  if (task.executionMode !== "fast") {
65996
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps during completed-task recovery")) {
65997
+ return false;
65998
+ }
65685
65999
  const workflowResult = await this.runWorkflowSteps(task, task.worktree, settings);
66000
+ if (workflowResult === "deferred-paused") {
66001
+ if (this.pausedAborted.has(task.id)) {
66002
+ this.pausedAborted.delete(task.id);
66003
+ }
66004
+ return false;
66005
+ }
65686
66006
  if (!workflowResult.allPassed) {
65687
66007
  await this.sendTaskBackForFix(task, task.worktree, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed during recovery", false);
65688
66008
  return true;
@@ -65691,6 +66011,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65691
66011
  executorLog.log(`${task.id}: fast mode \u2014 skipping workflow steps on auto-recovery`);
65692
66012
  }
65693
66013
  }
66014
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition during completed-task recovery")) {
66015
+ return false;
66016
+ }
65694
66017
  await this.persistTokenUsage(task.id);
65695
66018
  await this.store.moveTask(task.id, "in-review");
65696
66019
  this.clearCompletedTaskWatchdog(task.id);
@@ -65756,6 +66079,13 @@ The tool prevents your session from being killed by the inactivity watchdog duri
65756
66079
  * directly to in-review without spawning a new agent session.
65757
66080
  */
65758
66081
  async resumeOrphaned() {
66082
+ const settings = await this.store.getSettings();
66083
+ if (settings.globalPause || settings.enginePaused) {
66084
+ executorLog.log(
66085
+ `resumeOrphaned skipped \u2014 ${settings.globalPause ? "global pause" : "engine pause"} is active`
66086
+ );
66087
+ return;
66088
+ }
65759
66089
  const tasks = await this.store.listTasks({ slim: true, column: "in-progress" });
65760
66090
  const inProgress = tasks.filter(
65761
66091
  (t) => t.column === "in-progress" && !this.executing.has(t.id) && !t.paused
@@ -66194,8 +66524,21 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66194
66524
  await audit.filesystem({ type: "file:capture-modified", target: task.id, metadata: { files: modifiedFiles } });
66195
66525
  }
66196
66526
  this.scheduleCompletedTaskWatchdog(task.id, "step-session completion");
66527
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after step-session completion")) {
66528
+ return;
66529
+ }
66197
66530
  if (executionMode !== "fast") {
66198
66531
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
66532
+ if (workflowResult === "deferred-paused") {
66533
+ if (await this.parkTaskAfterWorkflowStepPause(task.id)) {
66534
+ this.pausedAborted.delete(task.id);
66535
+ return;
66536
+ }
66537
+ if (this.pausedAborted.has(task.id)) {
66538
+ this.pausedAborted.delete(task.id);
66539
+ }
66540
+ return;
66541
+ }
66199
66542
  if (!workflowResult.allPassed) {
66200
66543
  if (workflowResult.revisionRequested) {
66201
66544
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -66213,6 +66556,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66213
66556
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
66214
66557
  }
66215
66558
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
66559
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition after step-session completion")) {
66560
+ return;
66561
+ }
66216
66562
  await this.store.moveTask(task.id, "in-review");
66217
66563
  this.clearCompletedTaskWatchdog(task.id);
66218
66564
  await audit.database({ type: "task:move", target: task.id, metadata: { to: "in-review" } });
@@ -66565,6 +66911,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66565
66911
  this.pausedAborted.delete(task.id);
66566
66912
  wasPaused = true;
66567
66913
  if (await this.shouldFinalizeCompletedTask(task.id, taskDone)) {
66914
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "paused after completion")) {
66915
+ return;
66916
+ }
66568
66917
  executorLog.log(`${task.id} paused after completion (graceful session exit) \u2014 finalizing to in-review`);
66569
66918
  await this.store.logEntry(task.id, "Execution paused after completion \u2014 finalizing to in-review");
66570
66919
  await this.persistTokenUsage(task.id);
@@ -66601,8 +66950,23 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66601
66950
  executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
66602
66951
  }
66603
66952
  this.scheduleCompletedTaskWatchdog(task.id, "task completion");
66953
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after task completion")) {
66954
+ return;
66955
+ }
66604
66956
  if (executionMode !== "fast") {
66605
66957
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
66958
+ if (workflowResult === "deferred-paused") {
66959
+ if (await this.parkTaskAfterWorkflowStepPause(task.id)) {
66960
+ this.pausedAborted.delete(task.id);
66961
+ wasPaused = true;
66962
+ return;
66963
+ }
66964
+ if (this.pausedAborted.has(task.id)) {
66965
+ this.pausedAborted.delete(task.id);
66966
+ wasPaused = true;
66967
+ }
66968
+ return;
66969
+ }
66606
66970
  if (!workflowResult.allPassed) {
66607
66971
  if (workflowResult.revisionRequested) {
66608
66972
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -66620,6 +66984,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66620
66984
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
66621
66985
  }
66622
66986
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
66987
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition after task completion")) {
66988
+ return;
66989
+ }
66623
66990
  await this.persistTokenUsage(task.id);
66624
66991
  await this.store.moveTask(task.id, "in-review");
66625
66992
  this.clearCompletedTaskWatchdog(task.id);
@@ -66744,8 +67111,23 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66744
67111
  executorLog.log(`${task.id}: captured ${modifiedFiles.length} modified files`);
66745
67112
  }
66746
67113
  this.scheduleCompletedTaskWatchdog(task.id, "task completion retry");
67114
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before workflow steps after task completion retry")) {
67115
+ return;
67116
+ }
66747
67117
  if (executionMode !== "fast") {
66748
67118
  const workflowResult = await this.runWorkflowSteps(task, worktreePath, settings);
67119
+ if (workflowResult === "deferred-paused") {
67120
+ if (await this.parkTaskAfterWorkflowStepPause(task.id)) {
67121
+ this.pausedAborted.delete(task.id);
67122
+ wasPaused = true;
67123
+ return;
67124
+ }
67125
+ if (this.pausedAborted.has(task.id)) {
67126
+ this.pausedAborted.delete(task.id);
67127
+ wasPaused = true;
67128
+ }
67129
+ return;
67130
+ }
66749
67131
  if (!workflowResult.allPassed) {
66750
67132
  if (workflowResult.revisionRequested) {
66751
67133
  await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
@@ -66759,6 +67141,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66759
67141
  await this.store.logEntry(task.id, "Fast mode \u2014 pre-merge workflow steps skipped", void 0, this.currentRunContext);
66760
67142
  }
66761
67143
  await this.store.updateTask(task.id, { workflowStepRetries: void 0, taskDoneRetryCount: null });
67144
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "before in-review transition after task completion retry")) {
67145
+ return;
67146
+ }
66762
67147
  await this.persistTokenUsage(task.id);
66763
67148
  await this.store.moveTask(task.id, "in-review");
66764
67149
  this.clearCompletedTaskWatchdog(task.id);
@@ -66851,6 +67236,9 @@ The tool prevents your session from being killed by the inactivity watchdog duri
66851
67236
  } else if (this.pausedAborted.has(task.id)) {
66852
67237
  this.pausedAborted.delete(task.id);
66853
67238
  if (await this.shouldFinalizeCompletedTask(task.id, taskDone)) {
67239
+ if (await this.shouldDeferCompletionForGlobalPause(task.id, "paused after completion")) {
67240
+ return;
67241
+ }
66854
67242
  executorLog.log(`${task.id} paused after completion \u2014 finalizing to in-review`);
66855
67243
  await this.store.logEntry(task.id, "Execution paused after completion \u2014 finalizing to in-review", void 0, this.currentRunContext);
66856
67244
  await this.persistTokenUsage(task.id);
@@ -67209,22 +67597,28 @@ The tool prevents your session from being killed by the inactivity watchdog duri
67209
67597
  if (params.summary) {
67210
67598
  await store.updateTask(taskId, { summary: params.summary });
67211
67599
  }
67212
- await store.updateTask(taskId, { paused: false, status: null });
67600
+ const settings = await store.getSettings();
67601
+ const hardPauseActive = Boolean(task.paused || settings.globalPause);
67602
+ if (hardPauseActive) {
67603
+ await store.updateTask(taskId, { status: null });
67604
+ } else {
67605
+ await store.updateTask(taskId, { paused: false, status: null });
67606
+ }
67213
67607
  await store.logEntry(taskId, "Task marked done by agent");
67214
67608
  const latestTask = await store.getTask(taskId);
67215
67609
  let latestColumn = latestTask.column;
67216
67610
  if (latestColumn === "todo") {
67217
67611
  await store.logEntry(
67218
67612
  taskId,
67219
- "fn_task_done called while task was in todo \u2014 promoting to in-progress before completion handoff"
67613
+ hardPauseActive ? "fn_task_done called while task was in todo during pause \u2014 promoting to in-progress for deferred completion handoff" : "fn_task_done called while task was in todo \u2014 promoting to in-progress before completion handoff"
67220
67614
  );
67221
67615
  await store.moveTask(taskId, "in-progress");
67222
67616
  latestColumn = "in-progress";
67223
67617
  }
67224
- if (latestColumn === "in-progress") {
67618
+ if (latestColumn === "in-progress" && !hardPauseActive) {
67225
67619
  this.scheduleCompletedTaskWatchdog(taskId, "fn_task_done");
67226
67620
  }
67227
- const successMessage = params.summary ? "Task marked complete with summary. All steps done. Moving to in-review." : "Task marked complete. All steps done. Moving to in-review.";
67621
+ const successMessage = hardPauseActive ? "Task marked complete. Completion handoff deferred until pause is cleared." : params.summary ? "Task marked complete with summary. All steps done. Moving to in-review." : "Task marked complete. All steps done. Moving to in-review.";
67228
67622
  return {
67229
67623
  content: [{ type: "text", text: successMessage }],
67230
67624
  details: {}
@@ -67799,6 +68193,9 @@ ${failureFeedback}
67799
68193
  await this.store.updateTask(task.id, { workflowStepResults: results });
67800
68194
  continue;
67801
68195
  }
68196
+ if (await this.shouldDeferWorkflowStepCompletion(task.id, `before workflow step '${ws.name}'`)) {
68197
+ return "deferred-paused";
68198
+ }
67802
68199
  await this.store.logEntry(task.id, `[pre-merge] Starting workflow step: ${ws.name} (${stepMode} mode)`);
67803
68200
  executorLog.log(`${task.id} \u2014 [pre-merge] running workflow step: ${ws.name} (${stepMode} mode)`);
67804
68201
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -67813,6 +68210,9 @@ ${failureFeedback}
67813
68210
  await this.store.updateTask(task.id, { workflowStepResults: results });
67814
68211
  try {
67815
68212
  const result = stepMode === "script" ? await this.executeScriptWorkflowStep(task, ws, worktreePath, settings) : await this.executeWorkflowStep(task, ws, worktreePath, settings);
68213
+ if (await this.shouldDeferWorkflowStepCompletion(task.id, `workflow step '${ws.name}'`)) {
68214
+ return "deferred-paused";
68215
+ }
67816
68216
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
67817
68217
  if (result.success) {
67818
68218
  await this.store.logEntry(task.id, `[timing] Workflow step '${ws.name}' completed in ${Date.now() - stepStartedAtMs}ms`);
@@ -67878,6 +68278,9 @@ ${failureFeedback}
67878
68278
  };
67879
68279
  }
67880
68280
  } catch (err) {
68281
+ if (await this.shouldDeferWorkflowStepCompletion(task.id, `workflow step '${ws.name}'`)) {
68282
+ return "deferred-paused";
68283
+ }
67881
68284
  const { message: errorMessage, detail: errorDetail, stack: errorStack } = formatError(err);
67882
68285
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
67883
68286
  await this.store.logEntry(
@@ -68043,6 +68446,7 @@ and show an appropriate message to the user.\`
68043
68446
  task.id,
68044
68447
  `Workflow step '${workflowStep.name}' using model: ${describeModel(session)}${useOverride && attemptLabel === "primary" ? " (workflow step override)" : ""}${attemptLabel === "fallback" ? " (fallback after timeout)" : ""}`
68045
68448
  );
68449
+ this.activeWorkflowStepSessions.set(task.id, session);
68046
68450
  let output = "";
68047
68451
  session.subscribe((event) => {
68048
68452
  if (event.type === "message_update") {
@@ -68115,6 +68519,10 @@ Review the work done in this worktree and evaluate it against the criteria in yo
68115
68519
  return { success: false, error: errorMessage };
68116
68520
  } finally {
68117
68521
  if (timeoutHandle) clearTimeout(timeoutHandle);
68522
+ const activeWorkflowStepSession = this.activeWorkflowStepSessions.get(task.id);
68523
+ if (activeWorkflowStepSession === session) {
68524
+ this.activeWorkflowStepSessions.delete(task.id);
68525
+ }
68118
68526
  void timedOut;
68119
68527
  }
68120
68528
  };
@@ -69858,6 +70266,15 @@ var init_scheduler = __esm({
69858
70266
  schedulerLog.log(`Task ${task.id} is paused \u2014 skipping dispatch`);
69859
70267
  continue;
69860
70268
  }
70269
+ const latestSettings = await this.store.getSettings();
70270
+ if (latestSettings.globalPause) {
70271
+ schedulerLog.log(`Task ${task.id} dispatch aborted \u2014 globalPause became active mid-pass`);
70272
+ continue;
70273
+ }
70274
+ if (latestSettings.enginePaused) {
70275
+ schedulerLog.log(`Task ${task.id} dispatch aborted \u2014 enginePaused became active mid-pass`);
70276
+ continue;
70277
+ }
69861
70278
  let effectiveNode = resolveEffectiveNode(freshTask, settings);
69862
70279
  schedulerLog.log(`Task ${task.id} routed to node=${effectiveNode.nodeId ?? "local"} (source=${effectiveNode.source})`);
69863
70280
  if (effectiveNode.nodeId !== void 0 && this.options.nodeHealthMonitor) {
@@ -74995,6 +75412,36 @@ function execCommand(command, options) {
74995
75412
  });
74996
75413
  });
74997
75414
  }
75415
+ function isInProcessBackupCommand(command) {
75416
+ if (!command) return false;
75417
+ const trimmed = command.trim();
75418
+ if (!trimmed) return false;
75419
+ if (SHELL_METACHARACTERS_REGEX.test(trimmed)) return false;
75420
+ const tokens = trimmed.split(/\s+/).map((tok) => tok.toLowerCase());
75421
+ let cursor = 0;
75422
+ if (tokens[cursor] === "npx") {
75423
+ cursor += 1;
75424
+ while (cursor < tokens.length) {
75425
+ const tok = tokens[cursor];
75426
+ if (tok === void 0 || !tok.startsWith("-")) break;
75427
+ const takesValue = (tok === "-p" || tok === "--package") && cursor + 1 < tokens.length && tokens[cursor + 1] !== void 0 && !tokens[cursor + 1].startsWith("-");
75428
+ cursor += takesValue ? 2 : 1;
75429
+ }
75430
+ }
75431
+ const binary = tokens[cursor];
75432
+ if (!binary || !FUSION_BINARY_TOKENS.has(binary)) return false;
75433
+ cursor += 1;
75434
+ if (tokens[cursor] !== "backup") return false;
75435
+ cursor += 1;
75436
+ if (tokens[cursor] !== "--create") return false;
75437
+ cursor += 1;
75438
+ for (; cursor < tokens.length; cursor += 1) {
75439
+ const tok = tokens[cursor];
75440
+ if (!tok) continue;
75441
+ if (!tok.startsWith("-")) return false;
75442
+ }
75443
+ return true;
75444
+ }
74998
75445
  async function createAiPromptExecutor(cwd) {
74999
75446
  const disposeLog = createLogger2("cron-runner");
75000
75447
  return async (prompt, modelProvider, modelId) => {
@@ -75034,7 +75481,7 @@ function truncateOutput(stdout, stderr) {
75034
75481
  }
75035
75482
  return combined;
75036
75483
  }
75037
- var log14, DEFAULT_TIMEOUT_MS6, MAX_BUFFER, MAX_OUTPUT_LENGTH, DEFAULT_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, CronRunner, AI_AUTOMATION_SYSTEM_PROMPT;
75484
+ var log14, FUSION_BINARY_TOKENS, SHELL_METACHARACTERS_REGEX, DEFAULT_TIMEOUT_MS6, MAX_BUFFER, MAX_OUTPUT_LENGTH, DEFAULT_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, CronRunner, AI_AUTOMATION_SYSTEM_PROMPT;
75038
75485
  var init_cron_runner = __esm({
75039
75486
  "../engine/src/cron-runner.ts"() {
75040
75487
  "use strict";
@@ -75043,6 +75490,14 @@ var init_cron_runner = __esm({
75043
75490
  init_shell_utils();
75044
75491
  init_pi();
75045
75492
  log14 = createLogger2("cron-runner");
75493
+ FUSION_BINARY_TOKENS = /* @__PURE__ */ new Set([
75494
+ "fn",
75495
+ "fusion",
75496
+ "runfusion",
75497
+ "runfusion.ai",
75498
+ "@runfusion/fusion"
75499
+ ]);
75500
+ SHELL_METACHARACTERS_REGEX = /[&|;<>`$()]/;
75046
75501
  DEFAULT_TIMEOUT_MS6 = 5 * 60 * 1e3;
75047
75502
  MAX_BUFFER = 1024 * 1024;
75048
75503
  MAX_OUTPUT_LENGTH = 10 * 1024;
@@ -75183,6 +75638,9 @@ var init_cron_runner = __esm({
75183
75638
  */
75184
75639
  async executeLegacyCommand(schedule, startedAt) {
75185
75640
  log14.log(`Executing ${schedule.name} (${schedule.id}): ${schedule.command}`);
75641
+ if (isInProcessBackupCommand(schedule.command)) {
75642
+ return this.executeBackupInProcess(schedule, startedAt);
75643
+ }
75186
75644
  try {
75187
75645
  const timeoutMs = schedule.timeoutMs ?? DEFAULT_TIMEOUT_MS6;
75188
75646
  const { stdout, stderr } = await execCommand(schedule.command, {
@@ -75213,6 +75671,47 @@ var init_cron_runner = __esm({
75213
75671
  };
75214
75672
  }
75215
75673
  }
75674
+ /**
75675
+ * Run an auto-backup schedule in-process via the engine's open TaskStore,
75676
+ * bypassing the shell-out that would otherwise invoke an outdated fusion
75677
+ * binary on PATH. See `isInProcessBackupCommand` for the matching contract.
75678
+ */
75679
+ async executeBackupInProcess(schedule, startedAt) {
75680
+ const action = await this.runBackupActionInProcess();
75681
+ if (action.success) {
75682
+ log14.log(`\u2713 ${schedule.name} completed in-process`);
75683
+ } else {
75684
+ log14.warn(`\u2717 ${schedule.name} in-process backup ${action.error ? `threw: ${action.error}` : `reported failure: ${action.output}`}`);
75685
+ }
75686
+ return {
75687
+ success: action.success,
75688
+ output: action.output,
75689
+ error: action.error,
75690
+ startedAt,
75691
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
75692
+ };
75693
+ }
75694
+ /**
75695
+ * Shared in-process backup execution used by both the legacy-command path
75696
+ * and the command-step path. Returns the success/output/error tuple in
75697
+ * a shape that callers can wrap into either a run or a step result.
75698
+ */
75699
+ async runBackupActionInProcess() {
75700
+ try {
75701
+ const { runBackupCommand: runBackupCommand2 } = await Promise.resolve().then(() => (init_src(), src_exports));
75702
+ const fusionDir = this.store.getFusionDir();
75703
+ const settings = await this.store.getSettings();
75704
+ const result = await runBackupCommand2(fusionDir, settings);
75705
+ return {
75706
+ success: result.success,
75707
+ output: truncateOutput(result.output ?? "", ""),
75708
+ error: result.success ? void 0 : result.output
75709
+ };
75710
+ } catch (err) {
75711
+ const message = err instanceof Error ? err.message : String(err);
75712
+ return { success: false, output: "", error: message };
75713
+ }
75714
+ }
75216
75715
  /**
75217
75716
  * Execute multiple steps sequentially.
75218
75717
  * Aggregates per-step results into an overall AutomationRunResult.
@@ -75301,6 +75800,19 @@ var init_cron_runner = __esm({
75301
75800
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
75302
75801
  };
75303
75802
  }
75803
+ if (isInProcessBackupCommand(step.command)) {
75804
+ const action = await this.runBackupActionInProcess();
75805
+ return {
75806
+ stepId: step.id,
75807
+ stepName: step.name,
75808
+ stepIndex,
75809
+ success: action.success,
75810
+ output: action.output,
75811
+ error: action.error,
75812
+ startedAt,
75813
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
75814
+ };
75815
+ }
75304
75816
  try {
75305
75817
  const { stdout, stderr } = await execCommand(step.command, {
75306
75818
  timeout: timeoutMs,
@@ -75492,6 +76004,7 @@ var init_routine_runner = __esm({
75492
76004
  "../engine/src/routine-runner.ts"() {
75493
76005
  "use strict";
75494
76006
  import_cron_parser4 = __toESM(require_dist2(), 1);
76007
+ init_cron_runner();
75495
76008
  init_logger2();
75496
76009
  init_shell_utils();
75497
76010
  log15 = createLogger2("routine-runner");
@@ -75649,6 +76162,30 @@ var init_routine_runner = __esm({
75649
76162
  return this.executeCommand(routine.command ?? "", routine.timeoutMs, startedAt);
75650
76163
  }
75651
76164
  async executeCommand(command, timeoutMs, startedAt) {
76165
+ if (isInProcessBackupCommand(command) && this.options.taskStore) {
76166
+ try {
76167
+ const { runBackupCommand: runBackupCommand2 } = await Promise.resolve().then(() => (init_src(), src_exports));
76168
+ const fusionDir = this.options.taskStore.getFusionDir();
76169
+ const settings = await this.options.taskStore.getSettings();
76170
+ const result = await runBackupCommand2(fusionDir, settings);
76171
+ return {
76172
+ success: result.success,
76173
+ output: truncateOutput2(result.output ?? "", ""),
76174
+ error: result.success ? void 0 : result.output,
76175
+ startedAt,
76176
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
76177
+ };
76178
+ } catch (err) {
76179
+ const message = err instanceof Error ? err.message : String(err);
76180
+ return {
76181
+ success: false,
76182
+ output: "",
76183
+ error: message,
76184
+ startedAt,
76185
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
76186
+ };
76187
+ }
76188
+ }
75652
76189
  try {
75653
76190
  const { stdout, stderr } = await execAsync6(command, {
75654
76191
  timeout: timeoutMs ?? DEFAULT_TIMEOUT_MS7,
@@ -76440,6 +76977,13 @@ var init_self_healing = __esm({
76440
76977
  * stale in-progress/planning tasks that no longer have a live worker.
76441
76978
  */
76442
76979
  async runStartupRecovery() {
76980
+ const settings = await this.store.getSettings();
76981
+ if (settings.globalPause || settings.enginePaused) {
76982
+ log16.log(
76983
+ `Startup recovery skipped \u2014 ${settings.globalPause ? "global pause" : "engine pause"} is active`
76984
+ );
76985
+ return;
76986
+ }
76443
76987
  const steps = [
76444
76988
  { name: "no-progress-no-task-done", fn: () => this.recoverNoProgressNoTaskDoneFailures().then(() => void 0) },
76445
76989
  { name: "completed-tasks", fn: () => this.recoverCompletedTasks().then(() => void 0) },
@@ -76790,27 +77334,34 @@ var init_self_healing = __esm({
76790
77334
  log16.error(`Maintenance batch 1 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
76791
77335
  }
76792
77336
  }
76793
- const batch2Fns = [
76794
- { name: "recover-completed-tasks", fn: () => this.recoverCompletedTasks() },
76795
- { name: "recover-stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks() },
76796
- { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
76797
- { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
76798
- { name: "recover-mergeable-review", fn: () => this.recoverMergeableReviewTasks() },
76799
- { name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
76800
- { name: "recover-misclassified-failures", fn: () => this.recoverMisclassifiedFailures() },
76801
- { name: "recover-no-progress-no-task-done", fn: () => this.recoverNoProgressNoTaskDoneFailures() },
76802
- { name: "recover-partial-progress-no-task-done", fn: () => this.recoverPartialProgressNoTaskDoneFailures() },
76803
- { name: "recover-orphaned-executions", fn: () => this.recoverOrphanedExecutions() },
76804
- { name: "recover-approved-triage", fn: () => this.recoverApprovedTriageTasks() },
76805
- { name: "recover-orphaned-planning", fn: () => this.recoverOrphanedPlanningTasks() },
76806
- { name: "recover-ghost-review", fn: () => this.recoverGhostReviewTasks() }
76807
- ];
76808
- for (const fn of batch2Fns) {
76809
- try {
76810
- await fn.fn();
76811
- log16.log(`Maintenance batch 2 step "${fn.name}" succeeded`);
76812
- } catch (stepErr) {
76813
- log16.error(`Maintenance batch 2 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
77337
+ const recoverySettings = await this.store.getSettings();
77338
+ if (recoverySettings.globalPause || recoverySettings.enginePaused) {
77339
+ log16.log(
77340
+ `Maintenance batch 2 skipped \u2014 ${recoverySettings.globalPause ? "global pause" : "engine pause"} is active`
77341
+ );
77342
+ } else {
77343
+ const batch2Fns = [
77344
+ { name: "recover-completed-tasks", fn: () => this.recoverCompletedTasks() },
77345
+ { name: "recover-stale-incomplete-review", fn: () => this.recoverStaleIncompleteReviewTasks() },
77346
+ { name: "recover-failed-pre-merge-steps", fn: () => this.recoverReviewTasksWithFailedPreMergeSteps() },
77347
+ { name: "recover-interrupted-merging", fn: () => this.recoverInterruptedMergingTasks() },
77348
+ { name: "recover-mergeable-review", fn: () => this.recoverMergeableReviewTasks() },
77349
+ { name: "recover-merged-review", fn: () => this.recoverMergedReviewTasks() },
77350
+ { name: "recover-misclassified-failures", fn: () => this.recoverMisclassifiedFailures() },
77351
+ { name: "recover-no-progress-no-task-done", fn: () => this.recoverNoProgressNoTaskDoneFailures() },
77352
+ { name: "recover-partial-progress-no-task-done", fn: () => this.recoverPartialProgressNoTaskDoneFailures() },
77353
+ { name: "recover-orphaned-executions", fn: () => this.recoverOrphanedExecutions() },
77354
+ { name: "recover-approved-triage", fn: () => this.recoverApprovedTriageTasks() },
77355
+ { name: "recover-orphaned-planning", fn: () => this.recoverOrphanedPlanningTasks() },
77356
+ { name: "recover-ghost-review", fn: () => this.recoverGhostReviewTasks() }
77357
+ ];
77358
+ for (const fn of batch2Fns) {
77359
+ try {
77360
+ await fn.fn();
77361
+ log16.log(`Maintenance batch 2 step "${fn.name}" succeeded`);
77362
+ } catch (stepErr) {
77363
+ log16.error(`Maintenance batch 2 step "${fn.name}" failed: ${stepErr instanceof Error ? stepErr.message : String(stepErr)}`);
77364
+ }
76814
77365
  }
76815
77366
  }
76816
77367
  const batch3Fns = [
@@ -78543,6 +79094,10 @@ var init_in_process_runtime = __esm({
78543
79094
  * before `start()` via `setMergeEnqueuer`.
78544
79095
  */
78545
79096
  mergeEnqueuer;
79097
+ /** Tracks whether startup recovery was intentionally deferred due to pause state. */
79098
+ startupRecoveryDeferred = false;
79099
+ /** Prevent duplicate unpause recovery dispatches from racing each other. */
79100
+ resumeAfterUnpauseRunning = false;
78546
79101
  /**
78547
79102
  * Start the runtime and initialize all subsystems.
78548
79103
  *
@@ -78576,7 +79131,7 @@ var init_in_process_runtime = __esm({
78576
79131
  runtimeLog.log(`TaskStore initialized for project ${this.config.projectId}`);
78577
79132
  }
78578
79133
  this.messageStore = new MessageStoreClass(this.taskStore.getDatabase());
78579
- this.pluginStore = new PluginStoreClass(this.taskStore.getFusionDir());
79134
+ this.pluginStore = new PluginStoreClass(this.config.workingDirectory);
78580
79135
  await this.pluginStore.init();
78581
79136
  this.pluginLoader = new PluginLoaderClass({
78582
79137
  pluginStore: this.pluginStore,
@@ -78968,11 +79523,16 @@ var init_in_process_runtime = __esm({
78968
79523
  this.selfHealingManager.start();
78969
79524
  this.stuckTaskDetector.start();
78970
79525
  this.setupEventForwarding();
78971
- await this.selfHealingManager.recoverNoProgressNoTaskDoneFailures();
78972
- await this.executor.resumeOrphaned();
78973
- void this.selfHealingManager.runStartupRecovery().catch((err) => {
78974
- runtimeLog.error("Self-healing startup recovery failed:", err);
78975
- });
79526
+ const startupSettings = await this.taskStore.getSettings();
79527
+ if (startupSettings.globalPause || startupSettings.enginePaused) {
79528
+ this.startupRecoveryDeferred = true;
79529
+ runtimeLog.log(
79530
+ `Startup recovery deferred \u2014 ${startupSettings.globalPause ? "global pause" : "engine pause"} is active`
79531
+ );
79532
+ } else {
79533
+ this.startupRecoveryDeferred = false;
79534
+ await this.resumeStartupRecoverySequence();
79535
+ }
78976
79536
  this.scheduler.start();
78977
79537
  this.triageProcessor?.start();
78978
79538
  this.missionExecutionLoop = missionExecutionLoop;
@@ -79138,6 +79698,45 @@ var init_in_process_runtime = __esm({
79138
79698
  setMergeEnqueuer(enqueueMerge) {
79139
79699
  this.mergeEnqueuer = enqueueMerge;
79140
79700
  }
79701
+ /**
79702
+ * Resume executor/self-healing activity after an unpause transition.
79703
+ *
79704
+ * When startup recovery had been deferred, this replays the original startup
79705
+ * ordering so orphan resume and self-healing cannot race each other.
79706
+ */
79707
+ async resumeAfterUnpause() {
79708
+ if (!this.taskStore || !this.executor || !this.selfHealingManager) {
79709
+ return;
79710
+ }
79711
+ if (this.resumeAfterUnpauseRunning) {
79712
+ return;
79713
+ }
79714
+ this.resumeAfterUnpauseRunning = true;
79715
+ try {
79716
+ const settings = await this.taskStore.getSettings();
79717
+ if (settings.globalPause || settings.enginePaused) {
79718
+ runtimeLog.log(
79719
+ `Unpause recovery still blocked \u2014 ${settings.globalPause ? "global pause" : "engine pause"} remains active`
79720
+ );
79721
+ return;
79722
+ }
79723
+ if (this.startupRecoveryDeferred) {
79724
+ await this.resumeStartupRecoverySequence();
79725
+ this.startupRecoveryDeferred = false;
79726
+ return;
79727
+ }
79728
+ await this.executor.resumeOrphaned();
79729
+ } finally {
79730
+ this.resumeAfterUnpauseRunning = false;
79731
+ }
79732
+ }
79733
+ async resumeStartupRecoverySequence() {
79734
+ await this.selfHealingManager.recoverNoProgressNoTaskDoneFailures();
79735
+ await this.executor.resumeOrphaned();
79736
+ void this.selfHealingManager.runStartupRecovery().catch((err) => {
79737
+ runtimeLog.error("Self-healing startup recovery failed:", err);
79738
+ });
79739
+ }
79141
79740
  /**
79142
79741
  * Get the project's TaskStore instance.
79143
79742
  * @throws Error if runtime has not been started
@@ -82977,13 +83576,13 @@ ${detail}`
82977
83576
  if (prev.globalPause && !s.globalPause) {
82978
83577
  runtimeLog.log("Global unpause \u2014 resuming agentic activity");
82979
83578
  try {
82980
- const executor = this.runtime.executor;
82981
- executor?.resumeOrphaned?.().catch(
82982
- (err) => runtimeLog.error("Failed to resume orphaned tasks on unpause:", err)
83579
+ const runtime = this.runtime;
83580
+ runtime.resumeAfterUnpause?.().catch(
83581
+ (err) => runtimeLog.error("Failed to resume agentic activity on unpause:", err)
82983
83582
  );
82984
83583
  } catch (err) {
82985
83584
  runtimeLog.warn(
82986
- `Global unpause: failed to dispatch resumeOrphaned: ${err instanceof Error ? err.message : String(err)}`
83585
+ `Global unpause: failed to dispatch resumeAfterUnpause: ${err instanceof Error ? err.message : String(err)}`
82987
83586
  );
82988
83587
  }
82989
83588
  if (s.autoMerge) {
@@ -83007,13 +83606,13 @@ ${detail}`
83007
83606
  if (prev.enginePaused && !s.enginePaused) {
83008
83607
  runtimeLog.log("Engine unpaused \u2014 resuming agentic activity");
83009
83608
  try {
83010
- const executor = this.runtime.executor;
83011
- executor?.resumeOrphaned?.().catch(
83012
- (err) => runtimeLog.error("Failed to resume orphaned tasks on engine unpause:", err)
83609
+ const runtime = this.runtime;
83610
+ runtime.resumeAfterUnpause?.().catch(
83611
+ (err) => runtimeLog.error("Failed to resume agentic activity on engine unpause:", err)
83013
83612
  );
83014
83613
  } catch (err) {
83015
83614
  runtimeLog.warn(
83016
- `Engine unpause: failed to dispatch resumeOrphaned: ${err instanceof Error ? err.message : String(err)}`
83615
+ `Engine unpause: failed to dispatch resumeAfterUnpause: ${err instanceof Error ? err.message : String(err)}`
83017
83616
  );
83018
83617
  }
83019
83618
  if (s.autoMerge) {
@@ -85733,7 +86332,7 @@ var init_src3 = __esm({
85733
86332
  });
85734
86333
 
85735
86334
  // ../../plugins/fusion-plugin-hermes-runtime/dist/cli-spawn.js
85736
- import { spawn as spawn6, spawnSync } from "node:child_process";
86335
+ import { spawn as spawn6, spawnSync as spawnSync2 } from "node:child_process";
85737
86336
  import os2 from "node:os";
85738
86337
  import path, { sep as PATH_SEP } from "node:path";
85739
86338
  function resolveBinaryForSpawn(binary) {
@@ -85746,7 +86345,7 @@ function resolveBinaryForSpawn(binary) {
85746
86345
  if (cached)
85747
86346
  return cached;
85748
86347
  try {
85749
- const result = spawnSync("where", [binary], { encoding: "utf-8" });
86348
+ const result = spawnSync2("where", [binary], { encoding: "utf-8" });
85750
86349
  if (result.status === 0) {
85751
86350
  const first = (result.stdout ?? "").trim().split(/\r?\n/)[0];
85752
86351
  if (first?.length) {
@@ -98615,7 +99214,8 @@ async function clearDefaultProject(globalDir) {
98615
99214
  await globalStore.updateSettings(rest);
98616
99215
  }
98617
99216
  async function detectProjectFromCwd(cwd, central) {
98618
- let currentDir = resolve17(cwd);
99217
+ const startDir = resolve17(cwd);
99218
+ let currentDir = startDir;
98619
99219
  while (true) {
98620
99220
  const kbPath = resolve17(currentDir, ".fusion", "fusion.db");
98621
99221
  if (isValidSqliteDatabaseFile(kbPath)) {
@@ -98623,11 +99223,13 @@ async function detectProjectFromCwd(cwd, central) {
98623
99223
  if (project) {
98624
99224
  return project;
98625
99225
  }
98626
- return {
98627
- id: "",
98628
- name: basename9(currentDir) || "current-project",
98629
- path: currentDir
98630
- };
99226
+ if (currentDir === startDir) {
99227
+ return {
99228
+ id: "",
99229
+ name: basename9(currentDir) || "current-project",
99230
+ path: currentDir
99231
+ };
99232
+ }
98631
99233
  }
98632
99234
  const parentDir = dirname12(currentDir);
98633
99235
  if (parentDir === currentDir) {