@runfusion/fusion 0.27.0 → 0.27.1

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 (52) hide show
  1. package/dist/bin.js +1865 -576
  2. package/dist/client/assets/{AgentDetailView-DwLmRXTY.js → AgentDetailView-shgiiUb4.js} +1 -1
  3. package/dist/client/assets/{AgentsView-CV3vm7Qk.css → AgentsView-B3ADnF0D.css} +1 -1
  4. package/dist/client/assets/{AgentsView-D-N6aA0P.js → AgentsView-CpwqOVDz.js} +8 -8
  5. package/dist/client/assets/ChatView-DyRBOIKL.js +1 -0
  6. package/dist/client/assets/{DevServerView-BiA1nYtt.js → DevServerView-Cdelj9-m.js} +1 -1
  7. package/dist/client/assets/{DirectoryPicker-DvBviDG6.js → DirectoryPicker-C0kmRv0u.js} +1 -1
  8. package/dist/client/assets/{DocumentsView-BWXOxpuq.js → DocumentsView-B94U9ijs.js} +1 -1
  9. package/dist/client/assets/{EvalsView-CJFbtL7i.js → EvalsView-O_4YWy--.js} +1 -1
  10. package/dist/client/assets/{ExperimentalAgentOnboardingModal-DuGIPd0B.js → ExperimentalAgentOnboardingModal-CkEiF85-.js} +1 -1
  11. package/dist/client/assets/{InsightsView-BBpRiolN.js → InsightsView-D-Qe0tRr.js} +1 -1
  12. package/dist/client/assets/{MemoryView-48LuNkKk.js → MemoryView-CoRUmRvb.js} +1 -1
  13. package/dist/client/assets/{NodesView-CGQWSNZM.js → NodesView-DQzXjcLc.js} +1 -1
  14. package/dist/client/assets/{PiExtensionsManager-i-7UL2oh.js → PiExtensionsManager-Dn1LmFbq.js} +1 -1
  15. package/dist/client/assets/{PluginManager-DoSAykD6.js → PluginManager-Y0fs-6No.js} +1 -1
  16. package/dist/client/assets/{ResearchView-XZuRtOxE.js → ResearchView-CjOxKhdS.js} +1 -1
  17. package/dist/client/assets/{SettingsModal-CmeF8CN4.js → SettingsModal-Bg1-3JO_.js} +1 -1
  18. package/dist/client/assets/SettingsModal-DL7tjJQa.js +31 -0
  19. package/dist/client/assets/{SetupWizardModal-CgtvpMX9.js → SetupWizardModal-DuzYPbuJ.js} +1 -1
  20. package/dist/client/assets/{SkillsView-DErYRumF.js → SkillsView-BIFoVNUf.js} +1 -1
  21. package/dist/client/assets/{StashRecoveryView-QJrNS4Vg.js → StashRecoveryView-C52KsV7f.js} +1 -1
  22. package/dist/client/assets/{TodoView-BD9NRwq0.js → TodoView-sS_mT0Y7.js} +1 -1
  23. package/dist/client/assets/{dashboard-view-Ws9_ZnKu.js → dashboard-view-MB-86hAu.js} +1 -1
  24. package/dist/client/assets/{folder-open-CHSlllzf.js → folder-open-B9cwJ-OX.js} +1 -1
  25. package/dist/client/assets/{index-bEwSVl7B.js → index-BOjPRqEk.js} +161 -161
  26. package/dist/client/assets/index-BmSEq8Rb.css +1 -0
  27. package/dist/client/assets/{star-BgVwWAPz.js → star-BDn04UYV.js} +1 -1
  28. package/dist/client/assets/{upload-CAzycxr9.js → upload-zdPPycKQ.js} +1 -1
  29. package/dist/client/assets/{users-CZnxCCCJ.js → users-CPYZjK2g.js} +1 -1
  30. package/dist/client/index.html +2 -2
  31. package/dist/client/version.json +1 -1
  32. package/dist/droid-cli/package.json +1 -1
  33. package/dist/extension.js +1721 -532
  34. package/dist/pi-claude-cli/package.json +1 -1
  35. package/dist/plugins/fusion-plugin-cli-printing-press/package.json +1 -1
  36. package/dist/plugins/fusion-plugin-cursor-runtime/package.json +1 -1
  37. package/dist/plugins/fusion-plugin-dependency-graph/package.json +1 -1
  38. package/dist/plugins/fusion-plugin-droid-runtime/package.json +1 -1
  39. package/dist/plugins/fusion-plugin-hermes-runtime/package.json +1 -1
  40. package/dist/plugins/fusion-plugin-openclaw-runtime/package.json +1 -1
  41. package/dist/plugins/fusion-plugin-paperclip-runtime/package.json +1 -1
  42. package/dist/plugins/fusion-plugin-reports/package.json +1 -1
  43. package/dist/plugins/fusion-plugin-roadmap/package.json +1 -1
  44. package/dist/plugins/fusion-plugin-whatsapp-chat/package.json +1 -1
  45. package/package.json +1 -1
  46. package/skill/fusion/SKILL.md +1 -1
  47. package/skill/fusion/references/engine-tools.md +1 -1
  48. package/skill/fusion/references/extension-tools.md +1 -0
  49. package/skill/fusion/workflows/task-management.md +3 -1
  50. package/dist/client/assets/ChatView-DnCdKu8Z.js +0 -1
  51. package/dist/client/assets/SettingsModal-DBcjf9Bu.js +0 -31
  52. package/dist/client/assets/index-DCovGm5b.css +0 -1
package/dist/extension.js CHANGED
@@ -64,6 +64,7 @@ var init_settings_schema = __esm({
64
64
  ntfyEnabled: false,
65
65
  ntfyTopic: void 0,
66
66
  ntfyBaseUrl: void 0,
67
+ ntfyAccessToken: void 0,
67
68
  ntfyEvents: [
68
69
  "in-review",
69
70
  "merged",
@@ -247,6 +248,7 @@ var init_settings_schema = __esm({
247
248
  worktreeRebaseLocalBase: true,
248
249
  mergeConflictStrategy: "smart-prefer-main",
249
250
  workflowStepTimeoutMs: 36e4,
251
+ workflowRevisionForkOnScopeMismatch: true,
250
252
  strictScopeEnforcement: false,
251
253
  buildRetryCount: 0,
252
254
  verificationFixRetries: 3,
@@ -3357,9 +3359,9 @@ var init_sqlite_adapter = __esm({
3357
3359
 
3358
3360
  // ../core/src/db.ts
3359
3361
  import { isAbsolute, join as join2 } from "node:path";
3360
- import { mkdirSync, existsSync } from "node:fs";
3362
+ import { mkdirSync, existsSync, statSync } from "node:fs";
3361
3363
  import { spawnSync } from "node:child_process";
3362
- import { randomUUID } from "node:crypto";
3364
+ import { createHash as createHash2, randomUUID } from "node:crypto";
3363
3365
  function toJson(value) {
3364
3366
  if (value === void 0 || value === null) return "[]";
3365
3367
  if (Array.isArray(value) && value.length === 0) return "[]";
@@ -3379,6 +3381,15 @@ function fromJson(json) {
3379
3381
  return void 0;
3380
3382
  }
3381
3383
  }
3384
+ function isSqliteLockError(error) {
3385
+ const message = error instanceof Error ? error.message : String(error);
3386
+ return /SQLITE_(?:BUSY|LOCKED)|database is locked|database table is locked/i.test(message);
3387
+ }
3388
+ function sleepSync(ms) {
3389
+ if (ms <= 0) return;
3390
+ const signal = new Int32Array(new SharedArrayBuffer(4));
3391
+ Atomics.wait(signal, 0, 0, ms);
3392
+ }
3382
3393
  function probeFts5(db) {
3383
3394
  if (process.env.FUSION_DISABLE_FTS5 === "1" || process.env.FUSION_DISABLE_FTS5 === "true") {
3384
3395
  return false;
@@ -3478,15 +3489,28 @@ function getSchemaCompatibilityTableSchemas() {
3478
3489
  }
3479
3490
  return tables;
3480
3491
  }
3492
+ function canonicalizeSchemaTables(tables) {
3493
+ return Object.fromEntries(
3494
+ [...tables.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([tableName, columns]) => [
3495
+ tableName,
3496
+ Object.fromEntries(
3497
+ [...columns.entries()].sort(([left], [right]) => left.localeCompare(right))
3498
+ )
3499
+ ])
3500
+ );
3501
+ }
3481
3502
  function createDatabase(fusionDir, options) {
3482
3503
  return new Database(fusionDir, options);
3483
3504
  }
3484
- var SCHEMA_VERSION, SCHEMA_SQL, TABLE_LEVEL_CONSTRAINT_PREFIXES, SCHEMA_TABLE_SCHEMAS, MIGRATION_ONLY_TABLE_SCHEMAS, Database;
3505
+ var DEFAULT_SQLITE_BUSY_TIMEOUT_MS, DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS, DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS, SCHEMA_VERSION, SCHEMA_SQL, TABLE_LEVEL_CONSTRAINT_PREFIXES, SCHEMA_TABLE_SCHEMAS, MIGRATION_ONLY_TABLE_SCHEMAS, SCHEMA_COMPAT_FINGERPRINT, Database;
3485
3506
  var init_db = __esm({
3486
3507
  "../core/src/db.ts"() {
3487
3508
  "use strict";
3488
3509
  init_sqlite_adapter();
3489
3510
  init_types();
3511
+ DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5e3;
3512
+ DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS = 1e3;
3513
+ DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS = 50;
3490
3514
  SCHEMA_VERSION = 72;
3491
3515
  SCHEMA_SQL = `
3492
3516
  -- Tasks table with JSON columns for nested data
@@ -3578,6 +3602,9 @@ CREATE TABLE IF NOT EXISTS tasks (
3578
3602
  );
3579
3603
 
3580
3604
  -- Config table (single row with project settings)
3605
+ -- nextId is a deprecated legacy allocator counter retained read-only for one
3606
+ -- release so older databases/config consumers can still load it during the
3607
+ -- distributed_task_id_state transition.
3581
3608
  CREATE TABLE IF NOT EXISTS config (
3582
3609
  id INTEGER PRIMARY KEY CHECK (id = 1),
3583
3610
  nextId INTEGER DEFAULT 1,
@@ -4352,6 +4379,20 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
4352
4379
  createdAt: "TEXT NOT NULL"
4353
4380
  }
4354
4381
  };
4382
+ SCHEMA_COMPAT_FINGERPRINT = createHash2("sha1").update(
4383
+ JSON.stringify({
4384
+ schemaVersion: SCHEMA_VERSION,
4385
+ schemaSqlTables: canonicalizeSchemaTables(SCHEMA_TABLE_SCHEMAS),
4386
+ migrationOnlyTableSchemas: Object.fromEntries(
4387
+ Object.entries(MIGRATION_ONLY_TABLE_SCHEMAS).sort(([left], [right]) => left.localeCompare(right)).map(([tableName, columns]) => [
4388
+ tableName,
4389
+ Object.fromEntries(
4390
+ Object.entries(columns).sort(([left], [right]) => left.localeCompare(right))
4391
+ )
4392
+ ])
4393
+ )
4394
+ })
4395
+ ).digest("hex");
4355
4396
  Database = class _Database {
4356
4397
  static sharedIntegrityChecks = /* @__PURE__ */ new Map();
4357
4398
  db;
@@ -4369,10 +4410,16 @@ CREATE INDEX IF NOT EXISTS idxTodoItemsSortOrder ON todo_items(listId, sortOrder
4369
4410
  _fts5Available;
4370
4411
  integrityCheckScheduled = false;
4371
4412
  closed = false;
4413
+ busyTimeoutMs;
4414
+ lockRecoveryWindowMs;
4415
+ lockRecoveryDelayMs;
4372
4416
  constructor(fusionDir, options) {
4373
4417
  const inMemory = options?.inMemory === true;
4374
4418
  this.inMemory = inMemory;
4375
4419
  this.dbPath = inMemory ? ":memory:" : join2(fusionDir, "fusion.db");
4420
+ this.busyTimeoutMs = Math.max(0, options?.busyTimeoutMs ?? DEFAULT_SQLITE_BUSY_TIMEOUT_MS);
4421
+ this.lockRecoveryWindowMs = Math.max(0, options?.lockRecoveryWindowMs ?? DEFAULT_SQLITE_LOCK_RECOVERY_WINDOW_MS);
4422
+ this.lockRecoveryDelayMs = Math.max(1, options?.lockRecoveryDelayMs ?? DEFAULT_SQLITE_LOCK_RECOVERY_DELAY_MS);
4376
4423
  if (!inMemory && !isAbsolute(fusionDir)) {
4377
4424
  throw new Error(`[fusion] Database constructor requires an absolute fusionDir path, got: ${fusionDir}`);
4378
4425
  }
@@ -4392,13 +4439,13 @@ This means a caller passed a .fusion directory where a project root was expected
4392
4439
  throw new Error(`Failed to open Fusion database at ${this.dbPath}: ${message}`);
4393
4440
  }
4394
4441
  if (!inMemory) {
4395
- this.db.exec("PRAGMA busy_timeout = 5000");
4442
+ this.db.exec(`PRAGMA busy_timeout = ${this.busyTimeoutMs}`);
4396
4443
  this.db.exec("PRAGMA journal_mode = WAL");
4397
4444
  this.db.exec("PRAGMA synchronous = NORMAL");
4398
4445
  this.db.exec("PRAGMA wal_autocheckpoint = 100");
4399
4446
  this.db.exec("PRAGMA journal_size_limit = 4194304");
4400
4447
  } else {
4401
- this.db.exec("PRAGMA busy_timeout = 5000");
4448
+ this.db.exec(`PRAGMA busy_timeout = ${this.busyTimeoutMs}`);
4402
4449
  }
4403
4450
  this.db.exec("PRAGMA foreign_keys = ON");
4404
4451
  this._fts5Available = probeFts5(this.db);
@@ -4509,6 +4556,44 @@ This means a caller passed a .fusion directory where a project root was expected
4509
4556
  });
4510
4557
  return rebuilt.status === 0;
4511
4558
  }
4559
+ /**
4560
+ * Run WAL truncation + VACUUM and report compaction stats.
4561
+ *
4562
+ * In-memory databases no-op and return zeroed stats. Disk-backed databases
4563
+ * sample file size before/after compaction, run `wal_checkpoint(TRUNCATE)`,
4564
+ * and then run `VACUUM` while the connection is in EXCLUSIVE locking mode to
4565
+ * prevent concurrent writes from other connections during maintenance.
4566
+ */
4567
+ vacuum() {
4568
+ if (this.inMemory) {
4569
+ return { beforeBytes: 0, afterBytes: 0, durationMs: 0 };
4570
+ }
4571
+ const beforeBytes = existsSync(this.dbPath) ? statSync(this.dbPath).size : 0;
4572
+ const startedAt = Date.now();
4573
+ this.db.exec("PRAGMA locking_mode=EXCLUSIVE");
4574
+ try {
4575
+ try {
4576
+ this.walCheckpoint("TRUNCATE");
4577
+ } catch (error) {
4578
+ const message = error instanceof Error ? error.message : String(error);
4579
+ throw new Error(`Database vacuum maintenance failed during WAL checkpoint (dbPath=${this.dbPath}): ${message}`);
4580
+ }
4581
+ try {
4582
+ this.db.exec("VACUUM");
4583
+ } catch (error) {
4584
+ const message = error instanceof Error ? error.message : String(error);
4585
+ throw new Error(`Database vacuum maintenance failed during VACUUM (dbPath=${this.dbPath}): ${message}`);
4586
+ }
4587
+ const afterBytes = existsSync(this.dbPath) ? statSync(this.dbPath).size : 0;
4588
+ return {
4589
+ beforeBytes,
4590
+ afterBytes,
4591
+ durationMs: Date.now() - startedAt
4592
+ };
4593
+ } finally {
4594
+ this.db.exec("PRAGMA locking_mode=NORMAL");
4595
+ }
4596
+ }
4512
4597
  /**
4513
4598
  * Initialize the database: create tables if they don't exist
4514
4599
  * and seed meta values.
@@ -4523,10 +4608,20 @@ This means a caller passed a .fusion directory where a project root was expected
4523
4608
  `INSERT OR IGNORE INTO __meta (key, value) VALUES ('lastModified', '${Date.now()}')`
4524
4609
  );
4525
4610
  this.migrate();
4526
- this.ensureSchemaCompatibility();
4527
- this.ensureRoutinesSchemaCompatibility();
4528
- this.ensureInsightRunsSchemaCompatibility();
4529
- this.ensureEvalTaskResultsSchemaCompatibility();
4611
+ const schemaCompatFingerprint = this.getMetaValue("schemaCompatFingerprint");
4612
+ const skipColumnReconciliation = schemaCompatFingerprint === SCHEMA_COMPAT_FINGERPRINT;
4613
+ const tableColumnsCache = skipColumnReconciliation ? void 0 : /* @__PURE__ */ new Map();
4614
+ const compatibilityOptions = {
4615
+ tableColumnsCache,
4616
+ skipColumnReconciliation
4617
+ };
4618
+ this.ensureSchemaCompatibility(compatibilityOptions);
4619
+ this.ensureRoutinesSchemaCompatibility(compatibilityOptions);
4620
+ this.ensureInsightRunsSchemaCompatibility(compatibilityOptions);
4621
+ this.ensureEvalTaskResultsSchemaCompatibility(compatibilityOptions);
4622
+ if (!skipColumnReconciliation) {
4623
+ this.setMetaValue("schemaCompatFingerprint", SCHEMA_COMPAT_FINGERPRINT);
4624
+ }
4530
4625
  const configNow = (/* @__PURE__ */ new Date()).toISOString();
4531
4626
  this.db.exec(
4532
4627
  `INSERT OR IGNORE INTO config (id, nextId, nextWorkflowStepId, settings, workflowSteps, updatedAt) VALUES (1, 1, 1, '${JSON.stringify(DEFAULT_PROJECT_SETTINGS)}', '[]', '${configNow}')`
@@ -4544,20 +4639,27 @@ This means a caller passed a .fusion directory where a project root was expected
4544
4639
  * re-run even if a previous migration partially applied.
4545
4640
  */
4546
4641
  /**
4547
- * Applies unconditional column reconciliation for all known project DB tables.
4642
+ * Reconciles additive columns for every known project DB table unless the
4643
+ * persisted `schemaCompatFingerprint` already matches SCHEMA_COMPAT_FINGERPRINT.
4548
4644
  *
4549
- * FN-3879 introduced a tasks checkout-column self-heal, FN-3898 formalized it,
4550
- * and FN-3887 generalized the guardrail so migration-version drift no longer
4551
- * determines whether additive columns exist. Invariant: every column declared
4552
- * in SCHEMA_SQL or MIGRATION_ONLY_TABLE_SCHEMAS exists on any live table after
4553
- * this method returns, regardless of the persisted schemaVersion.
4645
+ * The fingerprint is invalidated automatically by SCHEMA_VERSION changes and by
4646
+ * edits to the canonicalized column declarations from SCHEMA_SQL or
4647
+ * MIGRATION_ONLY_TABLE_SCHEMAS. When it is absent or stale, this method runs the
4648
+ * full FN-3879/FN-3887/FN-3898 safety pass so every declared column exists on
4649
+ * every live table after init() returns.
4554
4650
  */
4555
- ensureSchemaCompatibility() {
4651
+ ensureSchemaCompatibility(options = {}) {
4652
+ if (options.skipColumnReconciliation) {
4653
+ return;
4654
+ }
4556
4655
  const knownTableSchemas = getSchemaCompatibilityTableSchemas();
4656
+ const tableColumnsCache = options.tableColumnsCache;
4557
4657
  for (const [tableName, columns] of knownTableSchemas) {
4558
4658
  if (!this.hasTable(tableName)) continue;
4659
+ const cachedColumns = this.getTableColumns(tableName, true, tableColumnsCache);
4559
4660
  for (const [columnName, columnDefinition] of columns) {
4560
- this.addColumnIfMissing(tableName, columnName, columnDefinition);
4661
+ if (cachedColumns.has(columnName)) continue;
4662
+ this.addColumnIfMissingCached(tableName, columnName, columnDefinition, tableColumnsCache);
4561
4663
  }
4562
4664
  }
4563
4665
  }
@@ -4568,10 +4670,14 @@ This means a caller passed a .fusion directory where a project root was expected
4568
4670
  * agent IDs from earlier table definitions. `RoutineStore.rowToRoutine()` and
4569
4671
  * backup routine sync expect a safe string value, so normalize to ''.
4570
4672
  */
4571
- ensureRoutinesSchemaCompatibility() {
4673
+ ensureRoutinesSchemaCompatibility(options = {}) {
4572
4674
  if (!this.hasTable("routines")) {
4573
4675
  return;
4574
4676
  }
4677
+ if (!options.skipColumnReconciliation) {
4678
+ this.addColumnIfMissingCached("routines", "agentId", "TEXT DEFAULT ''", options.tableColumnsCache);
4679
+ this.addColumnIfMissingCached("routines", "scope", "TEXT DEFAULT 'project'", options.tableColumnsCache);
4680
+ }
4575
4681
  this.db.exec("UPDATE routines SET agentId = '' WHERE agentId IS NULL");
4576
4682
  this.db.exec("UPDATE routines SET scope = 'project' WHERE scope IS NULL OR TRIM(scope) = ''");
4577
4683
  this.db.exec("CREATE INDEX IF NOT EXISTS idxRoutinesNextRunAt ON routines(nextRunAt)");
@@ -4585,13 +4691,17 @@ This means a caller passed a .fusion directory where a project root was expected
4585
4691
  * remains focused on index creation that should run after the generic column
4586
4692
  * backfill pass.
4587
4693
  */
4588
- ensureInsightRunsSchemaCompatibility() {
4694
+ ensureInsightRunsSchemaCompatibility(options = {}) {
4589
4695
  if (!this.hasTable("project_insight_runs")) {
4590
4696
  return;
4591
4697
  }
4698
+ if (!options.skipColumnReconciliation) {
4699
+ this.addColumnIfMissingCached("project_insight_runs", "lifecycle", "TEXT", options.tableColumnsCache);
4700
+ this.addColumnIfMissingCached("project_insight_runs", "cancelledAt", "TEXT", options.tableColumnsCache);
4701
+ }
4592
4702
  this.db.exec(`CREATE INDEX IF NOT EXISTS idxInsightRunsProjectTriggerStatus ON project_insight_runs(projectId, trigger, status)`);
4593
4703
  }
4594
- ensureEvalTaskResultsSchemaCompatibility() {
4704
+ ensureEvalTaskResultsSchemaCompatibility(_options = {}) {
4595
4705
  if (!this.hasTable("eval_task_results")) {
4596
4706
  return;
4597
4707
  }
@@ -5933,12 +6043,26 @@ This means a caller passed a .fusion directory where a project root was expected
5933
6043
  const lower = message.toLowerCase();
5934
6044
  return lower.includes("corruption found reading blob") || lower.includes("database disk image is malformed") || lower.includes("fts5") && lower.includes("corrupt");
5935
6045
  }
6046
+ /**
6047
+ * Read the declared columns for a table.
6048
+ */
6049
+ getTableColumns(table, useCache = false, cache) {
6050
+ if (useCache && cache?.has(table)) {
6051
+ return cache.get(table) ?? /* @__PURE__ */ new Set();
6052
+ }
6053
+ const columns = new Set(
6054
+ this.db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name)
6055
+ );
6056
+ if (useCache && cache) {
6057
+ cache.set(table, columns);
6058
+ }
6059
+ return columns;
6060
+ }
5936
6061
  /**
5937
6062
  * Check whether a table has a given column.
5938
6063
  */
5939
6064
  hasColumn(table, column) {
5940
- const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
5941
- return cols.some((c) => c.name === column);
6065
+ return this.getTableColumns(table).has(column);
5942
6066
  }
5943
6067
  /**
5944
6068
  * Add a column to a table if it does not already exist.
@@ -5948,6 +6072,20 @@ This means a caller passed a .fusion directory where a project root was expected
5948
6072
  this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
5949
6073
  }
5950
6074
  }
6075
+ /**
6076
+ * Add a column using a per-init table-info cache when available.
6077
+ */
6078
+ addColumnIfMissingCached(table, column, definition, cache) {
6079
+ const columns = this.getTableColumns(table, Boolean(cache), cache);
6080
+ if (columns.has(column)) {
6081
+ return;
6082
+ }
6083
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
6084
+ columns.add(column);
6085
+ if (cache) {
6086
+ cache.set(table, columns);
6087
+ }
6088
+ }
5951
6089
  /**
5952
6090
  * Normalize legacy steering comments into the unified comments field exactly once.
5953
6091
  *
@@ -6048,25 +6186,67 @@ This means a caller passed a .fusion directory where a project root was expected
6048
6186
  this.integrityCheckPending = false;
6049
6187
  this.db.close();
6050
6188
  }
6189
+ runWithLockRecovery(action, fn) {
6190
+ const deadline = Date.now() + this.lockRecoveryWindowMs;
6191
+ let attempt = 0;
6192
+ while (true) {
6193
+ try {
6194
+ fn();
6195
+ return;
6196
+ } catch (error) {
6197
+ if (!isSqliteLockError(error)) {
6198
+ throw error;
6199
+ }
6200
+ if (Date.now() >= deadline) {
6201
+ throw new Error(
6202
+ `SQLite ${action} failed after ${attempt + 1} attempt${attempt === 0 ? "" : "s"}: ${error instanceof Error ? error.message : String(error)}`
6203
+ );
6204
+ }
6205
+ const remainingMs = Math.max(0, deadline - Date.now());
6206
+ const delayMs = Math.min(this.lockRecoveryDelayMs * Math.max(1, attempt + 1), remainingMs);
6207
+ sleepSync(delayMs);
6208
+ attempt += 1;
6209
+ }
6210
+ }
6211
+ }
6051
6212
  /**
6052
6213
  * Execute a function inside a SQLite transaction.
6053
6214
  * Supports nested calls via SAVEPOINTs.
6054
6215
  * If the function throws, the transaction/savepoint is rolled back.
6055
6216
  * If the function returns normally, the transaction/savepoint is committed.
6217
+ *
6218
+ * Outermost transactions default to `BEGIN` (DEFERRED) so read-only callers
6219
+ * avoid taking a writer lock until they actually mutate state.
6220
+ * Use `transactionImmediate()` for write-heavy paths that should acquire the
6221
+ * RESERVED lock before user code runs and fail/retry before the callback executes.
6056
6222
  */
6057
- transaction(fn) {
6223
+ transaction(fn, options) {
6058
6224
  const depth = this.transactionDepth++;
6059
6225
  const isOutermost = depth === 0;
6060
6226
  const savepointName = `sp_${depth}`;
6061
- if (isOutermost) {
6062
- this.db.exec("BEGIN");
6063
- } else {
6064
- this.db.exec(`SAVEPOINT ${savepointName}`);
6227
+ const mode = options?.mode ?? "deferred";
6228
+ try {
6229
+ if (isOutermost) {
6230
+ if (mode === "immediate") {
6231
+ this.runWithLockRecovery("BEGIN IMMEDIATE", () => {
6232
+ this.db.exec("BEGIN IMMEDIATE");
6233
+ });
6234
+ } else {
6235
+ this.db.exec("BEGIN");
6236
+ }
6237
+ } else {
6238
+ this.db.exec(`SAVEPOINT ${savepointName}`);
6239
+ }
6240
+ } catch (error) {
6241
+ this.transactionDepth--;
6242
+ throw error;
6065
6243
  }
6066
6244
  try {
6067
6245
  const result = fn();
6068
6246
  if (isOutermost) {
6069
- this.db.exec("COMMIT");
6247
+ this.runWithLockRecovery("COMMIT", () => {
6248
+ this.db.exec("COMMIT");
6249
+ });
6070
6250
  } else {
6071
6251
  this.db.exec(`RELEASE ${savepointName}`);
6072
6252
  }
@@ -6083,6 +6263,9 @@ This means a caller passed a .fusion directory where a project root was expected
6083
6263
  this.transactionDepth--;
6084
6264
  }
6085
6265
  }
6266
+ transactionImmediate(fn) {
6267
+ return this.transaction(fn, { mode: "immediate" });
6268
+ }
6086
6269
  /**
6087
6270
  * Execute plugin-provided schema initialization hooks.
6088
6271
  *
@@ -6118,14 +6301,24 @@ This means a caller passed a .fusion directory where a project root was expected
6118
6301
  exec(sql) {
6119
6302
  this.db.exec(sql);
6120
6303
  }
6304
+ getMetaValue(key) {
6305
+ const row = this.db.prepare("SELECT value FROM __meta WHERE key = ?").get(key);
6306
+ return row?.value;
6307
+ }
6308
+ /**
6309
+ * Persist a __meta value idempotently.
6310
+ */
6311
+ setMetaValue(key, value) {
6312
+ this.db.prepare("INSERT OR REPLACE INTO __meta (key, value) VALUES (?, ?)").run(key, value);
6313
+ }
6121
6314
  /**
6122
6315
  * Get the last modification timestamp (epoch ms).
6123
6316
  * Returns 0 if the value is not set.
6124
6317
  */
6125
6318
  getLastModified() {
6126
- const row = this.db.prepare("SELECT value FROM __meta WHERE key = 'lastModified'").get();
6127
- if (!row) return 0;
6128
- return parseInt(row.value, 10) || 0;
6319
+ const value = this.getMetaValue("lastModified");
6320
+ if (!value) return 0;
6321
+ return parseInt(value, 10) || 0;
6129
6322
  }
6130
6323
  /**
6131
6324
  * Update the last modification timestamp to the current time.
@@ -6144,9 +6337,9 @@ This means a caller passed a .fusion directory where a project root was expected
6144
6337
  * Get the schema version number.
6145
6338
  */
6146
6339
  getSchemaVersion() {
6147
- const row = this.db.prepare("SELECT value FROM __meta WHERE key = 'schemaVersion'").get();
6148
- if (!row) return 0;
6149
- return parseInt(row.value, 10) || 0;
6340
+ const value = this.getMetaValue("schemaVersion");
6341
+ if (!value) return 0;
6342
+ return parseInt(value, 10) || 0;
6150
6343
  }
6151
6344
  /**
6152
6345
  * Get the database file path.
@@ -6162,7 +6355,7 @@ This means a caller passed a .fusion directory where a project root was expected
6162
6355
  import { mkdir, readFile, writeFile, readdir, unlink, rename, access, appendFile } from "node:fs/promises";
6163
6356
  import { constants as fsConstants } from "node:fs";
6164
6357
  import { basename, dirname, join as join3, resolve as resolve2 } from "node:path";
6165
- import { randomUUID as randomUUID2, randomBytes, createHash as createHash2 } from "node:crypto";
6358
+ import { randomUUID as randomUUID2, randomBytes, createHash as createHash3 } from "node:crypto";
6166
6359
  import { EventEmitter } from "node:events";
6167
6360
  function resolveCreationRuntimeConfig(incoming, metadata) {
6168
6361
  const isEphemeral = isEphemeralAgent({ metadata });
@@ -7328,7 +7521,7 @@ var init_agent_store = __esm({
7328
7521
  throw new Error(`Agent ${agentId} not found`);
7329
7522
  }
7330
7523
  const token = randomBytes(32).toString("hex");
7331
- const tokenHash = createHash2("sha256").update(token).digest("hex");
7524
+ const tokenHash = createHash3("sha256").update(token).digest("hex");
7332
7525
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
7333
7526
  const label = options?.label?.trim();
7334
7527
  const key = {
@@ -9484,10 +9677,10 @@ END;
9484
9677
  mkdirSync3(fusionDir, { recursive: true });
9485
9678
  }
9486
9679
  this.db = new DatabaseSync(inMemory ? ":memory:" : join6(fusionDir, "archive.db"));
9680
+ this.db.exec("PRAGMA busy_timeout = 5000");
9487
9681
  if (!inMemory) {
9488
9682
  this.db.exec("PRAGMA journal_mode = WAL");
9489
9683
  }
9490
- this.db.exec("PRAGMA busy_timeout = 5000");
9491
9684
  this._fts5Available = probeFts5(this.db);
9492
9685
  }
9493
9686
  /** True when this SQLite build has FTS5. See db.ts#probeFts5. */
@@ -13162,9 +13355,15 @@ CREATE INDEX IF NOT EXISTS idxMeshWriteQueueReplay ON meshWriteQueue(targetNodeI
13162
13355
  globalDir;
13163
13356
  /** Tracks transaction nesting depth for savepoint-based nested transactions. */
13164
13357
  transactionDepth = 0;
13165
- constructor(globalDir) {
13358
+ busyTimeoutMs;
13359
+ lockRecoveryWindowMs;
13360
+ lockRecoveryDelayMs;
13361
+ constructor(globalDir, options) {
13166
13362
  this.globalDir = resolveGlobalDir(globalDir);
13167
13363
  this.dbPath = join8(this.globalDir, "fusion-central.db");
13364
+ this.busyTimeoutMs = Math.max(0, options?.busyTimeoutMs ?? 5e3);
13365
+ this.lockRecoveryWindowMs = Math.max(0, options?.lockRecoveryWindowMs ?? 1e3);
13366
+ this.lockRecoveryDelayMs = Math.max(1, options?.lockRecoveryDelayMs ?? 50);
13168
13367
  if (!existsSync6(this.globalDir)) {
13169
13368
  mkdirSync4(this.globalDir, { recursive: true });
13170
13369
  }
@@ -13174,8 +13373,8 @@ CREATE INDEX IF NOT EXISTS idxMeshWriteQueueReplay ON meshWriteQueue(targetNodeI
13174
13373
  const message = error instanceof Error ? error.message : String(error);
13175
13374
  throw new Error(`Failed to open Fusion central database at ${this.dbPath}: ${message}`);
13176
13375
  }
13376
+ this.db.exec(`PRAGMA busy_timeout = ${this.busyTimeoutMs}`);
13177
13377
  this.db.exec("PRAGMA journal_mode = WAL");
13178
- this.db.exec("PRAGMA busy_timeout = 5000");
13179
13378
  this.db.exec("PRAGMA foreign_keys = ON");
13180
13379
  }
13181
13380
  /**
@@ -13281,6 +13480,29 @@ CREATE INDEX IF NOT EXISTS idxMeshWriteQueueReplay ON meshWriteQueue(targetNodeI
13281
13480
  close() {
13282
13481
  this.db.close();
13283
13482
  }
13483
+ runWithLockRecovery(action, fn) {
13484
+ const deadline = Date.now() + this.lockRecoveryWindowMs;
13485
+ let attempt = 0;
13486
+ while (true) {
13487
+ try {
13488
+ fn();
13489
+ return;
13490
+ } catch (error) {
13491
+ if (!isSqliteLockError(error)) {
13492
+ throw error;
13493
+ }
13494
+ if (Date.now() >= deadline) {
13495
+ throw new Error(
13496
+ `SQLite ${action} failed after ${attempt + 1} attempt${attempt === 0 ? "" : "s"}: ${error instanceof Error ? error.message : String(error)}`
13497
+ );
13498
+ }
13499
+ const remainingMs = Math.max(0, deadline - Date.now());
13500
+ const delayMs = Math.min(this.lockRecoveryDelayMs * Math.max(1, attempt + 1), remainingMs);
13501
+ sleepSync(delayMs);
13502
+ attempt += 1;
13503
+ }
13504
+ }
13505
+ }
13284
13506
  /**
13285
13507
  * Execute a function inside a SQLite transaction.
13286
13508
  * Supports nested calls via SAVEPOINTs.
@@ -13291,15 +13513,24 @@ CREATE INDEX IF NOT EXISTS idxMeshWriteQueueReplay ON meshWriteQueue(targetNodeI
13291
13513
  const depth = this.transactionDepth++;
13292
13514
  const isOutermost = depth === 0;
13293
13515
  const savepointName = `sp_${depth}`;
13294
- if (isOutermost) {
13295
- this.db.exec("BEGIN");
13296
- } else {
13297
- this.db.exec(`SAVEPOINT ${savepointName}`);
13516
+ try {
13517
+ if (isOutermost) {
13518
+ this.runWithLockRecovery("BEGIN IMMEDIATE", () => {
13519
+ this.db.exec("BEGIN IMMEDIATE");
13520
+ });
13521
+ } else {
13522
+ this.db.exec(`SAVEPOINT ${savepointName}`);
13523
+ }
13524
+ } catch (error) {
13525
+ this.transactionDepth--;
13526
+ throw error;
13298
13527
  }
13299
13528
  try {
13300
13529
  const result = fn();
13301
13530
  if (isOutermost) {
13302
- this.db.exec("COMMIT");
13531
+ this.runWithLockRecovery("COMMIT", () => {
13532
+ this.db.exec("COMMIT");
13533
+ });
13303
13534
  } else {
13304
13535
  this.db.exec(`RELEASE ${savepointName}`);
13305
13536
  }
@@ -19920,8 +20151,8 @@ var init_system_metrics = __esm({
19920
20151
 
19921
20152
  // ../core/src/central-core.ts
19922
20153
  import { EventEmitter as EventEmitter11 } from "node:events";
19923
- import { createHash as createHash3, randomUUID as randomUUID9 } from "node:crypto";
19924
- import { existsSync as existsSync7, statSync } from "node:fs";
20154
+ import { createHash as createHash4, randomUUID as randomUUID9 } from "node:crypto";
20155
+ import { existsSync as existsSync7, statSync as statSync2 } from "node:fs";
19925
20156
  import { mkdir as mkdir4 } from "node:fs/promises";
19926
20157
  import { isAbsolute as isAbsolute2, join as join11, basename as basename2, resolve as resolve5 } from "node:path";
19927
20158
  var CentralCore;
@@ -20030,7 +20261,7 @@ var init_central_core = __esm({
20030
20261
  if (!existsSync7(input.path)) {
20031
20262
  throw new Error(`Project path does not exist: ${input.path}`);
20032
20263
  }
20033
- if (!statSync(input.path).isDirectory()) {
20264
+ if (!statSync2(input.path).isDirectory()) {
20034
20265
  throw new Error(`Project path must be a directory: ${input.path}`);
20035
20266
  }
20036
20267
  const existingByPath = await this.getProjectByPath(input.path);
@@ -21769,7 +22000,7 @@ var init_central_core = __esm({
21769
22000
  const dbPath = this.db.getPath();
21770
22001
  let dbSizeBytes = 0;
21771
22002
  try {
21772
- dbSizeBytes = statSync(dbPath).size;
22003
+ dbSizeBytes = statSync2(dbPath).size;
21773
22004
  } catch {
21774
22005
  }
21775
22006
  return { projectCount, totalTasksCompleted, dbSizeBytes };
@@ -21987,10 +22218,10 @@ var init_central_core = __esm({
21987
22218
  */
21988
22219
  async generateProjectName(projectPath) {
21989
22220
  try {
21990
- const { execFile: execFile8 } = await import("node:child_process");
21991
- const { promisify: promisify14 } = await import("node:util");
21992
- const execFileAsync6 = promisify14(execFile8);
21993
- const { stdout } = await execFileAsync6(
22221
+ const { execFile: execFile9 } = await import("node:child_process");
22222
+ const { promisify: promisify15 } = await import("node:util");
22223
+ const execFileAsync7 = promisify15(execFile9);
22224
+ const { stdout } = await execFileAsync7(
21994
22225
  "git",
21995
22226
  ["remote", "get-url", "origin"],
21996
22227
  { cwd: projectPath, timeout: 5e3 }
@@ -22292,7 +22523,7 @@ var init_central_core = __esm({
22292
22523
  exportedAt: snapshot.exportedAt,
22293
22524
  version: 1
22294
22525
  };
22295
- const checksum = createHash3("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22526
+ const checksum = createHash4("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22296
22527
  return this.applyRemoteSettings({ ...payloadWithoutChecksum, checksum });
22297
22528
  }
22298
22529
  getAuthMaterialSnapshot(providerAuth) {
@@ -22335,7 +22566,7 @@ var init_central_core = __esm({
22335
22566
  exportedAt,
22336
22567
  version: 1
22337
22568
  };
22338
- const checksum = createHash3("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22569
+ const checksum = createHash4("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22339
22570
  return {
22340
22571
  ...payloadWithoutChecksum,
22341
22572
  checksum
@@ -22370,7 +22601,7 @@ var init_central_core = __esm({
22370
22601
  exportedAt: payload.exportedAt,
22371
22602
  version: payload.version
22372
22603
  };
22373
- const computedChecksum = createHash3("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22604
+ const computedChecksum = createHash4("sha256").update(JSON.stringify(payloadWithoutChecksum)).digest("hex");
22374
22605
  if (computedChecksum !== payload.checksum) {
22375
22606
  return {
22376
22607
  success: false,
@@ -22492,13 +22723,13 @@ var init_central_core = __esm({
22492
22723
  });
22493
22724
 
22494
22725
  // ../core/src/sqlite-validation.ts
22495
- import { existsSync as existsSync8, statSync as statSync2 } from "node:fs";
22726
+ import { existsSync as existsSync8, statSync as statSync3 } from "node:fs";
22496
22727
  function isValidSqliteDatabaseFile(dbPath) {
22497
22728
  if (!existsSync8(dbPath)) {
22498
22729
  return false;
22499
22730
  }
22500
22731
  try {
22501
- if (!statSync2(dbPath).isFile()) {
22732
+ if (!statSync3(dbPath).isFile()) {
22502
22733
  return false;
22503
22734
  }
22504
22735
  } catch {
@@ -22691,10 +22922,10 @@ var init_migration = __esm({
22691
22922
  return basename3(projectPath);
22692
22923
  }
22693
22924
  try {
22694
- const { execFile: execFile8 } = await import("node:child_process");
22695
- const { promisify: promisify14 } = await import("node:util");
22696
- const execFileAsync6 = promisify14(execFile8);
22697
- const { stdout } = await execFileAsync6(
22925
+ const { execFile: execFile9 } = await import("node:child_process");
22926
+ const { promisify: promisify15 } = await import("node:util");
22927
+ const execFileAsync7 = promisify15(execFile9);
22928
+ const { stdout } = await execFileAsync7(
22698
22929
  "git",
22699
22930
  ["remote", "get-url", "origin"],
22700
22931
  { cwd: projectPath, timeout: 1e3 }
@@ -32967,7 +33198,7 @@ var init_memory_dreams = __esm({
32967
33198
  import { readFile as readFile6, writeFile as writeFile5, mkdir as mkdir6, access as access3, constants, readdir as readdir4, stat as stat2 } from "node:fs/promises";
32968
33199
  import { existsSync as existsSync11 } from "node:fs";
32969
33200
  import { basename as basename4, dirname as dirname5, isAbsolute as isAbsolute4, join as join15, normalize as normalize2, relative, resolve as resolve7, sep as sep3 } from "node:path";
32970
- import { createHash as createHash4 } from "node:crypto";
33201
+ import { createHash as createHash5 } from "node:crypto";
32971
33202
  function shouldSkipBackgroundQmdRefresh() {
32972
33203
  return (process.env.VITEST === "true" || process.env.NODE_ENV === "test") && process.env.FUSION_ENABLE_QMD_REFRESH_IN_TESTS !== "1";
32973
33204
  }
@@ -32983,7 +33214,7 @@ function memoryDreamsPath(rootDir) {
32983
33214
  function qmdMemoryCollectionName(rootDir) {
32984
33215
  const absoluteRoot = resolve7(rootDir);
32985
33216
  const slug = basename4(absoluteRoot).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "project";
32986
- const hash = createHash4("sha1").update(absoluteRoot).digest("hex").slice(0, 12);
33217
+ const hash = createHash5("sha1").update(absoluteRoot).digest("hex").slice(0, 12);
32987
33218
  return `${QMD_COLLECTION_PREFIX}-${slug}-${hash}`;
32988
33219
  }
32989
33220
  function buildQmdSearchArgs(rootDir, options) {
@@ -33019,7 +33250,7 @@ function buildQmdRefreshCommands(rootDir) {
33019
33250
  function qmdAgentMemoryCollectionName(rootDir, agentId) {
33020
33251
  const absoluteRoot = resolve7(rootDir);
33021
33252
  const safeAgentId = agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
33022
- const hash = createHash4("sha1").update(`${absoluteRoot}:${agentId}`).digest("hex").slice(0, 12);
33253
+ const hash = createHash5("sha1").update(`${absoluteRoot}:${agentId}`).digest("hex").slice(0, 12);
33023
33254
  return `fusion-agent-memory-${safeAgentId.toLowerCase()}-${hash}`;
33024
33255
  }
33025
33256
  function dailyMemoryPath(rootDir, date = /* @__PURE__ */ new Date()) {
@@ -33423,13 +33654,13 @@ async function searchWithQmd(rootDir, options) {
33423
33654
  const command = "qmd";
33424
33655
  const limit = Math.max(1, Math.min(options.limit ?? 5, 20));
33425
33656
  try {
33426
- const { execFile: execFile8 } = await import("node:child_process");
33427
- const { promisify: promisify14 } = await import("node:util");
33428
- const execFileAsync6 = promisify14(execFile8);
33429
- await ensureQmdProjectMemoryCollection(rootDir, execFileAsync6);
33657
+ const { execFile: execFile9 } = await import("node:child_process");
33658
+ const { promisify: promisify15 } = await import("node:util");
33659
+ const execFileAsync7 = promisify15(execFile9);
33660
+ await ensureQmdProjectMemoryCollection(rootDir, execFileAsync7);
33430
33661
  scheduleQmdProjectMemoryRefresh(rootDir);
33431
33662
  const args = buildQmdSearchArgs(rootDir, options);
33432
- const { stdout } = await execFileAsync6(command, args, {
33663
+ const { stdout } = await execFileAsync7(command, args, {
33433
33664
  cwd: rootDir,
33434
33665
  timeout: 4e3,
33435
33666
  maxBuffer: 1024 * 1024
@@ -33454,12 +33685,12 @@ async function searchWithQmd(rootDir, options) {
33454
33685
  return [];
33455
33686
  }
33456
33687
  }
33457
- async function ensureQmdProjectMemoryCollection(rootDir, execFileAsync6) {
33688
+ async function ensureQmdProjectMemoryCollection(rootDir, execFileAsync7) {
33458
33689
  const collectionName = qmdMemoryCollectionName(rootDir);
33459
33690
  const memoryDir = memoryWorkspacePath(rootDir);
33460
33691
  await mkdir6(memoryDir, { recursive: true });
33461
33692
  try {
33462
- await execFileAsync6("qmd", buildQmdCollectionAddArgs(rootDir), {
33693
+ await execFileAsync7("qmd", buildQmdCollectionAddArgs(rootDir), {
33463
33694
  cwd: rootDir,
33464
33695
  timeout: 4e3,
33465
33696
  maxBuffer: 512 * 1024
@@ -33475,9 +33706,9 @@ ${stderr}`)) {
33475
33706
  return collectionName;
33476
33707
  }
33477
33708
  async function getDefaultExecFileAsync() {
33478
- const { execFile: execFile8 } = await import("node:child_process");
33479
- const { promisify: promisify14 } = await import("node:util");
33480
- return promisify14(execFile8);
33709
+ const { execFile: execFile9 } = await import("node:child_process");
33710
+ const { promisify: promisify15 } = await import("node:util");
33711
+ return promisify15(execFile9);
33481
33712
  }
33482
33713
  async function refreshQmdProjectMemoryIndex(rootDir, options) {
33483
33714
  const key = resolve7(rootDir);
@@ -33492,14 +33723,14 @@ async function refreshQmdProjectMemoryIndex(rootDir, options) {
33492
33723
  }
33493
33724
  }
33494
33725
  const promise = (async () => {
33495
- const execFileAsync6 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33496
- await ensureQmdProjectMemoryCollection(rootDir, execFileAsync6);
33497
- await execFileAsync6("qmd", ["update"], {
33726
+ const execFileAsync7 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33727
+ await ensureQmdProjectMemoryCollection(rootDir, execFileAsync7);
33728
+ await execFileAsync7("qmd", ["update"], {
33498
33729
  cwd: rootDir,
33499
33730
  timeout: 3e4,
33500
33731
  maxBuffer: 1024 * 1024
33501
33732
  });
33502
- await execFileAsync6("qmd", ["embed"], {
33733
+ await execFileAsync7("qmd", ["embed"], {
33503
33734
  cwd: rootDir,
33504
33735
  timeout: 12e4,
33505
33736
  maxBuffer: 1024 * 1024
@@ -33535,12 +33766,12 @@ async function refreshQmdAgentMemoryIndex(rootDir, agentId, options) {
33535
33766
  }
33536
33767
  }
33537
33768
  const promise = (async () => {
33538
- const execFileAsync6 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33769
+ const execFileAsync7 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33539
33770
  const { agentMemoryWorkspacePath: agentMemoryWorkspacePath2 } = await Promise.resolve().then(() => (init_memory_dreams(), memory_dreams_exports));
33540
33771
  const workspacePath = agentMemoryWorkspacePath2(rootDir, agentId);
33541
33772
  await mkdir6(workspacePath, { recursive: true });
33542
33773
  try {
33543
- await execFileAsync6("qmd", ["collection", "add", workspacePath, "--name", qmdAgentMemoryCollectionName(rootDir, agentId), "--mask", "**/*.md"], {
33774
+ await execFileAsync7("qmd", ["collection", "add", workspacePath, "--name", qmdAgentMemoryCollectionName(rootDir, agentId), "--mask", "**/*.md"], {
33544
33775
  cwd: rootDir,
33545
33776
  timeout: 4e3,
33546
33777
  maxBuffer: 512 * 1024
@@ -33553,8 +33784,8 @@ ${stderr}`)) {
33553
33784
  throw err;
33554
33785
  }
33555
33786
  }
33556
- await execFileAsync6("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
33557
- await execFileAsync6("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
33787
+ await execFileAsync7("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
33788
+ await execFileAsync7("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
33558
33789
  })();
33559
33790
  qmdAgentRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
33560
33791
  try {
@@ -33575,8 +33806,8 @@ function scheduleQmdAgentMemoryRefresh(rootDir, agentId) {
33575
33806
  }
33576
33807
  async function isQmdAvailable() {
33577
33808
  try {
33578
- const execFileAsync6 = await getDefaultExecFileAsync();
33579
- await execFileAsync6("qmd", ["--help"], {
33809
+ const execFileAsync7 = await getDefaultExecFileAsync();
33810
+ await execFileAsync7("qmd", ["--help"], {
33580
33811
  timeout: 3e3,
33581
33812
  maxBuffer: 128 * 1024
33582
33813
  });
@@ -33586,12 +33817,12 @@ async function isQmdAvailable() {
33586
33817
  }
33587
33818
  }
33588
33819
  async function installQmd(options) {
33589
- const execFileAsync6 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33820
+ const execFileAsync7 = options?.execFileAsync ?? await getDefaultExecFileAsync();
33590
33821
  const [command, ...args] = QMD_INSTALL_COMMAND.split(" ");
33591
33822
  if (!command || args.length === 0) {
33592
33823
  throw new MemoryBackendError("BACKEND_UNAVAILABLE", "qmd install command is not configured", "qmd");
33593
33824
  }
33594
- await execFileAsync6(command, args, {
33825
+ await execFileAsync7(command, args, {
33595
33826
  timeout: 12e4,
33596
33827
  maxBuffer: 1024 * 1024
33597
33828
  });
@@ -34892,6 +35123,135 @@ function resolveLocalNodeId(nodes, fallback = "local") {
34892
35123
  const localNode = nodes?.find((node) => node.type === "local");
34893
35124
  return localNode?.id ?? fallback;
34894
35125
  }
35126
+ function parseTaskId(taskId) {
35127
+ const match = taskId.trim().toUpperCase().match(TASK_ID_PATTERN);
35128
+ if (!match) {
35129
+ return null;
35130
+ }
35131
+ const sequence = Number.parseInt(match[2], 10);
35132
+ if (!Number.isFinite(sequence)) {
35133
+ return null;
35134
+ }
35135
+ return { prefix: match[1], sequence };
35136
+ }
35137
+ function getConfiguredPrefixAndLegacyNextId(db) {
35138
+ try {
35139
+ const row = db.prepare("SELECT nextId, settings FROM config WHERE id = 1").get();
35140
+ if (!row) {
35141
+ return { prefix: "KB", nextId: null };
35142
+ }
35143
+ const settings = row.settings ? JSON.parse(row.settings) : null;
35144
+ return {
35145
+ prefix: (settings?.taskPrefix ?? "KB").trim().toUpperCase(),
35146
+ nextId: typeof row.nextId === "number" ? row.nextId : null
35147
+ };
35148
+ } catch {
35149
+ return { prefix: "KB", nextId: null };
35150
+ }
35151
+ }
35152
+ function getKnownPrefixes(db) {
35153
+ const prefixes = /* @__PURE__ */ new Set();
35154
+ const configured = getConfiguredPrefixAndLegacyNextId(db).prefix;
35155
+ if (configured) {
35156
+ prefixes.add(configured);
35157
+ }
35158
+ const addFromQuery = (sql, mapper) => {
35159
+ try {
35160
+ const rows = db.prepare(sql).all();
35161
+ for (const row of rows) {
35162
+ const prefix = mapper(row)?.trim().toUpperCase();
35163
+ if (prefix) {
35164
+ prefixes.add(prefix);
35165
+ }
35166
+ }
35167
+ } catch {
35168
+ }
35169
+ };
35170
+ addFromQuery("SELECT prefix FROM distributed_task_id_state", (row) => row.prefix);
35171
+ addFromQuery("SELECT prefix FROM distributed_task_id_reservations", (row) => row.prefix);
35172
+ addFromQuery("SELECT id FROM tasks", (row) => parseTaskId(String(row.id ?? ""))?.prefix);
35173
+ addFromQuery("SELECT id FROM archivedTasks", (row) => parseTaskId(String(row.id ?? ""))?.prefix);
35174
+ return prefixes;
35175
+ }
35176
+ function getMaxTaskSequenceFromTable(db, table, prefix) {
35177
+ try {
35178
+ const rows = db.prepare(`SELECT id FROM ${table} WHERE id LIKE ?`).all(`${prefix}-%`);
35179
+ let maxSequence = 0;
35180
+ for (const row of rows) {
35181
+ const parsed = parseTaskId(row.id);
35182
+ if (parsed?.prefix === prefix && parsed.sequence > maxSequence) {
35183
+ maxSequence = parsed.sequence;
35184
+ }
35185
+ }
35186
+ return maxSequence;
35187
+ } catch {
35188
+ return 0;
35189
+ }
35190
+ }
35191
+ function getMaxReservationSequence(db, prefix) {
35192
+ try {
35193
+ const row = db.prepare("SELECT MAX(sequence) AS maxSeq FROM distributed_task_id_reservations WHERE prefix = ?").get(prefix);
35194
+ return typeof row?.maxSeq === "number" ? row.maxSeq : 0;
35195
+ } catch {
35196
+ return 0;
35197
+ }
35198
+ }
35199
+ function getNextSequenceFloor(db, prefix) {
35200
+ const configured = getConfiguredPrefixAndLegacyNextId(db);
35201
+ let nextSequence = 1;
35202
+ if (configured.prefix === prefix && configured.nextId && configured.nextId > nextSequence) {
35203
+ nextSequence = configured.nextId;
35204
+ }
35205
+ const taskHighWaterMark = getMaxTaskSequenceFromTable(db, "tasks", prefix) + 1;
35206
+ const archivedHighWaterMark = getMaxTaskSequenceFromTable(db, "archivedTasks", prefix) + 1;
35207
+ const reservationHighWaterMark = getMaxReservationSequence(db, prefix) + 1;
35208
+ nextSequence = Math.max(nextSequence, taskHighWaterMark, archivedHighWaterMark, reservationHighWaterMark);
35209
+ return nextSequence;
35210
+ }
35211
+ function ensureStateRow(db, prefix) {
35212
+ const nowIso3 = (/* @__PURE__ */ new Date()).toISOString();
35213
+ const nextSequence = getNextSequenceFloor(db, prefix);
35214
+ db.prepare(
35215
+ `INSERT OR IGNORE INTO distributed_task_id_state (
35216
+ prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt
35217
+ ) VALUES (?, ?, 0, NULL, ?)`
35218
+ ).run(prefix, nextSequence, nowIso3);
35219
+ db.prepare(
35220
+ `UPDATE distributed_task_id_state
35221
+ SET nextSequence = MAX(nextSequence, ?),
35222
+ updatedAt = ?
35223
+ WHERE prefix = ?`
35224
+ ).run(nextSequence, nowIso3, prefix);
35225
+ }
35226
+ function reconcileTaskIdState(db) {
35227
+ const nowIso3 = (/* @__PURE__ */ new Date()).toISOString();
35228
+ return db.transaction(() => {
35229
+ const reconciled = [];
35230
+ for (const prefix of getKnownPrefixes(db)) {
35231
+ const nextSequence = getNextSequenceFloor(db, prefix);
35232
+ db.prepare(
35233
+ `INSERT OR IGNORE INTO distributed_task_id_state (
35234
+ prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt
35235
+ ) VALUES (?, ?, 0, NULL, ?)`
35236
+ ).run(prefix, nextSequence, nowIso3);
35237
+ const before = db.prepare("SELECT nextSequence FROM distributed_task_id_state WHERE prefix = ?").get(prefix);
35238
+ db.prepare(
35239
+ `UPDATE distributed_task_id_state
35240
+ SET nextSequence = MAX(nextSequence, ?),
35241
+ updatedAt = ?
35242
+ WHERE prefix = ?`
35243
+ ).run(nextSequence, nowIso3, prefix);
35244
+ const after = db.prepare("SELECT nextSequence FROM distributed_task_id_state WHERE prefix = ?").get(prefix);
35245
+ if (!before || !after || after.nextSequence !== before.nextSequence) {
35246
+ reconciled.push(prefix);
35247
+ }
35248
+ }
35249
+ if (reconciled.length > 0) {
35250
+ db.bumpLastModified();
35251
+ }
35252
+ return reconciled;
35253
+ });
35254
+ }
34895
35255
  function formatDistributedTaskId(prefix, sequence) {
34896
35256
  const normalizedPrefix = prefix.trim().toUpperCase();
34897
35257
  if (!normalizedPrefix) {
@@ -34934,48 +35294,6 @@ function createDistributedTaskIdAllocator(db) {
34934
35294
  };
34935
35295
  return existsInTable("tasks") || existsInTable("archivedTasks");
34936
35296
  };
34937
- const ensureStateRow = (prefix) => {
34938
- let seedSequence = 1;
34939
- try {
34940
- const configRow = db.prepare("SELECT nextId, settings FROM config WHERE id = 1").get();
34941
- if (configRow) {
34942
- const settings = configRow.settings ? JSON.parse(configRow.settings) : null;
34943
- const configuredPrefix = (settings?.taskPrefix ?? "KB").trim().toUpperCase();
34944
- if (configuredPrefix === prefix && typeof configRow.nextId === "number" && configRow.nextId > seedSequence) {
34945
- seedSequence = configRow.nextId;
34946
- }
34947
- }
34948
- } catch {
34949
- }
34950
- const idPattern = `${prefix}-%`;
34951
- const probeTable = (table) => {
34952
- try {
34953
- const row = db.prepare(
34954
- `SELECT MAX(CAST(substr(id, ${prefix.length + 2}) AS INTEGER)) AS maxSeq
34955
- FROM ${table}
34956
- WHERE id LIKE ? AND substr(id, ${prefix.length + 2}) GLOB '[0-9]*'`
34957
- ).get(idPattern);
34958
- if (row && typeof row.maxSeq === "number" && row.maxSeq + 1 > seedSequence) {
34959
- seedSequence = row.maxSeq + 1;
34960
- }
34961
- } catch {
34962
- }
34963
- };
34964
- probeTable("tasks");
34965
- probeTable("archivedTasks");
34966
- const nowIso3 = (/* @__PURE__ */ new Date()).toISOString();
34967
- db.prepare(
34968
- `INSERT OR IGNORE INTO distributed_task_id_state (
34969
- prefix, nextSequence, committedClusterTaskCount, lastCommittedTaskId, updatedAt
34970
- ) VALUES (?, ?, 0, NULL, ?)`
34971
- ).run(prefix, seedSequence, nowIso3);
34972
- db.prepare(
34973
- `UPDATE distributed_task_id_state
34974
- SET nextSequence = MAX(nextSequence, ?),
34975
- updatedAt = ?
34976
- WHERE prefix = ?`
34977
- ).run(seedSequence, nowIso3, prefix);
34978
- };
34979
35297
  return {
34980
35298
  formatDistributedTaskId,
34981
35299
  reserveDistributedTaskId: async (input) => withLock(async () => {
@@ -34989,7 +35307,7 @@ function createDistributedTaskIdAllocator(db) {
34989
35307
  if (!prefix) {
34990
35308
  throw new DistributedTaskIdError("prefix is required", "invalid_prefix");
34991
35309
  }
34992
- ensureStateRow(prefix);
35310
+ ensureStateRow(db, prefix);
34993
35311
  const state = db.prepare(
34994
35312
  "SELECT nextSequence, committedClusterTaskCount FROM distributed_task_id_state WHERE prefix = ?"
34995
35313
  ).get(prefix);
@@ -35043,7 +35361,7 @@ function createDistributedTaskIdAllocator(db) {
35043
35361
  SET status = 'committed', committedAt = ?, updatedAt = ?
35044
35362
  WHERE reservationId = ?`
35045
35363
  ).run(nowIso3, nowIso3, row.reservationId);
35046
- ensureStateRow(row.prefix);
35364
+ ensureStateRow(db, row.prefix);
35047
35365
  db.prepare(
35048
35366
  `UPDATE distributed_task_id_state
35049
35367
  SET committedClusterTaskCount = committedClusterTaskCount + 1,
@@ -35089,7 +35407,7 @@ function createDistributedTaskIdAllocator(db) {
35089
35407
  WHERE reservationId = ?`
35090
35408
  ).run(input.reason, nowIso3, nowIso3, row.reservationId);
35091
35409
  }
35092
- ensureStateRow(row.prefix);
35410
+ ensureStateRow(db, row.prefix);
35093
35411
  const state = db.prepare(
35094
35412
  "SELECT committedClusterTaskCount FROM distributed_task_id_state WHERE prefix = ?"
35095
35413
  ).get(row.prefix);
@@ -35111,7 +35429,7 @@ function createDistributedTaskIdAllocator(db) {
35111
35429
  if (!prefix) {
35112
35430
  throw new DistributedTaskIdError("prefix is required", "invalid_prefix");
35113
35431
  }
35114
- ensureStateRow(prefix);
35432
+ ensureStateRow(db, prefix);
35115
35433
  const row = db.prepare(
35116
35434
  `SELECT nextSequence, committedClusterTaskCount, lastCommittedTaskId
35117
35435
  FROM distributed_task_id_state
@@ -35136,11 +35454,12 @@ function createDistributedTaskIdAllocator(db) {
35136
35454
  })
35137
35455
  };
35138
35456
  }
35139
- var DEFAULT_RESERVATION_TTL_MS, DistributedTaskIdError;
35457
+ var DEFAULT_RESERVATION_TTL_MS, TASK_ID_PATTERN, DistributedTaskIdError;
35140
35458
  var init_distributed_task_id = __esm({
35141
35459
  "../core/src/distributed-task-id.ts"() {
35142
35460
  "use strict";
35143
35461
  DEFAULT_RESERVATION_TTL_MS = 15 * 60 * 1e3;
35462
+ TASK_ID_PATTERN = /^([A-Z][A-Z0-9]*)-(\d+)$/;
35144
35463
  DistributedTaskIdError = class extends Error {
35145
35464
  constructor(message, code) {
35146
35465
  super(message);
@@ -35435,6 +35754,8 @@ var init_store = __esm({
35435
35754
  worktreeAllocationLock = Promise.resolve();
35436
35755
  /** Promise chain for serializing config.json read-modify-write cycles */
35437
35756
  configLock = Promise.resolve();
35757
+ /** Startup/open guard for distributed_task_id_state reconciliation. */
35758
+ taskIdStateReconciled = false;
35438
35759
  /** Cached workflow steps — invalidated on create/update/delete */
35439
35760
  workflowStepsCache = null;
35440
35761
  /** Plugin-contributed workflow step templates injected by engine runtime. */
@@ -35501,6 +35822,7 @@ var init_store = __esm({
35501
35822
  throw error;
35502
35823
  }
35503
35824
  this._db = db;
35825
+ this.reconcileDistributedTaskIdStateOnOpen();
35504
35826
  if (detectLegacyData(this.fusionDir)) {
35505
35827
  }
35506
35828
  }
@@ -35520,6 +35842,13 @@ var init_store = __esm({
35520
35842
  }
35521
35843
  return this._archiveDb;
35522
35844
  }
35845
+ reconcileDistributedTaskIdStateOnOpen() {
35846
+ if (this.taskIdStateReconciled) {
35847
+ return;
35848
+ }
35849
+ reconcileTaskIdState(this.db);
35850
+ this.taskIdStateReconciled = true;
35851
+ }
35523
35852
  async init() {
35524
35853
  await mkdir7(this.tasksDir, { recursive: true });
35525
35854
  if (!this._db) {
@@ -35532,15 +35861,18 @@ var init_store = __esm({
35532
35861
  }
35533
35862
  this._db = db;
35534
35863
  }
35864
+ this.reconcileDistributedTaskIdStateOnOpen();
35535
35865
  if (detectLegacyData(this.fusionDir)) {
35536
35866
  await migrateFromLegacy(this.fusionDir, this._db);
35537
35867
  }
35538
35868
  await this.migrateActiveArchivedTasksToArchiveDb();
35539
35869
  await this.importLegacyAgentLogsOnce();
35870
+ this.taskIdStateReconciled = false;
35871
+ this.reconcileDistributedTaskIdStateOnOpen();
35540
35872
  if (!existsSync13(this.configPath)) {
35541
35873
  const config = await this.readConfig();
35542
35874
  try {
35543
- await writeFile6(this.configPath, JSON.stringify(config, null, 2));
35875
+ await writeFile6(this.configPath, this.serializeConfigForDisk(config));
35544
35876
  } catch (err) {
35545
35877
  storeLog.warn("Backward-compat config.json sync failed during init", {
35546
35878
  phase: "init:config-sync",
@@ -36129,117 +36461,8 @@ ${outcome}`;
36129
36461
  `;
36130
36462
  return [...columns, limitedLog].join(", ");
36131
36463
  }
36132
- /**
36133
- * Upsert a task to the database. Used by create and update operations.
36134
- */
36135
- upsertTask(task) {
36136
- this.db.prepare(`
36137
- INSERT INTO tasks (
36138
- id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep,
36139
- worktree, blockedBy, paused, baseBranch, branch, executionStartBranch, baseCommitSha, modelPresetId, modelProvider,
36140
- modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
36141
- workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, nextRecoveryAt, error,
36142
- summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
36143
- tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
36144
- executionStartedAt, executionCompletedAt,
36145
- dependencies, steps, log, attachments, steeringComments,
36146
- comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
36147
- sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
36148
- mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
36149
- ) VALUES (
36150
- ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
36151
- )
36152
- ON CONFLICT(id) DO UPDATE SET
36153
- lineageId = excluded.lineageId,
36154
- title = excluded.title,
36155
- description = excluded.description,
36156
- priority = excluded.priority,
36157
- "column" = excluded."column",
36158
- status = excluded.status,
36159
- size = excluded.size,
36160
- reviewLevel = excluded.reviewLevel,
36161
- currentStep = excluded.currentStep,
36162
- worktree = excluded.worktree,
36163
- blockedBy = excluded.blockedBy,
36164
- paused = excluded.paused,
36165
- baseBranch = excluded.baseBranch,
36166
- branch = excluded.branch,
36167
- executionStartBranch = excluded.executionStartBranch,
36168
- baseCommitSha = excluded.baseCommitSha,
36169
- modelPresetId = excluded.modelPresetId,
36170
- modelProvider = excluded.modelProvider,
36171
- modelId = excluded.modelId,
36172
- validatorModelProvider = excluded.validatorModelProvider,
36173
- validatorModelId = excluded.validatorModelId,
36174
- planningModelProvider = excluded.planningModelProvider,
36175
- planningModelId = excluded.planningModelId,
36176
- mergeRetries = excluded.mergeRetries,
36177
- workflowStepRetries = excluded.workflowStepRetries,
36178
- stuckKillCount = excluded.stuckKillCount,
36179
- postReviewFixCount = excluded.postReviewFixCount,
36180
- recoveryRetryCount = excluded.recoveryRetryCount,
36181
- taskDoneRetryCount = excluded.taskDoneRetryCount,
36182
- verificationFailureCount = excluded.verificationFailureCount,
36183
- mergeConflictBounceCount = excluded.mergeConflictBounceCount,
36184
- nextRecoveryAt = excluded.nextRecoveryAt,
36185
- error = excluded.error,
36186
- summary = excluded.summary,
36187
- thinkingLevel = excluded.thinkingLevel,
36188
- executionMode = excluded.executionMode,
36189
- tokenUsageInputTokens = excluded.tokenUsageInputTokens,
36190
- tokenUsageOutputTokens = excluded.tokenUsageOutputTokens,
36191
- tokenUsageCachedTokens = excluded.tokenUsageCachedTokens,
36192
- tokenUsageTotalTokens = excluded.tokenUsageTotalTokens,
36193
- tokenUsageFirstUsedAt = excluded.tokenUsageFirstUsedAt,
36194
- tokenUsageLastUsedAt = excluded.tokenUsageLastUsedAt,
36195
- createdAt = excluded.createdAt,
36196
- updatedAt = excluded.updatedAt,
36197
- columnMovedAt = excluded.columnMovedAt,
36198
- executionStartedAt = excluded.executionStartedAt,
36199
- executionCompletedAt = excluded.executionCompletedAt,
36200
- dependencies = excluded.dependencies,
36201
- steps = excluded.steps,
36202
- log = excluded.log,
36203
- attachments = excluded.attachments,
36204
- steeringComments = excluded.steeringComments,
36205
- comments = excluded.comments,
36206
- review = excluded.review,
36207
- reviewState = excluded.reviewState,
36208
- workflowStepResults = excluded.workflowStepResults,
36209
- prInfo = excluded.prInfo,
36210
- issueInfo = excluded.issueInfo,
36211
- githubTracking = excluded.githubTracking,
36212
- sourceIssueProvider = excluded.sourceIssueProvider,
36213
- sourceIssueRepository = excluded.sourceIssueRepository,
36214
- sourceIssueExternalIssueId = excluded.sourceIssueExternalIssueId,
36215
- sourceIssueNumber = excluded.sourceIssueNumber,
36216
- sourceIssueUrl = excluded.sourceIssueUrl,
36217
- mergeDetails = excluded.mergeDetails,
36218
- breakIntoSubtasks = excluded.breakIntoSubtasks,
36219
- enabledWorkflowSteps = excluded.enabledWorkflowSteps,
36220
- modifiedFiles = excluded.modifiedFiles,
36221
- missionId = excluded.missionId,
36222
- sliceId = excluded.sliceId,
36223
- assignedAgentId = excluded.assignedAgentId,
36224
- pausedByAgentId = excluded.pausedByAgentId,
36225
- assigneeUserId = excluded.assigneeUserId,
36226
- nodeId = excluded.nodeId,
36227
- effectiveNodeId = excluded.effectiveNodeId,
36228
- effectiveNodeSource = excluded.effectiveNodeSource,
36229
- sourceType = excluded.sourceType,
36230
- sourceAgentId = excluded.sourceAgentId,
36231
- sourceRunId = excluded.sourceRunId,
36232
- sourceSessionId = excluded.sourceSessionId,
36233
- sourceMessageId = excluded.sourceMessageId,
36234
- sourceParentTaskId = excluded.sourceParentTaskId,
36235
- sourceMetadata = excluded.sourceMetadata,
36236
- checkedOutBy = excluded.checkedOutBy,
36237
- checkedOutAt = excluded.checkedOutAt,
36238
- checkoutNodeId = excluded.checkoutNodeId,
36239
- checkoutRunId = excluded.checkoutRunId,
36240
- checkoutLeaseRenewedAt = excluded.checkoutLeaseRenewedAt,
36241
- checkoutLeaseEpoch = excluded.checkoutLeaseEpoch
36242
- `).run(
36464
+ getTaskPersistValues(task) {
36465
+ return [
36243
36466
  task.id,
36244
36467
  task.lineageId ?? generateTaskLineageId(),
36245
36468
  task.title ?? null,
@@ -36330,9 +36553,193 @@ ${outcome}`;
36330
36553
  task.checkoutRunId ?? null,
36331
36554
  task.checkoutLeaseRenewedAt ?? null,
36332
36555
  task.checkoutLeaseEpoch ?? 0
36333
- );
36556
+ ];
36557
+ }
36558
+ /**
36559
+ * Insert a brand-new task row. Create paths must use this so SQLite raises on
36560
+ * duplicate IDs instead of silently rewriting the existing row.
36561
+ */
36562
+ insertTask(task) {
36563
+ this.db.prepare(`
36564
+ INSERT INTO tasks (
36565
+ id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep,
36566
+ worktree, blockedBy, paused, baseBranch, branch, executionStartBranch, baseCommitSha, modelPresetId, modelProvider,
36567
+ modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
36568
+ workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, nextRecoveryAt, error,
36569
+ summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
36570
+ tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
36571
+ executionStartedAt, executionCompletedAt,
36572
+ dependencies, steps, log, attachments, steeringComments,
36573
+ comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
36574
+ sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
36575
+ mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
36576
+ ) VALUES (
36577
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
36578
+ )
36579
+ `).run(...this.getTaskPersistValues(task));
36334
36580
  this.db.bumpLastModified();
36335
36581
  }
36582
+ /**
36583
+ * Upsert a task to the database. Update paths intentionally retain ON CONFLICT
36584
+ * semantics; create paths must use insertTask() instead.
36585
+ */
36586
+ upsertTask(task) {
36587
+ this.db.prepare(`
36588
+ INSERT INTO tasks (
36589
+ id, lineageId, title, description, priority, "column", status, size, reviewLevel, currentStep,
36590
+ worktree, blockedBy, paused, baseBranch, branch, executionStartBranch, baseCommitSha, modelPresetId, modelProvider,
36591
+ modelId, validatorModelProvider, validatorModelId, planningModelProvider, planningModelId, mergeRetries,
36592
+ workflowStepRetries, stuckKillCount, postReviewFixCount, recoveryRetryCount, taskDoneRetryCount, verificationFailureCount, mergeConflictBounceCount, nextRecoveryAt, error,
36593
+ summary, thinkingLevel, executionMode, tokenUsageInputTokens, tokenUsageOutputTokens, tokenUsageCachedTokens,
36594
+ tokenUsageTotalTokens, tokenUsageFirstUsedAt, tokenUsageLastUsedAt, createdAt, updatedAt, columnMovedAt,
36595
+ executionStartedAt, executionCompletedAt,
36596
+ dependencies, steps, log, attachments, steeringComments,
36597
+ comments, review, reviewState, workflowStepResults, prInfo, issueInfo, githubTracking,
36598
+ sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
36599
+ mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt, checkoutNodeId, checkoutRunId, checkoutLeaseRenewedAt, checkoutLeaseEpoch
36600
+ ) VALUES (
36601
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
36602
+ )
36603
+ ON CONFLICT(id) DO UPDATE SET
36604
+ lineageId = excluded.lineageId,
36605
+ title = excluded.title,
36606
+ description = excluded.description,
36607
+ priority = excluded.priority,
36608
+ "column" = excluded."column",
36609
+ status = excluded.status,
36610
+ size = excluded.size,
36611
+ reviewLevel = excluded.reviewLevel,
36612
+ currentStep = excluded.currentStep,
36613
+ worktree = excluded.worktree,
36614
+ blockedBy = excluded.blockedBy,
36615
+ paused = excluded.paused,
36616
+ baseBranch = excluded.baseBranch,
36617
+ branch = excluded.branch,
36618
+ executionStartBranch = excluded.executionStartBranch,
36619
+ baseCommitSha = excluded.baseCommitSha,
36620
+ modelPresetId = excluded.modelPresetId,
36621
+ modelProvider = excluded.modelProvider,
36622
+ modelId = excluded.modelId,
36623
+ validatorModelProvider = excluded.validatorModelProvider,
36624
+ validatorModelId = excluded.validatorModelId,
36625
+ planningModelProvider = excluded.planningModelProvider,
36626
+ planningModelId = excluded.planningModelId,
36627
+ mergeRetries = excluded.mergeRetries,
36628
+ workflowStepRetries = excluded.workflowStepRetries,
36629
+ stuckKillCount = excluded.stuckKillCount,
36630
+ postReviewFixCount = excluded.postReviewFixCount,
36631
+ recoveryRetryCount = excluded.recoveryRetryCount,
36632
+ taskDoneRetryCount = excluded.taskDoneRetryCount,
36633
+ verificationFailureCount = excluded.verificationFailureCount,
36634
+ mergeConflictBounceCount = excluded.mergeConflictBounceCount,
36635
+ nextRecoveryAt = excluded.nextRecoveryAt,
36636
+ error = excluded.error,
36637
+ summary = excluded.summary,
36638
+ thinkingLevel = excluded.thinkingLevel,
36639
+ executionMode = excluded.executionMode,
36640
+ tokenUsageInputTokens = excluded.tokenUsageInputTokens,
36641
+ tokenUsageOutputTokens = excluded.tokenUsageOutputTokens,
36642
+ tokenUsageCachedTokens = excluded.tokenUsageCachedTokens,
36643
+ tokenUsageTotalTokens = excluded.tokenUsageTotalTokens,
36644
+ tokenUsageFirstUsedAt = excluded.tokenUsageFirstUsedAt,
36645
+ tokenUsageLastUsedAt = excluded.tokenUsageLastUsedAt,
36646
+ createdAt = excluded.createdAt,
36647
+ updatedAt = excluded.updatedAt,
36648
+ columnMovedAt = excluded.columnMovedAt,
36649
+ executionStartedAt = excluded.executionStartedAt,
36650
+ executionCompletedAt = excluded.executionCompletedAt,
36651
+ dependencies = excluded.dependencies,
36652
+ steps = excluded.steps,
36653
+ log = excluded.log,
36654
+ attachments = excluded.attachments,
36655
+ steeringComments = excluded.steeringComments,
36656
+ comments = excluded.comments,
36657
+ review = excluded.review,
36658
+ reviewState = excluded.reviewState,
36659
+ workflowStepResults = excluded.workflowStepResults,
36660
+ prInfo = excluded.prInfo,
36661
+ issueInfo = excluded.issueInfo,
36662
+ githubTracking = excluded.githubTracking,
36663
+ sourceIssueProvider = excluded.sourceIssueProvider,
36664
+ sourceIssueRepository = excluded.sourceIssueRepository,
36665
+ sourceIssueExternalIssueId = excluded.sourceIssueExternalIssueId,
36666
+ sourceIssueNumber = excluded.sourceIssueNumber,
36667
+ sourceIssueUrl = excluded.sourceIssueUrl,
36668
+ mergeDetails = excluded.mergeDetails,
36669
+ breakIntoSubtasks = excluded.breakIntoSubtasks,
36670
+ enabledWorkflowSteps = excluded.enabledWorkflowSteps,
36671
+ modifiedFiles = excluded.modifiedFiles,
36672
+ missionId = excluded.missionId,
36673
+ sliceId = excluded.sliceId,
36674
+ assignedAgentId = excluded.assignedAgentId,
36675
+ pausedByAgentId = excluded.pausedByAgentId,
36676
+ assigneeUserId = excluded.assigneeUserId,
36677
+ nodeId = excluded.nodeId,
36678
+ effectiveNodeId = excluded.effectiveNodeId,
36679
+ effectiveNodeSource = excluded.effectiveNodeSource,
36680
+ sourceType = excluded.sourceType,
36681
+ sourceAgentId = excluded.sourceAgentId,
36682
+ sourceRunId = excluded.sourceRunId,
36683
+ sourceSessionId = excluded.sourceSessionId,
36684
+ sourceMessageId = excluded.sourceMessageId,
36685
+ sourceParentTaskId = excluded.sourceParentTaskId,
36686
+ sourceMetadata = excluded.sourceMetadata,
36687
+ checkedOutBy = excluded.checkedOutBy,
36688
+ checkedOutAt = excluded.checkedOutAt,
36689
+ checkoutNodeId = excluded.checkoutNodeId,
36690
+ checkoutRunId = excluded.checkoutRunId,
36691
+ checkoutLeaseRenewedAt = excluded.checkoutLeaseRenewedAt,
36692
+ checkoutLeaseEpoch = excluded.checkoutLeaseEpoch
36693
+ `).run(...this.getTaskPersistValues(task));
36694
+ this.db.bumpLastModified();
36695
+ }
36696
+ isTaskIdConflictError(error) {
36697
+ const message = error instanceof Error ? error.message : String(error);
36698
+ return /SQLITE_CONSTRAINT|UNIQUE constraint failed: tasks\.id|PRIMARY KEY constraint failed: tasks\.id/i.test(message);
36699
+ }
36700
+ logTaskCreateConflict(task, operation, error) {
36701
+ storeLog.error("Refused colliding task create", {
36702
+ phase: "task-create:id-conflict",
36703
+ operation,
36704
+ taskId: task.id,
36705
+ column: task.column,
36706
+ sourceType: task.sourceType,
36707
+ error: error instanceof Error ? error.message : String(error)
36708
+ });
36709
+ }
36710
+ insertTaskWithFtsRecovery(task, operation) {
36711
+ const normalizeConflict = (error) => {
36712
+ this.logTaskCreateConflict(task, operation, error);
36713
+ throw new Error(`Task ID already exists: ${task.id}`);
36714
+ };
36715
+ try {
36716
+ this.insertTask(task);
36717
+ return;
36718
+ } catch (error) {
36719
+ if (this.isTaskIdConflictError(error)) {
36720
+ normalizeConflict(error);
36721
+ }
36722
+ if (!this.db.isFts5CorruptionError(error)) {
36723
+ throw error;
36724
+ }
36725
+ console.warn(`[fusion:store] FTS5 corruption detected during insert for task ${task.id}; rebuilding index and retrying once`);
36726
+ try {
36727
+ this.db.rebuildFts5Index();
36728
+ } catch (rebuildError) {
36729
+ console.warn("[fusion:store] FTS5 rebuild failed; propagating original insert error", rebuildError);
36730
+ throw error;
36731
+ }
36732
+ try {
36733
+ this.insertTask(task);
36734
+ } catch (retryError) {
36735
+ if (this.isTaskIdConflictError(retryError)) {
36736
+ normalizeConflict(retryError);
36737
+ }
36738
+ console.warn("[fusion:store] Insert retry after FTS5 rebuild failed; propagating original insert error", retryError);
36739
+ throw error;
36740
+ }
36741
+ }
36742
+ }
36336
36743
  upsertTaskWithFtsRecovery(task) {
36337
36744
  try {
36338
36745
  this.upsertTask(task);
@@ -36365,6 +36772,28 @@ ${outcome}`;
36365
36772
  if (!row) return void 0;
36366
36773
  return this.rowToTask(row);
36367
36774
  }
36775
+ isTaskIdPresentInArchivedTasksTable(id) {
36776
+ try {
36777
+ const row = this.db.prepare("SELECT 1 as found FROM archivedTasks WHERE id = ? LIMIT 1").get(id);
36778
+ return row?.found === 1;
36779
+ } catch {
36780
+ return false;
36781
+ }
36782
+ }
36783
+ taskIdExistsAnywhere(id) {
36784
+ if (this.readTaskFromDb(id)) {
36785
+ return true;
36786
+ }
36787
+ if (this.isTaskIdPresentInArchivedTasksTable(id)) {
36788
+ return true;
36789
+ }
36790
+ return this.archiveDb.get(id) !== void 0;
36791
+ }
36792
+ assertTaskIdAvailable(id) {
36793
+ if (this.taskIdExistsAnywhere(id)) {
36794
+ throw new Error(`Task ID already exists: ${id}`);
36795
+ }
36796
+ }
36368
36797
  isTaskArchived(id) {
36369
36798
  const row = this.db.prepare('SELECT "column" FROM tasks WHERE id = ?').get(id);
36370
36799
  if (row) {
@@ -36592,7 +37021,16 @@ ${outcome}`;
36592
37021
  await rename4(tmpPath, taskJsonPath);
36593
37022
  }
36594
37023
  /**
36595
- * Write a task to SQLite (primary store) and also write task.json to disk
37024
+ * Write a brand-new task to SQLite (primary store) and also write task.json to disk
37025
+ * for backward compatibility and debugging. Create paths must call this variant
37026
+ * so duplicate IDs fail safely instead of overwriting existing rows.
37027
+ */
37028
+ async atomicCreateTaskJson(dir, task, operation) {
37029
+ this.insertTaskWithFtsRecovery(task, operation);
37030
+ await this.writeTaskJsonFile(dir, task);
37031
+ }
37032
+ /**
37033
+ * Write an existing task to SQLite (primary store) and also write task.json to disk
36596
37034
  * for backward compatibility and debugging.
36597
37035
  */
36598
37036
  async atomicWriteTaskJson(dir, task) {
@@ -36608,7 +37046,7 @@ ${outcome}`;
36608
37046
  * @param auditInput - Optional audit event input to record atomically with the task write
36609
37047
  */
36610
37048
  async atomicWriteTaskJsonWithAudit(dir, task, auditInput) {
36611
- this.db.transaction(() => {
37049
+ this.db.transactionImmediate(() => {
36612
37050
  this.upsertTaskWithFtsRecovery(task);
36613
37051
  if (auditInput) {
36614
37052
  const eventId = randomUUID13();
@@ -36886,6 +37324,10 @@ ${outcome}`;
36886
37324
  settings: fromJson(row.settings)
36887
37325
  };
36888
37326
  }
37327
+ serializeConfigForDisk(config) {
37328
+ const { nextId: _deprecatedNextId, ...configForDisk } = config;
37329
+ return JSON.stringify(configForDisk, null, 2);
37330
+ }
36889
37331
  async writeConfig(config, options) {
36890
37332
  const now = (/* @__PURE__ */ new Date()).toISOString();
36891
37333
  const row = this.db.prepare("SELECT nextWorkflowStepId FROM config WHERE id = 1").get();
@@ -36893,10 +37335,14 @@ ${outcome}`;
36893
37335
  const legacyWorkflowSteps = config.workflowSteps;
36894
37336
  const workflowStepsJson = Array.isArray(legacyWorkflowSteps) ? JSON.stringify(legacyWorkflowSteps) : "[]";
36895
37337
  this.db.prepare(
36896
- `INSERT OR REPLACE INTO config (id, nextId, nextWorkflowStepId, settings, workflowSteps, updatedAt)
36897
- VALUES (1, ?, ?, ?, ?, ?)`
37338
+ `INSERT INTO config (id, nextWorkflowStepId, settings, workflowSteps, updatedAt)
37339
+ VALUES (1, ?, ?, ?, ?)
37340
+ ON CONFLICT(id) DO UPDATE SET
37341
+ nextWorkflowStepId = excluded.nextWorkflowStepId,
37342
+ settings = excluded.settings,
37343
+ workflowSteps = excluded.workflowSteps,
37344
+ updatedAt = excluded.updatedAt`
36898
37345
  ).run(
36899
- config.nextId || 1,
36900
37346
  nextWorkflowStepId,
36901
37347
  JSON.stringify(config.settings || {}),
36902
37348
  workflowStepsJson,
@@ -36905,7 +37351,7 @@ ${outcome}`;
36905
37351
  this.db.bumpLastModified();
36906
37352
  try {
36907
37353
  const tmpPath = this.configPath + ".tmp";
36908
- await writeFile6(tmpPath, JSON.stringify(config, null, 2));
37354
+ await writeFile6(tmpPath, this.serializeConfigForDisk(config));
36909
37355
  await rename4(tmpPath, this.configPath);
36910
37356
  } catch (err) {
36911
37357
  storeLog.warn("Backward-compat config.json sync failed after config write", {
@@ -37152,9 +37598,7 @@ ${outcome}`;
37152
37598
  if (input.dependencies?.includes(id)) {
37153
37599
  throw new Error(`Task ${id} cannot depend on itself`);
37154
37600
  }
37155
- if (this.readTaskFromDb(id)) {
37156
- throw new Error(`Task ID already exists: ${id}`);
37157
- }
37601
+ this.assertTaskIdAvailable(id);
37158
37602
  const title = input.title?.trim() || void 0;
37159
37603
  let resolvedWorkflowSteps = input.enabledWorkflowSteps?.length ? await this.resolveEnabledWorkflowSteps(input.enabledWorkflowSteps) : void 0;
37160
37604
  if (input.enabledWorkflowSteps === void 0 && options.applyDefaultWorkflowSteps !== false) {
@@ -37250,9 +37694,9 @@ ${outcome}`;
37250
37694
  createdAt: now,
37251
37695
  updatedAt: options?.updatedAt ?? now
37252
37696
  };
37697
+ this.assertTaskIdAvailable(id);
37253
37698
  const dir = this.taskDir(id);
37254
- await mkdir7(dir, { recursive: true });
37255
- await this.atomicWriteTaskJson(dir, task);
37699
+ await this.atomicCreateTaskJson(dir, task, "createTask");
37256
37700
  if (this.isWatching) this.taskCache.set(id, { ...task });
37257
37701
  const prompt = options?.promptOverride ?? (task.column === "triage" ? buildBootstrapPrompt(id, task.title, task.description) : this.generateSpecifiedPrompt(task));
37258
37702
  await mkdir7(dir, { recursive: true });
@@ -37291,9 +37735,9 @@ ${outcome}`;
37291
37735
  updatedAt: now,
37292
37736
  baseBranch: sourceTask.baseBranch
37293
37737
  };
37738
+ this.assertTaskIdAvailable(newId);
37294
37739
  const newDir = this.taskDir(newId);
37295
- await mkdir7(newDir, { recursive: true });
37296
- await this.atomicWriteTaskJson(newDir, newTask);
37740
+ await this.atomicCreateTaskJson(newDir, newTask, "duplicateTask");
37297
37741
  await mkdir7(newDir, { recursive: true });
37298
37742
  await writeFile6(join16(newDir, "PROMPT.md"), sourceTask.prompt);
37299
37743
  if (this.isWatching) this.taskCache.set(newId, { ...newTask });
@@ -37347,9 +37791,9 @@ Refines: ${id}`,
37347
37791
  updatedAt: now,
37348
37792
  attachments: sourceTask.attachments ? [...sourceTask.attachments] : void 0
37349
37793
  };
37794
+ this.assertTaskIdAvailable(newId);
37350
37795
  const newDir = this.taskDir(newId);
37351
- await mkdir7(newDir, { recursive: true });
37352
- await this.atomicWriteTaskJson(newDir, newTask);
37796
+ await this.atomicCreateTaskJson(newDir, newTask, "refineTask");
37353
37797
  const prompt = `# ${newTask.title}
37354
37798
 
37355
37799
  ${newTask.description}
@@ -37731,6 +38175,8 @@ ${newTask.description}
37731
38175
  task.status = void 0;
37732
38176
  task.error = void 0;
37733
38177
  task.blockedBy = void 0;
38178
+ task.paused = void 0;
38179
+ task.pausedByAgentId = void 0;
37734
38180
  const hasNonPendingStepProgress = task.steps.some((step) => step.status !== "pending");
37735
38181
  const preserveStepProgress = options?.preserveResumeState || options?.preserveProgress === true && hasNonPendingStepProgress;
37736
38182
  if (!options?.preserveWorktree) {
@@ -38402,21 +38848,23 @@ ${newTask.description}
38402
38848
  target: input.target,
38403
38849
  metadata: input.metadata
38404
38850
  };
38405
- this.db.prepare(`
38406
- INSERT INTO runAuditEvents (
38407
- id, timestamp, taskId, agentId, runId, domain, mutationType, target, metadata
38408
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
38409
- `).run(
38410
- event.id,
38411
- event.timestamp,
38412
- event.taskId ?? null,
38413
- event.agentId,
38414
- event.runId,
38415
- event.domain,
38416
- event.mutationType,
38417
- event.target,
38418
- toJsonNullable(event.metadata)
38419
- );
38851
+ this.db.transactionImmediate(() => {
38852
+ this.db.prepare(`
38853
+ INSERT INTO runAuditEvents (
38854
+ id, timestamp, taskId, agentId, runId, domain, mutationType, target, metadata
38855
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
38856
+ `).run(
38857
+ event.id,
38858
+ event.timestamp,
38859
+ event.taskId ?? null,
38860
+ event.agentId,
38861
+ event.runId,
38862
+ event.domain,
38863
+ event.mutationType,
38864
+ event.target,
38865
+ toJsonNullable(event.metadata)
38866
+ );
38867
+ });
38420
38868
  return event;
38421
38869
  }
38422
38870
  /**
@@ -40549,6 +40997,7 @@ ${stepsSection}`;
40549
40997
  if (this._db) {
40550
40998
  this._db.close();
40551
40999
  this._db = null;
41000
+ this.taskIdStateReconciled = false;
40552
41001
  }
40553
41002
  if (this._archiveDb) {
40554
41003
  this._archiveDb.close();
@@ -40582,9 +41031,9 @@ ${stepsSection}`;
40582
41031
  }
40583
41032
  getDatabaseHealth() {
40584
41033
  return {
40585
- corruptionDetected: this.db.corruptionDetected,
40586
- integrityCheckPending: this.db.integrityCheckPending,
40587
- integrityCheckLastRunAt: this.db.integrityCheckLastRunAt
41034
+ healthy: !this.db.corruptionDetected,
41035
+ lastCheckedAt: this.db.integrityCheckLastRunAt ? new Date(this.db.integrityCheckLastRunAt) : null,
41036
+ isRunning: this.db.integrityCheckPending
40588
41037
  };
40589
41038
  }
40590
41039
  getDistributedTaskIdAllocator() {
@@ -41098,7 +41547,7 @@ var init_daemon_token = __esm({
41098
41547
  });
41099
41548
 
41100
41549
  // ../core/src/pi-extensions.ts
41101
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync3, statSync as statSync3, writeFileSync } from "node:fs";
41550
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync, readFileSync as readFileSync3, statSync as statSync4, writeFileSync } from "node:fs";
41102
41551
  import { homedir as homedir3 } from "node:os";
41103
41552
  import { basename as basename5, isAbsolute as isAbsolute5, join as join17, relative as relative2, resolve as resolve8, sep as sep4, win32 } from "node:path";
41104
41553
  function getHomeDir3(home) {
@@ -41188,7 +41637,7 @@ function discoverExtensionsInDir(dir, cwd, home) {
41188
41637
  let isDirectory = entry.isDirectory();
41189
41638
  if (entry.isSymbolicLink()) {
41190
41639
  try {
41191
- isDirectory = statSync3(entryPath).isDirectory();
41640
+ isDirectory = statSync4(entryPath).isDirectory();
41192
41641
  } catch {
41193
41642
  isDirectory = false;
41194
41643
  }
@@ -49064,11 +49513,11 @@ var require_extract_zip = __commonJS({
49064
49513
  var { createWriteStream, promises: fs2 } = __require("fs");
49065
49514
  var getStream = require_get_stream();
49066
49515
  var path2 = __require("path");
49067
- var { promisify: promisify14 } = __require("util");
49516
+ var { promisify: promisify15 } = __require("util");
49068
49517
  var stream = __require("stream");
49069
49518
  var yauzl = require_yauzl();
49070
- var openZip = promisify14(yauzl.open);
49071
- var pipeline = promisify14(stream.pipeline);
49519
+ var openZip = promisify15(yauzl.open);
49520
+ var pipeline = promisify15(stream.pipeline);
49072
49521
  var Extractor = class {
49073
49522
  constructor(zipPath, opts) {
49074
49523
  this.zipPath = zipPath;
@@ -49150,7 +49599,7 @@ var require_extract_zip = __commonJS({
49150
49599
  await fs2.mkdir(destDir, mkdirOptions);
49151
49600
  if (isDir) return;
49152
49601
  debug("opening read stream", dest);
49153
- const readStream = await promisify14(this.zipfile.openReadStream.bind(this.zipfile))(entry);
49602
+ const readStream = await promisify15(this.zipfile.openReadStream.bind(this.zipfile))(entry);
49154
49603
  if (symlink) {
49155
49604
  const link = await getStream(readStream);
49156
49605
  debug("creating symlink", link, dest);
@@ -56496,7 +56945,7 @@ var require_dist3 = __commonJS({
56496
56945
  });
56497
56946
 
56498
56947
  // ../core/src/agent-companies-parser.ts
56499
- import { existsSync as existsSync19, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync, statSync as statSync4 } from "node:fs";
56948
+ import { existsSync as existsSync19, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync, statSync as statSync5 } from "node:fs";
56500
56949
  import { tmpdir as tmpdir3 } from "node:os";
56501
56950
  import { isAbsolute as isAbsolute7, join as join22, normalize as normalize3, resolve as resolve11 } from "node:path";
56502
56951
  function slugifyAgentReference(value) {
@@ -56772,7 +57221,7 @@ function parseCompanyDirectory(dirPath) {
56772
57221
  if (!existsSync19(resolvedPath)) {
56773
57222
  throw new AgentCompaniesParseError(`Company directory does not exist: ${resolvedPath}`);
56774
57223
  }
56775
- if (!statSync4(resolvedPath).isDirectory()) {
57224
+ if (!statSync5(resolvedPath).isDirectory()) {
56776
57225
  throw new AgentCompaniesParseError(`Company path is not a directory: ${resolvedPath}`);
56777
57226
  }
56778
57227
  const companyPath = join22(resolvedPath, "COMPANY.md");
@@ -56811,12 +57260,12 @@ function resolveExtractionRoot(tempDir) {
56811
57260
  return tempDir;
56812
57261
  }
56813
57262
  async function extractTarArchive(archivePath, outputDir) {
56814
- const [{ execFile: execFile8 }, { promisify: promisify14 }] = await Promise.all([
57263
+ const [{ execFile: execFile9 }, { promisify: promisify15 }] = await Promise.all([
56815
57264
  import("node:child_process"),
56816
57265
  import("node:util")
56817
57266
  ]);
56818
- const execFileAsync6 = promisify14(execFile8);
56819
- await execFileAsync6("tar", ["xzf", archivePath, "-C", outputDir]);
57267
+ const execFileAsync7 = promisify15(execFile9);
57268
+ await execFileAsync7("tar", ["xzf", archivePath, "-C", outputDir]);
56820
57269
  }
56821
57270
  function sanitizeCompanySubPath(subPath) {
56822
57271
  const trimmed = subPath.trim();
@@ -59369,15 +59818,15 @@ function evaluateAgentActionGate(params) {
59369
59818
  let resourceId;
59370
59819
  if (params.toolName === "bash") {
59371
59820
  const command = extractShellCommand(args);
59372
- const git = classifyGitCommand(command);
59373
- if (git?.write) {
59821
+ const git2 = classifyGitCommand(command);
59822
+ if (git2?.write) {
59374
59823
  category = "git_write";
59375
- operation = git.operation;
59824
+ operation = git2.operation;
59376
59825
  resourceType = "git";
59377
59826
  } else {
59378
59827
  category = "command_execution";
59379
- operation = git?.operation ?? "shell command";
59380
- resourceType = git ? "git" : "command";
59828
+ operation = git2?.operation ?? "shell command";
59829
+ resourceType = git2 ? "git" : "command";
59381
59830
  }
59382
59831
  } else if (params.toolName === "write" || params.toolName === "edit") {
59383
59832
  category = "file_write_delete";
@@ -61236,6 +61685,71 @@ _... existing specification middle trimmed ..._
61236
61685
 
61237
61686
  ${tail}`;
61238
61687
  }
61688
+ function extractMarkdownSection(document2, headingName) {
61689
+ const heading = `## ${headingName}`;
61690
+ const start = document2.indexOf(heading);
61691
+ if (start === -1) {
61692
+ return "";
61693
+ }
61694
+ const afterHeading = start + heading.length;
61695
+ const nextH2 = document2.indexOf("\n## ", afterHeading);
61696
+ const nextH1 = document2.indexOf("\n# ", afterHeading);
61697
+ const endCandidates = [nextH2, nextH1].filter((value) => value !== -1);
61698
+ const end = endCandidates.length > 0 ? Math.min(...endCandidates) : document2.length;
61699
+ return document2.slice(start, end).trim();
61700
+ }
61701
+ function compactTaskPromptStepsSection(section) {
61702
+ const stepTitles = Array.from(section.matchAll(/^### Step \d+:.*$/gm), (match) => match[0].trim());
61703
+ if (stepTitles.length === 0) {
61704
+ return section.trim();
61705
+ }
61706
+ return [
61707
+ "## Steps",
61708
+ ...stepTitles,
61709
+ "",
61710
+ "_... step checklist details trimmed for context limits ..._"
61711
+ ].join("\n").trim();
61712
+ }
61713
+ function truncateCompactedSection(section, maxChars, label) {
61714
+ const trimmed = section.trim();
61715
+ if (!trimmed || trimmed.length <= maxChars) {
61716
+ return trimmed;
61717
+ }
61718
+ const marker = `_... ${label} trimmed for context limits ..._`;
61719
+ const headBudget = Math.max(200, maxChars - marker.length - 2);
61720
+ return [
61721
+ `${trimmed.slice(0, headBudget).trimEnd()}\u2026`,
61722
+ "",
61723
+ marker
61724
+ ].join("\n").trim();
61725
+ }
61726
+ function compactTaskPromptSectionBody(body) {
61727
+ const trimmed = body.trim();
61728
+ if (trimmed.length <= MAX_COMPACTED_TASK_PROMPT_CHARS) {
61729
+ return trimmed;
61730
+ }
61731
+ const fencedMatch = /^```markdown\s*\n([\s\S]*?)\n```$/m.exec(trimmed);
61732
+ const promptContent = fencedMatch ? fencedMatch[1].trim() : trimmed;
61733
+ const firstSectionIndex = promptContent.indexOf("\n## ");
61734
+ const preamble = (firstSectionIndex === -1 ? promptContent : promptContent.slice(0, firstSectionIndex)).trim();
61735
+ const missionSection = extractMarkdownSection(promptContent, "Mission");
61736
+ const dependenciesSection = extractMarkdownSection(promptContent, "Dependencies");
61737
+ const fileScopeSection = extractMarkdownSection(promptContent, "File Scope");
61738
+ const stepsSection = compactTaskPromptStepsSection(extractMarkdownSection(promptContent, "Steps"));
61739
+ const compactedContent = [
61740
+ truncateCompactedSection(preamble, 400, "task header"),
61741
+ truncateCompactedSection(missionSection, 900, "mission"),
61742
+ truncateCompactedSection(dependenciesSection, 500, "dependencies"),
61743
+ truncateCompactedSection(fileScopeSection, 1e3, "file scope"),
61744
+ truncateCompactedSection(stepsSection, 1200, "steps outline"),
61745
+ "_... remaining PROMPT.md sections trimmed for context limits ..._"
61746
+ ].filter(Boolean).join("\n\n").trim();
61747
+ const narrowedContent = compactedContent.length <= MAX_COMPACTED_TASK_PROMPT_CHARS ? compactedContent : compactExistingSpecificationSectionBody(compactedContent);
61748
+ const finalContent = fencedMatch ? `\`\`\`markdown
61749
+ ${narrowedContent}
61750
+ \`\`\`` : narrowedContent;
61751
+ return finalContent.length < trimmed.length ? finalContent : compactExistingSpecificationSectionBody(trimmed);
61752
+ }
61239
61753
  function compactUserCommentsSectionBody(body) {
61240
61754
  const trimmed = body.trim();
61241
61755
  if (trimmed.length <= MAX_COMPACTED_USER_COMMENTS_CHARS) {
@@ -61268,7 +61782,7 @@ function compactUserCommentsSectionBody(body) {
61268
61782
  ].join("\n").trim();
61269
61783
  }
61270
61784
  function compactLargePromptSections(prompt) {
61271
- const sectionPattern = /(^|\n)(## (?:Subtask Consideration|Subtask Breakdown Requested|Attachments|Existing Specification|User Comments)\n)([\s\S]*?)(?=\n## [^#]|\n# [^#]|$)/g;
61785
+ const sectionPattern = /(^|\n)(## (?:Subtask Consideration|Subtask Breakdown Requested|Attachments|Existing Specification|Task PROMPT\.md|User Comments)\n)((?:\n*```markdown[\s\S]*?\n```|[\s\S]*?))(?=\n## [^#]|\n# [^#]|$)/g;
61272
61786
  let changed = false;
61273
61787
  const compactedPrompt = prompt.replace(sectionPattern, (match, prefix, heading, body) => {
61274
61788
  const headingName = heading.trim().replace(/^##\s+/, "");
@@ -61278,6 +61792,7 @@ function compactLargePromptSections(prompt) {
61278
61792
  "Subtask Breakdown Requested": MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS,
61279
61793
  Attachments: MAX_COMPACTED_ATTACHMENTS_CHARS,
61280
61794
  "Existing Specification": MAX_COMPACTED_EXISTING_SPEC_CHARS,
61795
+ "Task PROMPT.md": MAX_COMPACTED_TASK_PROMPT_CHARS,
61281
61796
  "User Comments": MAX_COMPACTED_USER_COMMENTS_CHARS
61282
61797
  };
61283
61798
  const maxChars = maxByHeading[headingName] ?? MAX_COMPACTED_PROMPT_MEMORY_CHARS;
@@ -61291,6 +61806,8 @@ function compactLargePromptSections(prompt) {
61291
61806
  compactedBody = compactAttachmentSectionBody(trimmedBody);
61292
61807
  } else if (headingName === "Existing Specification") {
61293
61808
  compactedBody = compactExistingSpecificationSectionBody(trimmedBody);
61809
+ } else if (headingName === "Task PROMPT.md") {
61810
+ compactedBody = compactTaskPromptSectionBody(trimmedBody);
61294
61811
  } else if (headingName === "User Comments") {
61295
61812
  compactedBody = compactUserCommentsSectionBody(trimmedBody);
61296
61813
  }
@@ -62315,7 +62832,7 @@ async function createFnAgent2(options) {
62315
62832
  });
62316
62833
  return { session: promptableSession, sessionFile: promptableSession.sessionFile };
62317
62834
  }
62318
- var execAsync, hostExtensionPaths, FN_MEMORY_APPEND_TOOL_NAME, FUSION_SHUTDOWN_WRAP_FLAG, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS, MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS, MAX_COMPACTED_ATTACHMENTS_CHARS, MAX_COMPACTED_EXISTING_SPEC_CHARS, MAX_COMPACTED_USER_COMMENTS_CHARS, GATE_BYPASS_TOOL_NAMES;
62835
+ var execAsync, hostExtensionPaths, FN_MEMORY_APPEND_TOOL_NAME, FUSION_SHUTDOWN_WRAP_FLAG, COMPACTION_FALLBACK_INSTRUCTIONS, MAX_COMPACTED_PROMPT_MEMORY_CHARS, MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS, MAX_COMPACTED_ATTACHMENTS_CHARS, MAX_COMPACTED_EXISTING_SPEC_CHARS, MAX_COMPACTED_TASK_PROMPT_CHARS, MAX_COMPACTED_USER_COMMENTS_CHARS, GATE_BYPASS_TOOL_NAMES;
62319
62836
  var init_pi = __esm({
62320
62837
  "../engine/src/pi.ts"() {
62321
62838
  "use strict";
@@ -62341,6 +62858,7 @@ var init_pi = __esm({
62341
62858
  MAX_COMPACTED_SUBTASK_GUIDANCE_CHARS = 1200;
62342
62859
  MAX_COMPACTED_ATTACHMENTS_CHARS = 4e3;
62343
62860
  MAX_COMPACTED_EXISTING_SPEC_CHARS = 4e3;
62861
+ MAX_COMPACTED_TASK_PROMPT_CHARS = MAX_COMPACTED_EXISTING_SPEC_CHARS;
62344
62862
  MAX_COMPACTED_USER_COMMENTS_CHARS = 2e3;
62345
62863
  GATE_BYPASS_TOOL_NAMES = /* @__PURE__ */ new Set([
62346
62864
  "fn_heartbeat_done",
@@ -63609,7 +64127,7 @@ var init_research_step_runner = __esm({
63609
64127
  // ../engine/src/agent-tools.ts
63610
64128
  import { appendFile as appendFile3, mkdir as mkdir12, readFile as readFile14, readdir as readdir8, stat as stat5, writeFile as writeFile11 } from "node:fs/promises";
63611
64129
  import { existsSync as existsSync24 } from "node:fs";
63612
- import { createHash as createHash5 } from "node:crypto";
64130
+ import { createHash as createHash6 } from "node:crypto";
63613
64131
  import { join as join30, relative as relative6, resolve as resolve16 } from "node:path";
63614
64132
  import { Type } from "@mariozechner/pi-ai";
63615
64133
  function sanitizeAgentMemoryId(agentId) {
@@ -63652,7 +64170,7 @@ async function readAgentMemoryWorkspaceLongTerm(rootDir, agentId) {
63652
64170
  }
63653
64171
  }
63654
64172
  function qmdAgentMemoryCollectionName2(rootDir, agentId) {
63655
- const hash = createHash5("sha1").update(`${rootDir}:${agentId}`).digest("hex").slice(0, 12);
64173
+ const hash = createHash6("sha1").update(`${rootDir}:${agentId}`).digest("hex").slice(0, 12);
63656
64174
  return `fusion-agent-memory-${sanitizeAgentMemoryId(agentId).toLowerCase()}-${hash}`;
63657
64175
  }
63658
64176
  function buildQmdAgentMemoryCollectionAddArgs(rootDir, agentId) {
@@ -63781,11 +64299,11 @@ async function refreshAgentMemoryQmdIndex(rootDir, agentMemory) {
63781
64299
  return;
63782
64300
  }
63783
64301
  const promise = (async () => {
63784
- const { execFile: execFile8 } = await import("node:child_process");
63785
- const { promisify: promisify14 } = await import("node:util");
63786
- const execFileAsync6 = promisify14(execFile8);
64302
+ const { execFile: execFile9 } = await import("node:child_process");
64303
+ const { promisify: promisify15 } = await import("node:util");
64304
+ const execFileAsync7 = promisify15(execFile9);
63787
64305
  try {
63788
- await execFileAsync6("qmd", buildQmdAgentMemoryCollectionAddArgs(rootDir, agentMemory.agentId), {
64306
+ await execFileAsync7("qmd", buildQmdAgentMemoryCollectionAddArgs(rootDir, agentMemory.agentId), {
63789
64307
  cwd: rootDir,
63790
64308
  timeout: 4e3,
63791
64309
  maxBuffer: 512 * 1024
@@ -63798,8 +64316,8 @@ ${stderr}`)) {
63798
64316
  throw error;
63799
64317
  }
63800
64318
  }
63801
- await execFileAsync6("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
63802
- await execFileAsync6("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
64319
+ await execFileAsync7("qmd", ["update"], { cwd: rootDir, timeout: 3e4, maxBuffer: 1024 * 1024 });
64320
+ await execFileAsync7("qmd", ["embed"], { cwd: rootDir, timeout: 12e4, maxBuffer: 1024 * 1024 });
63803
64321
  })();
63804
64322
  agentQmdRefreshState.set(key, { lastStartedAt: now, inFlight: promise });
63805
64323
  try {
@@ -63854,10 +64372,10 @@ async function searchAgentMemoryWithQmd(rootDir, agentMemory, query, limit) {
63854
64372
  }
63855
64373
  try {
63856
64374
  await refreshAgentMemoryQmdIndex(rootDir, agentMemory);
63857
- const { execFile: execFile8 } = await import("node:child_process");
63858
- const { promisify: promisify14 } = await import("node:util");
63859
- const execFileAsync6 = promisify14(execFile8);
63860
- const { stdout } = await execFileAsync6("qmd", buildQmdAgentMemorySearchArgs(rootDir, agentMemory.agentId, query, limit), {
64375
+ const { execFile: execFile9 } = await import("node:child_process");
64376
+ const { promisify: promisify15 } = await import("node:util");
64377
+ const execFileAsync7 = promisify15(execFile9);
64378
+ const { stdout } = await execFileAsync7("qmd", buildQmdAgentMemorySearchArgs(rootDir, agentMemory.agentId, query, limit), {
63861
64379
  cwd: rootDir,
63862
64380
  timeout: 4e3,
63863
64381
  maxBuffer: 1024 * 1024
@@ -63936,24 +64454,36 @@ function createTaskCreateTool(store, provenance, options) {
63936
64454
  description: "Create a new task for out-of-scope work discovered during execution. The task goes into triage where it will be specified by the AI. Optionally set dependencies (e.g., the new task depends on the current one, or the current task should wait for the new one).",
63937
64455
  parameters: taskCreateParams,
63938
64456
  execute: async (_id, params) => {
63939
- const task = await createAgentTask(store, {
63940
- description: params.description,
63941
- dependencies: params.dependencies,
63942
- column: "triage",
63943
- source: provenance ? {
63944
- sourceType: provenance.sourceType,
63945
- sourceAgentId: provenance.sourceAgentId,
63946
- sourceRunId: provenance.sourceRunId
63947
- } : void 0
63948
- }, options);
63949
- const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
63950
- return {
63951
- content: [{
63952
- type: "text",
63953
- text: `Created ${task.id}: ${params.description}${deps}`
63954
- }],
63955
- details: { taskId: task.id }
63956
- };
64457
+ try {
64458
+ const task = await createAgentTask(store, {
64459
+ description: params.description,
64460
+ dependencies: params.dependencies,
64461
+ column: "triage",
64462
+ priority: params.priority,
64463
+ source: provenance ? {
64464
+ sourceType: provenance.sourceType,
64465
+ sourceAgentId: provenance.sourceAgentId,
64466
+ sourceRunId: provenance.sourceRunId
64467
+ } : void 0
64468
+ }, options);
64469
+ const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
64470
+ return {
64471
+ content: [{
64472
+ type: "text",
64473
+ text: `Created ${task.id}: ${params.description}${deps}`
64474
+ }],
64475
+ details: { taskId: task.id }
64476
+ };
64477
+ } catch (err) {
64478
+ if (err instanceof Error && err.message.startsWith("Task ID already exists:")) {
64479
+ return {
64480
+ content: [{ type: "text", text: `ERROR: ${err.message}` }],
64481
+ details: {},
64482
+ isError: true
64483
+ };
64484
+ }
64485
+ throw err;
64486
+ }
63957
64487
  }
63958
64488
  };
63959
64489
  }
@@ -64864,24 +65394,35 @@ function createDelegateTaskTool(agentStore, taskStore, options) {
64864
65394
  details: {}
64865
65395
  };
64866
65396
  }
64867
- const task = await createAgentTask(taskStore, {
64868
- description: params.description,
64869
- dependencies: params.dependencies,
64870
- column: "todo",
64871
- assignedAgentId: params.agent_id,
64872
- source: {
64873
- sourceType: "api",
64874
- ...override ? { sourceMetadata: { executorRoleOverride: true } } : {}
65397
+ try {
65398
+ const task = await createAgentTask(taskStore, {
65399
+ description: params.description,
65400
+ dependencies: params.dependencies,
65401
+ column: "todo",
65402
+ assignedAgentId: params.agent_id,
65403
+ source: {
65404
+ sourceType: "api",
65405
+ ...override ? { sourceMetadata: { executorRoleOverride: true } } : {}
65406
+ }
65407
+ }, options);
65408
+ const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
65409
+ return {
65410
+ content: [{
65411
+ type: "text",
65412
+ text: `Delegated to ${agent.name} (${agent.id}): Created ${task.id}${deps}. The task will be picked up by ${agent.name} on their next heartbeat cycle.`
65413
+ }],
65414
+ details: { taskId: task.id, agentId: agent.id, agentName: agent.name }
65415
+ };
65416
+ } catch (err) {
65417
+ if (err instanceof Error && err.message.startsWith("Task ID already exists:")) {
65418
+ return {
65419
+ content: [{ type: "text", text: `ERROR: ${err.message}` }],
65420
+ details: {},
65421
+ isError: true
65422
+ };
64875
65423
  }
64876
- }, options);
64877
- const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
64878
- return {
64879
- content: [{
64880
- type: "text",
64881
- text: `Delegated to ${agent.name} (${agent.id}): Created ${task.id}${deps}. The task will be picked up by ${agent.name} on their next heartbeat cycle.`
64882
- }],
64883
- details: { taskId: task.id, agentId: agent.id, agentName: agent.name }
64884
- };
65424
+ throw err;
65425
+ }
64885
65426
  }
64886
65427
  };
64887
65428
  }
@@ -65221,7 +65762,7 @@ ${lines.join("\n")}`
65221
65762
  }
65222
65763
  };
65223
65764
  }
65224
- var taskCreateParams, taskLogParams, taskDocumentWriteParams, taskDocumentReadParams, reflectOnPerformanceParams, readEvaluationsParams, updateIdentityParams, listAgentsParams, delegateTaskParams, getAgentConfigParams, updateAgentConfigParams, createAgentParams, deleteAgentParams, sendMessageParams, readMessagesParams, memorySearchParams, memoryGetParams, webFetchParams, researchRunParams, researchListParams, researchGetParams, researchCancelParams, memoryAppendParams, log10, MAX_INSTRUCTIONS_TEXT_LENGTH, MAX_MEMORY_LENGTH, MAX_SOUL_LENGTH, AGENT_MEMORY_ROOT3, AGENT_MEMORY_FILENAME2, AGENT_DREAMS_FILENAME2, agentQmdRefreshState, AGENT_QMD_REFRESH_INTERVAL_MS, DAILY_AGENT_MEMORY_RE2;
65765
+ var TASK_CREATE_PRIORITY_VALUES, taskCreateParams, taskLogParams, taskDocumentWriteParams, taskDocumentReadParams, reflectOnPerformanceParams, readEvaluationsParams, updateIdentityParams, listAgentsParams, delegateTaskParams, getAgentConfigParams, updateAgentConfigParams, createAgentParams, deleteAgentParams, sendMessageParams, readMessagesParams, memorySearchParams, memoryGetParams, webFetchParams, researchRunParams, researchListParams, researchGetParams, researchCancelParams, memoryAppendParams, log10, MAX_INSTRUCTIONS_TEXT_LENGTH, MAX_MEMORY_LENGTH, MAX_SOUL_LENGTH, AGENT_MEMORY_ROOT3, AGENT_MEMORY_FILENAME2, AGENT_DREAMS_FILENAME2, agentQmdRefreshState, AGENT_QMD_REFRESH_INTERVAL_MS, DAILY_AGENT_MEMORY_RE2;
65225
65766
  var init_agent_tools = __esm({
65226
65767
  "../engine/src/agent-tools.ts"() {
65227
65768
  "use strict";
@@ -65232,10 +65773,16 @@ var init_agent_tools = __esm({
65232
65773
  init_logger2();
65233
65774
  init_web_fetch();
65234
65775
  init_agent_action_gate();
65776
+ TASK_CREATE_PRIORITY_VALUES = ["low", "normal", "high", "urgent"];
65235
65777
  taskCreateParams = Type.Object({
65236
65778
  description: Type.String({ description: "What needs to be done" }),
65237
65779
  dependencies: Type.Optional(
65238
65780
  Type.Array(Type.String(), { description: 'Task IDs this new task depends on (e.g. ["KB-001"])' })
65781
+ ),
65782
+ priority: Type.Optional(
65783
+ Type.Union(TASK_CREATE_PRIORITY_VALUES.map((priority) => Type.Literal(priority)), {
65784
+ description: "Task priority (low, normal, high, urgent)"
65785
+ })
65239
65786
  )
65240
65787
  });
65241
65788
  taskLogParams = Type.Object({
@@ -66473,6 +67020,7 @@ var init_ntfy_provider = __esm({
66473
67020
  );
66474
67021
  const response = await sendNtfyNotificationWithResult({
66475
67022
  ntfyBaseUrl: this.config.ntfyBaseUrl,
67023
+ ntfyAccessToken: this.config.ntfyAccessToken,
66476
67024
  topic: this.config.topic,
66477
67025
  title: content.title,
66478
67026
  message: content.message,
@@ -66777,7 +67325,7 @@ var init_notification_service = __esm({
66777
67325
  handleSettingsUpdated = async (data) => {
66778
67326
  const { settings, previous } = data;
66779
67327
  this.setNotificationsEnabledFromSettings(settings);
66780
- if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyBaseUrl !== previous.ntfyBaseUrl || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
67328
+ if (settings.ntfyEnabled !== previous.ntfyEnabled || settings.ntfyTopic !== previous.ntfyTopic || settings.ntfyBaseUrl !== previous.ntfyBaseUrl || settings.ntfyAccessToken !== previous.ntfyAccessToken || settings.ntfyDashboardHost !== previous.ntfyDashboardHost || JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
66781
67329
  const wasEnabled = Boolean(previous.ntfyEnabled && previous.ntfyTopic);
66782
67330
  const isEnabled = Boolean(settings.ntfyEnabled && settings.ntfyTopic);
66783
67331
  await this.syncNtfyProvider(settings);
@@ -66789,6 +67337,8 @@ var init_notification_service = __esm({
66789
67337
  schedulerLog.log("NotificationService ntfy topic updated");
66790
67338
  } else if (settings.ntfyBaseUrl !== previous.ntfyBaseUrl) {
66791
67339
  schedulerLog.log("NotificationService ntfy base URL updated");
67340
+ } else if (settings.ntfyAccessToken !== previous.ntfyAccessToken) {
67341
+ schedulerLog.log("NotificationService ntfy access token updated");
66792
67342
  } else if (settings.ntfyDashboardHost !== previous.ntfyDashboardHost) {
66793
67343
  schedulerLog.log("NotificationService ntfy dashboard host updated");
66794
67344
  } else if (JSON.stringify(settings.ntfyEvents) !== JSON.stringify(previous.ntfyEvents)) {
@@ -66817,6 +67367,7 @@ var init_notification_service = __esm({
66817
67367
  await this.ntfyProvider.initialize?.({
66818
67368
  topic: settings.ntfyTopic,
66819
67369
  ntfyBaseUrl: settings.ntfyBaseUrl ?? this.options.ntfyBaseUrl,
67370
+ ntfyAccessToken: settings.ntfyAccessToken,
66820
67371
  dashboardHost: settings.ntfyDashboardHost,
66821
67372
  events: settings.ntfyEvents ?? [...DEFAULT_NTFY_EVENTS],
66822
67373
  projectId: this.options.projectId
@@ -67023,6 +67574,7 @@ function buildNtfyClickUrl(options) {
67023
67574
  }
67024
67575
  async function sendNtfyNotificationWithResult({
67025
67576
  ntfyBaseUrl,
67577
+ ntfyAccessToken,
67026
67578
  topic,
67027
67579
  title,
67028
67580
  message,
@@ -67039,6 +67591,10 @@ async function sendNtfyNotificationWithResult({
67039
67591
  if (clickUrl) {
67040
67592
  headers.Click = clickUrl;
67041
67593
  }
67594
+ const trimmedToken = ntfyAccessToken?.trim();
67595
+ if (trimmedToken) {
67596
+ headers.Authorization = `Bearer ${trimmedToken}`;
67597
+ }
67042
67598
  const resolvedBaseUrl = resolveNtfyBaseUrl(ntfyBaseUrl);
67043
67599
  const response = await fetch(`${resolvedBaseUrl}/${topic}`, {
67044
67600
  method: "POST",
@@ -67354,9 +67910,33 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
67354
67910
  this.reason = reason;
67355
67911
  }
67356
67912
  }
67357
- let session;
67358
- try {
67359
- ({ session } = await createResolvedAgentSession({
67913
+ const activeSessions = /* @__PURE__ */ new Set();
67914
+ let reviewText = "";
67915
+ const endSession = (session2) => {
67916
+ if (!activeSessions.delete(session2)) {
67917
+ return;
67918
+ }
67919
+ session2.dispose();
67920
+ options.onSessionEnded?.(session2);
67921
+ };
67922
+ const buildPauseUnavailableResult = async (reason) => {
67923
+ reviewerLog.log(
67924
+ `${taskId}: ${reviewType} review for Step ${stepNumber} aborted before spawn \u2014 ${reason} active`
67925
+ );
67926
+ if (options.store && options.taskId) {
67927
+ await options.store.logEntry(
67928
+ options.taskId,
67929
+ `${reviewType} review aborted before spawn \u2014 ${reason} active`
67930
+ ).catch(() => void 0);
67931
+ }
67932
+ return {
67933
+ verdict: "UNAVAILABLE",
67934
+ review: `${reason} active \u2014 reviewer not spawned. Stop calling fn_review_* and exit cleanly; the parent task will resume after unpause.`,
67935
+ summary: `Skipped: ${reason}`
67936
+ };
67937
+ };
67938
+ const createReviewerSession = async () => {
67939
+ const { session: session2 } = await createResolvedAgentSession({
67360
67940
  sessionPurpose: "reviewer",
67361
67941
  runtimeHint: extractRuntimeHint(memoryAgent?.runtimeConfig),
67362
67942
  pluginRunner: options.pluginRunner,
@@ -67374,7 +67954,6 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
67374
67954
  fallbackProvider: validatorFallbackProvider,
67375
67955
  fallbackModelId: validatorFallbackModelId,
67376
67956
  defaultThinkingLevel: options.defaultThinkingLevel,
67377
- // Skill selection: use assigned agent skills if available, otherwise role fallback
67378
67957
  ...skillContext?.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {},
67379
67958
  taskId: options.taskId,
67380
67959
  taskTitle: options.taskTitle,
@@ -67398,52 +67977,138 @@ async function reviewStep(cwd, taskId, stepNumber, stepName, reviewType, promptC
67398
67977
  throw new ReviewerPauseAbortError(reason);
67399
67978
  }
67400
67979
  }
67401
- }));
67980
+ });
67981
+ const reviewerModelDesc = describeModel(session2);
67982
+ const reviewerModelMarker = `Reviewer using model: ${reviewerModelDesc}`;
67983
+ reviewerLog.log(`${taskId}: reviewer using model ${reviewerModelDesc}`);
67984
+ if (options.store && options.taskId) {
67985
+ await options.store.logEntry(options.taskId, reviewerModelMarker);
67986
+ await options.store.appendAgentLog(options.taskId, reviewerModelMarker, "text", void 0, "reviewer").catch(() => void 0);
67987
+ }
67988
+ activeSessions.add(session2);
67989
+ options.onSessionCreated?.(session2);
67990
+ session2.subscribe((event) => {
67991
+ if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
67992
+ reviewText += event.assistantMessageEvent.delta;
67993
+ }
67994
+ });
67995
+ return session2;
67996
+ };
67997
+ const runReviewPrompt = async (session2, prompt) => {
67998
+ await promptWithFallback(session2, prompt);
67999
+ checkSessionError(session2);
68000
+ };
68001
+ let session;
68002
+ try {
68003
+ session = await createReviewerSession();
67402
68004
  } catch (err) {
67403
68005
  if (err instanceof ReviewerPauseAbortError) {
67404
- reviewerLog.log(
67405
- `${taskId}: ${reviewType} review for Step ${stepNumber} aborted before spawn \u2014 ${err.reason} active`
67406
- );
67407
- if (options.store && options.taskId) {
67408
- await options.store.logEntry(
67409
- options.taskId,
67410
- `${reviewType} review aborted before spawn \u2014 ${err.reason} active`
67411
- ).catch(() => void 0);
67412
- }
67413
- return {
67414
- verdict: "UNAVAILABLE",
67415
- review: `${err.reason} active \u2014 reviewer not spawned. Stop calling fn_review_* and exit cleanly; the parent task will resume after unpause.`,
67416
- summary: `Skipped: ${err.reason}`
67417
- };
68006
+ return buildPauseUnavailableResult(err.reason);
67418
68007
  }
67419
68008
  throw err;
67420
68009
  }
67421
- const reviewerModelDesc = describeModel(session);
67422
- const reviewerModelMarker = `Reviewer using model: ${reviewerModelDesc}`;
67423
- reviewerLog.log(`${taskId}: reviewer using model ${reviewerModelDesc}`);
67424
- if (options.store && options.taskId) {
67425
- await options.store.logEntry(options.taskId, reviewerModelMarker);
67426
- await options.store.appendAgentLog(options.taskId, reviewerModelMarker, "text", void 0, "reviewer").catch(() => void 0);
67427
- }
67428
- options.onSessionCreated?.(session);
67429
- let reviewText = "";
67430
- session.subscribe((event) => {
67431
- if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
67432
- reviewText += event.assistantMessageEvent.delta;
67433
- }
67434
- });
67435
68010
  try {
67436
- await promptWithFallback(session, request2);
67437
- checkSessionError(session);
68011
+ try {
68012
+ await runReviewPrompt(session, request2);
68013
+ } catch (err) {
68014
+ const errorMessage = err instanceof Error ? err.message : String(err);
68015
+ if (!isContextLimitError(errorMessage)) {
68016
+ throw err;
68017
+ }
68018
+ const retryLogMessage = reviewType === "code" ? "code review hit context limit \u2014 retrying with compacted request" : `${reviewType} review hit context limit \u2014 retrying with compacted request`;
68019
+ reviewerLog.warn(`${taskId}: ${retryLogMessage}`);
68020
+ if (options.store && options.taskId) {
68021
+ await options.store.logEntry(options.taskId, retryLogMessage).catch(() => void 0);
68022
+ }
68023
+ reviewText = "";
68024
+ const reducedRequest = buildReducedReviewRequest(
68025
+ taskId,
68026
+ stepNumber,
68027
+ stepName,
68028
+ reviewType,
68029
+ promptContent,
68030
+ cwd,
68031
+ baseline
68032
+ );
68033
+ try {
68034
+ await runReviewPrompt(session, reducedRequest);
68035
+ } catch (retryErr) {
68036
+ if (!isReviewerSessionReuseError(retryErr)) {
68037
+ throw retryErr;
68038
+ }
68039
+ endSession(session);
68040
+ try {
68041
+ session = await createReviewerSession();
68042
+ } catch (recreateErr) {
68043
+ if (recreateErr instanceof ReviewerPauseAbortError) {
68044
+ return buildPauseUnavailableResult(recreateErr.reason);
68045
+ }
68046
+ throw recreateErr;
68047
+ }
68048
+ await runReviewPrompt(session, reducedRequest);
68049
+ }
68050
+ }
67438
68051
  } finally {
67439
- if (agentLogger) await agentLogger.flush();
67440
- session.dispose();
67441
- options.onSessionEnded?.(session);
68052
+ if (agentLogger) {
68053
+ await agentLogger.flush();
68054
+ }
68055
+ for (const activeSession of [...activeSessions]) {
68056
+ endSession(activeSession);
68057
+ }
67442
68058
  }
67443
68059
  const verdict = extractVerdict(reviewText);
67444
68060
  const summary = extractSummary(reviewText);
67445
68061
  return { verdict, review: reviewText, summary };
67446
68062
  }
68063
+ function isReviewerSessionReuseError(error) {
68064
+ const message = error instanceof Error ? error.message : String(error);
68065
+ return /prompt is in progress|session (?:is )?(?:closed|disposed|ended)|conversation already active/i.test(message);
68066
+ }
68067
+ function extractPromptSection(promptContent, sectionName) {
68068
+ const heading = `## ${sectionName}`;
68069
+ const start = promptContent.indexOf(heading);
68070
+ if (start === -1) {
68071
+ return "";
68072
+ }
68073
+ const afterHeading = start + heading.length;
68074
+ const nextH2 = promptContent.indexOf("\n## ", afterHeading);
68075
+ const nextH1 = promptContent.indexOf("\n# ", afterHeading);
68076
+ const endCandidates = [nextH2, nextH1].filter((value) => value !== -1);
68077
+ const end = endCandidates.length > 0 ? Math.min(...endCandidates) : promptContent.length;
68078
+ return promptContent.slice(start, end).trim();
68079
+ }
68080
+ function summarizePromptSteps(promptContent) {
68081
+ const stepTitles = Array.from(promptContent.matchAll(/^### Step \d+:.*$/gm), (match) => match[0].trim());
68082
+ if (stepTitles.length === 0) {
68083
+ return "";
68084
+ }
68085
+ return ["## Steps", ...stepTitles].join("\n");
68086
+ }
68087
+ function buildReducedTaskPromptSummary(promptContent) {
68088
+ const firstSectionIndex = promptContent.indexOf("\n## ");
68089
+ const header = (firstSectionIndex === -1 ? promptContent : promptContent.slice(0, firstSectionIndex)).trim();
68090
+ const sections = [
68091
+ header,
68092
+ extractPromptSection(promptContent, "Mission"),
68093
+ extractPromptSection(promptContent, "Dependencies"),
68094
+ extractPromptSection(promptContent, "File Scope"),
68095
+ summarizePromptSteps(promptContent),
68096
+ "_... additional PROMPT.md sections omitted after context-limit retry ..._"
68097
+ ].filter(Boolean);
68098
+ return sections.join("\n\n").trim();
68099
+ }
68100
+ function buildReducedReviewRequest(taskId, stepNumber, stepName, reviewType, promptContent, cwd, baseline) {
68101
+ return buildReviewRequest(
68102
+ taskId,
68103
+ stepNumber,
68104
+ stepName,
68105
+ reviewType,
68106
+ buildReducedTaskPromptSummary(promptContent),
68107
+ cwd,
68108
+ baseline,
68109
+ void 0
68110
+ );
68111
+ }
67447
68112
  function buildReviewRequest(taskId, stepNumber, stepName, reviewType, promptContent, cwd, baseline, userComments) {
67448
68113
  const parts = [
67449
68114
  `Review request for task ${taskId}, Step ${stepNumber}: ${stepName}`,
@@ -67568,6 +68233,7 @@ var init_reviewer = __esm({
67568
68233
  "use strict";
67569
68234
  init_src();
67570
68235
  init_pi();
68236
+ init_context_limit_detector();
67571
68237
  init_agent_session_helpers();
67572
68238
  init_session_skill_context();
67573
68239
  init_agent_logger();
@@ -69544,11 +70210,17 @@ You are ${assignedAgent.name}${assignedAgent.title?.trim() ? `, ${assignedAgent.
69544
70210
  const taskGetParams = Type2.Object({
69545
70211
  id: Type2.String({ description: "Task ID (e.g. KB-001)" })
69546
70212
  });
70213
+ const taskCreatePriorityValues = ["low", "normal", "high", "urgent"];
69547
70214
  const taskCreateParams3 = Type2.Object({
69548
70215
  title: Type2.Optional(Type2.String({ description: "Short child task title" })),
69549
70216
  description: Type2.String({ description: "Child task description/mission" }),
69550
70217
  dependencies: Type2.Optional(
69551
70218
  Type2.Array(Type2.String({ description: "Task ID dependency (e.g. KB-001)" }))
70219
+ ),
70220
+ priority: Type2.Optional(
70221
+ Type2.Union(taskCreatePriorityValues.map((priority) => Type2.Literal(priority)), {
70222
+ description: "Task priority (low, normal, high, urgent)"
70223
+ })
69552
70224
  )
69553
70225
  });
69554
70226
  const taskList = {
@@ -69670,6 +70342,7 @@ Remove or replace these ids and call fn_task_create again.`
69670
70342
  description: params.description,
69671
70343
  dependencies: validDeps,
69672
70344
  column: "triage",
70345
+ priority: params.priority,
69673
70346
  // Inherit parent's model settings if available
69674
70347
  modelProvider: parentTask?.modelProvider,
69675
70348
  modelId: parentTask?.modelId,
@@ -70485,11 +71158,146 @@ var init_run_audit = __esm({
70485
71158
  }
70486
71159
  });
70487
71160
 
70488
- // ../engine/src/merger.ts
70489
- import { execSync, exec as exec3, execFile as execFile3 } from "node:child_process";
71161
+ // ../engine/src/merger-squash-audit.ts
71162
+ import { execFile as execFile3 } from "node:child_process";
70490
71163
  import { promisify as promisify4 } from "node:util";
71164
+ async function auditSquashMerge({
71165
+ rootDir,
71166
+ squashSha,
71167
+ lookback = DEFAULT_LOOKBACK
71168
+ }) {
71169
+ const normalizedLookback = normalizeLookback(lookback);
71170
+ const parentSha = await git(rootDir, ["rev-parse", `${squashSha}^`]);
71171
+ const squashSubject = await git(rootDir, ["log", "-1", "--format=%s", squashSha]);
71172
+ const branchSubjects = normalizeLines(await git(rootDir, ["log", "-1", "--format=%b", squashSha])).map((line) => line.replace(/^- /, "").trim()).filter(Boolean);
71173
+ const recentMainCommits = await listRecentMainCommits(rootDir, parentSha, normalizedLookback);
71174
+ const recentMainSubjects = recentMainCommits.map((entry) => entry.subject);
71175
+ const duplicateSubjects = branchSubjects.filter((subject) => recentMainSubjects.includes(subject)).map((subject) => ({ type: "duplicate-subject", subject }));
71176
+ const touchedFiles = normalizeLines(await git(rootDir, ["diff", "--name-only", parentSha, squashSha]));
71177
+ const touchedFileOverlaps = [];
71178
+ for (const file of touchedFiles) {
71179
+ const overlappingCommits = [];
71180
+ for (const commit of recentMainCommits) {
71181
+ const touchedInCommit = await git(rootDir, ["diff-tree", "--no-commit-id", "--name-only", "-r", commit.sha, "--", file]);
71182
+ if (normalizeLines(touchedInCommit).includes(file)) {
71183
+ overlappingCommits.push({ sha: commit.shortSha, subject: commit.subject });
71184
+ }
71185
+ }
71186
+ if (overlappingCommits.length > 0) {
71187
+ touchedFileOverlaps.push({
71188
+ type: "touched-file-overlap",
71189
+ file,
71190
+ recentMainCommits: overlappingCommits
71191
+ });
71192
+ }
71193
+ }
71194
+ const findings = [...duplicateSubjects, ...touchedFileOverlaps];
71195
+ return {
71196
+ squashSha,
71197
+ parentSha,
71198
+ squashSubject,
71199
+ lookback: normalizedLookback,
71200
+ branchSubjects,
71201
+ recentMainSubjects,
71202
+ duplicateSubjects,
71203
+ touchedFiles,
71204
+ touchedFileOverlaps,
71205
+ findings,
71206
+ issueCount: findings.length,
71207
+ clean: findings.length === 0
71208
+ };
71209
+ }
71210
+ function formatSquashAuditReport(findings) {
71211
+ const lines = [
71212
+ `Auditing squash: ${findings.squashSha} \u2014 ${findings.squashSubject}`,
71213
+ `Parent (main before squash): ${findings.parentSha}`,
71214
+ `Lookback window on main: ${findings.lookback} commits`,
71215
+ "",
71216
+ "=== Duplicate-cherry-pick risk ==="
71217
+ ];
71218
+ if (findings.duplicateSubjects.length === 0) {
71219
+ lines.push("(none \u2014 no branch commit subjects match recent main commits)", "");
71220
+ } else {
71221
+ lines.push(
71222
+ "WARN: branch contains commits whose subjects match recent main commits.",
71223
+ "Auto-resolve may have picked the older side, dropping refinements.",
71224
+ "Action: diff each main commit below against HEAD and confirm its",
71225
+ "net contribution survived. Restore anything dropped as a follow-up.",
71226
+ "",
71227
+ ...findings.duplicateSubjects.map((entry) => ` - ${entry.subject}`),
71228
+ ""
71229
+ );
71230
+ }
71231
+ lines.push(`=== Touched-file overlap (${findings.touchedFiles.length} files in squash) ===`);
71232
+ if (findings.touchedFileOverlaps.length === 0) {
71233
+ lines.push("(none \u2014 squash touches files no recent main commit touched)", "");
71234
+ } else {
71235
+ lines.push(
71236
+ "Files the squash touched that also have recent main activity.",
71237
+ "Action: for each commit below, verify its changes still appear",
71238
+ "in HEAD. Reapply any silently dropped changes on the same branch.",
71239
+ ""
71240
+ );
71241
+ for (const overlap of findings.touchedFileOverlaps) {
71242
+ lines.push(` ${overlap.file}`);
71243
+ for (const commit of overlap.recentMainCommits) {
71244
+ lines.push(` - ${commit.sha} ${commit.subject}`);
71245
+ }
71246
+ }
71247
+ lines.push("");
71248
+ }
71249
+ lines.push(`Audit complete. ${findings.issueCount} item(s) for the calling agent to review.`);
71250
+ return lines.join("\n");
71251
+ }
71252
+ async function git(rootDir, args) {
71253
+ const { stdout } = await execFileAsync("git", args, {
71254
+ cwd: rootDir,
71255
+ encoding: "utf-8",
71256
+ maxBuffer: GIT_OUTPUT_MAX_BUFFER
71257
+ });
71258
+ return stdout.trim();
71259
+ }
71260
+ function normalizeLines(value) {
71261
+ const trimmed = value.trim();
71262
+ if (!trimmed) return [];
71263
+ return trimmed.split("\n").map((line) => line.trim()).filter(Boolean);
71264
+ }
71265
+ async function listRecentMainCommits(rootDir, parentSha, lookback) {
71266
+ const entries = normalizeLines(await git(rootDir, ["log", `--format=%H~%h~%s`, `-n`, String(lookback), parentSha]));
71267
+ return entries.map((entry) => {
71268
+ const [sha, shortSha, ...subjectParts] = entry.split("~");
71269
+ const subject = subjectParts.join("~").trim();
71270
+ if (!sha?.trim() || !shortSha?.trim() || !subject) {
71271
+ return null;
71272
+ }
71273
+ return {
71274
+ sha: sha.trim(),
71275
+ shortSha: shortSha.trim(),
71276
+ subject
71277
+ };
71278
+ }).filter((entry) => entry !== null);
71279
+ }
71280
+ function normalizeLookback(value) {
71281
+ if (!Number.isFinite(value) || !value || value < 1) {
71282
+ return DEFAULT_LOOKBACK;
71283
+ }
71284
+ return Math.trunc(value);
71285
+ }
71286
+ var execFileAsync, DEFAULT_LOOKBACK, GIT_OUTPUT_MAX_BUFFER;
71287
+ var init_merger_squash_audit = __esm({
71288
+ "../engine/src/merger-squash-audit.ts"() {
71289
+ "use strict";
71290
+ execFileAsync = promisify4(execFile3);
71291
+ DEFAULT_LOOKBACK = 30;
71292
+ GIT_OUTPUT_MAX_BUFFER = 10 * 1024 * 1024;
71293
+ }
71294
+ });
71295
+
71296
+ // ../engine/src/merger.ts
71297
+ import { execSync, exec as exec3, execFile as execFile4 } from "node:child_process";
71298
+ import { promisify as promisify5 } from "node:util";
70491
71299
  import { existsSync as existsSync25, readFileSync as readFileSync11, writeFileSync as writeFileSync2, unlinkSync, renameSync as renameSync2 } from "node:fs";
70492
- import { createHash as createHash6 } from "node:crypto";
71300
+ import { createHash as createHash7 } from "node:crypto";
70493
71301
  import { join as join32 } from "node:path";
70494
71302
  import { hostname } from "node:os";
70495
71303
  import { Type as Type3 } from "typebox";
@@ -70558,7 +71366,7 @@ function computeLockfileHash(rootDir) {
70558
71366
  const p = join32(rootDir, name);
70559
71367
  if (existsSync25(p)) {
70560
71368
  try {
70561
- return createHash6("sha256").update(readFileSync11(p)).digest("hex");
71369
+ return createHash7("sha256").update(readFileSync11(p)).digest("hex");
70562
71370
  } catch {
70563
71371
  return null;
70564
71372
  }
@@ -70616,8 +71424,8 @@ function commitOwnedByTask(taskId, subject, body) {
70616
71424
  async function findOwnedLandedCommitForTask(rootDir, task) {
70617
71425
  const tryHydrate = async (sha) => {
70618
71426
  try {
70619
- await execFileAsync("git", ["merge-base", "--is-ancestor", sha, "HEAD"], { cwd: rootDir });
70620
- const { stdout } = await execFileAsync("git", ["log", "-1", "--format=%H%x1f%s%x1f%b", sha], {
71427
+ await execFileAsync2("git", ["merge-base", "--is-ancestor", sha, "HEAD"], { cwd: rootDir });
71428
+ const { stdout } = await execFileAsync2("git", ["log", "-1", "--format=%H%x1f%s%x1f%b", sha], {
70621
71429
  cwd: rootDir,
70622
71430
  encoding: "utf-8"
70623
71431
  });
@@ -70625,7 +71433,7 @@ async function findOwnedLandedCommitForTask(rootDir, task) {
70625
71433
  if (!resolvedSha || !commitOwnedByTask(task.id, subject, body)) return null;
70626
71434
  const owned = { sha: resolvedSha, subject };
70627
71435
  try {
70628
- const { stdout: statsOut } = await execFileAsync("git", ["show", "--shortstat", "--format=", resolvedSha], {
71436
+ const { stdout: statsOut } = await execFileAsync2("git", ["show", "--shortstat", "--format=", resolvedSha], {
70629
71437
  cwd: rootDir,
70630
71438
  encoding: "utf-8"
70631
71439
  });
@@ -70648,7 +71456,7 @@ async function findOwnedLandedCommitForTask(rootDir, task) {
70648
71456
  ];
70649
71457
  for (const args of searches) {
70650
71458
  try {
70651
- const { stdout } = await execFileAsync("git", args, { cwd: rootDir, encoding: "utf-8" });
71459
+ const { stdout } = await execFileAsync2("git", args, { cwd: rootDir, encoding: "utf-8" });
70652
71460
  const first = stdout.trim().split("\n").find(Boolean);
70653
71461
  if (!first) continue;
70654
71462
  const [sha] = first.split("");
@@ -70711,15 +71519,15 @@ async function snapshotDirtyFiles(rootDir) {
70711
71519
  const paths = /* @__PURE__ */ new Set();
70712
71520
  try {
70713
71521
  const [unstagedOut, stagedOut, porcelainOut] = await Promise.all([
70714
- execFileAsync("git", ["diff", "-z", "--name-only"], { cwd: rootDir, encoding: "utf-8" }).then(
71522
+ execFileAsync2("git", ["diff", "-z", "--name-only"], { cwd: rootDir, encoding: "utf-8" }).then(
70715
71523
  (r) => r.stdout,
70716
71524
  () => ""
70717
71525
  ),
70718
- execFileAsync("git", ["diff", "-z", "--cached", "--name-only"], { cwd: rootDir, encoding: "utf-8" }).then(
71526
+ execFileAsync2("git", ["diff", "-z", "--cached", "--name-only"], { cwd: rootDir, encoding: "utf-8" }).then(
70719
71527
  (r) => r.stdout,
70720
71528
  () => ""
70721
71529
  ),
70722
- execFileAsync("git", ["status", "-z", "--porcelain"], { cwd: rootDir, encoding: "utf-8" }).then(
71530
+ execFileAsync2("git", ["status", "-z", "--porcelain"], { cwd: rootDir, encoding: "utf-8" }).then(
70723
71531
  (r) => r.stdout,
70724
71532
  () => ""
70725
71533
  )
@@ -70744,18 +71552,18 @@ async function snapshotDirtyFiles(rootDir) {
70744
71552
  async function gitDirtyFingerprint(rootDir) {
70745
71553
  try {
70746
71554
  const [diffOut, statusOut] = await Promise.all([
70747
- execFileAsync("git", ["diff", "HEAD"], {
71555
+ execFileAsync2("git", ["diff", "HEAD"], {
70748
71556
  cwd: rootDir,
70749
71557
  encoding: "utf-8",
70750
71558
  maxBuffer: 64 * 1024 * 1024
70751
71559
  }).then((r) => r.stdout, () => ""),
70752
- execFileAsync("git", ["status", "-z", "--porcelain"], { cwd: rootDir, encoding: "utf-8" }).then(
71560
+ execFileAsync2("git", ["status", "-z", "--porcelain"], { cwd: rootDir, encoding: "utf-8" }).then(
70753
71561
  (r) => r.stdout,
70754
71562
  () => ""
70755
71563
  )
70756
71564
  ]);
70757
71565
  if (!diffOut && !statusOut) return "";
70758
- return createHash6("sha256").update(diffOut).update("\0").update(statusOut).digest("hex");
71566
+ return createHash7("sha256").update(diffOut).update("\0").update(statusOut).digest("hex");
70759
71567
  } catch {
70760
71568
  return "";
70761
71569
  }
@@ -72434,7 +73242,7 @@ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, i
72434
73242
  encoding: "utf-8"
72435
73243
  });
72436
73244
  const unstaged = new Set(unstagedOut.split("\n").map((l) => l.trim()).filter(Boolean));
72437
- const { stdout: porcelainOut } = await execFileAsync("git", ["status", "-z", "--porcelain"], {
73245
+ const { stdout: porcelainOut } = await execFileAsync2("git", ["status", "-z", "--porcelain"], {
72438
73246
  cwd: rootDir,
72439
73247
  encoding: "utf-8"
72440
73248
  });
@@ -72455,7 +73263,7 @@ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, i
72455
73263
  }
72456
73264
  }
72457
73265
  if (unstagedToStage.length > 0) {
72458
- await execFileAsync("git", ["add", "--", ...unstagedToStage], { cwd: rootDir });
73266
+ await execFileAsync2("git", ["add", "--", ...unstagedToStage], { cwd: rootDir });
72459
73267
  }
72460
73268
  const untrackedToStage = [];
72461
73269
  for (const p of untracked) {
@@ -72468,7 +73276,7 @@ async function commitOrAmendMergeWithFixes(rootDir, taskId, branch, commitLog, i
72468
73276
  }
72469
73277
  }
72470
73278
  if (untrackedToStage.length > 0) {
72471
- await execFileAsync("git", ["add", "--", ...untrackedToStage], { cwd: rootDir });
73279
+ await execFileAsync2("git", ["add", "--", ...untrackedToStage], { cwd: rootDir });
72472
73280
  }
72473
73281
  const cap = (arr, n = 20) => arr.length <= n ? arr.join(", ") : `${arr.slice(0, n).join(", ")} ... (+${arr.length - n} more)`;
72474
73282
  mergerLog.log(
@@ -72971,8 +73779,8 @@ async function classifyConflict(filePath, cwd) {
72971
73779
  }
72972
73780
  async function resolveWithOurs(filePath, cwd) {
72973
73781
  try {
72974
- await execFileAsync("git", ["checkout", "--ours", "--", filePath], { cwd });
72975
- await execFileAsync("git", ["add", "--", filePath], { cwd });
73782
+ await execFileAsync2("git", ["checkout", "--ours", "--", filePath], { cwd });
73783
+ await execFileAsync2("git", ["add", "--", filePath], { cwd });
72976
73784
  mergerLog.log(`Auto-resolved ${filePath} using --ours`);
72977
73785
  } catch (error) {
72978
73786
  throw new Error(`Failed to auto-resolve ${filePath} with ours: ${error}`);
@@ -72980,8 +73788,8 @@ async function resolveWithOurs(filePath, cwd) {
72980
73788
  }
72981
73789
  async function resolveWithTheirs(filePath, cwd) {
72982
73790
  try {
72983
- await execFileAsync("git", ["checkout", "--theirs", "--", filePath], { cwd });
72984
- await execFileAsync("git", ["add", "--", filePath], { cwd });
73791
+ await execFileAsync2("git", ["checkout", "--theirs", "--", filePath], { cwd });
73792
+ await execFileAsync2("git", ["add", "--", filePath], { cwd });
72985
73793
  mergerLog.log(`Auto-resolved ${filePath} using --theirs`);
72986
73794
  } catch (error) {
72987
73795
  throw new Error(`Failed to auto-resolve ${filePath} with theirs: ${error}`);
@@ -72989,7 +73797,7 @@ async function resolveWithTheirs(filePath, cwd) {
72989
73797
  }
72990
73798
  async function resolveTrivialWhitespace(filePath, cwd) {
72991
73799
  try {
72992
- await execFileAsync("git", ["add", "--", filePath], { cwd });
73800
+ await execFileAsync2("git", ["add", "--", filePath], { cwd });
72993
73801
  mergerLog.log(`Auto-resolved ${filePath} (trivial whitespace)`);
72994
73802
  } catch (error) {
72995
73803
  throw new Error(`Failed to auto-resolve ${filePath} trivial conflict: ${error}`);
@@ -73183,6 +73991,43 @@ async function findWorktreeUser(store, worktreePath, excludeTaskId) {
73183
73991
  function quoteArg(value) {
73184
73992
  return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
73185
73993
  }
73994
+ function shouldRunPostSquashAudit(result, mergeWasEmpty, isEmptyCommit, commitSha) {
73995
+ if (mergeWasEmpty || isEmptyCommit || !commitSha) {
73996
+ return false;
73997
+ }
73998
+ return (result.autoResolvedCount ?? 0) > 0 || result.attemptsMade === 3;
73999
+ }
74000
+ function buildSquashAuditBlockingMessage(taskId, squashSha, findings) {
74001
+ const riskParts = [];
74002
+ if (findings.duplicateSubjects.length > 0) {
74003
+ riskParts.push(`${findings.duplicateSubjects.length} duplicate-subject risk${findings.duplicateSubjects.length === 1 ? "" : "s"}`);
74004
+ }
74005
+ if (findings.touchedFileOverlaps.length > 0) {
74006
+ riskParts.push(`${findings.touchedFileOverlaps.length} touched-file overlap risk${findings.touchedFileOverlaps.length === 1 ? "" : "s"}`);
74007
+ }
74008
+ const summary = riskParts.length > 0 ? riskParts.join(", ") : `${findings.issueCount} audit finding(s)`;
74009
+ return `${taskId}: post-squash audit blocked auto-completion for ${squashSha.slice(0, 8)} (${summary})`;
74010
+ }
74011
+ function formatSquashAuditAgentLog(findings) {
74012
+ const lines = [];
74013
+ if (findings.duplicateSubjects.length > 0) {
74014
+ lines.push("Duplicate-subject risks:");
74015
+ for (const duplicate of findings.duplicateSubjects) {
74016
+ lines.push(`- ${duplicate.subject}`);
74017
+ }
74018
+ }
74019
+ if (findings.touchedFileOverlaps.length > 0) {
74020
+ if (lines.length > 0) lines.push("");
74021
+ lines.push("Touched-file overlap risks:");
74022
+ for (const overlap of findings.touchedFileOverlaps) {
74023
+ lines.push(`- ${overlap.file}`);
74024
+ for (const commit of overlap.recentMainCommits) {
74025
+ lines.push(` - ${commit.sha} ${commit.subject}`);
74026
+ }
74027
+ }
74028
+ }
74029
+ return lines.join("\n");
74030
+ }
73186
74031
  async function resolveSafeCommitBody(opts) {
73187
74032
  const cleanLog = opts.commitLog.trim();
73188
74033
  if (cleanLog.length > 0) return cleanLog;
@@ -74514,6 +75359,26 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
74514
75359
  }
74515
75360
  const isEmptyCommit = filesChanged === 0;
74516
75361
  const recordedSha = isEmptyCommit || mergeWasEmpty ? void 0 : commitSha;
75362
+ const auditSha = recordedSha;
75363
+ if (auditSha && shouldRunPostSquashAudit(result, mergeWasEmpty, isEmptyCommit, auditSha)) {
75364
+ const auditFindings = await auditSquashMerge({
75365
+ rootDir,
75366
+ squashSha: auditSha
75367
+ });
75368
+ if (!auditFindings.clean) {
75369
+ const auditError = new SquashAuditError(taskId, auditSha, auditFindings);
75370
+ await store.appendAgentLog(
75371
+ taskId,
75372
+ auditError.message,
75373
+ "tool_error",
75374
+ formatSquashAuditAgentLog(auditFindings),
75375
+ "merger"
75376
+ );
75377
+ await store.updateTask(taskId, { status: null });
75378
+ throw auditError;
75379
+ }
75380
+ await store.appendAgentLog(taskId, "post-squash audit clean", "text", void 0, "merger");
75381
+ }
74517
75382
  if (isEmptyCommit) {
74518
75383
  mergerLog.warn(
74519
75384
  `${taskId}: local squash produced an empty commit (${commitSha?.slice(0, 8)}) \u2014 branch likely contained dupes of main. Skipping commitSha; recovery will backfill when real commit lands.`
@@ -74578,6 +75443,9 @@ async function aiMergeTask(store, rootDir, taskId, options = {}) {
74578
75443
  "merger"
74579
75444
  );
74580
75445
  } catch (err) {
75446
+ if (err instanceof SquashAuditError || err?.name === "SquashAuditError") {
75447
+ throw err;
75448
+ }
74581
75449
  mergerLog.warn(`${taskId}: failed to collect/store merge details: ${err.message}`);
74582
75450
  }
74583
75451
  try {
@@ -75813,7 +76681,7 @@ async function completeTask(store, taskId, result) {
75813
76681
  result.task = task;
75814
76682
  store.emit("task:merged", result);
75815
76683
  }
75816
- var execAsync2, execFileAsync, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, INSTALL_MARKER_RELPATH, LOCKFILE_CANDIDATES, VerificationError, MergeAbortedError, VERIFICATION_EXTRA_ENV, AUTOSTASH_LABEL_PREFIX, AUTOSTASH_TIMESTAMP_RE, ACTIVE_MERGER_STATUS_FILENAME, FUSION_TASK_ID_TRAILER_KEY;
76684
+ var execAsync2, execFileAsync2, LOCKFILE_PATTERNS, GENERATED_PATTERNS, DEPENDENCY_SYNC_TRIGGER_PATTERNS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS, PULL_REBASE_TIMEOUT_MS, PUSH_TIMEOUT_MS, MERGE_COMMIT_LOG_MAX_CHARS, MERGE_DIFF_STAT_MAX_CHARS, INSTALL_MARKER_RELPATH, LOCKFILE_CANDIDATES, VerificationError, MergeAbortedError, SquashAuditError, VERIFICATION_EXTRA_ENV, AUTOSTASH_LABEL_PREFIX, AUTOSTASH_TIMESTAMP_RE, ACTIVE_MERGER_STATUS_FILENAME, FUSION_TASK_ID_TRAILER_KEY;
75817
76685
  var init_merger = __esm({
75818
76686
  "../engine/src/merger.ts"() {
75819
76687
  "use strict";
@@ -75833,8 +76701,9 @@ var init_merger = __esm({
75833
76701
  init_agent_instructions();
75834
76702
  init_run_audit();
75835
76703
  init_agent_tools();
75836
- execAsync2 = promisify4(exec3);
75837
- execFileAsync = promisify4(execFile3);
76704
+ init_merger_squash_audit();
76705
+ execAsync2 = promisify5(exec3);
76706
+ execFileAsync2 = promisify5(execFile4);
75838
76707
  LOCKFILE_PATTERNS = [
75839
76708
  "package-lock.json",
75840
76709
  "pnpm-lock.yaml",
@@ -75891,6 +76760,14 @@ var init_merger = __esm({
75891
76760
  this.name = "MergeAbortedError";
75892
76761
  }
75893
76762
  };
76763
+ SquashAuditError = class extends Error {
76764
+ constructor(taskId, squashSha, findings) {
76765
+ super(buildSquashAuditBlockingMessage(taskId, squashSha, findings));
76766
+ this.squashSha = squashSha;
76767
+ this.findings = findings;
76768
+ this.name = "SquashAuditError";
76769
+ }
76770
+ };
75894
76771
  VERIFICATION_EXTRA_ENV = Object.fromEntries(
75895
76772
  [
75896
76773
  ["FUSION_TEST_TOTAL_WORKERS", "8"],
@@ -76092,7 +76969,7 @@ __export(worktree_pool_exports, {
76092
76969
  scanOrphanedBranches: () => scanOrphanedBranches
76093
76970
  });
76094
76971
  import { exec as exec4 } from "node:child_process";
76095
- import { promisify as promisify5 } from "node:util";
76972
+ import { promisify as promisify6 } from "node:util";
76096
76973
  import { existsSync as existsSync27, lstatSync, readdirSync as readdirSync4, rmSync as rmSync2 } from "node:fs";
76097
76974
  import { join as join34, relative as relative8, resolve as resolve18, isAbsolute as isAbsolute10 } from "node:path";
76098
76975
  function getExecStdout(result) {
@@ -76303,7 +77180,7 @@ var init_worktree_pool = __esm({
76303
77180
  "../engine/src/worktree-pool.ts"() {
76304
77181
  "use strict";
76305
77182
  init_logger2();
76306
- execAsync3 = promisify5(exec4);
77183
+ execAsync3 = promisify6(exec4);
76307
77184
  WorktreePool = class {
76308
77185
  idle = /* @__PURE__ */ new Set();
76309
77186
  /**
@@ -76495,7 +77372,7 @@ var init_token_cap_detector = __esm({
76495
77372
 
76496
77373
  // ../engine/src/step-session-executor.ts
76497
77374
  import { exec as exec5 } from "node:child_process";
76498
- import { promisify as promisify6 } from "node:util";
77375
+ import { promisify as promisify7 } from "node:util";
76499
77376
  import { existsSync as existsSync28 } from "node:fs";
76500
77377
  import { join as join35 } from "node:path";
76501
77378
  function parseStepFileScopes(prompt) {
@@ -76758,7 +77635,7 @@ var init_step_session_executor = __esm({
76758
77635
  init_context_limit_detector();
76759
77636
  init_usage_limit_detector();
76760
77637
  init_agent_tools();
76761
- execAsync4 = promisify6(exec5);
77638
+ execAsync4 = promisify7(exec5);
76762
77639
  stepExecLog = createLogger2("step-session-executor");
76763
77640
  MAX_STEP_RETRIES = 3;
76764
77641
  RETRY_DELAYS_MS = [1e3, 5e3, 15e3];
@@ -77412,7 +78289,9 @@ async function hydrateWorktreeDb({
77412
78289
  ensureWorktreeSchema(worktreePath);
77413
78290
  }
77414
78291
  srcDb = new DatabaseSync(srcDbPath);
78292
+ srcDb.exec("PRAGMA busy_timeout = 5000");
77415
78293
  dstDb = openWorktreeDbWithRecovery(dstDbPath, worktreePath);
78294
+ dstDb.exec("PRAGMA busy_timeout = 5000");
77416
78295
  dstDb.exec("PRAGMA journal_mode = WAL");
77417
78296
  const srcTaskCols = getColumns(srcDb, "tasks");
77418
78297
  const dstTaskCols = getColumns(dstDb, "tasks");
@@ -77861,12 +78740,120 @@ var init_run_verification_tool = __esm({
77861
78740
 
77862
78741
  // ../engine/src/executor.ts
77863
78742
  import { exec as exec6 } from "node:child_process";
77864
- import { promisify as promisify7 } from "node:util";
78743
+ import { promisify as promisify8 } from "node:util";
77865
78744
  import { delimiter, isAbsolute as isAbsolute12, join as join39, relative as relative9, resolve as resolvePath } from "node:path";
77866
78745
  import { existsSync as existsSync31 } from "node:fs";
77867
78746
  import { readFile as readFile17, writeFile as writeFile13 } from "node:fs/promises";
77868
78747
  import { Type as Type5 } from "@mariozechner/pi-ai";
77869
78748
  import { ModelRegistry as ModelRegistry2, SessionManager as SessionManager2 } from "@mariozechner/pi-coding-agent";
78749
+ function normalizeWorkflowScopePath(pathValue) {
78750
+ return pathValue.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/").replace(/\/$/, "");
78751
+ }
78752
+ function stripTrailingPathPunctuation(pathValue) {
78753
+ return pathValue.replace(/[),.:;!?]+$/g, "");
78754
+ }
78755
+ function extractReferencedPathsFromWorkflowFeedback(feedback) {
78756
+ const extracted = [];
78757
+ const seen = /* @__PURE__ */ new Set();
78758
+ for (const match of feedback.matchAll(WORKFLOW_FEEDBACK_PATH_REGEX)) {
78759
+ const candidate = stripTrailingPathPunctuation(match[1] ?? match[2] ?? "");
78760
+ const normalized = normalizeWorkflowScopePath(candidate);
78761
+ if (!normalized.includes("/") || !normalized) continue;
78762
+ if (seen.has(normalized)) continue;
78763
+ seen.add(normalized);
78764
+ extracted.push(normalized);
78765
+ }
78766
+ return extracted;
78767
+ }
78768
+ function workflowPathMatchesDeclaredScope(filePath, scopePatterns) {
78769
+ const normalizedPath = normalizeWorkflowScopePath(filePath);
78770
+ for (const rawPattern of scopePatterns) {
78771
+ const pattern = normalizeWorkflowScopePath(rawPattern);
78772
+ if (!pattern) continue;
78773
+ if (/\/\*+$/.test(pattern)) {
78774
+ const directory = pattern.replace(/\/\*+$/, "");
78775
+ if (normalizedPath === directory || normalizedPath.startsWith(`${directory}/`)) return true;
78776
+ continue;
78777
+ }
78778
+ if (pattern.endsWith("/")) {
78779
+ if (normalizedPath.startsWith(pattern)) return true;
78780
+ continue;
78781
+ }
78782
+ if (normalizedPath === pattern) return true;
78783
+ }
78784
+ return false;
78785
+ }
78786
+ function partitionWorkflowRevisionFeedback(feedback, declaredFileScope) {
78787
+ const trimmedFeedback = feedback.trim();
78788
+ if (!trimmedFeedback || declaredFileScope.length === 0) {
78789
+ return {
78790
+ inScopeFeedback: trimmedFeedback,
78791
+ outOfScopeFeedback: "",
78792
+ inScopeSegments: trimmedFeedback ? [trimmedFeedback] : [],
78793
+ outOfScopeSegments: [],
78794
+ detectedPaths: extractReferencedPathsFromWorkflowFeedback(trimmedFeedback)
78795
+ };
78796
+ }
78797
+ const segments = trimmedFeedback.split(/\n\s*\n/).map((segment) => segment.trim()).filter(Boolean);
78798
+ const allDetectedPaths = extractReferencedPathsFromWorkflowFeedback(trimmedFeedback);
78799
+ if (allDetectedPaths.length === 0) {
78800
+ return {
78801
+ inScopeFeedback: trimmedFeedback,
78802
+ outOfScopeFeedback: "",
78803
+ inScopeSegments: trimmedFeedback ? [trimmedFeedback] : [],
78804
+ outOfScopeSegments: [],
78805
+ detectedPaths: []
78806
+ };
78807
+ }
78808
+ const inScopeSegments = [];
78809
+ const outOfScopeSegments = [];
78810
+ for (const segment of segments) {
78811
+ const segmentPaths = extractReferencedPathsFromWorkflowFeedback(segment);
78812
+ if (segmentPaths.length === 0) {
78813
+ inScopeSegments.push(segment);
78814
+ continue;
78815
+ }
78816
+ const hasOutOfScopePath = segmentPaths.some((path2) => !workflowPathMatchesDeclaredScope(path2, declaredFileScope));
78817
+ if (hasOutOfScopePath) {
78818
+ outOfScopeSegments.push(segment);
78819
+ } else {
78820
+ inScopeSegments.push(segment);
78821
+ }
78822
+ }
78823
+ return {
78824
+ inScopeFeedback: inScopeSegments.join("\n\n"),
78825
+ outOfScopeFeedback: outOfScopeSegments.join("\n\n"),
78826
+ inScopeSegments,
78827
+ outOfScopeSegments,
78828
+ detectedPaths: allDetectedPaths
78829
+ };
78830
+ }
78831
+ function normalizeWorktreePath(pathValue) {
78832
+ return resolvePath(pathValue).replace(/\\/g, "/").replace(/\/+$/, "");
78833
+ }
78834
+ async function extractPersistedSessionWorktreePath(sessionFile) {
78835
+ try {
78836
+ const content = await readFile17(sessionFile, "utf-8");
78837
+ const matches = content.match(SESSION_WORKTREE_PATH_REGEX) ?? [];
78838
+ if (matches.length === 0) return null;
78839
+ const normalizedCounts = /* @__PURE__ */ new Map();
78840
+ for (const match of matches) {
78841
+ const normalized = normalizeWorktreePath(match);
78842
+ normalizedCounts.set(normalized, (normalizedCounts.get(normalized) ?? 0) + 1);
78843
+ }
78844
+ let best = null;
78845
+ for (const [path2, count] of normalizedCounts.entries()) {
78846
+ if (!best || count > best.count) best = { path: path2, count };
78847
+ }
78848
+ return best?.path ?? null;
78849
+ } catch {
78850
+ return null;
78851
+ }
78852
+ }
78853
+ function isSessionWorktreeCompatible(persistedWorktreePath, currentWorktreePath) {
78854
+ if (!persistedWorktreePath) return true;
78855
+ return persistedWorktreePath === normalizeWorktreePath(currentWorktreePath);
78856
+ }
77870
78857
  function truncateWorkflowScriptOutput2(output) {
77871
78858
  if (output.length <= WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2) return output;
77872
78859
  return `... output truncated to last ${WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2} characters ...
@@ -78146,7 +79133,7 @@ function detectReviewHandoffIntent(commentText) {
78146
79133
  ];
78147
79134
  return handoffPhrases.some((phrase) => text.includes(phrase));
78148
79135
  }
78149
- var execAsync5, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, COMPLETED_TASK_WATCHDOG_MS, WORKFLOW_RERUN_WATCHDOG_MS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
79136
+ var execAsync5, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, COMPLETED_TASK_WATCHDOG_MS, WORKFLOW_RERUN_WATCHDOG_MS, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, WORKFLOW_FEEDBACK_PATH_REGEX, NonRetryableWorktreeError, SESSION_WORKTREE_PATH_REGEX, taskUpdateParams, taskAddDepParams, spawnAgentParams, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
78150
79137
  var init_executor = __esm({
78151
79138
  "../engine/src/executor.ts"() {
78152
79139
  "use strict";
@@ -78183,7 +79170,7 @@ var init_executor = __esm({
78183
79170
  init_fallback_model_observer();
78184
79171
  init_agent_logger();
78185
79172
  init_agent_tools();
78186
- execAsync5 = promisify7(exec6);
79173
+ execAsync5 = promisify8(exec6);
78187
79174
  STEP_STATUSES = ["pending", "in-progress", "done", "skipped"];
78188
79175
  MAX_WORKFLOW_STEP_RETRIES = 3;
78189
79176
  MAX_TASK_DONE_SESSION_RETRIES = 3;
@@ -78191,8 +79178,10 @@ var init_executor = __esm({
78191
79178
  COMPLETED_TASK_WATCHDOG_MS = 6e4;
78192
79179
  WORKFLOW_RERUN_WATCHDOG_MS = 15e3;
78193
79180
  WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2 = 4e3;
79181
+ WORKFLOW_FEEDBACK_PATH_REGEX = /`([^`\n]+)`|(?<![A-Za-z0-9_.-])((?:\.\.?\/)?(?:@?[A-Za-z0-9._-]+\/)+[A-Za-z0-9._-]+(?:\.[A-Za-z0-9._-]+)?)/g;
78194
79182
  NonRetryableWorktreeError = class extends Error {
78195
79183
  };
79184
+ SESSION_WORKTREE_PATH_REGEX = /([A-Za-z]:)?[^"'\s]*\.worktrees[\\/][^"'\s]+/g;
78196
79185
  taskUpdateParams = Type5.Object({
78197
79186
  step: Type5.Number({ description: "Step number (1-indexed)" }),
78198
79187
  status: Type5.Union(
@@ -80266,15 +81255,18 @@ ${summary}`,
80266
81255
  }
80267
81256
  if (!workflowResult.allPassed) {
80268
81257
  if (workflowResult.revisionRequested) {
80269
- await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
80270
- return;
80271
- }
80272
- const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown");
80273
- if (retried) {
81258
+ const rerunScheduled = await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName, settings);
81259
+ if (rerunScheduled) {
81260
+ return;
81261
+ }
81262
+ } else {
81263
+ const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown");
81264
+ if (retried) {
81265
+ return;
81266
+ }
81267
+ await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
80274
81268
  return;
80275
81269
  }
80276
- await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
80277
- return;
80278
81270
  }
80279
81271
  } else {
80280
81272
  executorLog.log(`${task.id}: fast mode \u2014 skipping pre-merge workflow steps`);
@@ -80530,7 +81522,23 @@ ${summary}`,
80530
81522
  const executorFallbackProvider = settings.fallbackProvider;
80531
81523
  const executorFallbackModelId = settings.fallbackModelId;
80532
81524
  const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
80533
- const isResuming = !!task.sessionFile && existsSync31(task.sessionFile);
81525
+ let isResuming = !!task.sessionFile && existsSync31(task.sessionFile);
81526
+ if (isResuming) {
81527
+ const persistedWorktreePath = await extractPersistedSessionWorktreePath(task.sessionFile);
81528
+ if (!isSessionWorktreeCompatible(persistedWorktreePath, worktreePath)) {
81529
+ executorLog.warn(
81530
+ `${task.id}: stale sessionFile worktree mismatch (session=${persistedWorktreePath}, task=${worktreePath}); starting fresh session`
81531
+ );
81532
+ await this.store.logEntry(
81533
+ task.id,
81534
+ `Detected stale persisted session metadata (worktree mismatch: ${persistedWorktreePath} vs ${worktreePath}) \u2014 discarded resume state and started fresh session`,
81535
+ void 0,
81536
+ this.currentRunContext
81537
+ );
81538
+ await this.store.updateTask(task.id, { sessionFile: null });
81539
+ isResuming = false;
81540
+ }
81541
+ }
80534
81542
  const sessionManager = isResuming ? SessionManager2.open(task.sessionFile) : SessionManager2.create(worktreePath);
80535
81543
  executorLog.log(`${task.id}: creating agent session (provider=${executorProvider ?? "default"}, model=${executorModelId ?? "default"}, resuming=${isResuming})`);
80536
81544
  const executorInstructions = await this.resolveInstructionsForRole("executor");
@@ -80760,15 +81768,18 @@ ${summary}`,
80760
81768
  }
80761
81769
  if (!workflowResult.allPassed) {
80762
81770
  if (workflowResult.revisionRequested) {
80763
- await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
80764
- return;
80765
- }
80766
- const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown");
80767
- if (retried) {
81771
+ const rerunScheduled = await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName, settings);
81772
+ if (rerunScheduled) {
81773
+ return;
81774
+ }
81775
+ } else {
81776
+ const retried = await this.handleWorkflowStepFailure(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown");
81777
+ if (retried) {
81778
+ return;
81779
+ }
81780
+ await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
80768
81781
  return;
80769
81782
  }
80770
- await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed");
80771
- return;
80772
81783
  }
80773
81784
  } else {
80774
81785
  executorLog.log(`${task.id}: fast mode \u2014 skipping pre-merge workflow steps`);
@@ -80928,11 +81939,14 @@ ${summary}`,
80928
81939
  }
80929
81940
  if (!workflowResult.allPassed) {
80930
81941
  if (workflowResult.revisionRequested) {
80931
- await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName);
81942
+ const rerunScheduled = await this.handleWorkflowRevisionRequest(task, worktreePath, workflowResult.feedback, workflowResult.stepName, settings);
81943
+ if (rerunScheduled) {
81944
+ return;
81945
+ }
81946
+ } else {
81947
+ await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed on retry");
80932
81948
  return;
80933
81949
  }
80934
- await this.sendTaskBackForFix(task, worktreePath, workflowResult.feedback, workflowResult.stepName || "Unknown", "Workflow step failed on retry");
80935
- return;
80936
81950
  }
80937
81951
  } else {
80938
81952
  executorLog.log(`${task.id}: fast mode \u2014 skipping pre-merge workflow steps`);
@@ -81732,18 +82746,38 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
81732
82746
  * the injected feedback from PROMPT.md and applies an in-place fix rather
81733
82747
  * than redoing any completed step.
81734
82748
  */
81735
- async handleWorkflowRevisionRequest(task, worktreePath, feedback, stepName) {
82749
+ async handleWorkflowRevisionRequest(task, worktreePath, feedback, stepName, settings) {
81736
82750
  executorLog.log(`${task.id}: workflow revision requested by step "${stepName}"`);
81737
82751
  this.clearCompletedTaskWatchdog(task.id);
82752
+ const shouldForkOnScopeMismatch = settings.workflowRevisionForkOnScopeMismatch !== false;
82753
+ let inScopeFeedback = feedback.trim();
82754
+ let outOfScopeFeedback = "";
82755
+ let followUpTaskId;
82756
+ if (shouldForkOnScopeMismatch) {
82757
+ const declaredFileScope = await this.store.parseFileScopeFromPrompt(task.id).catch(() => []);
82758
+ const partition = partitionWorkflowRevisionFeedback(feedback, declaredFileScope);
82759
+ inScopeFeedback = partition.inScopeFeedback;
82760
+ outOfScopeFeedback = partition.outOfScopeFeedback;
82761
+ if (outOfScopeFeedback) {
82762
+ const followUpTask = await this.createWorkflowRevisionFollowUpTask(task, stepName, outOfScopeFeedback);
82763
+ followUpTaskId = followUpTask.id;
82764
+ }
82765
+ }
82766
+ if (!inScopeFeedback) {
82767
+ await this.store.logEntry(
82768
+ task.id,
82769
+ followUpTaskId ? `Workflow step "${stepName}" requested revision \u2014 feedback forked to follow-up ${followUpTaskId}; original task left unchanged` : `Workflow step "${stepName}" requested revision \u2014 no in-scope feedback detected`,
82770
+ outOfScopeFeedback || feedback,
82771
+ this.currentRunContext
82772
+ );
82773
+ return false;
82774
+ }
81738
82775
  const updatedTask = await this.store.getTask(task.id);
81739
82776
  const reopen = await this.reopenLastStepForRevision(task.id, updatedTask);
81740
82777
  const reopenSummary = reopen ? `re-opening Step ${reopen.index + 1} ("${reopen.name}") for in-place fix` : "no step to re-open (none were completed)";
81741
- await this.store.logEntry(
81742
- task.id,
81743
- `Workflow step "${stepName}" requested revision \u2014 ${reopenSummary}`,
81744
- feedback
81745
- );
81746
- await this.injectWorkflowRevisionInstructions(task, feedback);
82778
+ const logMessage = followUpTaskId ? `Workflow step "${stepName}" requested revision \u2014 split feedback: appended in-scope guidance and forked out-of-scope work to ${followUpTaskId}; ${reopenSummary}` : `Workflow step "${stepName}" requested revision \u2014 feedback appended to original task; ${reopenSummary}`;
82779
+ await this.store.logEntry(task.id, logMessage, inScopeFeedback, this.currentRunContext);
82780
+ await this.injectWorkflowRevisionInstructions(task, inScopeFeedback);
81747
82781
  await this.store.updateTask(task.id, {
81748
82782
  status: null,
81749
82783
  sessionFile: null
@@ -81754,6 +82788,35 @@ Take a different approach. Do NOT repeat the rejected strategy. Re-read the step
81754
82788
  worktreePath,
81755
82789
  `${task.id}: revision rerun scheduled \u2014 moved to todo then in-progress`
81756
82790
  );
82791
+ return true;
82792
+ }
82793
+ async createWorkflowRevisionFollowUpTask(task, stepName, feedback) {
82794
+ const title = `${task.id}: workflow follow-up from ${stepName}`;
82795
+ const description = [
82796
+ `Follow-up work forked from workflow revision feedback on ${task.id}.`,
82797
+ "",
82798
+ `Original task: ${task.id}${task.title ? ` \u2014 ${task.title}` : ""}`,
82799
+ `Workflow step: ${stepName}`,
82800
+ "",
82801
+ "This feedback referenced files outside the original task's declared File Scope, so it was forked into a follow-up task instead of mutating the original PROMPT.md.",
82802
+ "",
82803
+ "## Out-of-Scope Workflow Revision Feedback",
82804
+ "",
82805
+ feedback
82806
+ ].join("\n");
82807
+ return this.store.createTask({
82808
+ title,
82809
+ description,
82810
+ dependencies: [task.id],
82811
+ source: {
82812
+ sourceType: "workflow_step",
82813
+ sourceParentTaskId: task.id,
82814
+ sourceMetadata: {
82815
+ workflowStepName: stepName,
82816
+ routing: "scope-mismatch-fork"
82817
+ }
82818
+ }
82819
+ });
81757
82820
  }
81758
82821
  /**
81759
82822
  * Re-open the last non-pending step so a revision/failure handler gives the
@@ -87050,7 +88113,7 @@ Follow this process:
87050
88113
 
87051
88114
  // ../engine/src/agent-heartbeat.ts
87052
88115
  import { Type as Type6 } from "@mariozechner/pi-ai";
87053
- import { createHash as createHash7 } from "node:crypto";
88116
+ import { createHash as createHash8 } from "node:crypto";
87054
88117
  function formatDuration(ms) {
87055
88118
  const totalMinutes = Math.floor(ms / 6e4);
87056
88119
  if (totalMinutes < 1) return "<1m";
@@ -87094,7 +88157,7 @@ function truncatePrompt(text, maxChars) {
87094
88157
  ... (truncated, ${text.length} chars)`;
87095
88158
  }
87096
88159
  function shortContentHash(value) {
87097
- return createHash7("sha256").update(value).digest("hex").slice(0, 8);
88160
+ return createHash8("sha256").update(value).digest("hex").slice(0, 8);
87098
88161
  }
87099
88162
  function buildIdentitySnapshot(args) {
87100
88163
  const { agent, resolvedInstructions, workspaceMemory } = args;
@@ -88946,13 +90009,13 @@ ${taskDetail.prompt}` : "No PROMPT.md available.",
88946
90009
  });
88947
90010
  }
88948
90011
  async buildReportsHealthSection(agentId, agentStore) {
88949
- const getReports = agentStore.getAgentsByReportsTo;
88950
- if (typeof getReports !== "function") {
90012
+ const storeWithReports = agentStore;
90013
+ if (typeof storeWithReports.getAgentsByReportsTo !== "function") {
88951
90014
  return null;
88952
90015
  }
88953
90016
  let reports;
88954
90017
  try {
88955
- reports = await getReports(agentId);
90018
+ reports = await storeWithReports.getAgentsByReportsTo(agentId);
88956
90019
  } catch (err) {
88957
90020
  heartbeatLog.warn(`Failed to load reports for ${agentId}: ${err instanceof Error ? err.message : String(err)}`);
88958
90021
  return null;
@@ -91347,7 +92410,7 @@ var init_cron_runner = __esm({
91347
92410
 
91348
92411
  // ../engine/src/routine-runner.ts
91349
92412
  import { exec as exec8 } from "node:child_process";
91350
- import { promisify as promisify8 } from "node:util";
92413
+ import { promisify as promisify9 } from "node:util";
91351
92414
  function truncateOutput3(stdout, stderr) {
91352
92415
  let output = stdout;
91353
92416
  if (stderr) {
@@ -91369,7 +92432,7 @@ var init_routine_runner = __esm({
91369
92432
  init_logger2();
91370
92433
  init_shell_utils();
91371
92434
  log16 = createLogger2("routine-runner");
91372
- execAsync6 = promisify8(exec8);
92435
+ execAsync6 = promisify9(exec8);
91373
92436
  DEFAULT_TIMEOUT_MS8 = 5 * 60 * 1e3;
91374
92437
  MAX_BUFFER2 = 1024 * 1024;
91375
92438
  MAX_OUTPUT_LENGTH2 = 10 * 1024;
@@ -92306,17 +93369,25 @@ function isMissingWorktreeSessionStartFailure(error) {
92306
93369
  if (typeof error !== "string") {
92307
93370
  return false;
92308
93371
  }
92309
- return error.includes("Refusing to start coding agent in missing worktree:");
93372
+ return error.includes(MISSING_WORKTREE_SESSION_PREFIX);
93373
+ }
93374
+ function extractMissingWorktreePathFromSessionStartFailure(error) {
93375
+ if (typeof error !== "string") return null;
93376
+ const idx = error.indexOf(MISSING_WORKTREE_SESSION_PREFIX);
93377
+ if (idx < 0) return null;
93378
+ const pathPart = error.slice(idx + MISSING_WORKTREE_SESSION_PREFIX.length).trim();
93379
+ return pathPart.length > 0 ? pathPart : null;
92310
93380
  }
92311
93381
  function isRecoverableMissingWorktreeReviewFailure(task) {
92312
93382
  return task.column === "in-review" && !task.paused && task.status === "failed" && isMissingWorktreeSessionStartFailure(task.error) && hasStepProgress(task);
92313
93383
  }
92314
- var log17, RestartRecoveryCoordinator;
93384
+ var log17, MISSING_WORKTREE_SESSION_PREFIX, RestartRecoveryCoordinator;
92315
93385
  var init_restart_recovery_coordinator = __esm({
92316
93386
  "../engine/src/restart-recovery-coordinator.ts"() {
92317
93387
  "use strict";
92318
93388
  init_logger2();
92319
93389
  log17 = createLogger2("restart-recovery");
93390
+ MISSING_WORKTREE_SESSION_PREFIX = "Refusing to start coding agent in missing worktree:";
92320
93391
  RestartRecoveryCoordinator = class {
92321
93392
  constructor(store, executor) {
92322
93393
  this.store = store;
@@ -92360,8 +93431,8 @@ var init_restart_recovery_coordinator = __esm({
92360
93431
 
92361
93432
  // ../engine/src/self-healing.ts
92362
93433
  import { exec as exec9, execSync as execSync2 } from "node:child_process";
92363
- import { promisify as promisify9 } from "node:util";
92364
- import { existsSync as existsSync33, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync5 } from "node:fs";
93434
+ import { promisify as promisify10 } from "node:util";
93435
+ import { existsSync as existsSync33, readdirSync as readdirSync5, rmSync as rmSync3, statSync as statSync6 } from "node:fs";
92365
93436
  import { isAbsolute as isAbsolute14, join as join41, relative as relative10, resolve as resolve20 } from "node:path";
92366
93437
  function commitOwnedByTask2(taskId, lineageId, subject, body) {
92367
93438
  if (lineageId && body.includes(`Fusion-Task-Lineage: ${lineageId}`)) {
@@ -92454,7 +93525,7 @@ function isNoTaskDoneFailure2(task) {
92454
93525
  function hasStepProgress2(task) {
92455
93526
  return task.steps.some((step) => step.status !== "pending");
92456
93527
  }
92457
- var log18, execAsync7, APPROVED_TRIAGE_RECOVERY_GRACE_MS, ORPHANED_EXECUTION_RECOVERY_GRACE_MS, ACTIVE_MERGE_STATUSES, NON_TERMINAL_STEP_STATUSES2, GHOST_REVIEW_PRESERVED_STATUSES, ORPHANED_WITH_WORKTREE_GRACE_MS, MAX_TASK_DONE_RETRIES, MAX_AUTO_MERGE_RETRIES, DEADLOCK_RECOVERY_COOLDOWN_MS, DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS, DURABLE_ERROR_RECOVERY_MAX_RETRIES, DURABLE_ERROR_RECOVERY_BASE_COOLDOWN_MS, DURABLE_ERROR_RECOVERY_MAX_COOLDOWN_MS, SelfHealingManager;
93528
+ var log18, execAsync7, APPROVED_TRIAGE_RECOVERY_GRACE_MS, ORPHANED_EXECUTION_RECOVERY_GRACE_MS, ACTIVE_MERGE_STATUSES, NON_TERMINAL_STEP_STATUSES2, GHOST_REVIEW_PRESERVED_STATUSES, ORPHANED_WITH_WORKTREE_GRACE_MS, MAX_TASK_DONE_RETRIES, MAX_AUTO_MERGE_RETRIES, MAX_STARVATION_DROPS, DEADLOCK_RECOVERY_COOLDOWN_MS, DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS, DURABLE_ERROR_RECOVERY_MAX_RETRIES, DURABLE_ERROR_RECOVERY_BASE_COOLDOWN_MS, DURABLE_ERROR_RECOVERY_MAX_COOLDOWN_MS, SelfHealingManager;
92458
93529
  var init_self_healing = __esm({
92459
93530
  "../engine/src/self-healing.ts"() {
92460
93531
  "use strict";
@@ -92464,7 +93535,7 @@ var init_self_healing = __esm({
92464
93535
  init_restart_recovery_coordinator();
92465
93536
  init_transient_error_detector();
92466
93537
  log18 = createLogger2("self-healing");
92467
- execAsync7 = promisify9(exec9);
93538
+ execAsync7 = promisify10(exec9);
92468
93539
  APPROVED_TRIAGE_RECOVERY_GRACE_MS = 6e4;
92469
93540
  ORPHANED_EXECUTION_RECOVERY_GRACE_MS = 6e4;
92470
93541
  ACTIVE_MERGE_STATUSES = /* @__PURE__ */ new Set(["merging", "merging-pr", "merging-fix"]);
@@ -92480,6 +93551,7 @@ var init_self_healing = __esm({
92480
93551
  ORPHANED_WITH_WORKTREE_GRACE_MS = 3e5;
92481
93552
  MAX_TASK_DONE_RETRIES = 3;
92482
93553
  MAX_AUTO_MERGE_RETRIES = 3;
93554
+ MAX_STARVATION_DROPS = 3;
92483
93555
  DEADLOCK_RECOVERY_COOLDOWN_MS = 15 * 6e4;
92484
93556
  DEFAULT_STALE_MERGING_STATUS_MIN_AGE_MS = 5 * 6e4;
92485
93557
  DURABLE_ERROR_RECOVERY_MAX_RETRIES = 5;
@@ -92502,6 +93574,7 @@ var init_self_healing = __esm({
92502
93574
  settingsListener = null;
92503
93575
  // ── Per-task deadlock recovery cooldown ─────────────────────────────
92504
93576
  deadlockRecoveryCooldown = /* @__PURE__ */ new Map();
93577
+ mergeStarvationDrops = /* @__PURE__ */ new Map();
92505
93578
  // ── Lifecycle ───────────────────────────────────────────────────────
92506
93579
  start() {
92507
93580
  this.settingsListener = ({ settings, previous }) => {
@@ -93225,6 +94298,7 @@ var init_self_healing = __esm({
93225
94298
  try {
93226
94299
  log18.warn(`Clearing stale merge status for ${task.id}: ${previousStatus}`);
93227
94300
  await this.store.updateTask(task.id, { status: null });
94301
+ this.options.clearMergeActive?.(task.id);
93228
94302
  await this.store.logEntry(
93229
94303
  task.id,
93230
94304
  `Auto-recovered: cleared stale '${previousStatus}' status (no active merger)`
@@ -93295,6 +94369,8 @@ var init_self_healing = __esm({
93295
94369
  reason = `blocker ${blockerId} in-review + paused`;
93296
94370
  } else if (blocker.column === "in-review" && blocker.status === "failed" && (blocker.mergeRetries ?? 0) >= MAX_AUTO_MERGE_RETRIES) {
93297
94371
  reason = `blocker ${blockerId} in-review + failed (mergeRetries ${blocker.mergeRetries ?? 0}/${MAX_AUTO_MERGE_RETRIES})`;
94372
+ } else if (blocker.column === "in-review" && blocker.status === "failed" && isMissingWorktreeSessionStartFailure(blocker.error)) {
94373
+ reason = `blocker ${blockerId} in-review + failed (missing-worktree session start)`;
93298
94374
  } else if (task.dependencies.length > 0 && !unresolvedDeps.includes(blockerId)) {
93299
94375
  reason = `blocker ${blockerId} not among unresolved dependencies`;
93300
94376
  }
@@ -93404,7 +94480,7 @@ var init_self_healing = __esm({
93404
94480
  if (settings.globalPause || settings.enginePaused) return 0;
93405
94481
  const tasks = await this.store.listTasks({ column: "in-review", slim: true });
93406
94482
  const mergeable = tasks.filter(
93407
- (t) => t.column === "in-review" && !t.paused && // Exclude transient merge statuses. Active merges should be left alone;
94483
+ (t) => t.column === "in-review" && !t.paused && t.status !== "failed" && // Exclude transient merge statuses. Active merges should be left alone;
93408
94484
  // stale ones are handled by recoverStaleMergingStatus().
93409
94485
  t.status !== "merging" && t.status !== "merging-pr" && Boolean(t.worktree) && t.mergeDetails?.mergeConfirmed !== true && t.mergeDetails?.noOpMerge !== true && !hasTerminalInvalidDoneTransition(t) && // Mirror ProjectEngine.canMergeTask retry gate. If retries are already
93410
94486
  // exhausted, re-enqueueing here is a no-op and each recovery log write
@@ -93413,6 +94489,13 @@ var init_self_healing = __esm({
93413
94489
  // in case updateTask(moveTask) is briefly out-of-order during recovery.
93414
94490
  (t.mergeRetries ?? 0) < MAX_AUTO_MERGE_RETRIES && getTaskMergeBlocker(t) === void 0
93415
94491
  );
94492
+ const inReviewIds = new Set(tasks.map((task) => task.id));
94493
+ const mergeableIds = new Set(mergeable.map((task) => task.id));
94494
+ for (const taskId of [...this.mergeStarvationDrops.keys()]) {
94495
+ if (!inReviewIds.has(taskId) || !mergeableIds.has(taskId)) {
94496
+ this.mergeStarvationDrops.delete(taskId);
94497
+ }
94498
+ }
93416
94499
  if (mergeable.length === 0) return 0;
93417
94500
  log18.warn(`Found ${mergeable.length} mergeable review task(s) stuck in in-review`);
93418
94501
  const enqueueMerge = this.options.enqueueMerge;
@@ -93420,7 +94503,23 @@ var init_self_healing = __esm({
93420
94503
  for (const task of mergeable) {
93421
94504
  try {
93422
94505
  if (enqueueMerge) {
93423
- enqueueMerge(task.id);
94506
+ const queued = enqueueMerge(task.id);
94507
+ if (!queued) {
94508
+ const drops = (this.mergeStarvationDrops.get(task.id) ?? 0) + 1;
94509
+ this.mergeStarvationDrops.set(task.id, drops);
94510
+ log18.warn(
94511
+ `Auto-recovery enqueue dropped for ${task.id} (${drops}/${MAX_STARVATION_DROPS}); engine merge queue rejected re-enqueue`
94512
+ );
94513
+ if (drops >= MAX_STARVATION_DROPS) {
94514
+ const error = `Auto-merge starvation: ${MAX_STARVATION_DROPS} consecutive enqueue attempts were dropped by the engine merge queue; task requires manual intervention.`;
94515
+ await this.store.updateTask(task.id, { status: "failed", error });
94516
+ await this.store.logEntry(task.id, error);
94517
+ this.mergeStarvationDrops.delete(task.id);
94518
+ recovered++;
94519
+ }
94520
+ continue;
94521
+ }
94522
+ this.mergeStarvationDrops.delete(task.id);
93424
94523
  } else {
93425
94524
  await this.store.mergeTask(task.id);
93426
94525
  }
@@ -94470,16 +95569,18 @@ var init_self_healing = __esm({
94470
95569
  for (const task of candidates) {
94471
95570
  try {
94472
95571
  const staleWorktree = task.worktree;
95572
+ const missingWorktreePath = extractMissingWorktreePathFromSessionStartFailure(task.error);
95573
+ const hasMismatchedLiveWorktree = typeof staleWorktree === "string" && staleWorktree.length > 0 && typeof missingWorktreePath === "string" && missingWorktreePath.length > 0 && resolve20(staleWorktree) !== resolve20(missingWorktreePath);
94473
95574
  await this.store.updateTask(task.id, {
94474
95575
  status: null,
94475
95576
  error: null,
94476
- worktree: null,
94477
- branch: null,
95577
+ worktree: hasMismatchedLiveWorktree ? staleWorktree : null,
95578
+ branch: hasMismatchedLiveWorktree ? task.branch ?? null : null,
94478
95579
  sessionFile: null
94479
95580
  });
94480
95581
  await this.store.logEntry(
94481
95582
  task.id,
94482
- `Auto-recovered: retry/verification session targeted missing worktree${staleWorktree ? ` (${staleWorktree})` : ""} \u2014 cleared stale session metadata and requeued to todo`
95583
+ hasMismatchedLiveWorktree ? `Auto-recovered: stale resume referenced missing worktree (${missingWorktreePath}) while live task worktree is ${staleWorktree} \u2014 cleared stale session metadata and requeued to todo` : `Auto-recovered: retry/verification session targeted missing worktree${staleWorktree ? ` (${staleWorktree})` : ""} \u2014 cleared stale session metadata and requeued to todo`
94483
95584
  );
94484
95585
  await this.store.moveTask(task.id, "todo", { preserveProgress: true });
94485
95586
  recovered++;
@@ -94873,7 +95974,7 @@ var init_self_healing = __esm({
94873
95974
  if (idle.length === 0) return;
94874
95975
  const withMtime = idle.map((p) => {
94875
95976
  try {
94876
- return { path: p, mtime: statSync5(p).mtimeMs };
95977
+ return { path: p, mtime: statSync6(p).mtimeMs };
94877
95978
  } catch (err) {
94878
95979
  const errorMessage = err instanceof Error ? err.message : String(err);
94879
95980
  log18.warn(`Failed to read mtime for worktree ${p}: ${errorMessage} \u2014 defaulting mtime to 0`);
@@ -96088,6 +97189,7 @@ var init_in_process_runtime = __esm({
96088
97189
  * before `start()` via `setMergeEnqueuer`.
96089
97190
  */
96090
97191
  mergeEnqueuer;
97192
+ clearMergeActive;
96091
97193
  activeMergeTaskIdProvider;
96092
97194
  /** Tracks whether startup recovery was intentionally deferred due to pause state. */
96093
97195
  startupRecoveryDeferred = false;
@@ -96485,7 +97587,8 @@ var init_in_process_runtime = __esm({
96485
97587
  recoverApprovedTriageTask: (task) => this.triageProcessor?.recoverApprovedTask(task) ?? Promise.resolve(false),
96486
97588
  getPlanningTaskIds: () => this.triageProcessor?.getProcessingTaskIds() ?? /* @__PURE__ */ new Set(),
96487
97589
  evictStaleTriageProcessing: () => this.triageProcessor?.evictStaleProcessing() ?? /* @__PURE__ */ new Set(),
96488
- enqueueMerge: this.mergeEnqueuer ? (taskId) => this.mergeEnqueuer?.(taskId) : void 0,
97590
+ enqueueMerge: this.mergeEnqueuer ? (taskId) => this.mergeEnqueuer?.(taskId) ?? false : void 0,
97591
+ clearMergeActive: this.clearMergeActive ? (taskId) => this.clearMergeActive?.(taskId) : void 0,
96489
97592
  getActiveMergeTaskId: () => this.activeMergeTaskIdProvider?.() ?? null,
96490
97593
  leaseManager: this.leaseManager,
96491
97594
  hasActiveAgentExecution: (agentId) => this.heartbeatMonitor?.getTrackedAgents().includes(agentId) ?? false,
@@ -96679,6 +97782,9 @@ var init_in_process_runtime = __esm({
96679
97782
  setMergeEnqueuer(enqueueMerge) {
96680
97783
  this.mergeEnqueuer = enqueueMerge;
96681
97784
  }
97785
+ setMergeActiveClearer(clearMergeActive) {
97786
+ this.clearMergeActive = clearMergeActive;
97787
+ }
96682
97788
  setActiveMergeTaskIdProvider(getActiveMergeTaskId) {
96683
97789
  this.activeMergeTaskIdProvider = getActiveMergeTaskId;
96684
97790
  }
@@ -98783,8 +99889,8 @@ var init_provider_adapters = __esm({
98783
99889
 
98784
99890
  // ../engine/src/remote-access/tunnel-process-manager.ts
98785
99891
  import { EventEmitter as EventEmitter23 } from "node:events";
98786
- import { exec as exec10, execFile as execFile4, spawn as spawn5 } from "node:child_process";
98787
- import { promisify as promisify10 } from "node:util";
99892
+ import { exec as exec10, execFile as execFile5, spawn as spawn5 } from "node:child_process";
99893
+ import { promisify as promisify11 } from "node:util";
98788
99894
  function nowIso2() {
98789
99895
  return (/* @__PURE__ */ new Date()).toISOString();
98790
99896
  }
@@ -98824,7 +99930,7 @@ function toStateError(code, err) {
98824
99930
  at: nowIso2()
98825
99931
  };
98826
99932
  }
98827
- var DEFAULT_MAX_LOG_ENTRIES, DEFAULT_STOP_TIMEOUT_MS2, execFileAsync2, execAsync8, LineBuffer, TunnelProcessManager;
99933
+ var DEFAULT_MAX_LOG_ENTRIES, DEFAULT_STOP_TIMEOUT_MS2, execFileAsync3, execAsync8, LineBuffer, TunnelProcessManager;
98828
99934
  var init_tunnel_process_manager = __esm({
98829
99935
  "../engine/src/remote-access/tunnel-process-manager.ts"() {
98830
99936
  "use strict";
@@ -98832,8 +99938,8 @@ var init_tunnel_process_manager = __esm({
98832
99938
  init_provider_adapters();
98833
99939
  DEFAULT_MAX_LOG_ENTRIES = 400;
98834
99940
  DEFAULT_STOP_TIMEOUT_MS2 = 5e3;
98835
- execFileAsync2 = promisify10(execFile4);
98836
- execAsync8 = promisify10(exec10);
99941
+ execFileAsync3 = promisify11(execFile5);
99942
+ execAsync8 = promisify11(exec10);
98837
99943
  LineBuffer = class {
98838
99944
  pending = "";
98839
99945
  push(chunk) {
@@ -98910,7 +100016,7 @@ var init_tunnel_process_manager = __esm({
98910
100016
  return null;
98911
100017
  }
98912
100018
  try {
98913
- const { stdout } = await execFileAsync2("tailscale", ["status", "--json"], { timeout: 3e3 });
100019
+ const { stdout } = await execFileAsync3("tailscale", ["status", "--json"], { timeout: 3e3 });
98914
100020
  const data = JSON.parse(String(stdout));
98915
100021
  const dnsName = data.Self?.DNSName?.replace(/\.$/, "");
98916
100022
  if (!dnsName) {
@@ -98933,7 +100039,7 @@ var init_tunnel_process_manager = __esm({
98933
100039
  ];
98934
100040
  for (const resetCommand of resetCommands) {
98935
100041
  try {
98936
- await execFileAsync2(resetCommand.command, resetCommand.args, { timeout: 5e3 });
100042
+ await execFileAsync3(resetCommand.command, resetCommand.args, { timeout: 5e3 });
98937
100043
  return;
98938
100044
  } catch {
98939
100045
  }
@@ -99232,8 +100338,8 @@ var init_tunnel_process_manager = __esm({
99232
100338
  });
99233
100339
 
99234
100340
  // ../engine/src/project-engine.ts
99235
- import { execFile as execFile5 } from "node:child_process";
99236
- import { promisify as promisify11 } from "node:util";
100341
+ import { execFile as execFile6 } from "node:child_process";
100342
+ import { promisify as promisify12 } from "node:util";
99237
100343
  function formatErrorDetails(error) {
99238
100344
  if (error instanceof Error) {
99239
100345
  return {
@@ -99248,7 +100354,7 @@ function isInvalidDoneTransitionError(error) {
99248
100354
  const message = error instanceof Error ? error.message : String(error);
99249
100355
  return message.includes("Invalid transition:") && message.includes("\u2192 'done'");
99250
100356
  }
99251
- var execFileAsync3, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
100357
+ var execFileAsync4, MERGE_HANDOFF_GRACE_MS, isRemoteActive, ProjectEngine;
99252
100358
  var init_project_engine = __esm({
99253
100359
  "../engine/src/project-engine.ts"() {
99254
100360
  "use strict";
@@ -99266,7 +100372,7 @@ var init_project_engine = __esm({
99266
100372
  init_research_orchestrator();
99267
100373
  init_research_step_runner();
99268
100374
  init_tunnel_process_manager();
99269
- execFileAsync3 = promisify11(execFile5);
100375
+ execFileAsync4 = promisify12(execFile6);
99270
100376
  MERGE_HANDOFF_GRACE_MS = 300;
99271
100377
  isRemoteActive = (ra) => ra?.activeProvider != null && (ra.providers[ra.activeProvider]?.enabled ?? false);
99272
100378
  ProjectEngine = class _ProjectEngine {
@@ -99285,7 +100391,10 @@ var init_project_engine = __esm({
99285
100391
  this.activeMergeTaskId = null;
99286
100392
  }
99287
100393
  this.mergeActive.delete(taskId);
99288
- this.internalEnqueueMerge(taskId);
100394
+ return this.internalEnqueueMerge(taskId);
100395
+ });
100396
+ this.runtime.setMergeActiveClearer?.((taskId) => {
100397
+ this.mergeActive.delete(taskId);
99289
100398
  });
99290
100399
  }
99291
100400
  runtime;
@@ -99320,6 +100429,7 @@ var init_project_engine = __esm({
99320
100429
  mergeAbortController = null;
99321
100430
  mergeRetryTimer = null;
99322
100431
  autostashSweepTimer = null;
100432
+ mergeActiveReconcileTimer = null;
99323
100433
  /**
99324
100434
  * Pending manual merge resolvers — keyed by taskId.
99325
100435
  * When `onMerge` is called, the task is enqueued like auto-merge but a
@@ -99515,6 +100625,7 @@ ${detail}`
99515
100625
  this.wireAutostashOrphanRecovery(store);
99516
100626
  await this.startupMergeSweep(store);
99517
100627
  this.scheduleMergeRetry(store);
100628
+ this.scheduleMergeActiveReconciliation(settings.maintenanceIntervalMs ?? 9e5);
99518
100629
  void this.runStaleAutostashSweep(store, "startup");
99519
100630
  this.scheduleStaleAutostashSweep(store);
99520
100631
  this.started = true;
@@ -99540,6 +100651,10 @@ ${detail}`
99540
100651
  clearTimeout(this.autostashSweepTimer);
99541
100652
  this.autostashSweepTimer = null;
99542
100653
  }
100654
+ if (this.mergeActiveReconcileTimer) {
100655
+ clearInterval(this.mergeActiveReconcileTimer);
100656
+ this.mergeActiveReconcileTimer = null;
100657
+ }
99543
100658
  this.mergeAbortController?.abort();
99544
100659
  this.mergeAbortController = null;
99545
100660
  this.activeMergeTaskId = null;
@@ -99761,7 +100876,7 @@ ${detail}`
99761
100876
  * an external `onMerge` callback (e.g. dashboard's createServer call).
99762
100877
  */
99763
100878
  enqueueMerge(taskId) {
99764
- this.internalEnqueueMerge(taskId);
100879
+ return this.internalEnqueueMerge(taskId);
99765
100880
  }
99766
100881
  /**
99767
100882
  * Perform an AI-powered merge for a task, serialized through the merge queue.
@@ -99778,7 +100893,10 @@ ${detail}`
99778
100893
  }
99779
100894
  return new Promise((resolve24, reject) => {
99780
100895
  this.manualMergeResolvers.set(taskId, { resolve: resolve24, reject });
99781
- this.internalEnqueueMerge(taskId);
100896
+ if (!this.internalEnqueueMerge(taskId)) {
100897
+ this.manualMergeResolvers.delete(taskId);
100898
+ reject(new Error(`Merge enqueue rejected for ${taskId}`));
100899
+ }
99782
100900
  });
99783
100901
  }
99784
100902
  setRestoreDiagnostics(outcome, reason, provider, message) {
@@ -99956,7 +101074,7 @@ ${detail}`
99956
101074
  async checkExecutableAvailable(command) {
99957
101075
  const checker = process.platform === "win32" ? "where" : "which";
99958
101076
  try {
99959
- await execFileAsync3(checker, [command]);
101077
+ await execFileAsync4(checker, [command]);
99960
101078
  return { available: true };
99961
101079
  } catch {
99962
101080
  return {
@@ -100045,15 +101163,17 @@ ${detail}`
100045
101163
  return void 0;
100046
101164
  }
100047
101165
  internalEnqueueMerge(taskId) {
100048
- if (this.shuttingDown) return;
101166
+ if (this.shuttingDown) return false;
100049
101167
  if (this.mergeActive.has(taskId)) {
100050
101168
  const isActuallyLive = this.mergeQueue.includes(taskId) || this.activeMergeTaskId === taskId;
100051
101169
  if (!isActuallyLive) {
100052
101170
  runtimeLog.warn(
100053
- `internalEnqueueMerge(${taskId}): skipped \u2014 mergeActive entry is leaked (not queued, not active). reconcileStaleMergeActive() will clear it on the next sweep.`
101171
+ `internalEnqueueMerge(${taskId}): skipped \u2014 mergeActive entry is leaked (not queued, not active). Reconciling stale entry and retrying enqueue now.`
100054
101172
  );
101173
+ this.mergeActive.delete(taskId);
101174
+ } else {
101175
+ return false;
100055
101176
  }
100056
- return;
100057
101177
  }
100058
101178
  this.mergeActive.add(taskId);
100059
101179
  this.mergeQueue.push(taskId);
@@ -100062,6 +101182,7 @@ ${detail}`
100062
101182
  `Merge queue drain failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
100063
101183
  );
100064
101184
  });
101185
+ return true;
100065
101186
  }
100066
101187
  /**
100067
101188
  * Filter a sweep's listTasks() result to merge-eligible tasks, sort by
@@ -100080,6 +101201,27 @@ ${detail}`
100080
101201
  }
100081
101202
  return eligible.length;
100082
101203
  }
101204
+ reconcileStaleMergeActive() {
101205
+ let cleared = 0;
101206
+ for (const taskId of [...this.mergeActive]) {
101207
+ if (taskId === this.activeMergeTaskId) continue;
101208
+ if (this.mergeQueue.includes(taskId)) continue;
101209
+ this.mergeActive.delete(taskId);
101210
+ cleared++;
101211
+ }
101212
+ return cleared;
101213
+ }
101214
+ scheduleMergeActiveReconciliation(intervalMs) {
101215
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
101216
+ return;
101217
+ }
101218
+ this.mergeActiveReconcileTimer = setInterval(() => {
101219
+ const cleared = this.reconcileStaleMergeActive();
101220
+ if (cleared > 0) {
101221
+ runtimeLog.warn(`Reconciled ${cleared} stale mergeActive entr${cleared === 1 ? "y" : "ies"}`);
101222
+ }
101223
+ }, intervalMs);
101224
+ }
100083
101225
  async findActiveRecoveryFollowUp(store, parentTaskId, branch) {
100084
101226
  const tasks = await store.listTasks({ slim: true }).catch(() => []);
100085
101227
  const activeRecoveryTasks = tasks.filter(
@@ -100099,6 +101241,7 @@ ${detail}`
100099
101241
  if (this.mergeRunning) return;
100100
101242
  this.mergeRunning = true;
100101
101243
  try {
101244
+ this.reconcileStaleMergeActive();
100102
101245
  const store = this.runtime.getTaskStore();
100103
101246
  const cwd = this.config.workingDirectory;
100104
101247
  while (this.mergeQueue.length > 0 && !this.shuttingDown) {
@@ -100878,6 +102021,21 @@ ${detail}`
100878
102021
  };
100879
102022
  store.on("settings:updated", onEngineUnpause);
100880
102023
  this.settingsHandlers.push(onEngineUnpause);
102024
+ const onMaintenanceIntervalChange = ({
102025
+ settings: s,
102026
+ previous: prev
102027
+ }) => {
102028
+ if (s.maintenanceIntervalMs === prev.maintenanceIntervalMs) {
102029
+ return;
102030
+ }
102031
+ if (this.mergeActiveReconcileTimer) {
102032
+ clearInterval(this.mergeActiveReconcileTimer);
102033
+ this.mergeActiveReconcileTimer = null;
102034
+ }
102035
+ this.scheduleMergeActiveReconciliation(s.maintenanceIntervalMs ?? 9e5);
102036
+ };
102037
+ store.on("settings:updated", onMaintenanceIntervalChange);
102038
+ this.settingsHandlers.push(onMaintenanceIntervalChange);
100881
102039
  const onStuckTimeoutChange = async ({
100882
102040
  settings: s,
100883
102041
  previous: prev
@@ -101977,6 +103135,7 @@ __export(src_exports2, {
101977
103135
  applyAutostashBySha: () => applyAutostashBySha,
101978
103136
  applyUnavailableNodePolicy: () => applyUnavailableNodePolicy,
101979
103137
  assertSafeUrl: () => assertSafeUrl,
103138
+ auditSquashMerge: () => auditSquashMerge,
101980
103139
  buildAgentChatPrompt: () => buildAgentChatPrompt,
101981
103140
  buildNtfyClickUrl: () => buildNtfyClickUrl,
101982
103141
  buildRuntimeResolutionContext: () => buildRuntimeResolutionContext,
@@ -102002,6 +103161,7 @@ __export(src_exports2, {
102002
103161
  extractRuntimeHint: () => extractRuntimeHint,
102003
103162
  extractRuntimeModel: () => extractRuntimeModel,
102004
103163
  fetchWebContent: () => fetchWebContent,
103164
+ formatSquashAuditReport: () => formatSquashAuditReport,
102005
103165
  formatTaskIdentifier: () => formatTaskIdentifier,
102006
103166
  generateReservedWorktreeName: () => generateReservedWorktreeName,
102007
103167
  generateWorktreeName: () => generateWorktreeName,
@@ -102056,6 +103216,7 @@ var init_src2 = __esm({
102056
103216
  init_mission_autopilot();
102057
103217
  init_mission_execution_loop();
102058
103218
  init_merger();
103219
+ init_merger_squash_audit();
102059
103220
  init_reviewer();
102060
103221
  init_pi();
102061
103222
  init_pi();
@@ -107101,13 +108262,13 @@ var init_github_poll = __esm({
107101
108262
  });
107102
108263
 
107103
108264
  // ../dashboard/src/routes/resolve-diff-base.ts
107104
- import { execFile as execFile6 } from "node:child_process";
107105
- import { promisify as promisify12 } from "node:util";
107106
- var execFileAsync4;
108265
+ import { execFile as execFile7 } from "node:child_process";
108266
+ import { promisify as promisify13 } from "node:util";
108267
+ var execFileAsync5;
107107
108268
  var init_resolve_diff_base = __esm({
107108
108269
  "../dashboard/src/routes/resolve-diff-base.ts"() {
107109
108270
  "use strict";
107110
- execFileAsync4 = promisify12(execFile6);
108271
+ execFileAsync5 = promisify13(execFile7);
107111
108272
  }
107112
108273
  });
107113
108274
 
@@ -112299,13 +113460,13 @@ var init_register_agents_projects_nodes = __esm({
112299
113460
  });
112300
113461
 
112301
113462
  // ../dashboard/src/exec-file.ts
112302
- import { execFile as execFile7 } from "node:child_process";
112303
- import { promisify as promisify13 } from "node:util";
112304
- var execFileAsync5;
113463
+ import { execFile as execFile8 } from "node:child_process";
113464
+ import { promisify as promisify14 } from "node:util";
113465
+ var execFileAsync6;
112305
113466
  var init_exec_file = __esm({
112306
113467
  "../dashboard/src/exec-file.ts"() {
112307
113468
  "use strict";
112308
- execFileAsync5 = promisify13(execFile7);
113469
+ execFileAsync6 = promisify14(execFile8);
112309
113470
  }
112310
113471
  });
112311
113472
 
@@ -117849,7 +119010,7 @@ var require_websocket = __commonJS({
117849
119010
  var http = __require("http");
117850
119011
  var net = __require("net");
117851
119012
  var tls = __require("tls");
117852
- var { randomBytes: randomBytes3, createHash: createHash8 } = __require("crypto");
119013
+ var { randomBytes: randomBytes3, createHash: createHash9 } = __require("crypto");
117853
119014
  var { Duplex, Readable } = __require("stream");
117854
119015
  var { URL: URL2 } = __require("url");
117855
119016
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -118509,7 +119670,7 @@ var require_websocket = __commonJS({
118509
119670
  abortHandshake(websocket, socket, "Invalid Upgrade header");
118510
119671
  return;
118511
119672
  }
118512
- const digest = createHash8("sha1").update(key + GUID).digest("base64");
119673
+ const digest = createHash9("sha1").update(key + GUID).digest("base64");
118513
119674
  if (res.headers["sec-websocket-accept"] !== digest) {
118514
119675
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
118515
119676
  return;
@@ -118876,7 +120037,7 @@ var require_websocket_server = __commonJS({
118876
120037
  var EventEmitter32 = __require("events");
118877
120038
  var http = __require("http");
118878
120039
  var { Duplex } = __require("stream");
118879
- var { createHash: createHash8 } = __require("crypto");
120040
+ var { createHash: createHash9 } = __require("crypto");
118880
120041
  var extension2 = require_extension();
118881
120042
  var PerMessageDeflate2 = require_permessage_deflate();
118882
120043
  var subprotocol2 = require_subprotocol();
@@ -119177,7 +120338,7 @@ var require_websocket_server = __commonJS({
119177
120338
  );
119178
120339
  }
119179
120340
  if (this._state > RUNNING) return abortHandshake(socket, 503);
119180
- const digest = createHash8("sha1").update(key + GUID).digest("base64");
120341
+ const digest = createHash9("sha1").update(key + GUID).digest("base64");
119181
120342
  const headers = [
119182
120343
  "HTTP/1.1 101 Switching Protocols",
119183
120344
  "Upgrade: websocket",
@@ -119607,7 +120768,7 @@ __export(task_exports, {
119607
120768
  runTaskUpdate: () => runTaskUpdate
119608
120769
  });
119609
120770
  import { createInterface as createInterface2 } from "node:readline/promises";
119610
- import { watchFile, unwatchFile, statSync as statSync6, existsSync as existsSync35, readFileSync as readFileSync13 } from "node:fs";
120771
+ import { watchFile, unwatchFile, statSync as statSync7, existsSync as existsSync35, readFileSync as readFileSync13 } from "node:fs";
119611
120772
  import { basename as basename10, join as join46 } from "node:path";
119612
120773
  function getGitHubIssueUrl(sourceMetadata) {
119613
120774
  if (!sourceMetadata || typeof sourceMetadata !== "object") return void 0;
@@ -119929,7 +121090,7 @@ async function runTaskLogs(id, options = {}, projectName) {
119929
121090
  let lastPosition = 0;
119930
121091
  let lastSize = 0;
119931
121092
  try {
119932
- const stats = statSync6(logPath);
121093
+ const stats = statSync7(logPath);
119933
121094
  lastSize = stats.size;
119934
121095
  lastPosition = lastSize;
119935
121096
  } catch {
@@ -119946,7 +121107,7 @@ async function runTaskLogs(id, options = {}, projectName) {
119946
121107
  watchFile(logPath, { interval: 1e3 }, () => {
119947
121108
  if (isShuttingDown) return;
119948
121109
  try {
119949
- const stats = statSync6(logPath);
121110
+ const stats = statSync7(logPath);
119950
121111
  if (stats.size < lastPosition) {
119951
121112
  lastPosition = 0;
119952
121113
  }
@@ -121347,6 +122508,9 @@ function kbExtension(pi) {
121347
122508
  Type8.String({
121348
122509
  description: "Agent ID to assign this task to (e.g. 'agent-abc123')"
121349
122510
  })
122511
+ ),
122512
+ priority: Type8.Optional(
122513
+ StringEnum([...TASK_PRIORITIES], { description: "Task priority (low, normal, high, urgent)" })
121350
122514
  )
121351
122515
  }),
121352
122516
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -121363,31 +122527,45 @@ function kbExtension(pi) {
121363
122527
  };
121364
122528
  }
121365
122529
  }
121366
- const task = await store.createTask({
121367
- description: params.description.trim(),
121368
- dependencies: params.depends,
121369
- assignedAgentId: normalizedAgentId === null ? void 0 : normalizedAgentId,
121370
- source: { sourceType: "api" }
121371
- });
121372
- const label = task.description.length > 80 ? task.description.slice(0, 80) + "\u2026" : task.description;
121373
- return {
121374
- content: [
121375
- {
121376
- type: "text",
121377
- text: `Created ${task.id}: ${label}
122530
+ try {
122531
+ const task = await store.createTask({
122532
+ description: params.description.trim(),
122533
+ dependencies: params.depends,
122534
+ assignedAgentId: normalizedAgentId === null ? void 0 : normalizedAgentId,
122535
+ priority: params.priority,
122536
+ source: { sourceType: "api" }
122537
+ });
122538
+ const label = task.description.length > 80 ? task.description.slice(0, 80) + "\u2026" : task.description;
122539
+ return {
122540
+ content: [
122541
+ {
122542
+ type: "text",
122543
+ text: `Created ${task.id}: ${label}
121378
122544
  Column: triage
121379
122545
  ` + (task.dependencies.length ? `Dependencies: ${task.dependencies.join(", ")}
121380
122546
  ` : "") + (task.assignedAgentId ? `Assigned to: ${task.assignedAgentId}
121381
- ` : "") + `Path: .fusion/tasks/${task.id}/`
122547
+ ` : "") + `Priority: ${task.priority}
122548
+ Path: .fusion/tasks/${task.id}/`
122549
+ }
122550
+ ],
122551
+ details: {
122552
+ taskId: task.id,
122553
+ column: task.column,
122554
+ dependencies: task.dependencies,
122555
+ assignedAgentId: task.assignedAgentId,
122556
+ priority: task.priority
121382
122557
  }
121383
- ],
121384
- details: {
121385
- taskId: task.id,
121386
- column: task.column,
121387
- dependencies: task.dependencies,
121388
- assignedAgentId: task.assignedAgentId
122558
+ };
122559
+ } catch (error) {
122560
+ if (error instanceof Error && error.message.startsWith("Task ID already exists:")) {
122561
+ return {
122562
+ content: [{ type: "text", text: `ERROR: ${error.message}` }],
122563
+ isError: true,
122564
+ details: { error: error.message }
122565
+ };
121389
122566
  }
121390
- };
122567
+ throw error;
122568
+ }
121391
122569
  }
121392
122570
  });
121393
122571
  pi.registerTool({
@@ -123329,25 +124507,36 @@ ${lines.join("\n\n")}` }],
123329
124507
  const agentStore = new AgentStore2({ rootDir: getFusionDir(ctx.cwd) });
123330
124508
  await agentStore.init();
123331
124509
  const agent = await agentStore.getAgent(params.agent_id);
123332
- const store = await getStore2(ctx.cwd);
123333
- const task = await store.createTask({
123334
- description: params.description,
123335
- dependencies: params.dependencies,
123336
- column: "todo",
123337
- assignedAgentId: params.agent_id,
123338
- source: {
123339
- sourceType: "api",
123340
- ...params.override === true ? { sourceMetadata: { executorRoleOverride: true } } : {}
124510
+ try {
124511
+ const store = await getStore2(ctx.cwd);
124512
+ const task = await store.createTask({
124513
+ description: params.description,
124514
+ dependencies: params.dependencies,
124515
+ column: "todo",
124516
+ assignedAgentId: params.agent_id,
124517
+ source: {
124518
+ sourceType: "api",
124519
+ ...params.override === true ? { sourceMetadata: { executorRoleOverride: true } } : {}
124520
+ }
124521
+ });
124522
+ const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
124523
+ return {
124524
+ content: [{
124525
+ type: "text",
124526
+ text: `Delegated to ${agent.name} (${agent.id}): Created ${task.id}${deps}. The task will be picked up by ${agent.name} on their next heartbeat cycle.`
124527
+ }],
124528
+ details: { taskId: task.id, agentId: agent.id, agentName: agent.name }
124529
+ };
124530
+ } catch (error) {
124531
+ if (error instanceof Error && error.message.startsWith("Task ID already exists:")) {
124532
+ return {
124533
+ content: [{ type: "text", text: `ERROR: ${error.message}` }],
124534
+ isError: true,
124535
+ details: { error: error.message }
124536
+ };
123341
124537
  }
123342
- });
123343
- const deps = task.dependencies.length ? ` (depends on: ${task.dependencies.join(", ")})` : "";
123344
- return {
123345
- content: [{
123346
- type: "text",
123347
- text: `Delegated to ${agent.name} (${agent.id}): Created ${task.id}${deps}. The task will be picked up by ${agent.name} on their next heartbeat cycle.`
123348
- }],
123349
- details: { taskId: task.id, agentId: agent.id, agentName: agent.name }
123350
- };
124538
+ throw error;
124539
+ }
123351
124540
  }
123352
124541
  });
123353
124542
  pi.registerTool({