@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/bin.js CHANGED
@@ -1975,6 +1975,18 @@ function fromJson(json) {
1975
1975
  return void 0;
1976
1976
  }
1977
1977
  }
1978
+ function probeFts5(db) {
1979
+ if (process.env.FUSION_DISABLE_FTS5 === "1" || process.env.FUSION_DISABLE_FTS5 === "true") {
1980
+ return false;
1981
+ }
1982
+ try {
1983
+ db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS __fusion_fts5_probe USING fts5(x)");
1984
+ db.exec("DROP TABLE IF EXISTS __fusion_fts5_probe");
1985
+ return true;
1986
+ } catch {
1987
+ return false;
1988
+ }
1989
+ }
1978
1990
  function normalizeTaskComments(steeringComments, comments) {
1979
1991
  const normalizedComments = [];
1980
1992
  const seenKeys = /* @__PURE__ */ new Set();
@@ -2495,6 +2507,7 @@ CREATE INDEX IF NOT EXISTS idxInsightRunsProjectId
2495
2507
  dbPath;
2496
2508
  /** Tracks transaction nesting depth for savepoint-based nested transactions. */
2497
2509
  transactionDepth = 0;
2510
+ _fts5Available;
2498
2511
  constructor(kbDir) {
2499
2512
  this.dbPath = join(kbDir, "fusion.db");
2500
2513
  if (!isAbsolute(kbDir)) {
@@ -2507,6 +2520,16 @@ CREATE INDEX IF NOT EXISTS idxInsightRunsProjectId
2507
2520
  this.db.exec("PRAGMA journal_mode = WAL");
2508
2521
  this.db.exec("PRAGMA busy_timeout = 5000");
2509
2522
  this.db.exec("PRAGMA foreign_keys = ON");
2523
+ this._fts5Available = probeFts5(this.db);
2524
+ }
2525
+ /**
2526
+ * True when the underlying SQLite build has FTS5 (`CREATE VIRTUAL TABLE … USING fts5`).
2527
+ * Node's bundled SQLite only exposes FTS5 when built with `SQLITE_ENABLE_FTS5`;
2528
+ * older Node 22.x LTS builds do not. Consumers must fall back to LIKE-based scans
2529
+ * when this is false. Override with `FUSION_DISABLE_FTS5=1` to force the fallback path.
2530
+ */
2531
+ get fts5Available() {
2532
+ return this._fts5Available;
2510
2533
  }
2511
2534
  /**
2512
2535
  * Initialize the database: create tables if they don't exist
@@ -2816,6 +2839,9 @@ CREATE INDEX IF NOT EXISTS idxInsightRunsProjectId
2816
2839
  }
2817
2840
  if (version < 21) {
2818
2841
  this.applyMigration(21, () => {
2842
+ if (!this._fts5Available) {
2843
+ return;
2844
+ }
2819
2845
  this.db.exec(`
2820
2846
  CREATE VIRTUAL TABLE IF NOT EXISTS tasks_fts USING fts5(
2821
2847
  id,
@@ -3225,6 +3251,9 @@ CREATE INDEX IF NOT EXISTS idxInsightRunsProjectId
3225
3251
  }
3226
3252
  if (version < 35) {
3227
3253
  this.applyMigration(35, () => {
3254
+ if (!this._fts5Available) {
3255
+ return;
3256
+ }
3228
3257
  const hasTaskTitle = this.hasColumn("tasks", "title");
3229
3258
  const updateColumns = hasTaskTitle ? "id, title, description, comments" : "id, description, comments";
3230
3259
  const oldTitle = hasTaskTitle ? "COALESCE(old.title, '')" : "''";
@@ -3518,6 +3547,9 @@ function resolveCreationRuntimeConfig(incoming, metadata) {
3518
3547
  return incoming;
3519
3548
  }
3520
3549
  const rc = { ...incoming ?? {} };
3550
+ if (typeof rc.enabled !== "boolean") {
3551
+ rc.enabled = true;
3552
+ }
3521
3553
  if (typeof rc.heartbeatIntervalMs !== "number" || !Number.isFinite(rc.heartbeatIntervalMs)) {
3522
3554
  rc.heartbeatIntervalMs = DEFAULT_AGENT_HEARTBEAT_INTERVAL_MS;
3523
3555
  }
@@ -3563,6 +3595,7 @@ var init_agent_store = __esm({
3563
3595
  const _2 = this.db;
3564
3596
  await mkdir(this.agentsDir, { recursive: true });
3565
3597
  await this.importLegacyFileDataOnce();
3598
+ await this.normalizeHeartbeatDefaultsOnce();
3566
3599
  }
3567
3600
  /**
3568
3601
  * One-way migration helper for projects that still have legacy agent JSON
@@ -3667,6 +3700,51 @@ var init_agent_store = __esm({
3667
3700
  `).run(migrationKey, migrationVersion);
3668
3701
  this.db.bumpLastModified();
3669
3702
  }
3703
+ /**
3704
+ * One-time normalization for durable agents created before the heartbeat
3705
+ * toggle was exposed in the UI. Those agents could persist
3706
+ * `runtimeConfig.enabled = false` even though users had no supported way to
3707
+ * manage that flag, which caused timers to stay disabled after restart.
3708
+ *
3709
+ * We normalize only once per project. After this migration lands, explicit
3710
+ * user choices are preserved because the version gate prevents reruns.
3711
+ */
3712
+ async normalizeHeartbeatDefaultsOnce() {
3713
+ const migrationKey = "agentHeartbeatDefaultVersion";
3714
+ const migrationVersion = "1";
3715
+ const row = this.db.prepare("SELECT value FROM __meta WHERE key = ?").get(migrationKey);
3716
+ if (row?.value === migrationVersion) {
3717
+ return;
3718
+ }
3719
+ const agents = await this.listAgents({ includeEphemeral: true });
3720
+ let changed = 0;
3721
+ for (const agent of agents) {
3722
+ if (isEphemeralAgent(agent)) {
3723
+ continue;
3724
+ }
3725
+ const nextRuntimeConfig = {
3726
+ ...resolveCreationRuntimeConfig(agent.runtimeConfig, agent.metadata) ?? {},
3727
+ enabled: true
3728
+ };
3729
+ const currentRuntimeConfig = agent.runtimeConfig ?? void 0;
3730
+ if (JSON.stringify(nextRuntimeConfig) === JSON.stringify(currentRuntimeConfig)) {
3731
+ continue;
3732
+ }
3733
+ await this.writeAgent({
3734
+ ...agent,
3735
+ runtimeConfig: nextRuntimeConfig
3736
+ });
3737
+ changed++;
3738
+ }
3739
+ this.db.prepare(`
3740
+ INSERT INTO __meta (key, value)
3741
+ VALUES (?, ?)
3742
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
3743
+ `).run(migrationKey, migrationVersion);
3744
+ if (changed > 0) {
3745
+ this.db.bumpLastModified();
3746
+ }
3747
+ }
3670
3748
  /**
3671
3749
  * Create a new agent with "idle" state.
3672
3750
  *
@@ -5776,11 +5854,12 @@ var init_global_settings = __esm({
5776
5854
  import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
5777
5855
  import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "node:fs";
5778
5856
  import { join as join5 } from "node:path";
5779
- var ARCHIVE_SCHEMA_SQL, ArchiveDatabase;
5857
+ var BASE_SCHEMA_SQL, FTS5_SCHEMA_SQL, ArchiveDatabase;
5780
5858
  var init_archive_db = __esm({
5781
5859
  "../core/src/archive-db.ts"() {
5782
5860
  "use strict";
5783
- ARCHIVE_SCHEMA_SQL = `
5861
+ init_db();
5862
+ BASE_SCHEMA_SQL = `
5784
5863
  CREATE TABLE IF NOT EXISTS archived_tasks (
5785
5864
  id TEXT PRIMARY KEY,
5786
5865
  taskJson TEXT NOT NULL,
@@ -5796,7 +5875,8 @@ CREATE TABLE IF NOT EXISTS archived_tasks (
5796
5875
 
5797
5876
  CREATE INDEX IF NOT EXISTS idxArchivedTasksArchivedAt ON archived_tasks(archivedAt);
5798
5877
  CREATE INDEX IF NOT EXISTS idxArchivedTasksCreatedAt ON archived_tasks(createdAt);
5799
-
5878
+ `;
5879
+ FTS5_SCHEMA_SQL = `
5800
5880
  CREATE VIRTUAL TABLE IF NOT EXISTS archived_tasks_fts USING fts5(
5801
5881
  id,
5802
5882
  title,
@@ -5824,18 +5904,26 @@ CREATE TRIGGER IF NOT EXISTS archived_tasks_fts_ad AFTER DELETE ON archived_task
5824
5904
  END;
5825
5905
  `;
5826
5906
  ArchiveDatabase = class {
5907
+ db;
5908
+ _fts5Available;
5827
5909
  constructor(kbDir) {
5828
- this.kbDir = kbDir;
5829
5910
  if (!existsSync4(kbDir)) {
5830
5911
  mkdirSync3(kbDir, { recursive: true });
5831
5912
  }
5832
5913
  this.db = new DatabaseSync2(join5(kbDir, "archive.db"));
5833
5914
  this.db.exec("PRAGMA journal_mode = WAL");
5834
5915
  this.db.exec("PRAGMA busy_timeout = 5000");
5916
+ this._fts5Available = probeFts5(this.db);
5917
+ }
5918
+ /** True when this SQLite build has FTS5. See db.ts#probeFts5. */
5919
+ get fts5Available() {
5920
+ return this._fts5Available;
5835
5921
  }
5836
- db;
5837
5922
  init() {
5838
- this.db.exec(ARCHIVE_SCHEMA_SQL);
5923
+ this.db.exec(BASE_SCHEMA_SQL);
5924
+ if (this._fts5Available) {
5925
+ this.db.exec(FTS5_SCHEMA_SQL);
5926
+ }
5839
5927
  this.addColumnIfMissing("archived_tasks", "prompt", "TEXT");
5840
5928
  }
5841
5929
  upsert(entry) {
@@ -5876,15 +5964,48 @@ END;
5876
5964
  delete(id) {
5877
5965
  this.db.prepare("DELETE FROM archived_tasks WHERE id = ?").run(id);
5878
5966
  }
5967
+ /**
5968
+ * Full-text search over archived tasks. Accepts a raw user query and routes
5969
+ * through FTS5 when available, or a LIKE-based scan when not.
5970
+ */
5879
5971
  search(query, limit) {
5972
+ const trimmed = query?.trim();
5973
+ if (!trimmed) return [];
5974
+ const tokens = trimmed.split(/\s+/).filter((t) => t.length > 0).map((t) => t.replace(/["{}:*^+()]/g, "")).filter((t) => t.length > 0);
5975
+ if (tokens.length === 0) return [];
5976
+ if (this._fts5Available) {
5977
+ const ftsQuery = tokens.map((token) => {
5978
+ if (/[":(){}*^+-]/.test(token)) {
5979
+ return `"${token.replace(/"/g, '\\"')}"`;
5980
+ }
5981
+ return token;
5982
+ }).join(" OR ");
5983
+ const rows2 = this.db.prepare(`
5984
+ SELECT a.taskJson
5985
+ FROM archived_tasks a
5986
+ JOIN archived_tasks_fts fts ON a.rowid = fts.rowid
5987
+ WHERE archived_tasks_fts MATCH ?
5988
+ ORDER BY rank
5989
+ LIMIT ?
5990
+ `).all(ftsQuery, limit);
5991
+ return rows2.map((row) => JSON.parse(row.taskJson));
5992
+ }
5993
+ const searchColumns = ["id", "title", "description", "comments"];
5994
+ const perTokenClause = `(${searchColumns.map((c) => `"${c}" LIKE ? ESCAPE '\\'`).join(" OR ")})`;
5995
+ const whereTokens = tokens.map(() => perTokenClause).join(" OR ");
5996
+ const params = [];
5997
+ for (const token of tokens) {
5998
+ const pattern = `%${token.replace(/[\\%_]/g, "\\$&")}%`;
5999
+ for (let i = 0; i < searchColumns.length; i++) params.push(pattern);
6000
+ }
6001
+ params.push(limit);
5880
6002
  const rows = this.db.prepare(`
5881
- SELECT a.taskJson
5882
- FROM archived_tasks a
5883
- JOIN archived_tasks_fts fts ON a.rowid = fts.rowid
5884
- WHERE archived_tasks_fts MATCH ?
5885
- ORDER BY rank
6003
+ SELECT taskJson
6004
+ FROM archived_tasks
6005
+ WHERE ${whereTokens}
6006
+ ORDER BY archivedAt DESC
5886
6007
  LIMIT ?
5887
- `).all(query, limit);
6008
+ `).all(...params);
5888
6009
  return rows.map((row) => JSON.parse(row.taskJson));
5889
6010
  }
5890
6011
  close() {
@@ -12197,6 +12318,9 @@ var init_migration = __esm({
12197
12318
  * @returns Generated name
12198
12319
  */
12199
12320
  async generateProjectName(projectPath) {
12321
+ if (!existsSync7(join9(projectPath, ".git"))) {
12322
+ return basename3(projectPath);
12323
+ }
12200
12324
  try {
12201
12325
  const { execFile: execFile5 } = await import("node:child_process");
12202
12326
  const { promisify: promisify14 } = await import("node:util");
@@ -12204,7 +12328,7 @@ var init_migration = __esm({
12204
12328
  const { stdout } = await execFileAsync3(
12205
12329
  "git",
12206
12330
  ["remote", "get-url", "origin"],
12207
- { cwd: projectPath, timeout: 5e3 }
12331
+ { cwd: projectPath, timeout: 1e3 }
12208
12332
  );
12209
12333
  const remoteUrl = stdout.trim();
12210
12334
  if (remoteUrl) {
@@ -28698,6 +28822,27 @@ var init_run_command = __esm({
28698
28822
  }
28699
28823
  });
28700
28824
 
28825
+ // ../core/src/logger.ts
28826
+ function createLogger(prefix) {
28827
+ const tag = `[${prefix}]`;
28828
+ return {
28829
+ log(message, ...args) {
28830
+ console.error(`${tag} ${message}`, ...args);
28831
+ },
28832
+ warn(message, ...args) {
28833
+ console.warn(`${tag} ${message}`, ...args);
28834
+ },
28835
+ error(message, ...args) {
28836
+ console.error(`${tag} ${message}`, ...args);
28837
+ }
28838
+ };
28839
+ }
28840
+ var init_logger = __esm({
28841
+ "../core/src/logger.ts"() {
28842
+ "use strict";
28843
+ }
28844
+ });
28845
+
28701
28846
  // ../core/src/store.ts
28702
28847
  import { EventEmitter as EventEmitter11 } from "node:events";
28703
28848
  import { randomUUID as randomUUID6 } from "node:crypto";
@@ -28746,7 +28891,7 @@ function canonicalizeSettings(settings) {
28746
28891
  }
28747
28892
  return settings;
28748
28893
  }
28749
- 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;
28894
+ 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;
28750
28895
  var init_store = __esm({
28751
28896
  "../core/src/store.ts"() {
28752
28897
  "use strict";
@@ -28764,11 +28909,13 @@ var init_store = __esm({
28764
28909
  init_task_merge();
28765
28910
  init_project_memory();
28766
28911
  init_run_command();
28912
+ init_logger();
28767
28913
  LEGACY_BACKUP_DIR = ".kb/backups";
28768
28914
  TASK_ACTIVITY_LOG_ENTRY_LIMIT = 1e3;
28769
28915
  TASK_ACTIVITY_LOG_OUTCOME_LIMIT = 4e3;
28770
28916
  ARCHIVE_AGENT_LOG_SNAPSHOT_LIMIT = 25;
28771
28917
  ARCHIVE_AGENT_LOG_SNIPPET_LIMIT = 160;
28918
+ storeLog = createLogger("task-store");
28772
28919
  TaskHasDependentsError = class extends Error {
28773
28920
  taskId;
28774
28921
  dependentIds;
@@ -29478,45 +29625,53 @@ ${recentText}` : void 0
29478
29625
  */
29479
29626
  setupActivityLogListeners() {
29480
29627
  this.on("task:created", (task) => {
29481
- this.recordActivity({
29482
- type: "task:created",
29483
- taskId: task.id,
29484
- taskTitle: task.title,
29485
- details: `Task ${task.id} created${task.title ? `: ${task.title}` : ""}`
29486
- }).catch(() => {
29487
- });
29628
+ this.recordActivityFromListener(
29629
+ {
29630
+ type: "task:created",
29631
+ taskId: task.id,
29632
+ taskTitle: task.title,
29633
+ details: `Task ${task.id} created${task.title ? `: ${task.title}` : ""}`
29634
+ },
29635
+ "task:created"
29636
+ );
29488
29637
  });
29489
29638
  this.on("task:moved", (data) => {
29490
- this.recordActivity({
29491
- type: "task:moved",
29492
- taskId: data.task.id,
29493
- taskTitle: data.task.title,
29494
- details: `Task ${data.task.id} moved: ${data.from} \u2192 ${data.to}`,
29495
- metadata: { from: data.from, to: data.to }
29496
- }).catch(() => {
29497
- });
29639
+ this.recordActivityFromListener(
29640
+ {
29641
+ type: "task:moved",
29642
+ taskId: data.task.id,
29643
+ taskTitle: data.task.title,
29644
+ details: `Task ${data.task.id} moved: ${data.from} \u2192 ${data.to}`,
29645
+ metadata: { from: data.from, to: data.to }
29646
+ },
29647
+ "task:moved"
29648
+ );
29498
29649
  });
29499
29650
  this.on("task:merged", (result) => {
29500
29651
  const status = result.merged ? "successfully merged" : "merge attempted";
29501
- this.recordActivity({
29502
- type: "task:merged",
29503
- taskId: result.task.id,
29504
- taskTitle: result.task.title,
29505
- details: `Task ${result.task.id} ${status} to main`,
29506
- metadata: { merged: result.merged, branch: result.branch }
29507
- }).catch(() => {
29508
- });
29652
+ this.recordActivityFromListener(
29653
+ {
29654
+ type: "task:merged",
29655
+ taskId: result.task.id,
29656
+ taskTitle: result.task.title,
29657
+ details: `Task ${result.task.id} ${status} to main`,
29658
+ metadata: { merged: result.merged, branch: result.branch }
29659
+ },
29660
+ "task:merged"
29661
+ );
29509
29662
  });
29510
29663
  this.on("task:updated", (task) => {
29511
29664
  if (task.status === "failed") {
29512
- this.recordActivity({
29513
- type: "task:failed",
29514
- taskId: task.id,
29515
- taskTitle: task.title,
29516
- details: `Task ${task.id} failed${task.error ? `: ${task.error}` : ""}`,
29517
- metadata: task.error ? { error: task.error } : void 0
29518
- }).catch(() => {
29519
- });
29665
+ this.recordActivityFromListener(
29666
+ {
29667
+ type: "task:failed",
29668
+ taskId: task.id,
29669
+ taskTitle: task.title,
29670
+ details: `Task ${task.id} failed${task.error ? `: ${task.error}` : ""}`,
29671
+ metadata: task.error ? { error: task.error } : void 0
29672
+ },
29673
+ "task:updated"
29674
+ );
29520
29675
  }
29521
29676
  });
29522
29677
  this.on("settings:updated", (data) => {
@@ -29534,21 +29689,35 @@ ${recentText}` : void 0
29534
29689
  importantChanges.push(`engine pause ${data.settings.enginePaused ? "enabled" : "disabled"}`);
29535
29690
  }
29536
29691
  if (importantChanges.length > 0) {
29537
- this.recordActivity({
29538
- type: "settings:updated",
29539
- details: `Settings updated: ${importantChanges.join(", ")}`,
29540
- metadata: { changes: importantChanges }
29541
- }).catch(() => {
29542
- });
29692
+ this.recordActivityFromListener(
29693
+ {
29694
+ type: "settings:updated",
29695
+ details: `Settings updated: ${importantChanges.join(", ")}`,
29696
+ metadata: { changes: importantChanges }
29697
+ },
29698
+ "settings:updated"
29699
+ );
29543
29700
  }
29544
29701
  });
29545
29702
  this.on("task:deleted", (task) => {
29546
- this.recordActivity({
29547
- type: "task:deleted",
29548
- taskId: task.id,
29549
- taskTitle: task.title,
29550
- details: `Task ${task.id} deleted${task.title ? `: ${task.title}` : ""}`
29551
- }).catch(() => {
29703
+ this.recordActivityFromListener(
29704
+ {
29705
+ type: "task:deleted",
29706
+ taskId: task.id,
29707
+ taskTitle: task.title,
29708
+ details: `Task ${task.id} deleted${task.title ? `: ${task.title}` : ""}`
29709
+ },
29710
+ "task:deleted"
29711
+ );
29712
+ });
29713
+ }
29714
+ recordActivityFromListener(entry, sourceEvent) {
29715
+ this.recordActivity(entry).catch((err) => {
29716
+ storeLog.warn("Activity logging listener failed", {
29717
+ sourceEvent,
29718
+ type: entry.type,
29719
+ taskId: entry.taskId,
29720
+ error: err instanceof Error ? err.message : String(err)
29552
29721
  });
29553
29722
  });
29554
29723
  }
@@ -30070,7 +30239,11 @@ ${recentText}` : void 0
30070
30239
  if (defaultOnSteps.length > 0) {
30071
30240
  resolvedWorkflowSteps = defaultOnSteps;
30072
30241
  }
30073
- } catch {
30242
+ } catch (err) {
30243
+ storeLog.warn("Failed to auto-apply default workflow steps during task creation", {
30244
+ error: err instanceof Error ? err.message : String(err),
30245
+ descriptionLength: input.description.length
30246
+ });
30074
30247
  }
30075
30248
  } else if (input.enabledWorkflowSteps.length === 0) {
30076
30249
  resolvedWorkflowSteps = void 0;
@@ -30088,13 +30261,26 @@ ${recentText}` : void 0
30088
30261
  }
30089
30262
  }
30090
30263
  } catch (err) {
30091
- const errorMsg = err instanceof Error ? err.message : String(err);
30092
30264
  const autoEnabled = options?.settings?.autoSummarizeTitles === true;
30093
- console.warn(
30094
- `[TaskStore] Title summarization failed for task ${id}: ${errorMsg} (desc length: ${input.description.length}, auto-summarize: ${autoEnabled})`
30265
+ const errorMessage = err instanceof Error ? err.message : String(err);
30266
+ storeLog.warn(
30267
+ `Title summarization failed for task ${id}: ${errorMessage} (desc length: ${input.description.length}, auto-summarize: ${autoEnabled})`,
30268
+ {
30269
+ taskId: id,
30270
+ descriptionLength: input.description.length,
30271
+ autoSummarizeEnabled: autoEnabled,
30272
+ error: errorMessage
30273
+ }
30095
30274
  );
30096
30275
  }
30097
- }).catch(() => {
30276
+ }).catch((err) => {
30277
+ const autoEnabled = options?.settings?.autoSummarizeTitles === true;
30278
+ storeLog.error("Unexpected title summarization promise-chain failure", {
30279
+ taskId: id,
30280
+ descriptionLength: input.description.length,
30281
+ autoSummarizeEnabled: autoEnabled,
30282
+ error: err instanceof Error ? err.message : String(err)
30283
+ });
30098
30284
  });
30099
30285
  }
30100
30286
  return task;
@@ -30361,26 +30547,46 @@ ${newTask.description}
30361
30547
  if (sanitizedTokens.length === 0) {
30362
30548
  return this.listTasks(options);
30363
30549
  }
30364
- const ftsQuery = sanitizedTokens.map((token) => {
30365
- if (/[":(){}*^+-]/.test(token)) {
30366
- return `"${token.replace(/"/g, '\\"')}"`;
30367
- }
30368
- return token;
30369
- }).join(" OR ");
30370
30550
  const limit = options?.limit ?? -1;
30371
30551
  const offset = options?.offset ?? 0;
30372
30552
  const offsetClause = offset > 0 ? ` OFFSET ${offset}` : "";
30373
30553
  const includeArchived = options?.includeArchived ?? true;
30374
- const whereClause = includeArchived ? "" : ` AND t."column" != 'archived'`;
30375
- const selectClause = this.getTaskSelectClause(options?.slim ?? false, "t");
30376
- const rows = this.db.prepare(`
30377
- SELECT ${selectClause} FROM tasks t
30378
- JOIN tasks_fts fts ON t.rowid = fts.rowid
30379
- WHERE tasks_fts MATCH ?
30380
- ${whereClause}
30381
- ORDER BY rank
30382
- LIMIT ${limit >= 0 ? limit : -1}${offsetClause}
30383
- `).all(ftsQuery);
30554
+ const slim = options?.slim ?? false;
30555
+ const selectClause = this.getTaskSelectClause(slim, "t");
30556
+ let rows;
30557
+ if (this.db.fts5Available) {
30558
+ const ftsQuery = sanitizedTokens.map((token) => {
30559
+ if (/[":(){}*^+-]/.test(token)) {
30560
+ return `"${token.replace(/"/g, '\\"')}"`;
30561
+ }
30562
+ return token;
30563
+ }).join(" OR ");
30564
+ const whereClause = includeArchived ? "" : ` AND t."column" != 'archived'`;
30565
+ rows = this.db.prepare(`
30566
+ SELECT ${selectClause} FROM tasks t
30567
+ JOIN tasks_fts fts ON t.rowid = fts.rowid
30568
+ WHERE tasks_fts MATCH ?
30569
+ ${whereClause}
30570
+ ORDER BY rank
30571
+ LIMIT ${limit >= 0 ? limit : -1}${offsetClause}
30572
+ `).all(ftsQuery);
30573
+ } else {
30574
+ const searchColumns = ["id", "title", "description", "comments"];
30575
+ const perTokenClause = `(${searchColumns.map((c) => `t."${c}" LIKE ? ESCAPE '\\'`).join(" OR ")})`;
30576
+ const whereTokens = sanitizedTokens.map(() => perTokenClause).join(" OR ");
30577
+ const params = [];
30578
+ for (const token of sanitizedTokens) {
30579
+ const pattern = `%${token.replace(/[\\%_]/g, "\\$&")}%`;
30580
+ for (let i = 0; i < searchColumns.length; i++) params.push(pattern);
30581
+ }
30582
+ const archivedClause = includeArchived ? "" : ` AND t."column" != 'archived'`;
30583
+ rows = this.db.prepare(`
30584
+ SELECT ${selectClause} FROM tasks t
30585
+ WHERE (${whereTokens})${archivedClause}
30586
+ ORDER BY t.createdAt ASC
30587
+ LIMIT ${limit >= 0 ? limit : -1}${offsetClause}
30588
+ `).all(...params);
30589
+ }
30384
30590
  const activeMatches = await Promise.all(rows.map(async (row) => {
30385
30591
  const task = this.rowToTask(row);
30386
30592
  if (task.steps.length > 0) {
@@ -30389,7 +30595,7 @@ ${newTask.description}
30389
30595
  const steps = await this.parseStepsFromPrompt(task.id);
30390
30596
  return steps.length > 0 ? { ...task, steps } : task;
30391
30597
  }));
30392
- const archiveMatches = includeArchived ? this.archiveDb.search(ftsQuery, limit >= 0 ? limit : 100).map((entry) => this.archiveEntryToTask(entry, options?.slim ?? false)) : [];
30598
+ const archiveMatches = includeArchived ? this.archiveDb.search(trimmedQuery, limit >= 0 ? limit : 100).map((entry) => this.archiveEntryToTask(entry, slim)) : [];
30393
30599
  const matches = [...activeMatches, ...archiveMatches];
30394
30600
  return limit >= 0 ? matches.slice(0, limit) : matches;
30395
30601
  }
@@ -31525,9 +31731,17 @@ ${task.description}
31525
31731
  try {
31526
31732
  this.watcher = watch(this.tasksDir, { recursive: true }, (_event, _filename) => {
31527
31733
  });
31528
- this.watcher.on("error", () => {
31734
+ this.watcher.on("error", (err) => {
31735
+ storeLog.warn("fs.watch emitted an error; polling will continue", {
31736
+ error: err instanceof Error ? err.message : String(err),
31737
+ tasksDir: this.tasksDir
31738
+ });
31739
+ });
31740
+ } catch (err) {
31741
+ storeLog.warn("fs.watch unavailable; falling back to polling-only updates", {
31742
+ error: err instanceof Error ? err.message : String(err),
31743
+ tasksDir: this.tasksDir
31529
31744
  });
31530
- } catch {
31531
31745
  }
31532
31746
  this.pollInterval = setInterval(() => {
31533
31747
  void this.checkForChanges();
@@ -31583,9 +31797,17 @@ ${task.description}
31583
31797
  }
31584
31798
  const elapsed = Date.now() - startTime;
31585
31799
  if (elapsed > 100) {
31586
- console.warn(`[TaskStore] checkForChanges took ${elapsed}ms \u2014 event loop may have been blocked`);
31800
+ storeLog.warn("checkForChanges took longer than expected", {
31801
+ elapsedMs: elapsed,
31802
+ thresholdMs: 100
31803
+ });
31587
31804
  }
31588
- } catch {
31805
+ } catch (err) {
31806
+ storeLog.warn("checkForChanges poll cycle failed", {
31807
+ lastKnownModified: this.lastKnownModified,
31808
+ lastPollTime: this.lastPollTime,
31809
+ error: err instanceof Error ? err.message : String(err)
31810
+ });
31589
31811
  } finally {
31590
31812
  this.pollingInProgress = false;
31591
31813
  }
@@ -32840,7 +33062,15 @@ ${notificationsSection}
32840
33062
  );
32841
33063
  this.db.bumpLastModified();
32842
33064
  } catch (err) {
32843
- console.error("Failed to record activity:", err);
33065
+ storeLog.error("Failed to record activity", {
33066
+ id: fullEntry.id,
33067
+ type: fullEntry.type,
33068
+ taskId: fullEntry.taskId,
33069
+ taskTitle: fullEntry.taskTitle,
33070
+ detailsLength: fullEntry.details.length,
33071
+ hasMetadata: fullEntry.metadata !== void 0,
33072
+ error: err instanceof Error ? err.message : String(err)
33073
+ });
32844
33074
  }
32845
33075
  return fullEntry;
32846
33076
  }
@@ -33822,27 +34052,6 @@ var init_routine_store = __esm({
33822
34052
  }
33823
34053
  });
33824
34054
 
33825
- // ../core/src/logger.ts
33826
- function createLogger(prefix) {
33827
- const tag = `[${prefix}]`;
33828
- return {
33829
- log(message, ...args) {
33830
- console.error(`${tag} ${message}`, ...args);
33831
- },
33832
- warn(message, ...args) {
33833
- console.warn(`${tag} ${message}`, ...args);
33834
- },
33835
- error(message, ...args) {
33836
- console.error(`${tag} ${message}`, ...args);
33837
- }
33838
- };
33839
- }
33840
- var init_logger = __esm({
33841
- "../core/src/logger.ts"() {
33842
- "use strict";
33843
- }
33844
- });
33845
-
33846
34055
  // ../core/src/plugin-loader.ts
33847
34056
  import { isAbsolute as isAbsolute5, resolve as resolve8 } from "node:path";
33848
34057
  import { EventEmitter as EventEmitter13 } from "node:events";
@@ -59048,13 +59257,13 @@ function createLogger2(prefix) {
59048
59257
  const tag = `[${prefix}]`;
59049
59258
  return {
59050
59259
  log(message, ...args) {
59051
- console.error(`${tag} ${message}`, ...args);
59260
+ globalThis.console.error(`${tag} ${message}`, ...args);
59052
59261
  },
59053
59262
  warn(message, ...args) {
59054
- console.warn(`${tag} ${message}`, ...args);
59263
+ globalThis.console.warn(`${tag} ${message}`, ...args);
59055
59264
  },
59056
59265
  error(message, ...args) {
59057
- console.error(`${tag} ${message}`, ...args);
59266
+ globalThis.console.error(`${tag} ${message}`, ...args);
59058
59267
  }
59059
59268
  };
59060
59269
  }
@@ -60372,7 +60581,6 @@ function createSkillsOverrideFromSelection(selection, options = {}) {
60372
60581
  }
60373
60582
  }
60374
60583
  if (newDiagnostics.length > 0) {
60375
- const _purpose = sessionPurpose ? `[${sessionPurpose}]` : "skills";
60376
60584
  for (const diag of newDiagnostics) {
60377
60585
  piLog.warn(`[skills] ${diag.type}: ${diag.message}`);
60378
60586
  }
@@ -60442,7 +60650,7 @@ import { join as join27 } from "node:path";
60442
60650
  import { AuthStorage } from "@mariozechner/pi-coding-agent";
60443
60651
  import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
60444
60652
  function getHomeDir2() {
60445
- return process.env.HOME || process.env.USERPROFILE || homedir6();
60653
+ return globalThis.process.env.HOME || globalThis.process.env.USERPROFILE || homedir6();
60446
60654
  }
60447
60655
  function getFusionAuthPath2(home = getHomeDir2()) {
60448
60656
  return join27(home, ".fusion", "agent", "auth.json");
@@ -60488,7 +60696,7 @@ function readLegacyCredentials(authPaths = getLegacyAuthPaths()) {
60488
60696
  function resolveStoredApiKey(key) {
60489
60697
  if (!key)
60490
60698
  return void 0;
60491
- return process.env[key] ?? key;
60699
+ return globalThis.process.env[key] ?? key;
60492
60700
  }
60493
60701
  function resolveOAuthApiKey(providerId, credential) {
60494
60702
  if (credential.type !== "oauth" || typeof credential.access !== "string" || typeof credential.refresh !== "string" || typeof credential.expires !== "number" || Date.now() >= credential.expires) {
@@ -60791,8 +60999,8 @@ function createReadOnlyPiSettingsView(cwd, agentDir) {
60791
60999
  const fusionProjectSettings = readJsonObject2(join28(projectRoot, ".fusion", "settings.json"));
60792
61000
  const mergedSettings = { ...globalSettings, ...fusionProjectSettings };
60793
61001
  return {
60794
- getGlobalSettings: () => structuredClone(globalSettings),
60795
- getProjectSettings: () => structuredClone(fusionProjectSettings),
61002
+ getGlobalSettings: () => globalThis.structuredClone(globalSettings),
61003
+ getProjectSettings: () => globalThis.structuredClone(fusionProjectSettings),
60796
61004
  getNpmCommand: () => Array.isArray(mergedSettings.npmCommand) ? [...mergedSettings.npmCommand] : void 0
60797
61005
  };
60798
61006
  }
@@ -60911,9 +61119,7 @@ function wrapToolsWithBoundary(tools, worktreePath, projectRoot) {
60911
61119
  return {
60912
61120
  ...tool,
60913
61121
  execute: async (...args) => {
60914
- const _toolCallId = args[0];
60915
61122
  const params = args[1];
60916
- const _signal = args[2];
60917
61123
  const pathArg = params.path;
60918
61124
  if (pathArg && !isWorktreeAllowedPath(worktreePath, projectRoot, pathArg)) {
60919
61125
  const relToProject = relative4(projectRoot, pathArg);
@@ -67131,6 +67337,36 @@ function buildReducedStepPrompt(taskDetail, stepIndex) {
67131
67337
  ];
67132
67338
  return parts.join("\n").replace(/\n{3,}/g, "\n\n");
67133
67339
  }
67340
+ function resolveExecutorModelPair(taskModelProvider, taskModelId, settings) {
67341
+ if (taskModelProvider && taskModelId) {
67342
+ return { provider: taskModelProvider, modelId: taskModelId };
67343
+ }
67344
+ if (settings?.executionProvider && settings?.executionModelId) {
67345
+ return {
67346
+ provider: settings.executionProvider,
67347
+ modelId: settings.executionModelId
67348
+ };
67349
+ }
67350
+ if (settings?.executionGlobalProvider && settings?.executionGlobalModelId) {
67351
+ return {
67352
+ provider: settings.executionGlobalProvider,
67353
+ modelId: settings.executionGlobalModelId
67354
+ };
67355
+ }
67356
+ if (settings?.defaultProviderOverride && settings?.defaultModelIdOverride) {
67357
+ return {
67358
+ provider: settings.defaultProviderOverride,
67359
+ modelId: settings.defaultModelIdOverride
67360
+ };
67361
+ }
67362
+ if (settings?.defaultProvider && settings?.defaultModelId) {
67363
+ return {
67364
+ provider: settings.defaultProvider,
67365
+ modelId: settings.defaultModelId
67366
+ };
67367
+ }
67368
+ return { provider: void 0, modelId: void 0 };
67369
+ }
67134
67370
  function sleep2(ms) {
67135
67371
  return new Promise((resolve29) => setTimeout(resolve29, ms));
67136
67372
  }
@@ -67323,8 +67559,11 @@ var init_step_session_executor = __esm({
67323
67559
  createSendMessageTool(this.options.messageStore, taskDetail.assignedAgentId),
67324
67560
  createReadMessagesTool(this.options.messageStore, taskDetail.assignedAgentId)
67325
67561
  ] : [];
67326
- const executorProvider = taskDetail.modelProvider && taskDetail.modelId ? taskDetail.modelProvider : settings.executionProvider && settings.executionModelId ? settings.executionProvider : settings.executionGlobalProvider && settings.executionGlobalModelId ? settings.executionGlobalProvider : settings.defaultProvider;
67327
- const executorModelId = taskDetail.modelProvider && taskDetail.modelId ? taskDetail.modelId : settings.executionProvider && settings.executionModelId ? settings.executionModelId : settings.executionGlobalProvider && settings.executionGlobalModelId ? settings.executionGlobalModelId : settings.defaultModelId;
67562
+ const { provider: executorProvider, modelId: executorModelId } = resolveExecutorModelPair(
67563
+ taskDetail.modelProvider,
67564
+ taskDetail.modelId,
67565
+ settings
67566
+ );
67328
67567
  const createResult = await createFnAgent5({
67329
67568
  cwd: worktreePath,
67330
67569
  systemPrompt: `You are an AI agent executing step ${stepIndex} of task ${taskDetail.id}. Follow instructions precisely.`,
@@ -67758,6 +67997,36 @@ function getExecutorSystemPrompt(settings) {
67758
67997
  const customPrompt = resolveAgentPrompt("executor", settings.agentPrompts);
67759
67998
  return customPrompt || EXECUTOR_SYSTEM_PROMPT;
67760
67999
  }
68000
+ function resolveExecutorModelPair2(taskModelProvider, taskModelId, settings) {
68001
+ if (taskModelProvider && taskModelId) {
68002
+ return { provider: taskModelProvider, modelId: taskModelId };
68003
+ }
68004
+ if (settings?.executionProvider && settings?.executionModelId) {
68005
+ return {
68006
+ provider: settings.executionProvider,
68007
+ modelId: settings.executionModelId
68008
+ };
68009
+ }
68010
+ if (settings?.executionGlobalProvider && settings?.executionGlobalModelId) {
68011
+ return {
68012
+ provider: settings.executionGlobalProvider,
68013
+ modelId: settings.executionGlobalModelId
68014
+ };
68015
+ }
68016
+ if (settings?.defaultProviderOverride && settings?.defaultModelIdOverride) {
68017
+ return {
68018
+ provider: settings.defaultProviderOverride,
68019
+ modelId: settings.defaultModelIdOverride
68020
+ };
68021
+ }
68022
+ if (settings?.defaultProvider && settings?.defaultModelId) {
68023
+ return {
68024
+ provider: settings.defaultProvider,
68025
+ modelId: settings.defaultModelId
68026
+ };
68027
+ }
68028
+ return { provider: void 0, modelId: void 0 };
68029
+ }
67761
68030
  function formatTimestamp2(iso) {
67762
68031
  const date = new Date(iso);
67763
68032
  const now = /* @__PURE__ */ new Date();
@@ -67914,7 +68183,7 @@ function detectReviewHandoffIntent(commentText) {
67914
68183
  ];
67915
68184
  return handoffPhrases.some((phrase) => text.includes(phrase));
67916
68185
  }
67917
- var execAsync5, STEP_STATUSES, MAX_WORKFLOW_STEP_RETRIES, WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2, NonRetryableWorktreeError, taskUpdateParams, taskAddDepParams, spawnAgentParams, reviewStepParams, EXECUTOR_SYSTEM_PROMPT, TaskExecutor;
68186
+ 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;
67918
68187
  var init_executor = __esm({
67919
68188
  "../engine/src/executor.ts"() {
67920
68189
  "use strict";
@@ -67947,6 +68216,8 @@ var init_executor = __esm({
67947
68216
  execAsync5 = promisify6(exec5);
67948
68217
  STEP_STATUSES = ["pending", "in-progress", "done", "skipped"];
67949
68218
  MAX_WORKFLOW_STEP_RETRIES = 3;
68219
+ MAX_TASK_DONE_SESSION_RETRIES = 3;
68220
+ MAX_TASK_DONE_REQUEUE_RETRIES = 3;
67950
68221
  WORKFLOW_SCRIPT_OUTPUT_MAX_CHARS2 = 4e3;
67951
68222
  NonRetryableWorktreeError = class extends Error {
67952
68223
  };
@@ -68234,8 +68505,11 @@ Lint, tests, and typecheck are also hard quality gates:
68234
68505
  activeEntry.lastModelProvider = task.modelProvider;
68235
68506
  activeEntry.lastModelId = task.modelId;
68236
68507
  const settings = await this.store.getSettings();
68237
- const newProvider = task.modelProvider && task.modelId ? task.modelProvider : settings?.executionProvider && settings?.executionModelId ? settings.executionProvider : settings?.executionGlobalProvider && settings?.executionGlobalModelId ? settings.executionGlobalProvider : settings?.defaultProvider;
68238
- const newModelId = task.modelProvider && task.modelId ? task.modelId : settings?.executionProvider && settings?.executionModelId ? settings.executionModelId : settings?.executionGlobalProvider && settings?.executionGlobalModelId ? settings.executionGlobalModelId : settings?.defaultModelId;
68508
+ const { provider: newProvider, modelId: newModelId } = resolveExecutorModelPair2(
68509
+ task.modelProvider,
68510
+ task.modelId,
68511
+ settings
68512
+ );
68239
68513
  if (newProvider && newModelId) {
68240
68514
  try {
68241
68515
  const model = this.modelRegistry.find(newProvider, newModelId);
@@ -69142,8 +69416,11 @@ Lint, tests, and typecheck are also hard quality gates:
69142
69416
  }
69143
69417
  });
69144
69418
  const agentWork = async () => {
69145
- const executorProvider = detail.modelProvider && detail.modelId ? detail.modelProvider : settings.executionProvider && settings.executionModelId ? settings.executionProvider : settings.executionGlobalProvider && settings.executionGlobalModelId ? settings.executionGlobalProvider : settings.defaultProvider;
69146
- const executorModelId = detail.modelProvider && detail.modelId ? detail.modelId : settings.executionProvider && settings.executionModelId ? settings.executionModelId : settings.executionGlobalProvider && settings.executionGlobalModelId ? settings.executionGlobalModelId : settings.defaultModelId;
69419
+ const { provider: executorProvider, modelId: executorModelId } = resolveExecutorModelPair2(
69420
+ detail.modelProvider,
69421
+ detail.modelId,
69422
+ settings
69423
+ );
69147
69424
  const executorFallbackProvider = settings.fallbackProvider;
69148
69425
  const executorFallbackModelId = settings.fallbackModelId;
69149
69426
  const executorThinkingLevel = detail.thinkingLevel ?? settings.defaultThinkingLevel;
@@ -69325,63 +69602,74 @@ Lint, tests, and typecheck are also hard quality gates:
69325
69602
  executorLog.log(`\u2713 ${task.id} completed \u2192 in-review`);
69326
69603
  this.options.onComplete?.(task);
69327
69604
  } else {
69328
- executorLog.log(`\u26A0 ${task.id} finished without task_done \u2014 retrying with new session`);
69329
- await this.store.logEntry(task.id, "Agent finished without calling task_done \u2014 retrying with new session", void 0, this.currentRunContext);
69330
- this.activeSessions.delete(task.id);
69331
- session.dispose();
69332
- const { session: retrySession2, sessionFile: retrySessionFile } = await createResolvedAgentSession({
69333
- sessionPurpose: "executor",
69334
- pluginRunner: this.options.pluginRunner,
69335
- cwd: worktreePath,
69336
- systemPrompt: executorSystemPrompt,
69337
- tools: "coding",
69338
- customTools,
69339
- onText: agentLogger.onText,
69340
- onThinking: agentLogger.onThinking,
69341
- onToolStart: agentLogger.onToolStart,
69342
- onToolEnd: agentLogger.onToolEnd,
69343
- defaultProvider: executorProvider,
69344
- defaultModelId: executorModelId,
69345
- fallbackProvider: executorFallbackProvider,
69346
- fallbackModelId: executorFallbackModelId,
69347
- defaultThinkingLevel: executorThinkingLevel,
69348
- sessionManager: SessionManager2.create(worktreePath),
69349
- // Skill selection: use assigned agent skills if available, otherwise role fallback
69350
- ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
69351
- });
69352
- if (retrySessionFile) {
69353
- this.store.updateTask(task.id, { sessionFile: retrySessionFile }).catch((err) => {
69354
- const msg = err instanceof Error ? err.message : String(err);
69355
- executorLog.warn(`${task.id} failed to persist retry sessionFile: ${msg}`);
69605
+ let taskDoneSessionRetries = 0;
69606
+ while (!taskDone && taskDoneSessionRetries < MAX_TASK_DONE_SESSION_RETRIES) {
69607
+ taskDoneSessionRetries++;
69608
+ executorLog.log(
69609
+ `\u26A0 ${task.id} finished without task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`
69610
+ );
69611
+ await this.store.logEntry(
69612
+ task.id,
69613
+ `Agent finished without calling task_done \u2014 retrying with new session (${taskDoneSessionRetries}/${MAX_TASK_DONE_SESSION_RETRIES})`,
69614
+ void 0,
69615
+ this.currentRunContext
69616
+ );
69617
+ this.activeSessions.delete(task.id);
69618
+ session.dispose();
69619
+ const { session: retrySession2, sessionFile: retrySessionFile } = await createResolvedAgentSession({
69620
+ sessionPurpose: "executor",
69621
+ pluginRunner: this.options.pluginRunner,
69622
+ cwd: worktreePath,
69623
+ systemPrompt: executorSystemPrompt,
69624
+ tools: "coding",
69625
+ customTools,
69626
+ onText: agentLogger.onText,
69627
+ onThinking: agentLogger.onThinking,
69628
+ onToolStart: agentLogger.onToolStart,
69629
+ onToolEnd: agentLogger.onToolEnd,
69630
+ defaultProvider: executorProvider,
69631
+ defaultModelId: executorModelId,
69632
+ fallbackProvider: executorFallbackProvider,
69633
+ fallbackModelId: executorFallbackModelId,
69634
+ defaultThinkingLevel: executorThinkingLevel,
69635
+ sessionManager: SessionManager2.create(worktreePath),
69636
+ // Skill selection: use assigned agent skills if available, otherwise role fallback
69637
+ ...skillContext.skillSelectionContext ? { skillSelection: skillContext.skillSelectionContext } : {}
69356
69638
  });
69357
- }
69358
- session = retrySession2;
69359
- sessionRef.current = retrySession2;
69360
- this.activeSessions.set(task.id, {
69361
- session: retrySession2,
69362
- seenSteeringIds,
69363
- lastModelProvider: detail.modelProvider,
69364
- lastModelId: detail.modelId
69365
- });
69366
- stuckDetector?.trackTask(task.id, retrySession2);
69367
- const retryPrompt = [
69368
- "Your previous session ended without calling the task_done tool.",
69369
- "The task may already be complete \u2014 review the current state of the worktree and either:",
69370
- "1. If the work is done, call task_done with a summary of what was accomplished.",
69371
- "2. If there is remaining work, finish it and then call task_done.",
69372
- "",
69373
- "Original task:",
69374
- buildExecutionPrompt(detail, this.rootDir, settings, worktreePath)
69375
- ].join("\n");
69376
- stuckDetector?.recordActivity(task.id);
69377
- await promptWithFallback(retrySession2, retryPrompt);
69378
- checkSessionError(retrySession2);
69379
- if (!taskDone) {
69380
- const implicitCheck = await this.store.getTask(task.id);
69381
- if (implicitCheck.steps.length > 0 && implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
69382
- taskDone = true;
69383
- executorLog.log(`${task.id} all steps done \u2014 treating as implicit task_done`);
69384
- await this.store.logEntry(task.id, "All steps complete \u2014 implicit task_done (agent did not call tool explicitly)", void 0, this.currentRunContext);
69639
+ if (retrySessionFile) {
69640
+ this.store.updateTask(task.id, { sessionFile: retrySessionFile }).catch((err) => {
69641
+ const msg = err instanceof Error ? err.message : String(err);
69642
+ executorLog.warn(`${task.id} failed to persist retry sessionFile: ${msg}`);
69643
+ });
69644
+ }
69645
+ session = retrySession2;
69646
+ sessionRef.current = retrySession2;
69647
+ this.activeSessions.set(task.id, {
69648
+ session: retrySession2,
69649
+ seenSteeringIds,
69650
+ lastModelProvider: detail.modelProvider,
69651
+ lastModelId: detail.modelId
69652
+ });
69653
+ stuckDetector?.trackTask(task.id, retrySession2);
69654
+ const retryPrompt = [
69655
+ "Your previous session ended without calling the task_done tool.",
69656
+ "The task may already be complete \u2014 review the current state of the worktree and either:",
69657
+ "1. If the work is done, call task_done with a summary of what was accomplished.",
69658
+ "2. If there is remaining work, finish it and then call task_done.",
69659
+ "",
69660
+ "Original task:",
69661
+ buildExecutionPrompt(detail, this.rootDir, settings, worktreePath)
69662
+ ].join("\n");
69663
+ stuckDetector?.recordActivity(task.id);
69664
+ await promptWithFallback(retrySession2, retryPrompt);
69665
+ checkSessionError(retrySession2);
69666
+ if (!taskDone) {
69667
+ const implicitCheck = await this.store.getTask(task.id);
69668
+ if (implicitCheck.steps.length > 0 && implicitCheck.steps.every((s) => s.status === "done" || s.status === "skipped")) {
69669
+ taskDone = true;
69670
+ executorLog.log(`${task.id} all steps done \u2014 treating as implicit task_done`);
69671
+ await this.store.logEntry(task.id, "All steps complete \u2014 implicit task_done (agent did not call tool explicitly)", void 0, this.currentRunContext);
69672
+ }
69385
69673
  }
69386
69674
  }
69387
69675
  if (taskDone) {
@@ -69410,11 +69698,29 @@ Lint, tests, and typecheck are also hard quality gates:
69410
69698
  executorLog.log(`\u2713 ${task.id} completed on retry \u2192 in-review`);
69411
69699
  this.options.onComplete?.(task);
69412
69700
  } else {
69413
- const errorMessage = "Agent finished without calling task_done (after retry)";
69414
- await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
69415
- await this.store.logEntry(task.id, `${errorMessage} \u2014 moved to in-review for inspection`, void 0, this.currentRunContext);
69416
- await this.store.moveTask(task.id, "in-review");
69417
- executorLog.log(`\u2717 ${task.id} failed after retry \u2014 no task_done \u2192 in-review`);
69701
+ const priorRequeues = task.taskDoneRetryCount ?? 0;
69702
+ const nextRequeueCount = priorRequeues + 1;
69703
+ const errorMessage = `Agent finished without calling task_done (after ${MAX_TASK_DONE_SESSION_RETRIES} retries)`;
69704
+ if (priorRequeues < MAX_TASK_DONE_REQUEUE_RETRIES) {
69705
+ await this.store.updateTask(task.id, {
69706
+ status: "failed",
69707
+ error: errorMessage,
69708
+ taskDoneRetryCount: nextRequeueCount
69709
+ });
69710
+ await this.store.logEntry(
69711
+ task.id,
69712
+ `${errorMessage} \u2014 requeued to todo immediately (${nextRequeueCount}/${MAX_TASK_DONE_REQUEUE_RETRIES})`,
69713
+ void 0,
69714
+ this.currentRunContext
69715
+ );
69716
+ await this.store.moveTask(task.id, "todo");
69717
+ executorLog.log(`\u2717 ${task.id} failed after ${MAX_TASK_DONE_SESSION_RETRIES} retries \u2014 requeued to todo (${nextRequeueCount}/${MAX_TASK_DONE_REQUEUE_RETRIES})`);
69718
+ } else {
69719
+ await this.store.updateTask(task.id, { status: "failed", error: errorMessage });
69720
+ await this.store.logEntry(task.id, `${errorMessage} \u2014 moved to in-review for inspection`, void 0, this.currentRunContext);
69721
+ await this.store.moveTask(task.id, "in-review");
69722
+ executorLog.log(`\u2717 ${task.id} failed after ${MAX_TASK_DONE_SESSION_RETRIES} retries \u2014 no task_done \u2192 in-review`);
69723
+ }
69418
69724
  this.options.onError?.(task, new Error(errorMessage));
69419
69725
  }
69420
69726
  }
@@ -76000,7 +76306,7 @@ async function getHeartbeatMemorySettings(taskStore) {
76000
76306
  return maybeGetSettings.call(taskStore);
76001
76307
  }
76002
76308
  function isTickableState(state) {
76003
- return state === "active" || state === "running";
76309
+ return state === "active" || state === "running" || state === "idle";
76004
76310
  }
76005
76311
  function isHeartbeatManaged(agent) {
76006
76312
  return !isEphemeralAgent(agent);
@@ -115145,9 +115451,10 @@ function createApiRoutes(store, options) {
115145
115451
  try {
115146
115452
  const { store: scopedStore } = await getProjectContext2(req);
115147
115453
  const settings = await scopedStore.getSettingsFast();
115454
+ const prAuthAvailable = isGhAvailable() && isGhAuthenticated() || Boolean(githubToken);
115148
115455
  res.json({
115149
115456
  ...settings,
115150
- githubTokenConfigured: Boolean(githubToken)
115457
+ prAuthAvailable
115151
115458
  });
115152
115459
  } catch (err) {
115153
115460
  if (err instanceof ApiError) {
@@ -115159,7 +115466,7 @@ function createApiRoutes(store, options) {
115159
115466
  router.put("/settings", async (req, res) => {
115160
115467
  try {
115161
115468
  const { store: scopedStore, engine } = await getProjectContext2(req);
115162
- const { githubTokenConfigured, ...clientSettings } = req.body;
115469
+ const { githubTokenConfigured, prAuthAvailable, ...clientSettings } = req.body;
115163
115470
  const globalKeySet = new Set(GLOBAL_SETTINGS_KEYS);
115164
115471
  const globalFieldsFound = Object.keys(clientSettings).filter((k) => globalKeySet.has(k));
115165
115472
  if (globalFieldsFound.length > 0) {
@@ -133847,6 +134154,26 @@ function visibleTruncate(text, maxWidth) {
133847
134154
  }
133848
134155
  return result;
133849
134156
  }
134157
+ function formatConsoleArgs(args) {
134158
+ const stringified = args.map((arg) => {
134159
+ if (typeof arg === "string") return arg;
134160
+ if (arg instanceof Error) return arg.stack ?? arg.message;
134161
+ if (arg === null || arg === void 0) return String(arg);
134162
+ if (typeof arg === "object") {
134163
+ try {
134164
+ return JSON.stringify(arg);
134165
+ } catch {
134166
+ return String(arg);
134167
+ }
134168
+ }
134169
+ return String(arg);
134170
+ }).join(" ");
134171
+ const match = stringified.match(/^\[([^\]]+)\]\s*(.*)$/s);
134172
+ if (match) {
134173
+ return { prefix: match[1], message: match[2] };
134174
+ }
134175
+ return { message: stringified };
134176
+ }
133850
134177
  function centerText(text, width, padChar = " ") {
133851
134178
  const visibleLen = visibleLength(text);
133852
134179
  const padding = Math.max(0, width - visibleLen);
@@ -133854,11 +134181,6 @@ function centerText(text, width, padChar = " ") {
133854
134181
  const rightPad = padding - leftPad;
133855
134182
  return padChar.repeat(leftPad) + text + padChar.repeat(rightPad);
133856
134183
  }
133857
- function padRight(text, width) {
133858
- if (width <= 0) return "";
133859
- const visibleLen = visibleLength(text);
133860
- return text + " ".repeat(Math.max(0, width - visibleLen));
133861
- }
133862
134184
  function isTTYAvailable() {
133863
134185
  return Boolean(process.stdout.isTTY && process.stdin.isTTY);
133864
134186
  }
@@ -133896,9 +134218,9 @@ var init_dashboard_tui = __esm({
133896
134218
  return this.count;
133897
134219
  }
133898
134220
  };
133899
- SECTION_ORDER = ["logs", "system", "utilities", "stats", "settings"];
134221
+ SECTION_ORDER = ["system", "logs", "utilities", "stats", "settings"];
133900
134222
  DashboardTUI = class {
133901
- activeSection = "logs";
134223
+ activeSection = "system";
133902
134224
  logBuffer;
133903
134225
  systemInfo = null;
133904
134226
  taskStats = null;
@@ -133911,6 +134233,11 @@ var init_dashboard_tui = __esm({
133911
134233
  showHelp = false;
133912
134234
  uptimeTimer = null;
133913
134235
  resizeHandler = null;
134236
+ // Logs interaction state
134237
+ selectedLogIndex = 0;
134238
+ logsViewportStart = 0;
134239
+ logsWrapEnabled = false;
134240
+ logsExpandedMode = false;
133914
134241
  constructor() {
133915
134242
  this.logBuffer = new LogRingBuffer();
133916
134243
  }
@@ -133939,8 +134266,21 @@ var init_dashboard_tui = __esm({
133939
134266
  ...entry,
133940
134267
  timestamp: /* @__PURE__ */ new Date()
133941
134268
  });
134269
+ const newLength = this.logBuffer.getAll().length;
134270
+ if (this.selectedLogIndex >= newLength) {
134271
+ this.selectedLogIndex = Math.max(0, newLength - 1);
134272
+ }
133942
134273
  this.render();
133943
134274
  }
134275
+ /**
134276
+ * Clear logs and reset selection state.
134277
+ */
134278
+ clearLogs() {
134279
+ this.logBuffer.clear();
134280
+ this.selectedLogIndex = 0;
134281
+ this.logsViewportStart = 0;
134282
+ this.logsExpandedMode = false;
134283
+ }
133944
134284
  log(message, prefix) {
133945
134285
  this.addLog({ level: "info", message, prefix });
133946
134286
  }
@@ -133970,6 +134310,8 @@ var init_dashboard_tui = __esm({
133970
134310
  this.handleKeypress(str);
133971
134311
  } else if (key.ctrl && key.name === "c") {
133972
134312
  this.handleKeypress("");
134313
+ } else if (key.name === "return" || key.name === "enter") {
134314
+ this.handleKeypress("\r");
133973
134315
  } else if (key.name === "right") {
133974
134316
  this.handleKeypress("\x1B[C");
133975
134317
  } else if (key.name === "left") {
@@ -133980,6 +134322,12 @@ var init_dashboard_tui = __esm({
133980
134322
  this.handleKeypress("\x1B[B");
133981
134323
  } else if (key.name === "escape") {
133982
134324
  this.handleKeypress("\x1B");
134325
+ } else if (key.name === "home") {
134326
+ this.handleKeypress("Home");
134327
+ } else if (key.name === "end") {
134328
+ this.handleKeypress("End");
134329
+ } else if (key.name === "space") {
134330
+ this.handleKeypress(" ");
133983
134331
  }
133984
134332
  });
133985
134333
  this.uptimeTimer = setInterval(() => {
@@ -134079,6 +134427,79 @@ var init_dashboard_tui = __esm({
134079
134427
  }
134080
134428
  if (this.activeSection === "utilities") {
134081
134429
  this.handleUtilityKeypress(key);
134430
+ return;
134431
+ }
134432
+ if (this.activeSection === "logs") {
134433
+ this.handleLogsKeypress(key);
134434
+ return;
134435
+ }
134436
+ }
134437
+ handleLogsKeypress(key) {
134438
+ const entries = this.logBuffer.getAll();
134439
+ const maxIndex = Math.max(0, entries.length - 1);
134440
+ if (key === "\x1B") {
134441
+ if (this.logsExpandedMode) {
134442
+ this.logsExpandedMode = false;
134443
+ this.showHelp = false;
134444
+ this.render();
134445
+ } else if (this.showHelp) {
134446
+ this.showHelp = false;
134447
+ this.render();
134448
+ }
134449
+ return;
134450
+ }
134451
+ if (key === "\r") {
134452
+ if (entries.length === 0) {
134453
+ return;
134454
+ }
134455
+ this.logsExpandedMode = !this.logsExpandedMode;
134456
+ this.render();
134457
+ return;
134458
+ }
134459
+ if (key === "w" || key === "W") {
134460
+ this.logsWrapEnabled = !this.logsWrapEnabled;
134461
+ this.render();
134462
+ return;
134463
+ }
134464
+ if (key === "\x1B[A" || key === "k" || key === "K") {
134465
+ if (entries.length === 0) return;
134466
+ if (this.selectedLogIndex > 0) {
134467
+ this.selectedLogIndex--;
134468
+ this.render();
134469
+ }
134470
+ return;
134471
+ }
134472
+ if (key === "\x1B[B" || key === "j" || key === "J") {
134473
+ if (entries.length === 0) return;
134474
+ if (this.selectedLogIndex < maxIndex) {
134475
+ this.selectedLogIndex++;
134476
+ this.render();
134477
+ }
134478
+ return;
134479
+ }
134480
+ if (key === "Home") {
134481
+ if (entries.length === 0) return;
134482
+ if (this.selectedLogIndex !== 0) {
134483
+ this.selectedLogIndex = 0;
134484
+ this.render();
134485
+ }
134486
+ return;
134487
+ }
134488
+ if (key === "End") {
134489
+ if (entries.length === 0) return;
134490
+ if (this.selectedLogIndex !== maxIndex) {
134491
+ this.selectedLogIndex = maxIndex;
134492
+ this.render();
134493
+ }
134494
+ return;
134495
+ }
134496
+ if (key === " " || key === "e" || key === "E") {
134497
+ if (entries.length === 0) {
134498
+ return;
134499
+ }
134500
+ this.logsExpandedMode = !this.logsExpandedMode;
134501
+ this.render();
134502
+ return;
134082
134503
  }
134083
134504
  }
134084
134505
  async handleUtilityKeypress(key) {
@@ -134089,8 +134510,7 @@ var init_dashboard_tui = __esm({
134089
134510
  break;
134090
134511
  case "c":
134091
134512
  this.callbacks.onClearLogs();
134092
- this.logBuffer.clear();
134093
- this.render();
134513
+ this.clearLogs();
134094
134514
  break;
134095
134515
  case "t":
134096
134516
  if (this.systemInfo) {
@@ -134117,7 +134537,7 @@ var init_dashboard_tui = __esm({
134117
134537
  }
134118
134538
  renderHeader() {
134119
134539
  const cols = process.stdout.columns || 80;
134120
- const title = colorize(" fn board ", "cyan");
134540
+ const title = colorize(" fusion ", "cyan");
134121
134541
  const titleLen = visibleLength(title);
134122
134542
  process.stdout.write(title);
134123
134543
  if (cols >= 70) {
@@ -134199,28 +134619,159 @@ var init_dashboard_tui = __esm({
134199
134619
  break;
134200
134620
  }
134201
134621
  }
134622
+ getLogViewportStart(totalEntries, maxRows) {
134623
+ if (totalEntries <= 0) {
134624
+ this.logsViewportStart = 0;
134625
+ return 0;
134626
+ }
134627
+ const maxStart = Math.max(0, totalEntries - maxRows);
134628
+ let start = Math.min(this.logsViewportStart, maxStart);
134629
+ if (this.selectedLogIndex < start) {
134630
+ start = this.selectedLogIndex;
134631
+ } else if (this.selectedLogIndex >= start + maxRows) {
134632
+ start = this.selectedLogIndex - maxRows + 1;
134633
+ }
134634
+ this.logsViewportStart = Math.max(0, Math.min(start, maxStart));
134635
+ return this.logsViewportStart;
134636
+ }
134202
134637
  renderLogsSection() {
134203
134638
  const cols = process.stdout.columns || 80;
134204
134639
  const entries = this.logBuffer.getAll();
134205
- const maxRows = Math.max(1, (process.stdout.rows ?? 38) - 8);
134640
+ const maxRows = Math.max(1, (process.stdout.rows ?? 38) - 9);
134206
134641
  process.stdout.write(colorize("\n LOGS\n", "bold"));
134207
134642
  process.stdout.write(colorize(` Ring buffer: ${this.logBuffer.total}/${MAX_LOG_ENTRIES} entries
134208
-
134209
134643
  `, "dim"));
134210
134644
  if (entries.length === 0) {
134211
134645
  process.stdout.write(colorize(" No log entries yet.\n", "dim"));
134212
134646
  return;
134213
134647
  }
134214
- const displayEntries = entries.slice(-maxRows).reverse();
134215
- for (const entry of displayEntries) {
134648
+ const safeSelectedIndex = Math.min(this.selectedLogIndex, Math.max(0, entries.length - 1));
134649
+ if (safeSelectedIndex !== this.selectedLogIndex) {
134650
+ this.selectedLogIndex = safeSelectedIndex;
134651
+ }
134652
+ if (this.logsExpandedMode) {
134653
+ this.renderLogsExpandedPane(entries[safeSelectedIndex], safeSelectedIndex, entries.length);
134654
+ return;
134655
+ }
134656
+ const modeIndicator = this.logsWrapEnabled ? colorize(" [w] wrap on", "dim") : colorize(" [w] wrap off", "dim");
134657
+ process.stdout.write(modeIndicator + "\n\n");
134658
+ const startIndex = this.getLogViewportStart(entries.length, maxRows);
134659
+ const visibleEntries = entries.slice(startIndex, startIndex + maxRows);
134660
+ const visibleReversed = [...visibleEntries].reverse();
134661
+ const selectedDisplayIndex = safeSelectedIndex >= startIndex && safeSelectedIndex < startIndex + visibleEntries.length ? visibleEntries.length - 1 - (safeSelectedIndex - startIndex) : -1;
134662
+ const prefixLen = 30;
134663
+ const availableWidth = Math.max(8, cols - prefixLen);
134664
+ for (let displayIdx = 0; displayIdx < visibleReversed.length; displayIdx++) {
134665
+ const entry = visibleReversed[displayIdx];
134666
+ const isSelected = displayIdx === selectedDisplayIndex;
134667
+ const selector = isSelected ? colorize("\u25B8 ", "brightGreen") : " ";
134216
134668
  const ts = colorize(formatTimestamp3(entry.timestamp), "dim");
134217
134669
  const prefix = entry.prefix ? colorize(`[${entry.prefix}]`, "gray") : "";
134218
134670
  const levelChar = entry.level === "error" ? colorize("\u2717", "brightRed") : entry.level === "warn" ? colorize("\u26A0", "brightYellow") : colorize("\u2713", "brightGreen");
134219
- const messageWidth = Math.max(8, cols - 40);
134220
- const message = visibleTruncate(entry.message, messageWidth);
134221
- const line = ` ${ts} ${levelChar} ${prefix ? prefix + " " : ""}${message}`;
134222
- process.stdout.write(visibleTruncate(line, cols - 1) + "\n");
134671
+ if (this.logsWrapEnabled) {
134672
+ const wrappedLines = this.wrapText(entry.message, availableWidth);
134673
+ const firstLine = `${selector}${ts} ${levelChar} ${prefix ? prefix + " " : ""}${wrappedLines[0]}`;
134674
+ process.stdout.write(visibleTruncate(firstLine, cols - 1) + "\n");
134675
+ for (let i = 1; i < wrappedLines.length; i++) {
134676
+ const continuation = ` ${wrappedLines[i]}`;
134677
+ process.stdout.write(visibleTruncate(continuation, cols - 1) + "\n");
134678
+ }
134679
+ } else {
134680
+ const messageWidth = Math.max(8, cols - prefixLen);
134681
+ const message = visibleTruncate(entry.message, messageWidth);
134682
+ const line = `${selector}${ts} ${levelChar} ${prefix ? prefix + " " : ""}${message}`;
134683
+ process.stdout.write(visibleTruncate(line, cols - 1) + "\n");
134684
+ }
134685
+ }
134686
+ }
134687
+ /**
134688
+ * Render the expanded log entry detail pane.
134689
+ * Replaces the normal list view with a focused view of a single entry.
134690
+ */
134691
+ renderLogsExpandedPane(entry, index2, total) {
134692
+ const cols = process.stdout.columns || 80;
134693
+ const rows = process.stdout.rows ?? 24;
134694
+ const maxContentRows = Math.max(1, rows - 12);
134695
+ process.stdout.write(colorize(" EXPANDED LOG ENTRY\n", "bold"));
134696
+ const navHint = colorize(` Entry ${index2 + 1} of ${total} | [\u2191/k] older [\u2193/j] newer [Enter/Esc] close
134697
+ `, "dim");
134698
+ process.stdout.write(navHint);
134699
+ process.stdout.write(colorize(" " + "\u2500".repeat(Math.max(20, cols - 4)) + "\n", "dim"));
134700
+ const ts = formatTimestamp3(entry.timestamp);
134701
+ const levelLabel = entry.level === "error" ? colorize("ERROR", "brightRed") : entry.level === "warn" ? colorize("WARN", "brightYellow") : colorize("INFO", "brightGreen");
134702
+ process.stdout.write(colorize(` Timestamp: `, "gray") + colorize(ts, "white") + "\n");
134703
+ process.stdout.write(colorize(` Level: `, "gray") + levelLabel + "\n");
134704
+ if (entry.prefix) {
134705
+ process.stdout.write(colorize(` Prefix: `, "gray") + colorize(entry.prefix, "dim") + "\n");
134223
134706
  }
134707
+ process.stdout.write("\n");
134708
+ process.stdout.write(colorize(" MESSAGE\n", "bold"));
134709
+ const messageIndent = " ";
134710
+ const availableWidth = Math.max(8, cols - messageIndent.length);
134711
+ const wrappedMessage = this.wrapText(entry.message, availableWidth);
134712
+ let linesPrinted = 5;
134713
+ for (const line of wrappedMessage) {
134714
+ if (linesPrinted >= maxContentRows) {
134715
+ process.stdout.write(colorize(`
134716
+ ... (truncated)
134717
+ `, "dim"));
134718
+ break;
134719
+ }
134720
+ process.stdout.write(messageIndent + line + "\n");
134721
+ linesPrinted++;
134722
+ }
134723
+ const footerHint = colorize(`
134724
+ [Esc] or [Enter] to close expanded view
134725
+ `, "dim");
134726
+ process.stdout.write(footerHint);
134727
+ }
134728
+ /**
134729
+ * Wrap text to fit within available width, returning an array of lines.
134730
+ * Respects ANSI escape sequences via visibleLength.
134731
+ */
134732
+ wrapText(text, maxWidth) {
134733
+ if (maxWidth <= 0) return [""];
134734
+ if (visibleLength(text) <= maxWidth) return [text];
134735
+ const lines = [];
134736
+ let remaining = text;
134737
+ while (visibleLength(remaining) > maxWidth) {
134738
+ let breakIdx = 0;
134739
+ for (let i = 0; i < remaining.length; i++) {
134740
+ const char = remaining[i];
134741
+ if (char === " " || char === " ") {
134742
+ if (visibleLength(remaining.substring(0, i)) <= maxWidth) {
134743
+ breakIdx = i;
134744
+ }
134745
+ }
134746
+ if (visibleLength(remaining.substring(0, i + 1)) > maxWidth) {
134747
+ break;
134748
+ }
134749
+ }
134750
+ if (breakIdx === 0) {
134751
+ const firstTokenMatch = remaining.match(/^(\S+)/);
134752
+ if (firstTokenMatch) {
134753
+ const firstToken = firstTokenMatch[1];
134754
+ if (visibleLength(firstToken) > maxWidth) {
134755
+ const chunkSize = Math.max(1, maxWidth - 1);
134756
+ let chunkStart = 0;
134757
+ while (chunkStart < firstToken.length) {
134758
+ const chunk = firstToken.substring(chunkStart, chunkStart + chunkSize);
134759
+ lines.push(chunk);
134760
+ chunkStart += chunkSize;
134761
+ }
134762
+ remaining = remaining.substring(firstToken.length).trimStart();
134763
+ continue;
134764
+ }
134765
+ }
134766
+ breakIdx = Math.min(maxWidth, remaining.length);
134767
+ }
134768
+ lines.push(remaining.substring(0, breakIdx).trimEnd());
134769
+ remaining = remaining.substring(breakIdx).trimStart();
134770
+ }
134771
+ if (remaining.length > 0) {
134772
+ lines.push(remaining);
134773
+ }
134774
+ return lines.length > 0 ? lines : [""];
134224
134775
  }
134225
134776
  renderSystemSection() {
134226
134777
  if (!this.systemInfo) {
@@ -134355,39 +134906,59 @@ var init_dashboard_tui = __esm({
134355
134906
  process.stdout.write(right);
134356
134907
  process.stdout.write("\n");
134357
134908
  }
134358
- renderHelpOverlay() {
134359
- const cols = process.stdout.columns || 80;
134360
- const rows = process.stdout.rows || 24;
134361
- const boxWidth = Math.min(62, Math.max(cols - 4, 20));
134362
- const useBoxDrawing = cols >= boxWidth + 4;
134363
- let helpLines;
134909
+ /**
134910
+ * Build help overlay lines as an array of strings.
134911
+ * Uses dynamic visibleLength calculation to ensure consistent box width.
134912
+ * All box-drawing rows (including borders) have the same total visible width.
134913
+ *
134914
+ * @param boxWidth - The interior content width (excluding the two box characters)
134915
+ * @param useBoxDrawing - Whether to use box-drawing characters (true) or compact text (false)
134916
+ * @returns Array of help lines
134917
+ */
134918
+ buildHelpLines(boxWidth, useBoxDrawing) {
134364
134919
  if (useBoxDrawing) {
134365
- helpLines = [
134366
- colorize("\u250C" + "\u2500".repeat(boxWidth) + "\u2510", "brightBlue"),
134367
- colorize("\u2502" + centerText("KEYBOARD SHORTCUTS", boxWidth, " ") + "\u2502", "brightBlue"),
134368
- colorize("\u251C" + "\u2500".repeat(boxWidth) + "\u2524", "brightBlue"),
134369
- colorize("\u2502 [1-5] Switch to tab by number" + padRight("", boxWidth - 39) + "\u2502", "white"),
134370
- colorize("\u2502 [n] / \u2192 Next tab" + padRight("", boxWidth - 25) + "\u2502", "white"),
134371
- colorize("\u2502 [p] / \u2190 Previous tab" + padRight("", boxWidth - 27) + "\u2502", "white"),
134372
- colorize("\u2502 [r] Refresh stats (Utilities)" + padRight("", boxWidth - 36) + "\u2502", "white"),
134373
- colorize("\u2502 [c] Clear logs (Utilities)" + padRight("", boxWidth - 33) + "\u2502", "white"),
134374
- colorize("\u2502 [t] Toggle engine pause (Utilities)" + padRight("", boxWidth - 42) + "\u2502", "white"),
134375
- colorize("\u2502 [?] / [h] Toggle help" + padRight("", boxWidth - 24) + "\u2502", "white"),
134376
- colorize("\u2502 [q] Quit" + padRight("", boxWidth - 15) + "\u2502", "white"),
134377
- colorize("\u2502 [Ctrl+C] Force quit" + padRight("", boxWidth - 22) + "\u2502", "white"),
134378
- colorize("\u2514" + "\u2500".repeat(boxWidth) + "\u2518", "brightBlue")
134920
+ const boxRow = (content) => {
134921
+ const padding = Math.max(0, boxWidth - visibleLength(content));
134922
+ return "\u2502" + content + " ".repeat(padding) + "\u2502";
134923
+ };
134924
+ return [
134925
+ "\u250C" + "\u2500".repeat(boxWidth) + "\u2510",
134926
+ boxRow(centerText("KEYBOARD SHORTCUTS", boxWidth, " ")),
134927
+ "\u251C" + "\u2500".repeat(boxWidth) + "\u2524",
134928
+ boxRow(" [1-5] Switch to tab by number"),
134929
+ boxRow(" [n] / \u2192 Next tab"),
134930
+ boxRow(" [p] / \u2190 Previous tab"),
134931
+ boxRow(" [r] Refresh stats (Utilities)"),
134932
+ boxRow(" [c] Clear logs (Utilities)"),
134933
+ boxRow(" [t] Toggle engine pause (Utilities)"),
134934
+ boxRow(" [\u2191/\u2193/k/j] Navigate log entries (Logs)"),
134935
+ boxRow(" [Home/End] First/last log entry (Logs)"),
134936
+ boxRow(" [Enter/Space/e] Expand log (Logs)"),
134937
+ boxRow(" [w] Toggle word wrap (Logs)"),
134938
+ boxRow(" [?] / [h] Toggle help"),
134939
+ boxRow(" [q] Quit"),
134940
+ boxRow(" [Ctrl+C] Force quit"),
134941
+ "\u2514" + "\u2500".repeat(boxWidth) + "\u2518"
134379
134942
  ];
134380
134943
  } else {
134381
- const maxLineWidth = cols - 2;
134382
- helpLines = [
134383
- visibleTruncate(colorize("KEYBOARD SHORTCUTS", "brightBlue"), maxLineWidth),
134384
- visibleTruncate(colorize(" [1-5] Switch tab | [n/p] Next/Prev | [q] Quit", "white"), maxLineWidth),
134385
- visibleTruncate(colorize(" [r] Refresh | [c] Clear logs | [t] Toggle engine", "white"), maxLineWidth),
134386
- visibleTruncate(colorize(" [?/h] Help | [Ctrl+C] Force quit", "white"), maxLineWidth)
134944
+ return [
134945
+ "KEYBOARD SHORTCUTS",
134946
+ " [1-5] Switch tab | [n/p] Next/Prev | [q] Quit",
134947
+ " [\u2191\u2193/k/j] Navigate logs | [Home/End] First/Last (Logs)",
134948
+ " [Enter/Space/e] Expand log | [w] Toggle wrap (Logs)",
134949
+ " [r] Refresh | [c] Clear logs | [t] Toggle engine",
134950
+ " [?/h] Help | [Ctrl+C] Force quit"
134387
134951
  ];
134388
134952
  }
134389
- const compactBoxWidth = useBoxDrawing ? boxWidth : Math.max(...helpLines.map(visibleLength));
134390
- const boxHeight = helpLines.length;
134953
+ }
134954
+ renderHelpOverlay() {
134955
+ const cols = process.stdout.columns || 80;
134956
+ const rows = process.stdout.rows || 24;
134957
+ const boxWidth = Math.min(62, Math.max(cols - 4, 20));
134958
+ const useBoxDrawing = cols >= boxWidth + 4;
134959
+ const rawHelpLines = this.buildHelpLines(boxWidth, useBoxDrawing);
134960
+ const compactBoxWidth = useBoxDrawing ? boxWidth : Math.max(...rawHelpLines.map(visibleLength));
134961
+ const boxHeight = rawHelpLines.length;
134391
134962
  const safeStartX = Math.max(1, Math.floor((cols - compactBoxWidth) / 2));
134392
134963
  const safeStartY = Math.max(1, Math.floor((rows - boxHeight) / 2));
134393
134964
  const clearTop = Math.max(1, safeStartY - 1);
@@ -134396,15 +134967,17 @@ var init_dashboard_tui = __esm({
134396
134967
  moveCursorTo(1, y);
134397
134968
  clearLine();
134398
134969
  }
134399
- for (let i = 0; i < helpLines.length; i++) {
134970
+ for (let i = 0; i < rawHelpLines.length; i++) {
134971
+ const color = i === 0 || i === 2 || i === rawHelpLines.length - 1 ? "brightBlue" : "white";
134400
134972
  moveCursorTo(safeStartX, safeStartY + i);
134401
- process.stdout.write(helpLines[i]);
134973
+ process.stdout.write(colorize(rawHelpLines[i], color));
134402
134974
  }
134403
134975
  }
134404
134976
  };
134405
134977
  DashboardLogSink = class {
134406
134978
  tui = null;
134407
134979
  isTTY;
134980
+ originalConsole = null;
134408
134981
  constructor(tui) {
134409
134982
  this.tui = tui ?? null;
134410
134983
  this.isTTY = tui?.running ?? false;
@@ -134414,26 +134987,74 @@ var init_dashboard_tui = __esm({
134414
134987
  this.isTTY = true;
134415
134988
  }
134416
134989
  log(message, prefix) {
134990
+ const line = prefix ? `[${prefix}] ${message}` : message;
134417
134991
  if (this.tui && this.isTTY) {
134418
134992
  this.tui.log(message, prefix);
134993
+ } else if (this.originalConsole) {
134994
+ this.originalConsole.log.call(console, line);
134419
134995
  } else {
134420
- console.log(prefix ? `[${prefix}] ${message}` : message);
134996
+ console.log(line);
134421
134997
  }
134422
134998
  }
134423
134999
  warn(message, prefix) {
135000
+ const line = prefix ? `[${prefix}] ${message}` : message;
134424
135001
  if (this.tui && this.isTTY) {
134425
135002
  this.tui.warn(message, prefix);
135003
+ } else if (this.originalConsole) {
135004
+ this.originalConsole.warn.call(console, line);
134426
135005
  } else {
134427
- console.warn(prefix ? `[${prefix}] ${message}` : message);
135006
+ console.warn(line);
134428
135007
  }
134429
135008
  }
134430
135009
  error(message, prefix) {
135010
+ const line = prefix ? `[${prefix}] ${message}` : message;
134431
135011
  if (this.tui && this.isTTY) {
134432
135012
  this.tui.error(message, prefix);
135013
+ } else if (this.originalConsole) {
135014
+ this.originalConsole.error.call(console, line);
134433
135015
  } else {
134434
- console.error(prefix ? `[${prefix}] ${message}` : message);
135016
+ console.error(line);
134435
135017
  }
134436
135018
  }
135019
+ /**
135020
+ * Monkey-patch `console.log/warn/error` so everything (including the engine's
135021
+ * createLogger() output, which writes directly to console.error) surfaces in
135022
+ * the TUI's log ring buffer. Without this, most runtime logs render beneath
135023
+ * the alt-screen TUI and are immediately overwritten on the next render,
135024
+ * leaving the Logs tab nearly empty.
135025
+ *
135026
+ * Messages that start with `[prefix] rest` are unpacked so the TUI stores
135027
+ * `prefix="prefix"` and `message="rest"`. Idempotent; call `releaseConsole()`
135028
+ * on TUI shutdown to restore the originals.
135029
+ */
135030
+ captureConsole() {
135031
+ if (this.originalConsole) return;
135032
+ this.originalConsole = {
135033
+ log: console.log,
135034
+ warn: console.warn,
135035
+ error: console.error
135036
+ };
135037
+ console.log = (...args) => {
135038
+ const { message, prefix } = formatConsoleArgs(args);
135039
+ this.log(message, prefix);
135040
+ };
135041
+ console.warn = (...args) => {
135042
+ const { message, prefix } = formatConsoleArgs(args);
135043
+ this.warn(message, prefix);
135044
+ };
135045
+ console.error = (...args) => {
135046
+ const { message, prefix } = formatConsoleArgs(args);
135047
+ this.error(message, prefix);
135048
+ };
135049
+ }
135050
+ /** Restore console.log/warn/error to their pre-capture implementations. */
135051
+ releaseConsole() {
135052
+ if (!this.originalConsole) return;
135053
+ console.log = this.originalConsole.log;
135054
+ console.warn = this.originalConsole.warn;
135055
+ console.error = this.originalConsole.error;
135056
+ this.originalConsole = null;
135057
+ }
134437
135058
  };
134438
135059
  }
134439
135060
  });
@@ -134441,6 +135062,7 @@ var init_dashboard_tui = __esm({
134441
135062
  // src/commands/dashboard.ts
134442
135063
  var dashboard_exports = {};
134443
135064
  __export(dashboard_exports, {
135065
+ StreamedLogBuffer: () => StreamedLogBuffer,
134444
135066
  promptForPort: () => promptForPort,
134445
135067
  runDashboard: () => runDashboard
134446
135068
  });
@@ -134628,6 +135250,7 @@ async function runDashboard(port, opts = {}) {
134628
135250
  });
134629
135251
  await tui.start();
134630
135252
  logSink.setTUI(tui);
135253
+ logSink.captureConsole();
134631
135254
  }
134632
135255
  store = new TaskStore(cwd);
134633
135256
  await store.init();
@@ -134710,7 +135333,7 @@ async function runDashboard(port, opts = {}) {
134710
135333
  try {
134711
135334
  if (!store) {
134712
135335
  taskSummary = "tasks=unavailable (store not initialized)";
134713
- console.log(`[dashboard] shutdown requested reason=${reason} pid=${process.pid} ppid=${process.ppid} uptime=${uptimeSeconds}s ${taskSummary}`);
135336
+ logSink.log(`shutdown requested reason=${reason} pid=${process.pid} ppid=${process.ppid} uptime=${uptimeSeconds}s ${taskSummary}`, "dashboard");
134714
135337
  return;
134715
135338
  }
134716
135339
  const tasks = await store.listTasks({ slim: true, includeArchived: false });
@@ -134726,8 +135349,9 @@ async function runDashboard(port, opts = {}) {
134726
135349
  const message = error instanceof Error ? error.message : String(error);
134727
135350
  taskSummary = `tasks=unavailable (${message})`;
134728
135351
  }
134729
- console.log(
134730
- `[dashboard] shutdown requested reason=${reason} pid=${process.pid} ppid=${process.ppid} uptime=${uptimeSeconds}s ${taskSummary}`
135352
+ logSink.log(
135353
+ `shutdown requested reason=${reason} pid=${process.pid} ppid=${process.ppid} uptime=${uptimeSeconds}s ${taskSummary}`,
135354
+ "dashboard"
134731
135355
  );
134732
135356
  }
134733
135357
  function registerHandler(target, event, handler) {
@@ -134762,10 +135386,21 @@ async function runDashboard(port, opts = {}) {
134762
135386
  await store.updateSettings({ enginePaused: true });
134763
135387
  logSink.log("Starting in paused mode \u2014 automation disabled", "engine");
134764
135388
  }
134765
- const onMergeImpl = (taskId) => aiMergeTask(store, cwd, taskId, {
134766
- agentStore,
134767
- onAgentText: (delta) => process.stdout.write(delta)
134768
- });
135389
+ const onMergeImpl = async (taskId) => {
135390
+ const streamedMergeLog = new StreamedLogBuffer(
135391
+ (line) => logSink.log(line, "merge"),
135392
+ STREAM_LOG_FLUSH_IDLE_MS
135393
+ );
135394
+ try {
135395
+ return await aiMergeTask(store, cwd, taskId, {
135396
+ agentStore,
135397
+ onAgentText: (delta) => streamedMergeLog.push(delta)
135398
+ });
135399
+ } finally {
135400
+ streamedMergeLog.flush();
135401
+ streamedMergeLog.dispose();
135402
+ }
135403
+ };
134769
135404
  const onMerge = (taskId) => onMergeImpl(taskId);
134770
135405
  const missionAutopilotImpl = new MissionAutopilot(store, store.getMissionStore());
134771
135406
  const missionExecutionLoopImpl = new MissionExecutionLoop({
@@ -134877,6 +135512,7 @@ async function runDashboard(port, opts = {}) {
134877
135512
  tuiRefreshDebounceTimer = null;
134878
135513
  }
134879
135514
  if (tui) {
135515
+ logSink.releaseConsole();
134880
135516
  void tui.stop();
134881
135517
  }
134882
135518
  for (const { target, event, handler } of handlers) {
@@ -134910,7 +135546,7 @@ async function runDashboard(port, opts = {}) {
134910
135546
  peerExchangeService.start();
134911
135547
  } catch (err) {
134912
135548
  const message = err instanceof Error ? err.message : String(err);
134913
- console.warn(`[dashboard] Failed to start peer exchange service: ${message}`);
135549
+ logSink.warn(`Failed to start peer exchange service: ${message}`, "dashboard");
134914
135550
  }
134915
135551
  centralCoreForMesh = centralCoreForEngine;
134916
135552
  let cwdEngine;
@@ -134960,7 +135596,7 @@ async function runDashboard(port, opts = {}) {
134960
135596
  handleTypes[type] = (handleTypes[type] ?? 0) + 1;
134961
135597
  }
134962
135598
  const handleSummary = Object.entries(handleTypes).sort((a, b) => b[1] - a[1]).map(([type, count]) => `${type}:${count}`).join(", ");
134963
- console.log(`[dashboard] active handles at shutdown: ${handleSummary}`);
135599
+ logSink.log(`active handles at shutdown: ${handleSummary}`, "dashboard");
134964
135600
  } catch {
134965
135601
  }
134966
135602
  await logShutdownDiagnostics(signal);
@@ -134972,7 +135608,7 @@ async function runDashboard(port, opts = {}) {
134972
135608
  await peerExchangeService.stop();
134973
135609
  } catch (err) {
134974
135610
  const message = err instanceof Error ? err.message : String(err);
134975
- console.warn(`[dashboard] Failed to stop peer exchange service: ${message}`);
135611
+ logSink.warn(`Failed to stop peer exchange service: ${message}`, "dashboard");
134976
135612
  }
134977
135613
  }
134978
135614
  if (centralCoreForMesh && localNodeIdForMesh) {
@@ -134980,13 +135616,13 @@ async function runDashboard(port, opts = {}) {
134980
135616
  centralCoreForMesh.stopDiscovery();
134981
135617
  } catch (err) {
134982
135618
  const message = err instanceof Error ? err.message : String(err);
134983
- console.warn(`[dashboard] Failed to stop mDNS discovery: ${message}`);
135619
+ logSink.warn(`Failed to stop mDNS discovery: ${message}`, "dashboard");
134984
135620
  }
134985
135621
  try {
134986
135622
  await centralCoreForMesh.updateNode(localNodeIdForMesh, { status: "offline" });
134987
135623
  } catch (err) {
134988
135624
  const message = err instanceof Error ? err.message : String(err);
134989
- console.warn(`[dashboard] Failed to set local node offline: ${message}`);
135625
+ logSink.warn(`Failed to set local node offline: ${message}`, "dashboard");
134990
135626
  }
134991
135627
  }
134992
135628
  await centralCoreForEngine.close().catch(() => {
@@ -134997,7 +135633,7 @@ async function runDashboard(port, opts = {}) {
134997
135633
  registerHandler(process, "SIGINT", () => void shutdown("SIGINT"));
134998
135634
  registerHandler(process, "SIGTERM", () => void shutdown("SIGTERM"));
134999
135635
  registerHandler(process, "SIGHUP", () => {
135000
- console.log("[dashboard] Received SIGHUP (terminal disconnected) \u2014 ignoring");
135636
+ logSink.log("Received SIGHUP (terminal disconnected) \u2014 ignoring", "dashboard");
135001
135637
  });
135002
135638
  } else {
135003
135639
  try {
@@ -135007,7 +135643,7 @@ async function runDashboard(port, opts = {}) {
135007
135643
  peerExchangeService.start();
135008
135644
  } catch (err) {
135009
135645
  const message = err instanceof Error ? err.message : String(err);
135010
- console.warn(`[dashboard] Failed to initialize mesh networking: ${message}`);
135646
+ logSink.warn(`Failed to initialize mesh networking: ${message}`, "dashboard");
135011
135647
  }
135012
135648
  try {
135013
135649
  heartbeatMonitorImpl = new HeartbeatMonitor({
@@ -135104,7 +135740,7 @@ async function runDashboard(port, opts = {}) {
135104
135740
  handleTypes[type] = (handleTypes[type] ?? 0) + 1;
135105
135741
  }
135106
135742
  const handleSummary = Object.entries(handleTypes).sort((a, b) => b[1] - a[1]).map(([type, count]) => `${type}:${count}`).join(", ");
135107
- console.log(`[dashboard] active handles at shutdown: ${handleSummary}`);
135743
+ logSink.log(`active handles at shutdown: ${handleSummary}`, "dashboard");
135108
135744
  } catch {
135109
135745
  }
135110
135746
  await logShutdownDiagnostics(signal);
@@ -135117,7 +135753,7 @@ async function runDashboard(port, opts = {}) {
135117
135753
  await peerExchangeService.stop();
135118
135754
  } catch (err) {
135119
135755
  const message = err instanceof Error ? err.message : String(err);
135120
- console.warn(`[dashboard] Failed to stop peer exchange service: ${message}`);
135756
+ logSink.warn(`Failed to stop peer exchange service: ${message}`, "dashboard");
135121
135757
  }
135122
135758
  }
135123
135759
  if (centralCoreForMesh && localNodeIdForMesh) {
@@ -135125,13 +135761,13 @@ async function runDashboard(port, opts = {}) {
135125
135761
  centralCoreForMesh.stopDiscovery();
135126
135762
  } catch (err) {
135127
135763
  const message = err instanceof Error ? err.message : String(err);
135128
- console.warn(`[dashboard] Failed to stop mDNS discovery: ${message}`);
135764
+ logSink.warn(`Failed to stop mDNS discovery: ${message}`, "dashboard");
135129
135765
  }
135130
135766
  try {
135131
135767
  await centralCoreForMesh.updateNode(localNodeIdForMesh, { status: "offline" });
135132
135768
  } catch (err) {
135133
135769
  const message = err instanceof Error ? err.message : String(err);
135134
- console.warn(`[dashboard] Failed to set local node offline: ${message}`);
135770
+ logSink.warn(`Failed to set local node offline: ${message}`, "dashboard");
135135
135771
  }
135136
135772
  }
135137
135773
  if (centralCoreForMesh) {
@@ -135144,7 +135780,7 @@ async function runDashboard(port, opts = {}) {
135144
135780
  registerHandler(process, "SIGINT", () => void devShutdown("SIGINT"));
135145
135781
  registerHandler(process, "SIGTERM", () => void devShutdown("SIGTERM"));
135146
135782
  registerHandler(process, "SIGHUP", () => {
135147
- console.log("[dashboard] Received SIGHUP (terminal disconnected) \u2014 ignoring");
135783
+ logSink.log("Received SIGHUP (terminal disconnected) \u2014 ignoring", "dashboard");
135148
135784
  });
135149
135785
  }
135150
135786
  const server = app.listen(selectedPort, selectedHost);
@@ -135152,14 +135788,14 @@ async function runDashboard(port, opts = {}) {
135152
135788
  if (err.code === "EADDRINUSE") {
135153
135789
  server.listen(0, selectedHost);
135154
135790
  } else {
135155
- console.error(`Failed to start server: ${err.message}`);
135791
+ logSink.error(`Failed to start server: ${err.message}`, "dashboard");
135156
135792
  process.exit(1);
135157
135793
  }
135158
135794
  });
135159
135795
  server.on("listening", async () => {
135160
135796
  const actualPort = server.address().port;
135161
135797
  if (actualPort !== selectedPort) {
135162
- console.log(`\u26A0 Port ${selectedPort} in use, using ${actualPort} instead`);
135798
+ logSink.warn(`Port ${selectedPort} in use, using ${actualPort} instead`, "dashboard");
135163
135799
  }
135164
135800
  if (centralCoreForMesh) {
135165
135801
  try {
@@ -135172,7 +135808,7 @@ async function runDashboard(port, opts = {}) {
135172
135808
  });
135173
135809
  } catch (err) {
135174
135810
  const message = err instanceof Error ? err.message : String(err);
135175
- console.warn(`[dashboard] Failed to start mDNS discovery: ${message}`);
135811
+ logSink.warn(`Failed to start mDNS discovery: ${message}`, "dashboard");
135176
135812
  }
135177
135813
  }
135178
135814
  if (centralCoreForMesh) {
@@ -135185,7 +135821,7 @@ async function runDashboard(port, opts = {}) {
135185
135821
  }
135186
135822
  } catch (err) {
135187
135823
  const message = err instanceof Error ? err.message : String(err);
135188
- console.warn(`[dashboard] Failed to set local node online: ${message}`);
135824
+ logSink.warn(`Failed to set local node online: ${message}`, "dashboard");
135189
135825
  }
135190
135826
  }
135191
135827
  const displayHost = selectedHost === "0.0.0.0" || selectedHost === "::" ? selectedHost : "localhost";
@@ -135277,7 +135913,7 @@ async function runDashboard(port, opts = {}) {
135277
135913
  });
135278
135914
  return { dispose };
135279
135915
  }
135280
- var processDiagnosticsRegistered, diagnosticIntervalHandle, DIAGNOSTIC_INTERVAL_MS, diagnosticStartTime, diagnosticDbHealthCheck, diagnosticStoreListenerCheck;
135916
+ var processDiagnosticsRegistered, diagnosticIntervalHandle, DIAGNOSTIC_INTERVAL_MS, diagnosticStartTime, diagnosticDbHealthCheck, diagnosticStoreListenerCheck, STREAM_LOG_FLUSH_IDLE_MS, StreamedLogBuffer;
135281
135917
  var init_dashboard = __esm({
135282
135918
  "src/commands/dashboard.ts"() {
135283
135919
  "use strict";
@@ -135297,6 +135933,60 @@ var init_dashboard = __esm({
135297
135933
  diagnosticStartTime = 0;
135298
135934
  diagnosticDbHealthCheck = null;
135299
135935
  diagnosticStoreListenerCheck = null;
135936
+ STREAM_LOG_FLUSH_IDLE_MS = 100;
135937
+ StreamedLogBuffer = class {
135938
+ constructor(emitLine, flushIdleMs = STREAM_LOG_FLUSH_IDLE_MS) {
135939
+ this.emitLine = emitLine;
135940
+ this.flushIdleMs = flushIdleMs;
135941
+ }
135942
+ pending = "";
135943
+ flushTimer = null;
135944
+ push(delta) {
135945
+ if (!delta) return;
135946
+ this.pending += delta;
135947
+ this.flushCompletedLines();
135948
+ this.scheduleFlush();
135949
+ }
135950
+ flush() {
135951
+ this.clearFlushTimer();
135952
+ const trailing = this.pending.trim();
135953
+ if (trailing.length > 0) {
135954
+ this.emitLine(trailing);
135955
+ }
135956
+ this.pending = "";
135957
+ }
135958
+ dispose() {
135959
+ this.clearFlushTimer();
135960
+ this.pending = "";
135961
+ }
135962
+ flushCompletedLines() {
135963
+ if (!this.pending.includes("\n")) {
135964
+ return;
135965
+ }
135966
+ const splitLines = this.pending.split(/\r?\n/);
135967
+ const completeLines = splitLines.slice(0, -1);
135968
+ this.pending = splitLines[splitLines.length - 1] ?? "";
135969
+ for (const line of completeLines) {
135970
+ const normalized = line.trim();
135971
+ if (normalized.length > 0) {
135972
+ this.emitLine(normalized);
135973
+ }
135974
+ }
135975
+ }
135976
+ scheduleFlush() {
135977
+ this.clearFlushTimer();
135978
+ this.flushTimer = setTimeout(() => {
135979
+ this.flush();
135980
+ }, this.flushIdleMs);
135981
+ this.flushTimer.unref?.();
135982
+ }
135983
+ clearFlushTimer() {
135984
+ if (this.flushTimer) {
135985
+ clearTimeout(this.flushTimer);
135986
+ this.flushTimer = null;
135987
+ }
135988
+ }
135989
+ };
135300
135990
  }
135301
135991
  });
135302
135992