@runfusion/fusion 0.1.0 → 0.1.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.
package/dist/extension.js CHANGED
@@ -1974,6 +1974,18 @@ function fromJson(json) {
1974
1974
  return void 0;
1975
1975
  }
1976
1976
  }
1977
+ function probeFts5(db) {
1978
+ if (process.env.FUSION_DISABLE_FTS5 === "1" || process.env.FUSION_DISABLE_FTS5 === "true") {
1979
+ return false;
1980
+ }
1981
+ try {
1982
+ db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS __fusion_fts5_probe USING fts5(x)");
1983
+ db.exec("DROP TABLE IF EXISTS __fusion_fts5_probe");
1984
+ return true;
1985
+ } catch {
1986
+ return false;
1987
+ }
1988
+ }
1977
1989
  function normalizeTaskComments(steeringComments, comments) {
1978
1990
  const normalizedComments = [];
1979
1991
  const seenKeys = /* @__PURE__ */ new Set();
@@ -2494,6 +2506,7 @@ CREATE INDEX IF NOT EXISTS idxInsightRunsProjectId
2494
2506
  dbPath;
2495
2507
  /** Tracks transaction nesting depth for savepoint-based nested transactions. */
2496
2508
  transactionDepth = 0;
2509
+ _fts5Available;
2497
2510
  constructor(kbDir) {
2498
2511
  this.dbPath = join(kbDir, "fusion.db");
2499
2512
  if (!isAbsolute(kbDir)) {
@@ -2506,6 +2519,16 @@ CREATE INDEX IF NOT EXISTS idxInsightRunsProjectId
2506
2519
  this.db.exec("PRAGMA journal_mode = WAL");
2507
2520
  this.db.exec("PRAGMA busy_timeout = 5000");
2508
2521
  this.db.exec("PRAGMA foreign_keys = ON");
2522
+ this._fts5Available = probeFts5(this.db);
2523
+ }
2524
+ /**
2525
+ * True when the underlying SQLite build has FTS5 (`CREATE VIRTUAL TABLE … USING fts5`).
2526
+ * Node's bundled SQLite only exposes FTS5 when built with `SQLITE_ENABLE_FTS5`;
2527
+ * older Node 22.x LTS builds do not. Consumers must fall back to LIKE-based scans
2528
+ * when this is false. Override with `FUSION_DISABLE_FTS5=1` to force the fallback path.
2529
+ */
2530
+ get fts5Available() {
2531
+ return this._fts5Available;
2509
2532
  }
2510
2533
  /**
2511
2534
  * Initialize the database: create tables if they don't exist
@@ -2815,6 +2838,9 @@ CREATE INDEX IF NOT EXISTS idxInsightRunsProjectId
2815
2838
  }
2816
2839
  if (version < 21) {
2817
2840
  this.applyMigration(21, () => {
2841
+ if (!this._fts5Available) {
2842
+ return;
2843
+ }
2818
2844
  this.db.exec(`
2819
2845
  CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
2820
2846
  id,
@@ -3224,6 +3250,9 @@ CREATE INDEX IF NOT EXISTS idxInsightRunsProjectId
3224
3250
  }
3225
3251
  if (version < 35) {
3226
3252
  this.applyMigration(35, () => {
3253
+ if (!this._fts5Available) {
3254
+ return;
3255
+ }
3227
3256
  const hasTaskTitle = this.hasColumn("tasks", "title");
3228
3257
  const updateColumns = hasTaskTitle ? "id, title, description, comments" : "id, description, comments";
3229
3258
  const oldTitle = hasTaskTitle ? "COALESCE(old.title, '')" : "''";
@@ -3517,6 +3546,9 @@ function resolveCreationRuntimeConfig(incoming, metadata) {
3517
3546
  return incoming;
3518
3547
  }
3519
3548
  const rc = { ...incoming ?? {} };
3549
+ if (typeof rc.enabled !== "boolean") {
3550
+ rc.enabled = true;
3551
+ }
3520
3552
  if (typeof rc.heartbeatIntervalMs !== "number" || !Number.isFinite(rc.heartbeatIntervalMs)) {
3521
3553
  rc.heartbeatIntervalMs = DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS;
3522
3554
  }
@@ -3562,6 +3594,7 @@ var init_agent_store = __esm({
3562
3594
  const _ = this.db;
3563
3595
  await mkdir(this.agentsDir, { recursive: true });
3564
3596
  await this.importLegacyFileDataOnce();
3597
+ await this.normalizeHeartbeatDefaultsOnce();
3565
3598
  }
3566
3599
  /**
3567
3600
  * One-way migration helper for projects that still have legacy agent JSON
@@ -3666,6 +3699,51 @@ var init_agent_store = __esm({
3666
3699
  `).run(migrationKey, migrationVersion);
3667
3700
  this.db.bumpLastModified();
3668
3701
  }
3702
+ /**
3703
+ * One-time normalization for durable agents created before the heartbeat
3704
+ * toggle was exposed in the UI. Those agents could persist
3705
+ * `runtimeConfig.enabled = false` even though users had no supported way to
3706
+ * manage that flag, which caused timers to stay disabled after restart.
3707
+ *
3708
+ * We normalize only once per project. After this migration lands, explicit
3709
+ * user choices are preserved because the version gate prevents reruns.
3710
+ */
3711
+ async normalizeHeartbeatDefaultsOnce() {
3712
+ const migrationKey = "agentHeartbeatDefaultVersion";
3713
+ const migrationVersion = "1";
3714
+ const row = this.db.prepare("SELECT value FROM __meta WHERE key = ?").get(migrationKey);
3715
+ if (row?.value === migrationVersion) {
3716
+ return;
3717
+ }
3718
+ const agents = await this.listAgents({ includeEphemeral: true });
3719
+ let changed = 0;
3720
+ for (const agent of agents) {
3721
+ if (isEphemeralAgent(agent)) {
3722
+ continue;
3723
+ }
3724
+ const nextRuntimeConfig = {
3725
+ ...resolveCreationRuntimeConfig(agent.runtimeConfig, agent.metadata) ?? {},
3726
+ enabled: true
3727
+ };
3728
+ const currentRuntimeConfig = agent.runtimeConfig ?? void 0;
3729
+ if (JSON.stringify(nextRuntimeConfig) === JSON.stringify(currentRuntimeConfig)) {
3730
+ continue;
3731
+ }
3732
+ await this.writeAgent({
3733
+ ...agent,
3734
+ runtimeConfig: nextRuntimeConfig
3735
+ });
3736
+ changed++;
3737
+ }
3738
+ this.db.prepare(`
3739
+ INSERT INTO __meta (key, value)
3740
+ VALUES (?, ?)
3741
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
3742
+ `).run(migrationKey, migrationVersion);
3743
+ if (changed > 0) {
3744
+ this.db.bumpLastModified();
3745
+ }
3746
+ }
3669
3747
  /**
3670
3748
  * Create a new agent with "idle" state.
3671
3749
  *
@@ -5775,11 +5853,12 @@ var init_global_settings = __esm({
5775
5853
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
5776
5854
  import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
5777
5855
  import { join as join5 } from "node:path";
5778
- var ARCHIVE_SCHEMA_SQL, ArchiveDatabase;
5856
+ var BASE_SCHEMA_SQL, FTS5_SCHEMA_SQL, ArchiveDatabase;
5779
5857
  var init_archive_db = __esm({
5780
5858
  "../core/src/archive-db.ts"() {
5781
5859
  "use strict";
5782
- ARCHIVE_SCHEMA_SQL = `
5860
+ init_db();
5861
+ BASE_SCHEMA_SQL = `
5783
5862
  CREATE TABLE IF NOT EXISTS archived_tasks (
5784
5863
  id TEXT PRIMARY KEY,
5785
5864
  taskJson TEXT NOT NULL,
@@ -5795,7 +5874,8 @@ CREATE TABLE IF NOT EXISTS archived_tasks (
5795
5874
 
5796
5875
  CREATE INDEX IF NOT EXISTS idxArchivedTasksArchivedAt ON archived_tasks(archivedAt);
5797
5876
  CREATE INDEX IF NOT EXISTS idxArchivedTasksCreatedAt ON archived_tasks(createdAt);
5798
-
5877
+ `;
5878
+ FTS5_SCHEMA_SQL = `
5799
5879
  CREATE VIRTUAL TABLE IF NOT EXISTS archived_tasks_fts USING fts5(
5800
5880
  id,
5801
5881
  title,
@@ -5823,18 +5903,26 @@ CREATE TRIGGER IF NOT EXISTS archived_tasks_fts_ad AFTER DELETE ON archived_task
5823
5903
  END;
5824
5904
  `;
5825
5905
  ArchiveDatabase = class {
5906
+ db;
5907
+ _fts5Available;
5826
5908
  constructor(kbDir) {
5827
- this.kbDir = kbDir;
5828
5909
  if (!existsSync4(kbDir)) {
5829
5910
  mkdirSync3(kbDir, { recursive: true });
5830
5911
  }
5831
5912
  this.db = new DatabaseSync2(join5(kbDir, "archive.db"));
5832
5913
  this.db.exec("PRAGMA journal_mode = WAL");
5833
5914
  this.db.exec("PRAGMA busy_timeout = 5000");
5915
+ this._fts5Available = probeFts5(this.db);
5916
+ }
5917
+ /** True when this SQLite build has FTS5. See db.ts#probeFts5. */
5918
+ get fts5Available() {
5919
+ return this._fts5Available;
5834
5920
  }
5835
- db;
5836
5921
  init() {
5837
- this.db.exec(ARCHIVE_SCHEMA_SQL);
5922
+ this.db.exec(BASE_SCHEMA_SQL);
5923
+ if (this._fts5Available) {
5924
+ this.db.exec(FTS5_SCHEMA_SQL);
5925
+ }
5838
5926
  this.addColumnIfMissing("archived_tasks", "prompt", "TEXT");
5839
5927
  }
5840
5928
  upsert(entry) {
@@ -5875,15 +5963,48 @@ END;
5875
5963
  delete(id) {
5876
5964
  this.db.prepare("DELETE FROM archived_tasks WHERE id = ?").run(id);
5877
5965
  }
5966
+ /**
5967
+ * Full-text search over archived tasks. Accepts a raw user query and routes
5968
+ * through FTS5 when available, or a LIKE-based scan when not.
5969
+ */
5878
5970
  search(query, limit) {
5971
+ const trimmed = query?.trim();
5972
+ if (!trimmed) return [];
5973
+ const tokens = trimmed.split(/\s+/).filter((t) => t.length > 0).map((t) => t.replace(/["{}:*^+()]/g, "")).filter((t) => t.length > 0);
5974
+ if (tokens.length === 0) return [];
5975
+ if (this._fts5Available) {
5976
+ const ftsQuery = tokens.map((token) => {
5977
+ if (/[":(){}*^+-]/.test(token)) {
5978
+ return `"${token.replace(/"/g, '\\"')}"`;
5979
+ }
5980
+ return token;
5981
+ }).join(" OR ");
5982
+ const rows2 = this.db.prepare(`
5983
+ SELECT a.taskJson
5984
+ FROM archived_tasks a
5985
+ JOIN archived_tasks_fts fts ON a.rowid = fts.rowid
5986
+ WHERE archived_tasks_fts MATCH ?
5987
+ ORDER BY rank
5988
+ LIMIT ?
5989
+ `).all(ftsQuery, limit);
5990
+ return rows2.map((row) => JSON.parse(row.taskJson));
5991
+ }
5992
+ const searchColumns = ["id", "title", "description", "comments"];
5993
+ const perTokenClause = `(${searchColumns.map((c) => `"${c}" LIKE ? ESCAPE '\\'`).join(" OR ")})`;
5994
+ const whereTokens = tokens.map(() => perTokenClause).join(" OR ");
5995
+ const params = [];
5996
+ for (const token of tokens) {
5997
+ const pattern = `%${token.replace(/[\\%_]/g, "\\$&")}%`;
5998
+ for (let i = 0; i < searchColumns.length; i++) params.push(pattern);
5999
+ }
6000
+ params.push(limit);
5879
6001
  const rows = this.db.prepare(`
5880
- SELECT a.taskJson
5881
- FROM archived_tasks a
5882
- JOIN archived_tasks_fts fts ON a.rowid = fts.rowid
5883
- WHERE archived_tasks_fts MATCH ?
5884
- ORDER BY rank
6002
+ SELECT taskJson
6003
+ FROM archived_tasks
6004
+ WHERE ${whereTokens}
6005
+ ORDER BY archivedAt DESC
5885
6006
  LIMIT ?
5886
- `).all(query, limit);
6007
+ `).all(...params);
5887
6008
  return rows.map((row) => JSON.parse(row.taskJson));
5888
6009
  }
5889
6010
  close() {
@@ -12196,6 +12317,9 @@ var init_migration = __esm({
12196
12317
  * @returns Generated name
12197
12318
  */
12198
12319
  async generateProjectName(projectPath) {
12320
+ if (!existsSync7(join9(projectPath, ".git"))) {
12321
+ return basename3(projectPath);
12322
+ }
12199
12323
  try {
12200
12324
  const { execFile: execFile4 } = await import("node:child_process");
12201
12325
  const { promisify: promisify11 } = await import("node:util");
@@ -12203,7 +12327,7 @@ var init_migration = __esm({
12203
12327
  const { stdout } = await execFileAsync2(
12204
12328
  "git",
12205
12329
  ["remote", "get-url", "origin"],
12206
- { cwd: projectPath, timeout: 5e3 }
12330
+ { cwd: projectPath, timeout: 1e3 }
12207
12331
  );
12208
12332
  const remoteUrl = stdout.trim();
12209
12333
  if (remoteUrl) {
@@ -28697,6 +28821,27 @@ var init_run_command = __esm({
28697
28821
  }
28698
28822
  });
28699
28823
 
28824
+ // ../core/src/logger.ts
28825
+ function createLogger(prefix) {
28826
+ const tag = `[${prefix}]`;
28827
+ return {
28828
+ log(message, ...args) {
28829
+ console.error(`${tag} ${message}`, ...args);
28830
+ },
28831
+ warn(message, ...args) {
28832
+ console.warn(`${tag} ${message}`, ...args);
28833
+ },
28834
+ error(message, ...args) {
28835
+ console.error(`${tag} ${message}`, ...args);
28836
+ }
28837
+ };
28838
+ }
28839
+ var init_logger = __esm({
28840
+ "../core/src/logger.ts"() {
28841
+ "use strict";
28842
+ }
28843
+ });
28844
+
28700
28845
  // ../core/src/store.ts
28701
28846
  import { EventEmitter as EventEmitter11 } from "node:events";
28702
28847
  import { randomUUID as randomUUID6 } from "node:crypto";
@@ -28745,7 +28890,7 @@ function canonicalizeSettings(settings) {
28745
28890
  }
28746
28891
  return settings;
28747
28892
  }
28748
- var LEGACY_BACKUP_DIR, TASK_ACTIVITY_LOG_ENTRY_LIMIT, TASK_ACTIVITY_LOG_OUTCOME_LIMIT, ARCHIVE_AGENT_LOG_SNAPSHOT_LIMIT, ARCHIVE_AGENT_LOG_SNIPPET_LIMIT, TaskHasDependentsError, TaskStore;
28893
+ var LEGACY_BACKUP_DIR, TASK_ACTIVITY_LOG_ENTRY_LIMIT, TASK_ACTIVITY_LOG_OUTCOME_LIMIT, ARCHIVE_AGENT_LOG_SNAPSHOT_LIMIT, ARCHIVE_AGENT_LOG_SNIPPET_LIMIT, storeLog, TaskHasDependentsError, TaskStore;
28749
28894
  var init_store = __esm({
28750
28895
  "../core/src/store.ts"() {
28751
28896
  "use strict";
@@ -28763,11 +28908,13 @@ var init_store = __esm({
28763
28908
  init_task_merge();
28764
28909
  init_project_memory();
28765
28910
  init_run_command();
28911
+ init_logger();
28766
28912
  LEGACY_BACKUP_DIR = ".kb/backups";
28767
28913
  TASK_ACTIVITY_LOG_ENTRY_LIMIT = 1e3;
28768
28914
  TASK_ACTIVITY_LOG_OUTCOME_LIMIT = 4e3;
28769
28915
  ARCHIVE_AGENT_LOG_SNAPSHOT_LIMIT = 25;
28770
28916
  ARCHIVE_AGENT_LOG_SNIPPET_LIMIT = 160;
28917
+ storeLog = createLogger("task-store");
28771
28918
  TaskHasDependentsError = class extends Error {
28772
28919
  taskId;
28773
28920
  dependentIds;
@@ -29477,45 +29624,53 @@ ${recentText}` : void 0
29477
29624
  */
29478
29625
  setupActivityLogListeners() {
29479
29626
  this.on("task:created", (task) => {
29480
- this.recordActivity({
29481
- type: "task:created",
29482
- taskId: task.id,
29483
- taskTitle: task.title,
29484
- details: `Task ${task.id} created${task.title ? `: ${task.title}` : ""}`
29485
- }).catch(() => {
29486
- });
29627
+ this.recordActivityFromListener(
29628
+ {
29629
+ type: "task:created",
29630
+ taskId: task.id,
29631
+ taskTitle: task.title,
29632
+ details: `Task ${task.id} created${task.title ? `: ${task.title}` : ""}`
29633
+ },
29634
+ "task:created"
29635
+ );
29487
29636
  });
29488
29637
  this.on("task:moved", (data) => {
29489
- this.recordActivity({
29490
- type: "task:moved",
29491
- taskId: data.task.id,
29492
- taskTitle: data.task.title,
29493
- details: `Task ${data.task.id} moved: ${data.from} \u2192 ${data.to}`,
29494
- metadata: { from: data.from, to: data.to }
29495
- }).catch(() => {
29496
- });
29638
+ this.recordActivityFromListener(
29639
+ {
29640
+ type: "task:moved",
29641
+ taskId: data.task.id,
29642
+ taskTitle: data.task.title,
29643
+ details: `Task ${data.task.id} moved: ${data.from} \u2192 ${data.to}`,
29644
+ metadata: { from: data.from, to: data.to }
29645
+ },
29646
+ "task:moved"
29647
+ );
29497
29648
  });
29498
29649
  this.on("task:merged", (result) => {
29499
29650
  const status = result.merged ? "successfully merged" : "merge attempted";
29500
- this.recordActivity({
29501
- type: "task:merged",
29502
- taskId: result.task.id,
29503
- taskTitle: result.task.title,
29504
- details: `Task ${result.task.id} ${status} to main`,
29505
- metadata: { merged: result.merged, branch: result.branch }
29506
- }).catch(() => {
29507
- });
29651
+ this.recordActivityFromListener(
29652
+ {
29653
+ type: "task:merged",
29654
+ taskId: result.task.id,
29655
+ taskTitle: result.task.title,
29656
+ details: `Task ${result.task.id} ${status} to main`,
29657
+ metadata: { merged: result.merged, branch: result.branch }
29658
+ },
29659
+ "task:merged"
29660
+ );
29508
29661
  });
29509
29662
  this.on("task:updated", (task) => {
29510
29663
  if (task.status === "failed") {
29511
- this.recordActivity({
29512
- type: "task:failed",
29513
- taskId: task.id,
29514
- taskTitle: task.title,
29515
- details: `Task ${task.id} failed${task.error ? `: ${task.error}` : ""}`,
29516
- metadata: task.error ? { error: task.error } : void 0
29517
- }).catch(() => {
29518
- });
29664
+ this.recordActivityFromListener(
29665
+ {
29666
+ type: "task:failed",
29667
+ taskId: task.id,
29668
+ taskTitle: task.title,
29669
+ details: `Task ${task.id} failed${task.error ? `: ${task.error}` : ""}`,
29670
+ metadata: task.error ? { error: task.error } : void 0
29671
+ },
29672
+ "task:updated"
29673
+ );
29519
29674
  }
29520
29675
  });
29521
29676
  this.on("settings:updated", (data) => {
@@ -29533,21 +29688,35 @@ ${recentText}` : void 0
29533
29688
  importantChanges.push(`engine pause ${data.settings.enginePaused ? "enabled" : "disabled"}`);
29534
29689
  }
29535
29690
  if (importantChanges.length > 0) {
29536
- this.recordActivity({
29537
- type: "settings:updated",
29538
- details: `Settings updated: ${importantChanges.join(", ")}`,
29539
- metadata: { changes: importantChanges }
29540
- }).catch(() => {
29541
- });
29691
+ this.recordActivityFromListener(
29692
+ {
29693
+ type: "settings:updated",
29694
+ details: `Settings updated: ${importantChanges.join(", ")}`,
29695
+ metadata: { changes: importantChanges }
29696
+ },
29697
+ "settings:updated"
29698
+ );
29542
29699
  }
29543
29700
  });
29544
29701
  this.on("task:deleted", (task) => {
29545
- this.recordActivity({
29546
- type: "task:deleted",
29547
- taskId: task.id,
29548
- taskTitle: task.title,
29549
- details: `Task ${task.id} deleted${task.title ? `: ${task.title}` : ""}`
29550
- }).catch(() => {
29702
+ this.recordActivityFromListener(
29703
+ {
29704
+ type: "task:deleted",
29705
+ taskId: task.id,
29706
+ taskTitle: task.title,
29707
+ details: `Task ${task.id} deleted${task.title ? `: ${task.title}` : ""}`
29708
+ },
29709
+ "task:deleted"
29710
+ );
29711
+ });
29712
+ }
29713
+ recordActivityFromListener(entry, sourceEvent) {
29714
+ this.recordActivity(entry).catch((err) => {
29715
+ storeLog.warn("Activity logging listener failed", {
29716
+ sourceEvent,
29717
+ type: entry.type,
29718
+ taskId: entry.taskId,
29719
+ error: err instanceof Error ? err.message : String(err)
29551
29720
  });
29552
29721
  });
29553
29722
  }
@@ -30069,7 +30238,11 @@ ${recentText}` : void 0
30069
30238
  if (defaultOnSteps.length > 0) {
30070
30239
  resolvedWorkflowSteps = defaultOnSteps;
30071
30240
  }
30072
- } catch {
30241
+ } catch (err) {
30242
+ storeLog.warn("Failed to auto-apply default workflow steps during task creation", {
30243
+ error: err instanceof Error ? err.message : String(err),
30244
+ descriptionLength: input.description.length
30245
+ });
30073
30246
  }
30074
30247
  } else if (input.enabledWorkflowSteps.length === 0) {
30075
30248
  resolvedWorkflowSteps = void 0;
@@ -30087,13 +30260,26 @@ ${recentText}` : void 0
30087
30260
  }
30088
30261
  }
30089
30262
  } catch (err) {
30090
- const errorMsg = err instanceof Error ? err.message : String(err);
30091
30263
  const autoEnabled = options?.settings?.autoSummarizeTitles === true;
30092
- console.warn(
30093
- `[TaskStore] Title summarization failed for task ${id}: ${errorMsg} (desc length: ${input.description.length}, auto-summarize: ${autoEnabled})`
30264
+ const errorMessage = err instanceof Error ? err.message : String(err);
30265
+ storeLog.warn(
30266
+ `Title summarization failed for task ${id}: ${errorMessage} (desc length: ${input.description.length}, auto-summarize: ${autoEnabled})`,
30267
+ {
30268
+ taskId: id,
30269
+ descriptionLength: input.description.length,
30270
+ autoSummarizeEnabled: autoEnabled,
30271
+ error: errorMessage
30272
+ }
30094
30273
  );
30095
30274
  }
30096
- }).catch(() => {
30275
+ }).catch((err) => {
30276
+ const autoEnabled = options?.settings?.autoSummarizeTitles === true;
30277
+ storeLog.error("Unexpected title summarization promise-chain failure", {
30278
+ taskId: id,
30279
+ descriptionLength: input.description.length,
30280
+ autoSummarizeEnabled: autoEnabled,
30281
+ error: err instanceof Error ? err.message : String(err)
30282
+ });
30097
30283
  });
30098
30284
  }
30099
30285
  return task;
@@ -30360,26 +30546,46 @@ ${newTask.description}
30360
30546
  if (sanitizedTokens.length === 0) {
30361
30547
  return this.listTasks(options);
30362
30548
  }
30363
- const ftsQuery = sanitizedTokens.map((token) => {
30364
- if (/[":(){}*^+-]/.test(token)) {
30365
- return `"${token.replace(/"/g, '\\"')}"`;
30366
- }
30367
- return token;
30368
- }).join(" OR ");
30369
30549
  const limit = options?.limit ?? -1;
30370
30550
  const offset = options?.offset ?? 0;
30371
30551
  const offsetClause = offset > 0 ? ` OFFSET ${offset}` : "";
30372
30552
  const includeArchived = options?.includeArchived ?? true;
30373
- const whereClause = includeArchived ? "" : ` AND t."column" != 'archived'`;
30374
- const selectClause = this.getTaskSelectClause(options?.slim ?? false, "t");
30375
- const rows = this.db.prepare(`
30376
- SELECT ${selectClause} FROM tasks t
30377
- JOIN tasks_fts fts ON t.rowid = fts.rowid
30378
- WHERE tasks_fts MATCH ?
30379
- ${whereClause}
30380
- ORDER BY rank
30381
- LIMIT ${limit >= 0 ? limit : -1}${offsetClause}
30382
- `).all(ftsQuery);
30553
+ const slim = options?.slim ?? false;
30554
+ const selectClause = this.getTaskSelectClause(slim, "t");
30555
+ let rows;
30556
+ if (this.db.fts5Available) {
30557
+ const ftsQuery = sanitizedTokens.map((token) => {
30558
+ if (/[":(){}*^+-]/.test(token)) {
30559
+ return `"${token.replace(/"/g, '\\"')}"`;
30560
+ }
30561
+ return token;
30562
+ }).join(" OR ");
30563
+ const whereClause = includeArchived ? "" : ` AND t."column" != 'archived'`;
30564
+ rows = this.db.prepare(`
30565
+ SELECT ${selectClause} FROM tasks t
30566
+ JOIN tasks_fts fts ON t.rowid = fts.rowid
30567
+ WHERE tasks_fts MATCH ?
30568
+ ${whereClause}
30569
+ ORDER BY rank
30570
+ LIMIT ${limit >= 0 ? limit : -1}${offsetClause}
30571
+ `).all(ftsQuery);
30572
+ } else {
30573
+ const searchColumns = ["id", "title", "description", "comments"];
30574
+ const perTokenClause = `(${searchColumns.map((c) => `t."${c}" LIKE ? ESCAPE '\\'`).join(" OR ")})`;
30575
+ const whereTokens = sanitizedTokens.map(() => perTokenClause).join(" OR ");
30576
+ const params = [];
30577
+ for (const token of sanitizedTokens) {
30578
+ const pattern = `%${token.replace(/[\\%_]/g, "\\$&")}%`;
30579
+ for (let i = 0; i < searchColumns.length; i++) params.push(pattern);
30580
+ }
30581
+ const archivedClause = includeArchived ? "" : ` AND t."column" != 'archived'`;
30582
+ rows = this.db.prepare(`
30583
+ SELECT ${selectClause} FROM tasks t
30584
+ WHERE (${whereTokens})${archivedClause}
30585
+ ORDER BY t.createdAt ASC
30586
+ LIMIT ${limit >= 0 ? limit : -1}${offsetClause}
30587
+ `).all(...params);
30588
+ }
30383
30589
  const activeMatches = await Promise.all(rows.map(async (row) => {
30384
30590
  const task = this.rowToTask(row);
30385
30591
  if (task.steps.length > 0) {
@@ -30388,7 +30594,7 @@ ${newTask.description}
30388
30594
  const steps = await this.parseStepsFromPrompt(task.id);
30389
30595
  return steps.length > 0 ? { ...task, steps } : task;
30390
30596
  }));
30391
- const archiveMatches = includeArchived ? this.archiveDb.search(ftsQuery, limit >= 0 ? limit : 100).map((entry) => this.archiveEntryToTask(entry, options?.slim ?? false)) : [];
30597
+ const archiveMatches = includeArchived ? this.archiveDb.search(trimmedQuery, limit >= 0 ? limit : 100).map((entry) => this.archiveEntryToTask(entry, slim)) : [];
30392
30598
  const matches = [...activeMatches, ...archiveMatches];
30393
30599
  return limit >= 0 ? matches.slice(0, limit) : matches;
30394
30600
  }
@@ -31524,9 +31730,17 @@ ${task.description}
31524
31730
  try {
31525
31731
  this.watcher = watch(this.tasksDir, { recursive: true }, (_event, _filename) => {
31526
31732
  });
31527
- this.watcher.on("error", () => {
31733
+ this.watcher.on("error", (err) => {
31734
+ storeLog.warn("fs.watch emitted an error; polling will continue", {
31735
+ error: err instanceof Error ? err.message : String(err),
31736
+ tasksDir: this.tasksDir
31737
+ });
31738
+ });
31739
+ } catch (err) {
31740
+ storeLog.warn("fs.watch unavailable; falling back to polling-only updates", {
31741
+ error: err instanceof Error ? err.message : String(err),
31742
+ tasksDir: this.tasksDir
31528
31743
  });
31529
- } catch {
31530
31744
  }
31531
31745
  this.pollInterval = setInterval(() => {
31532
31746
  void this.checkForChanges();
@@ -31582,9 +31796,17 @@ ${task.description}
31582
31796
  }
31583
31797
  const elapsed = Date.now() - startTime;
31584
31798
  if (elapsed > 100) {
31585
- console.warn(`[TaskStore] checkForChanges took ${elapsed}ms \u2014 event loop may have been blocked`);
31799
+ storeLog.warn("checkForChanges took longer than expected", {
31800
+ elapsedMs: elapsed,
31801
+ thresholdMs: 100
31802
+ });
31586
31803
  }
31587
- } catch {
31804
+ } catch (err) {
31805
+ storeLog.warn("checkForChanges poll cycle failed", {
31806
+ lastKnownModified: this.lastKnownModified,
31807
+ lastPollTime: this.lastPollTime,
31808
+ error: err instanceof Error ? err.message : String(err)
31809
+ });
31588
31810
  } finally {
31589
31811
  this.pollingInProgress = false;
31590
31812
  }
@@ -32839,7 +33061,15 @@ ${notificationsSection}
32839
33061
  );
32840
33062
  this.db.bumpLastModified();
32841
33063
  } catch (err) {
32842
- console.error("Failed to record activity:", err);
33064
+ storeLog.error("Failed to record activity", {
33065
+ id: fullEntry.id,
33066
+ type: fullEntry.type,
33067
+ taskId: fullEntry.taskId,
33068
+ taskTitle: fullEntry.taskTitle,
33069
+ detailsLength: fullEntry.details.length,
33070
+ hasMetadata: fullEntry.metadata !== void 0,
33071
+ error: err instanceof Error ? err.message : String(err)
33072
+ });
32843
33073
  }
32844
33074
  return fullEntry;
32845
33075
  }
@@ -33821,27 +34051,6 @@ var init_routine_store = __esm({
33821
34051
  }
33822
34052
  });
33823
34053
 
33824
- // ../core/src/logger.ts
33825
- function createLogger(prefix) {
33826
- const tag = `[${prefix}]`;
33827
- return {
33828
- log(message, ...args) {
33829
- console.error(`${tag} ${message}`, ...args);
33830
- },
33831
- warn(message, ...args) {
33832
- console.warn(`${tag} ${message}`, ...args);
33833
- },
33834
- error(message, ...args) {
33835
- console.error(`${tag} ${message}`, ...args);
33836
- }
33837
- };
33838
- }
33839
- var init_logger = __esm({
33840
- "../core/src/logger.ts"() {
33841
- "use strict";
33842
- }
33843
- });
33844
-
33845
34054
  // ../core/src/plugin-loader.ts
33846
34055
  import { isAbsolute as isAbsolute5, resolve as resolve8 } from "node:path";
33847
34056
  import { EventEmitter as EventEmitter13 } from "node:events";
@@ -47406,13 +47615,13 @@ function createLogger2(prefix) {
47406
47615
  const tag = `[${prefix}]`;
47407
47616
  return {
47408
47617
  log(message, ...args) {
47409
- console.error(`${tag} ${message}`, ...args);
47618
+ globalThis.console.error(`${tag} ${message}`, ...args);
47410
47619
  },
47411
47620
  warn(message, ...args) {
47412
- console.warn(`${tag} ${message}`, ...args);
47621
+ globalThis.console.warn(`${tag} ${message}`, ...args);
47413
47622
  },
47414
47623
  error(message, ...args) {
47415
- console.error(`${tag} ${message}`, ...args);
47624
+ globalThis.console.error(`${tag} ${message}`, ...args);
47416
47625
  }
47417
47626
  };
47418
47627
  }
@@ -48730,7 +48939,6 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
48730
48939
  }
48731
48940
  }
48732
48941
  if (newDiagnostics.length > 0) {
48733
- const _purpose = sessionPurpose ? `[${sessionPurpose}]` : "skills";
48734
48942
  for (const diag of newDiagnostics) {
48735
48943
  piLog.warn(`[skills] ${diag.type}: ${diag.message}`);
48736
48944
  }
@@ -48800,7 +49008,7 @@ import { join as join24 } from "node:path";
48800
49008
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
48801
49009
  import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
48802
49010
  function getHomeDir2() {
48803
- return process.env.HOME || process.env.USERPROFILE || homedir5();
49011
+ return globalThis.process.env.HOME || globalThis.process.env.USERPROFILE || homedir5();
48804
49012
  }
48805
49013
  function getFusionAuthPath(home = getHomeDir2()) {
48806
49014
  return join24(home, ".fusion", "agent", "auth.json");
@@ -48846,7 +49054,7 @@ function readLegacyCredentials(authPaths = getLegacyAuthPaths()) {
48846
49054
  function resolveStoredApiKey(key) {
48847
49055
  if (!key)
48848
49056
  return void 0;
48849
- return process.env[key] ?? key;
49057
+ return globalThis.process.env[key] ?? key;
48850
49058
  }
48851
49059
  function resolveOAuthApiKey(providerId, credential) {
48852
49060
  if (credential.type !== "oauth" || typeof credential.access !== "string" || typeof credential.refresh !== "string" || typeof credential.expires !== "number" || Date.now() >= credential.expires) {
@@ -49149,8 +49357,8 @@ function createReadOnlyPiSettingsView(cwd, agentDir) {
49149
49357
  const fusionProjectSettings = readJsonObject2(join25(projectRoot, ".fusion", "settings.json"));
49150
49358
  const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
49151
49359
  return {
49152
- getGlobalSettings: () => structuredClone(globalSettings),
49153
- getProjectSettings: () => structuredClone(fusionProjectSettings),
49360
+ getGlobalSettings: () => globalThis.structuredClone(globalSettings),
49361
+ getProjectSettings: () => globalThis.structuredClone(fusionProjectSettings),
49154
49362
  getNpmCommand: () => Array.isArray(mergedSettings.npmCommand) ? [...mergedSettings.npmCommand] : void 0
49155
49363
  };
49156
49364
  }
@@ -49269,9 +49477,7 @@ function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
49269
49477
  return {
49270
49478
  ...tool,
49271
49479
  execute: async (...args) => {
49272
- const _toolCallId = args[0];
49273
49480
  const params = args[1];
49274
- const _signal = args[2];
49275
49481
  const pathArg = params.path;
49276
49482
  if (pathArg && !isWorktreeAllowedPath(worktreePath, projectRoot, pathArg)) {
49277
49483
  const relToProject = relative3(projectRoot, pathArg);
@@ -55430,6 +55636,36 @@ function buildReducedStepPrompt(taskDetail, stepIndex) {
55430
55636
  ];
55431
55637
  return parts.join("\n").replace(/\n{3,}/g, "\n\n");
55432
55638
  }
55639
+ function resolveExecutorModelPair(taskModelProvider, taskModelId, settings) {
55640
+ if (taskModelProvider && taskModelId) {
55641
+ return { provider: taskModelProvider, modelId: taskModelId };
55642
+ }
55643
+ if (settings?.executionProvider && settings?.executionModelId) {
55644
+ return {
55645
+ provider: settings.executionProvider,
55646
+ modelId: settings.executionModelId
55647
+ };
55648
+ }
55649
+ if (settings?.executionGlobalProvider && settings?.executionGlobalModelId) {
55650
+ return {
55651
+ provider: settings.executionGlobalProvider,
55652
+ modelId: settings.executionGlobalModelId
55653
+ };
55654
+ }
55655
+ if (settings?.defaultProviderOverride && settings?.defaultModelIdOverride) {
55656
+ return {
55657
+ provider: settings.defaultProviderOverride,
55658
+ modelId: settings.defaultModelIdOverride
55659
+ };
55660
+ }
55661
+ if (settings?.defaultProvider && settings?.defaultModelId) {
55662
+ return {
55663
+ provider: settings.defaultProvider,
55664
+ modelId: settings.defaultModelId
55665
+ };
55666
+ }
55667
+ return { provider: void 0, modelId: void 0 };
55668
+ }
55433
55669
  function sleep2(ms) {
55434
55670
  return new Promise((resolve17) => setTimeout(resolve17, ms));
55435
55671
  }
@@ -55622,8 +55858,11 @@ var init_step_session_executor = __esm({
55622
55858
  createSendMessageTool(this.options.messageStore, taskDetail.assignedAgentId),
55623
55859
  createReadMessagesTool(this.options.messageStore, taskDetail.assignedAgentId)
55624
55860
  ] : [];
55625
- const executorProvider = taskDetail.modelProvider && taskDetail.modelId ? taskDetail.modelProvider : settings.executionProvider && settings.executionModelId ? settings.executionProvider : settings.executionGlobalProvider && settings.executionGlobalModelId ? settings.executionGlobalProvider : settings.defaultProvider;
55626
- const executorModelId = taskDetail.modelProvider && taskDetail.modelId ? taskDetail.modelId : settings.executionProvider && settings.executionModelId ? settings.executionModelId : settings.executionGlobalProvider && settings.executionGlobalModelId ? settings.executionGlobalModelId : settings.defaultModelId;
55861
+ const { provider: executorProvider, modelId: executorModelId } = resolveExecutorModelPair(
55862
+ taskDetail.modelProvider,
55863
+ taskDetail.modelId,
55864
+ settings
55865
+ );
55627
55866
  const createResult = await createFnAgent2({
55628
55867
  cwd: worktreePath,
55629
55868
  systemPrompt: `You are an AI agent executing step ${stepIndex} of task ${taskDetail.id}. Follow instructions precisely.`,
@@ -56057,6 +56296,36 @@ function getExecutorSystemPrompt(settings) {
56057
56296
  const customPrompt = resolveAgentPrompt("executor", settings.agentPrompts);
56058
56297
  return customPrompt || EXECUTOR_SYSTEM_PROMPT;
56059
56298
  }
56299
+ function resolveExecutorModelPair2(taskModelProvider, taskModelId, settings) {
56300
+ if (taskModelProvider && taskModelId) {
56301
+ return { provider: taskModelProvider, modelId: taskModelId };
56302
+ }
56303
+ if (settings?.executionProvider && settings?.executionModelId) {
56304
+ return {
56305
+ provider: settings.executionProvider,
56306
+ modelId: settings.executionModelId
56307
+ };
56308
+ }
56309
+ if (settings?.executionGlobalProvider && settings?.executionGlobalModelId) {
56310
+ return {
56311
+ provider: settings.executionGlobalProvider,
56312
+ modelId: settings.executionGlobalModelId
56313
+ };
56314
+ }
56315
+ if (settings?.defaultProviderOverride && settings?.defaultModelIdOverride) {
56316
+ return {
56317
+ provider: settings.defaultProviderOverride,
56318
+ modelId: settings.defaultModelIdOverride
56319
+ };
56320
+ }
56321
+ if (settings?.defaultProvider && settings?.defaultModelId) {
56322
+ return {
56323
+ provider: settings.defaultProvider,
56324
+ modelId: settings.defaultModelId
56325
+ };
56326
+ }
56327
+ return { provider: void 0, modelId: void 0 };
56328
+ }
56060
56329
  function formatTimestamp2(iso) {
56061
56330
  const date = new Date(iso);
56062
56331
  const now = /* @__PURE__ */ new Date();
@@ -56213,7 +56482,7 @@ function detectReviewHandoffIntent(commentText) {
56213
56482
  ];
56214
56483
  return handoffPhrases.some((phrase) => text.includes(phrase));
56215
56484
  }
56216
- var execAsync5, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
56485
+ var execAsync5, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, MAX_TASK_DONE_SESSION_RETRIES, MAX_TASK_DONE_REQUEUE_RETRIES, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
56217
56486
  var init_executor = __esm({
56218
56487
  "../engine/src/executor.ts"() {
56219
56488
  "use strict";
@@ -56246,6 +56515,8 @@ var init_executor = __esm({
56246
56515
  execAsync5 = promisify6(exec5);
56247
56516
  STEP_STATUSES = ["pending", "in-progress", "done", "skipped"];
56248
56517
  MAX_WORKFLOW_STEP_RETRIES = 3;
56518
+ MAX_TASK_DONE_SESSION_RETRIES = 3;
56519
+ MAX_TASK_DONE_REQUEUE_RETRIES = 3;
56249
56520
  WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2 = 4e3;
56250
56521
  NonRetryableWorktreeError = class extends Error {
56251
56522
  };
@@ -56533,8 +56804,11 @@ Lint, tests, and typecheck are also hard quality gates:
56533
56804
  activeEntry.lastModelProvider = task.modelProvider;
56534
56805
  activeEntry.lastModelId = task.modelId;
56535
56806
  const settings = await this.store.getSettings();
56536
- const newProvider = task.modelProvider && task.modelId ? task.modelProvider : settings?.executionProvider && settings?.executionModelId ? settings.executionProvider : settings?.executionGlobalProvider && settings?.executionGlobalModelId ? settings.executionGlobalProvider : settings?.defaultProvider;
56537
- const newModelId = task.modelProvider && task.modelId ? task.modelId : settings?.executionProvider && settings?.executionModelId ? settings.executionModelId : settings?.executionGlobalProvider && settings?.executionGlobalModelId ? settings.executionGlobalModelId : settings?.defaultModelId;
56807
+ const { provider: newProvider, modelId: newModelId } = resolveExecutorModelPair2(
56808
+ task.modelProvider,
56809
+ task.modelId,
56810
+ settings
56811
+ );
56538
56812
  if (newProvider && newModelId) {
56539
56813
  try {
56540
56814
  const model = this.modelRegistry.find(newProvider, newModelId);
@@ -57441,8 +57715,11 @@ Lint, tests, and typecheck are also hard quality gates:
57441
57715
  }
57442
57716
  });
57443
57717
  const agentWork = async () => {
57444
- const executorProvider = detail.modelProvider && detail.modelId ? detail.modelProvider : settings.executionProvider && settings.executionModelId ? settings.executionProvider : settings.executionGlobalProvider && settings.executionGlobalModelId ? settings.executionGlobalProvider : settings.defaultProvider;
57445
- const executorModelId = detail.modelProvider && detail.modelId ? detail.modelId : settings.executionProvider && settings.executionModelId ? settings.executionModelId : settings.executionGlobalProvider && settings.executionGlobalModelId ? settings.executionGlobalModelId : settings.defaultModelId;
57718
+ const { provider: executorProvider, modelId: executorModelId } = resolveExecutorModelPair2(
57719
+ detail.modelProvider,
57720
+ detail.modelId,
57721
+ settings
57722
+ );
57446
57723
  const executorFallbackProvider = settings.fallbackProvider;
57447
57724
  const executorFallbackModelId = settings.fallbackModelId;
57448
57725
  const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
@@ -57624,63 +57901,74 @@ Lint, tests, and typecheck are also hard quality gates:
57624
57901
  executorLog.log(`\u2713 ${task.id} completed \u2192 in-review`);
57625
57902
  this.options.onComplete?.(task);
57626
57903
  } else {
57627
- executorLog.log(`\u26A0 ${task.id} finished without task_done \u2014 retrying with new session`);
57628
- await this.store.logEntry(task.id, "Agent finished without calling task_done \u2014 retrying with new session", void 0, this.currentRunContext);
57629
- this.activeSessions.delete(task.id);
57630
- session.dispose();
57631
- const { session: retrySession, sessionFile: retrySessionFile } = await createResolvedAgentSession({
57632
- sessionPurpose: "executor",
57633
- pluginRunner: this.options.pluginRunner,
57634
- cwd: worktreePath,
57635
- systemPrompt: executorSystemPrompt,
57636
- tools: "coding",
57637
- customTools,
57638
- onText: agentLogger.onText,
57639
- onThinking: agentLogger.onThinking,
57640
- onToolStart: agentLogger.onToolStart,
57641
- onToolEnd: agentLogger.onToolEnd,
57642
- defaultProvider: executorProvider,
57643
- defaultModelId: executorModelId,
57644
- fallbackProvider: executorFallbackProvider,
57645
- fallbackModelId: executorFallbackModelId,
57646
- defaultThinkingLevel: executorThinkingLevel,
57647
- sessionManager: SessionManager2.create(worktreePath),
57648
- // Skill selection: use assigned agent skills if available, otherwise role fallback
57649
- ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
57650
- });
57651
- if (retrySessionFile) {
57652
- this.store.updateTask(task.id, { sessionFile: retrySessionFile }).catch((err) => {
57653
- const msg = err instanceof Error ? err.message : String(err);
57654
- executorLog.warn(`${task.id} failed to persist retry sessionFile: ${msg}`);
57904
+ let taskDoneSessionRetries = 0;
57905
+ while (!taskDone && taskDoneSessionRetries < MAX_TASK_DONE_SESSION_RETRIES) {
57906
+ taskDoneSessionRetries++;
57907
+ executorLog.log(
57908
+ `\u26A0 ${task.id} finished without task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`
57909
+ );
57910
+ await this.store.logEntry(
57911
+ task.id,
57912
+ `Agent finished without calling task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`,
57913
+ void 0,
57914
+ this.currentRunContext
57915
+ );
57916
+ this.activeSessions.delete(task.id);
57917
+ session.dispose();
57918
+ const { session: retrySession, sessionFile: retrySessionFile } = await createResolvedAgentSession({
57919
+ sessionPurpose: "executor",
57920
+ pluginRunner: this.options.pluginRunner,
57921
+ cwd: worktreePath,
57922
+ systemPrompt: executorSystemPrompt,
57923
+ tools: "coding",
57924
+ customTools,
57925
+ onText: agentLogger.onText,
57926
+ onThinking: agentLogger.onThinking,
57927
+ onToolStart: agentLogger.onToolStart,
57928
+ onToolEnd: agentLogger.onToolEnd,
57929
+ defaultProvider: executorProvider,
57930
+ defaultModelId: executorModelId,
57931
+ fallbackProvider: executorFallbackProvider,
57932
+ fallbackModelId: executorFallbackModelId,
57933
+ defaultThinkingLevel: executorThinkingLevel,
57934
+ sessionManager: SessionManager2.create(worktreePath),
57935
+ // Skill selection: use assigned agent skills if available, otherwise role fallback
57936
+ ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
57655
57937
  });
57656
- }
57657
- session = retrySession;
57658
- sessionRef.current = retrySession;
57659
- this.activeSessions.set(task.id, {
57660
- session: retrySession,
57661
- seenSteeringIds,
57662
- lastModelProvider: detail.modelProvider,
57663
- lastModelId: detail.modelId
57664
- });
57665
- stuckDetector?.trackTask(task.id, retrySession);
57666
- const retryPrompt = [
57667
- "Your previous session ended without calling the task_done tool.",
57668
- "The task may already be complete \u2014 review the current state of the worktree and either:",
57669
- "1. If the work is done, call task_done with a summary of what was accomplished.",
57670
- "2. If there is remaining work, finish it and then call task_done.",
57671
- "",
57672
- "Original task:",
57673
- buildExecutionPrompt(detail, this.rootDir, settings, worktreePath)
57674
- ].join("\n");
57675
- stuckDetector?.recordActivity(task.id);
57676
- await promptWithFallback(retrySession, retryPrompt);
57677
- checkSessionError(retrySession);
57678
- if (!taskDone) {
57679
- const implicitCheck = await this.store.getTask(task.id);
57680
- if (implicitCheck.steps.length > 0 && implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
57681
- taskDone = true;
57682
- executorLog.log(`${task.id} all steps done \u2014 treating as implicit task_done`);
57683
- await this.store.logEntry(task.id, "All steps complete \u2014 implicit task_done (agent did not call tool explicitly)", void 0, this.currentRunContext);
57938
+ if (retrySessionFile) {
57939
+ this.store.updateTask(task.id, { sessionFile: retrySessionFile }).catch((err) => {
57940
+ const msg = err instanceof Error ? err.message : String(err);
57941
+ executorLog.warn(`${task.id} failed to persist retry sessionFile: ${msg}`);
57942
+ });
57943
+ }
57944
+ session = retrySession;
57945
+ sessionRef.current = retrySession;
57946
+ this.activeSessions.set(task.id, {
57947
+ session: retrySession,
57948
+ seenSteeringIds,
57949
+ lastModelProvider: detail.modelProvider,
57950
+ lastModelId: detail.modelId
57951
+ });
57952
+ stuckDetector?.trackTask(task.id, retrySession);
57953
+ const retryPrompt = [
57954
+ "Your previous session ended without calling the task_done tool.",
57955
+ "The task may already be complete \u2014 review the current state of the worktree and either:",
57956
+ "1. If the work is done, call task_done with a summary of what was accomplished.",
57957
+ "2. If there is remaining work, finish it and then call task_done.",
57958
+ "",
57959
+ "Original task:",
57960
+ buildExecutionPrompt(detail, this.rootDir, settings, worktreePath)
57961
+ ].join("\n");
57962
+ stuckDetector?.recordActivity(task.id);
57963
+ await promptWithFallback(retrySession, retryPrompt);
57964
+ checkSessionError(retrySession);
57965
+ if (!taskDone) {
57966
+ const implicitCheck = await this.store.getTask(task.id);
57967
+ if (implicitCheck.steps.length > 0 && implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
57968
+ taskDone = true;
57969
+ executorLog.log(`${task.id} all steps done \u2014 treating as implicit task_done`);
57970
+ await this.store.logEntry(task.id, "All steps complete \u2014 implicit task_done (agent did not call tool explicitly)", void 0, this.currentRunContext);
57971
+ }
57684
57972
  }
57685
57973
  }
57686
57974
  if (taskDone) {
@@ -57709,11 +57997,29 @@ Lint, tests, and typecheck are also hard quality gates:
57709
57997
  executorLog.log(`\u2713 ${task.id} completed on retry \u2192 in-review`);
57710
57998
  this.options.onComplete?.(task);
57711
57999
  } else {
57712
- const errorMessage = "Agent finished without calling task_done (after retry)";
57713
- await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
57714
- await this.store.logEntry(task.id, `${errorMessage} \u2014 moved to in-review for inspection`, void 0, this.currentRunContext);
57715
- await this.store.moveTask(task.id, "in-review");
57716
- executorLog.log(`\u2717 ${task.id} failed after retry \u2014 no task_done \u2192 in-review`);
58000
+ const priorRequeues = task.taskDoneRetryCount ?? 0;
58001
+ const nextRequeueCount = priorRequeues + 1;
58002
+ const errorMessage = `Agent finished without calling task_done (after ${MAX_TASK_DONE_SESSION_RETRIES} retries)`;
58003
+ if (priorRequeues < MAX_TASK_DONE_REQUEUE_RETRIES) {
58004
+ await this.store.updateTask(task.id, {
58005
+ status: "failed",
58006
+ error: errorMessage,
58007
+ taskDoneRetryCount: nextRequeueCount
58008
+ });
58009
+ await this.store.logEntry(
58010
+ task.id,
58011
+ `${errorMessage} \u2014 requeued to todo immediately (${nextRequeueCount}/${MAX_TASK_DONE_REQUEUE_RETRIES})`,
58012
+ void 0,
58013
+ this.currentRunContext
58014
+ );
58015
+ await this.store.moveTask(task.id, "todo");
58016
+ executorLog.log(`\u2717 ${task.id} failed after ${MAX_TASK_DONE_SESSION_RETRIES} retries \u2014 requeued to todo (${nextRequeueCount}/${MAX_TASK_DONE_REQUEUE_RETRIES})`);
58017
+ } else {
58018
+ await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
58019
+ await this.store.logEntry(task.id, `${errorMessage} \u2014 moved to in-review for inspection`, void 0, this.currentRunContext);
58020
+ await this.store.moveTask(task.id, "in-review");
58021
+ executorLog.log(`\u2717 ${task.id} failed after ${MAX_TASK_DONE_SESSION_RETRIES} retries \u2014 no task_done \u2192 in-review`);
58022
+ }
57717
58023
  this.options.onError?.(task, new Error(errorMessage));
57718
58024
  }
57719
58025
  }
@@ -64299,7 +64605,7 @@ async function getHeartbeatMemorySettings(taskStore) {
64299
64605
  return maybeGetSettings.call(taskStore);
64300
64606
  }
64301
64607
  function isTickableState(state) {
64302
- return state === "active" || state === "running";
64608
+ return state === "active" || state === "running" || state === "idle";
64303
64609
  }
64304
64610
  function isHeartbeatManaged(agent) {
64305
64611
  return !isEphemeralAgent(agent);