@vibedeckx/linux-x64 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin.js +790 -45
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -9400,7 +9400,7 @@ var require_InstrumentDescriptor = __commonJS({
9400
9400
  var api_1 = (init_esm(), __toCommonJS(esm_exports));
9401
9401
  var utils_1 = require_utils5();
9402
9402
  function createInstrumentDescriptor(name25, type, options) {
9403
- if (!isValidName(name25)) {
9403
+ if (!isValidName2(name25)) {
9404
9404
  api_1.diag.warn(`Invalid metric name: "${name25}". The metric name should be a ASCII string with a length no greater than 255 characters.`);
9405
9405
  }
9406
9406
  return {
@@ -9429,10 +9429,10 @@ var require_InstrumentDescriptor = __commonJS({
9429
9429
  }
9430
9430
  exports.isDescriptorCompatibleWith = isDescriptorCompatibleWith;
9431
9431
  var NAME_REGEXP = /^[a-z][a-z0-9_.\-/]{0,254}$/i;
9432
- function isValidName(name25) {
9432
+ function isValidName2(name25) {
9433
9433
  return NAME_REGEXP.test(name25);
9434
9434
  }
9435
- exports.isValidName = isValidName;
9435
+ exports.isValidName = isValidName2;
9436
9436
  }
9437
9437
  });
9438
9438
 
@@ -185912,6 +185912,217 @@ var createCrossRemoteAuditRepo = (kdb) => ({
185912
185912
  }
185913
185913
  });
185914
185914
 
185915
+ // src/storage/repositories/merge-targets.ts
185916
+ var createMergeTargetsRepo = (kdb) => ({
185917
+ mergeTargets: {
185918
+ getForBranches: async (projectId, branches) => {
185919
+ if (branches.length === 0) return /* @__PURE__ */ new Map();
185920
+ const rows = await kdb.selectFrom("branch_merge_targets").select(["branch", "target"]).where("project_id", "=", projectId).where("branch", "in", branches).execute();
185921
+ return new Map(rows.map((row) => [row.branch, row.target]));
185922
+ },
185923
+ upsert: async (projectId, branch, target) => {
185924
+ const result = await kdb.insertInto("branch_merge_targets").values({ project_id: projectId, branch, target }).onConflict(
185925
+ (conflict) => conflict.columns(["project_id", "branch"]).doUpdateSet({
185926
+ target,
185927
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
185928
+ }).where("target", "!=", target)
185929
+ ).executeTakeFirst();
185930
+ return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
185931
+ },
185932
+ insertIfAbsent: async (projectId, branch, target) => {
185933
+ const result = await kdb.insertInto("branch_merge_targets").values({ project_id: projectId, branch, target }).onConflict((conflict) => conflict.columns(["project_id", "branch"]).doNothing()).executeTakeFirst();
185934
+ return (result.numInsertedOrUpdatedRows ?? 0n) > 0n;
185935
+ },
185936
+ delete: async (projectId, branch) => {
185937
+ const result = await kdb.deleteFrom("branch_merge_targets").where("project_id", "=", projectId).where("branch", "=", branch).executeTakeFirst();
185938
+ return result.numDeletedRows > 0n;
185939
+ }
185940
+ }
185941
+ });
185942
+
185943
+ // src/storage/repositories/search-cache.ts
185944
+ var toDbBranch = (branch) => branch ?? "";
185945
+ var fromDbBranch = (branch) => branch === "" ? null : branch;
185946
+ var escapeLike = (s3) => s3.replace(/[\\%_]/g, (c) => `\\${c}`);
185947
+ var matchTier = (text2, q) => {
185948
+ if (!q) return 2;
185949
+ if (!text2) return 3;
185950
+ const t = text2.toLowerCase();
185951
+ if (t === q) return 0;
185952
+ if (t.startsWith(q)) return 1;
185953
+ if (t.includes(q)) return 2;
185954
+ return 3;
185955
+ };
185956
+ var parseDbTimestamp = (ts) => {
185957
+ if (!ts) return null;
185958
+ const ms = Date.parse(ts.replace(" ", "T") + "Z");
185959
+ return Number.isNaN(ms) ? null : ms;
185960
+ };
185961
+ var rankAndCap = (items, limit) => items.filter((x2) => x2.tier < 3).sort((a, b2) => a.tier - b2.tier || Number(b2.favorited) - Number(a.favorited) || b2.recency - a.recency).slice(0, limit).map((x2) => x2.item);
185962
+ var createSearchCacheRepos = (kdb, _h) => ({
185963
+ searchCache: {
185964
+ // Generation-based reconciliation: only a FULLY successful snapshot may
185965
+ // mark rows deleted. Runs in one transaction so a crash mid-apply can't
185966
+ // leave a half-deleted cache.
185967
+ applyCatalogSnapshot: async (projectId, targetId, snapshot) => {
185968
+ const now2 = Date.now();
185969
+ await kdb.transaction().execute(async (trx) => {
185970
+ const state = await trx.selectFrom("search_catalog_sync_state").select("snapshot_generation").where("project_id", "=", projectId).where("target_id", "=", targetId).executeTakeFirst();
185971
+ const generation = (state?.snapshot_generation ?? 0) + 1;
185972
+ for (const w2 of snapshot.workspaces) {
185973
+ await trx.insertInto("workspace_search_cache").values({ project_id: projectId, target_id: targetId, branch: toDbBranch(w2.branch), generation, deleted_at: null }).onConflict((oc) => oc.columns(["project_id", "target_id", "branch"]).doUpdateSet({ generation, deleted_at: null })).execute();
185974
+ }
185975
+ for (const s3 of snapshot.sessions) {
185976
+ await trx.insertInto("session_search_cache").values({
185977
+ local_session_id: s3.id,
185978
+ project_id: projectId,
185979
+ target_id: targetId,
185980
+ branch: toDbBranch(s3.branch),
185981
+ title: s3.title,
185982
+ last_active_at: s3.lastActiveAt,
185983
+ favorited_at: s3.favoritedAt,
185984
+ entry_count: s3.entryCount,
185985
+ generation,
185986
+ deleted_at: null
185987
+ }).onConflict((oc) => oc.column("local_session_id").doUpdateSet({
185988
+ project_id: projectId,
185989
+ target_id: targetId,
185990
+ branch: toDbBranch(s3.branch),
185991
+ title: s3.title,
185992
+ last_active_at: s3.lastActiveAt,
185993
+ favorited_at: s3.favoritedAt,
185994
+ entry_count: s3.entryCount,
185995
+ generation,
185996
+ deleted_at: null
185997
+ })).execute();
185998
+ }
185999
+ await trx.updateTable("workspace_search_cache").set({ deleted_at: now2 }).where("project_id", "=", projectId).where("target_id", "=", targetId).where("generation", "<", generation).where("deleted_at", "is", null).execute();
186000
+ await trx.updateTable("session_search_cache").set({ deleted_at: now2 }).where("project_id", "=", projectId).where("target_id", "=", targetId).where("generation", "<", generation).where("deleted_at", "is", null).execute();
186001
+ await trx.insertInto("search_catalog_sync_state").values({
186002
+ project_id: projectId,
186003
+ target_id: targetId,
186004
+ last_success_at: now2,
186005
+ last_attempt_at: now2,
186006
+ snapshot_generation: generation,
186007
+ last_error: null
186008
+ }).onConflict((oc) => oc.columns(["project_id", "target_id"]).doUpdateSet({
186009
+ last_success_at: now2,
186010
+ last_attempt_at: now2,
186011
+ snapshot_generation: generation,
186012
+ last_error: null
186013
+ })).execute();
186014
+ });
186015
+ },
186016
+ recordSyncFailure: async (projectId, targetId, error48) => {
186017
+ const now2 = Date.now();
186018
+ await kdb.insertInto("search_catalog_sync_state").values({
186019
+ project_id: projectId,
186020
+ target_id: targetId,
186021
+ last_success_at: null,
186022
+ last_attempt_at: now2,
186023
+ snapshot_generation: 0,
186024
+ last_error: error48
186025
+ }).onConflict((oc) => oc.columns(["project_id", "target_id"]).doUpdateSet({ last_attempt_at: now2, last_error: error48 })).execute();
186026
+ },
186027
+ getSyncStates: async (projectIds) => {
186028
+ if (projectIds.length === 0) return [];
186029
+ return kdb.selectFrom("search_catalog_sync_state").select(["project_id", "target_id", "last_success_at", "last_attempt_at", "last_error"]).where("project_id", "in", projectIds).execute();
186030
+ },
186031
+ // Opportunistic freshness: called where a title transits the server
186032
+ // anyway (remote title PATCH proxy). UPDATE-only — inserting here would
186033
+ // fabricate a row outside snapshot generations.
186034
+ updateCachedSessionTitle: async (localSessionId, title) => {
186035
+ await kdb.updateTable("session_search_cache").set({ title }).where("local_session_id", "=", localSessionId).execute();
186036
+ },
186037
+ search: async ({ userId, query, limitPerGroup }) => {
186038
+ const q = query.trim().slice(0, 256).toLowerCase();
186039
+ let projQuery = kdb.selectFrom("projects").select(["id", "name", "path"]).where("id", "not like", "path:%");
186040
+ if (userId) projQuery = projQuery.where("user_id", "=", userId);
186041
+ const allProjects = await projQuery.execute();
186042
+ const projectIds = allProjects.map((p2) => p2.id);
186043
+ const nameById = new Map(allProjects.map((p2) => [p2.id, p2.name]));
186044
+ if (projectIds.length === 0) return { projects: [], workspaces: [], sessions: [] };
186045
+ const pattern = `%${escapeLike(q)}%`;
186046
+ let localBase = kdb.selectFrom("agent_sessions as s").select(["s.id", "s.project_id", "s.branch", "s.title", "s.last_user_message_at", "s.updated_at", "s.favorited_at"]).where("s.project_id", "in", projectIds).where((eb) => eb.or([
186047
+ eb("s.title", "is not", null),
186048
+ eb.exists(
186049
+ eb.selectFrom("agent_session_entries").select("agent_session_entries.session_id").whereRef("agent_session_entries.session_id", "=", "s.id")
186050
+ )
186051
+ ]));
186052
+ if (q) localBase = localBase.where(sql`lower(coalesce(s.title, '')) like ${pattern} escape '\\'`);
186053
+ const localRows = await localBase.orderBy("s.updated_at", "desc").limit(200).execute();
186054
+ const localFavRows = await localBase.where("s.favorited_at", "is not", null).execute();
186055
+ let cacheBase = kdb.selectFrom("session_search_cache as c").leftJoin("project_remotes as pr", (join2) => join2.onRef("pr.project_id", "=", "c.project_id").onRef("pr.remote_server_id", "=", "c.target_id")).select(["c.local_session_id", "c.project_id", "c.target_id", "c.branch", "c.title", "c.last_active_at", "c.favorited_at"]).where("c.project_id", "in", projectIds).where("c.deleted_at", "is", null).where((eb) => eb.or([
186056
+ eb("c.target_id", "=", "local"),
186057
+ eb("pr.id", "is not", null)
186058
+ ]));
186059
+ if (q) cacheBase = cacheBase.where(sql`lower(coalesce(c.title, '')) like ${pattern} escape '\\'`);
186060
+ const cacheRows = await cacheBase.orderBy("c.last_active_at", "desc").limit(200).execute();
186061
+ const cacheFavRows = await cacheBase.where("c.favorited_at", "is not", null).execute();
186062
+ const candidateById = /* @__PURE__ */ new Map();
186063
+ for (const r of [...localRows, ...localFavRows]) {
186064
+ if (candidateById.has(r.id)) continue;
186065
+ candidateById.set(r.id, {
186066
+ sessionId: r.id,
186067
+ projectId: r.project_id,
186068
+ projectName: nameById.get(r.project_id) ?? "",
186069
+ targetId: "local",
186070
+ branch: fromDbBranch(r.branch),
186071
+ title: r.title ?? null,
186072
+ lastActiveAt: r.last_user_message_at ?? parseDbTimestamp(r.updated_at),
186073
+ favoritedAt: r.favorited_at ?? null
186074
+ });
186075
+ }
186076
+ for (const r of [...cacheRows, ...cacheFavRows]) {
186077
+ if (candidateById.has(r.local_session_id)) continue;
186078
+ candidateById.set(r.local_session_id, {
186079
+ sessionId: r.local_session_id,
186080
+ projectId: r.project_id,
186081
+ projectName: nameById.get(r.project_id) ?? "",
186082
+ targetId: r.target_id,
186083
+ branch: fromDbBranch(r.branch),
186084
+ title: r.title ?? null,
186085
+ lastActiveAt: r.last_active_at ?? null,
186086
+ favoritedAt: r.favorited_at ?? null
186087
+ });
186088
+ }
186089
+ const sessionCandidates = [...candidateById.values()];
186090
+ if (!q) {
186091
+ const sessions2 = [...sessionCandidates].sort((a, b2) => Number(!!b2.favoritedAt) - Number(!!a.favoritedAt) || (b2.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0)).slice(0, limitPerGroup);
186092
+ return { projects: [], workspaces: [], sessions: sessions2 };
186093
+ }
186094
+ const projects = rankAndCap(allProjects.map((p2) => ({
186095
+ item: { id: p2.id, name: p2.name, path: p2.path ?? null },
186096
+ tier: Math.min(matchTier(p2.name, q), matchTier(p2.path, q)),
186097
+ favorited: false,
186098
+ recency: 0
186099
+ })), limitPerGroup);
186100
+ const wsRows = await kdb.selectFrom("workspace_search_cache as w").leftJoin("project_remotes as pr", (join2) => join2.onRef("pr.project_id", "=", "w.project_id").onRef("pr.remote_server_id", "=", "w.target_id")).select(["w.project_id", "w.target_id", "w.branch"]).where("w.project_id", "in", projectIds).where("w.deleted_at", "is", null).where((eb) => eb.or([
186101
+ eb("w.target_id", "=", "local"),
186102
+ eb("pr.id", "is not", null)
186103
+ ])).execute();
186104
+ const workspaces = rankAndCap(wsRows.map((w2) => ({
186105
+ item: {
186106
+ projectId: w2.project_id,
186107
+ projectName: nameById.get(w2.project_id) ?? "",
186108
+ targetId: w2.target_id,
186109
+ branch: fromDbBranch(w2.branch)
186110
+ },
186111
+ tier: matchTier(fromDbBranch(w2.branch) ?? "main", q),
186112
+ favorited: false,
186113
+ recency: 0
186114
+ })), limitPerGroup);
186115
+ const sessions = rankAndCap(sessionCandidates.map((s3) => ({
186116
+ item: s3,
186117
+ tier: matchTier(s3.title, q),
186118
+ favorited: !!s3.favoritedAt,
186119
+ recency: s3.lastActiveAt ?? 0
186120
+ })), limitPerGroup);
186121
+ return { projects, workspaces, sessions };
186122
+ }
186123
+ }
186124
+ });
186125
+
185915
186126
  // src/storage/sqlite.ts
185916
186127
  var createDatabase = (dbPath) => {
185917
186128
  const db = new Database(dbPath);
@@ -186534,6 +186745,54 @@ var createDatabase = (dbPath) => {
186534
186745
  if (!scheduledRunCols.some((c) => c.name === "report")) {
186535
186746
  db.exec("ALTER TABLE scheduled_task_runs ADD COLUMN report TEXT DEFAULT NULL");
186536
186747
  }
186748
+ db.exec(`
186749
+ CREATE TABLE IF NOT EXISTS branch_merge_targets (
186750
+ project_id TEXT NOT NULL,
186751
+ branch TEXT NOT NULL,
186752
+ target TEXT NOT NULL,
186753
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
186754
+ PRIMARY KEY (project_id, branch),
186755
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
186756
+ );
186757
+
186758
+ -- Search caches: server-side searchable copies of worker catalogs.
186759
+ -- remote_session_mappings stays routing-only; these tables are reconciled
186760
+ -- from full catalog snapshots (generation-based) and rows are soft-deleted,
186761
+ -- so wiping them never breaks existing remote session URLs.
186762
+ CREATE TABLE IF NOT EXISTS session_search_cache (
186763
+ local_session_id TEXT PRIMARY KEY,
186764
+ project_id TEXT NOT NULL,
186765
+ target_id TEXT NOT NULL,
186766
+ branch TEXT NOT NULL DEFAULT '',
186767
+ title TEXT,
186768
+ last_active_at INTEGER,
186769
+ favorited_at INTEGER,
186770
+ entry_count INTEGER NOT NULL DEFAULT 0,
186771
+ generation INTEGER NOT NULL,
186772
+ deleted_at INTEGER
186773
+ );
186774
+ CREATE INDEX IF NOT EXISTS idx_session_search_cache_project
186775
+ ON session_search_cache(project_id, target_id);
186776
+
186777
+ CREATE TABLE IF NOT EXISTS workspace_search_cache (
186778
+ project_id TEXT NOT NULL,
186779
+ target_id TEXT NOT NULL,
186780
+ branch TEXT NOT NULL,
186781
+ generation INTEGER NOT NULL,
186782
+ deleted_at INTEGER,
186783
+ PRIMARY KEY (project_id, target_id, branch)
186784
+ );
186785
+
186786
+ CREATE TABLE IF NOT EXISTS search_catalog_sync_state (
186787
+ project_id TEXT NOT NULL,
186788
+ target_id TEXT NOT NULL,
186789
+ last_success_at INTEGER,
186790
+ last_attempt_at INTEGER,
186791
+ snapshot_generation INTEGER NOT NULL DEFAULT 0,
186792
+ last_error TEXT,
186793
+ PRIMARY KEY (project_id, target_id)
186794
+ );
186795
+ `);
186537
186796
  db.pragma("foreign_keys = ON");
186538
186797
  return db;
186539
186798
  };
@@ -186550,6 +186809,8 @@ var createSqliteStorage = async (dbPath) => {
186550
186809
  ...createAgentSessionRepos(kdb, h),
186551
186810
  ...createWorkspaceRepos(kdb, h),
186552
186811
  ...createCrossRemoteAuditRepo(kdb),
186812
+ ...createMergeTargetsRepo(kdb),
186813
+ ...createSearchCacheRepos(kdb, h),
186553
186814
  close: async () => {
186554
186815
  await kdb.destroy();
186555
186816
  }
@@ -223964,7 +224225,8 @@ var AgentSessionManager = class {
223964
224225
  eventChain: Promise.resolve(),
223965
224226
  bgSpawnHintsThisTurn: 0,
223966
224227
  taskStartedThisTurn: 0,
223967
- lastActiveAt: Date.now()
224228
+ lastActiveAt: Date.now(),
224229
+ turnOpenSince: null
223968
224230
  };
223969
224231
  this.sessions.set(sessionId, session);
223970
224232
  const provider = getProvider(agentType);
@@ -224104,21 +224366,26 @@ var AgentSessionManager = class {
224104
224366
  if (action.kind === "commit") {
224105
224367
  await this.commitCompletion(session, action.payload);
224106
224368
  }
224369
+ if (code !== 0 && !spawnFailed && !session.producedOutput) {
224370
+ try {
224371
+ await this.pushEntry(session.id, {
224372
+ type: "error",
224373
+ message: this.buildStartupFailureMessage(session.agentType, stderrTail),
224374
+ timestamp: Date.now()
224375
+ }, true);
224376
+ } catch (err) {
224377
+ console.error(`[AgentSession] Failed to push startup-failure entry for ${session.id}:`, err);
224378
+ }
224379
+ }
224380
+ await this.finalizeStreamingEntry(session);
224381
+ session.store.currentAssistantIndex = null;
224382
+ await this.endActiveTurn(session, "process_exit");
224107
224383
  session.status = code === 0 ? "stopped" : "error";
224108
224384
  if (!session.skipDb) {
224109
224385
  this.storage.agentSessions.updateStatus(session.id, session.status).catch((err) => {
224110
224386
  console.error(`[AgentSession] Failed to update status for ${session.id}:`, err);
224111
224387
  });
224112
224388
  }
224113
- if (code !== 0 && !spawnFailed && !session.producedOutput) {
224114
- this.pushEntry(session.id, {
224115
- type: "error",
224116
- message: this.buildStartupFailureMessage(session.agentType, stderrTail),
224117
- timestamp: Date.now()
224118
- }, true).catch((err) => {
224119
- console.error(`[AgentSession] Failed to push startup-failure entry for ${session.id}:`, err);
224120
- });
224121
- }
224122
224389
  this.broadcastPatch(session.id, ConversationPatch.updateStatus(session.status));
224123
224390
  this.eventBus?.emit({ type: "session:status", projectId: session.projectId, branch: session.branch, sessionId: session.id, status: session.status });
224124
224391
  this.broadcastRaw(session.id, { finished: true });
@@ -224227,6 +224494,7 @@ var AgentSessionManager = class {
224227
224494
  summaryText
224228
224495
  });
224229
224496
  await this.emitDerivedBranchActivity(session.projectId, session.branch);
224497
+ await this.endActiveTurn(session, "completed");
224230
224498
  if (session.status !== "stopped") {
224231
224499
  session.status = "stopped";
224232
224500
  if (!session.skipDb) await this.storage.agentSessions.updateStatus(sessionId, "stopped");
@@ -224421,6 +224689,7 @@ var AgentSessionManager = class {
224421
224689
  }
224422
224690
  if (event.subtype === "error") {
224423
224691
  this.applyCompletionTimerAction(session, session.completion.errorResult());
224692
+ await this.endActiveTurn(session, "failed");
224424
224693
  if (session.status !== "stopped") {
224425
224694
  session.status = "stopped";
224426
224695
  if (!session.skipDb) await this.storage.agentSessions.updateStatus(sessionId, "stopped");
@@ -224581,6 +224850,20 @@ ${details}`;
224581
224850
  await this.persistEntry(session, index, entry);
224582
224851
  }
224583
224852
  }
224853
+ /**
224854
+ * Close the open turn with a persisted turn_end stop-point entry.
224855
+ * turn_end entries are constructed ONLY here and in repairInterruptedTurn
224856
+ * (restore path). Wall clock only — see design doc for why the CLI's
224857
+ * payload.duration_ms is not used. Rides the normal best-effort entry
224858
+ * persistence on purpose (no strict path — design decision).
224859
+ */
224860
+ async endActiveTurn(session, outcome) {
224861
+ if (session.turnOpenSince === null) return;
224862
+ const endedAt = Date.now();
224863
+ const durationMs = endedAt - session.turnOpenSince;
224864
+ await this.pushEntry(session.id, { type: "turn_end", timestamp: endedAt, durationMs, outcome }, true);
224865
+ session.turnOpenSince = null;
224866
+ }
224584
224867
  /**
224585
224868
  * Send a user message to the agent
224586
224869
  */
@@ -224626,6 +224909,7 @@ ${details}`;
224626
224909
  `[AgentSession] sendUserMessage: wrote ${formatted.length}B to ${session.agentType} stdin (session=${sessionId})`
224627
224910
  );
224628
224911
  session.process.stdin.write(formatted);
224912
+ if (session.turnOpenSince === null) session.turnOpenSince = Date.now();
224629
224913
  return true;
224630
224914
  } catch (error48) {
224631
224915
  console.error(`[AgentSession] Failed to send message:`, error48);
@@ -224744,6 +225028,7 @@ ${details}`;
224744
225028
  content: "Session stopped by user.",
224745
225029
  timestamp: Date.now()
224746
225030
  });
225031
+ await this.endActiveTurn(session, "stopped");
224747
225032
  session.dormant = true;
224748
225033
  this.resetCompletion(session);
224749
225034
  session.status = "stopped";
@@ -224853,6 +225138,7 @@ ${details}`;
224853
225138
  session.store.currentAssistantIndex = null;
224854
225139
  session.buffer = "";
224855
225140
  session.dormant = false;
225141
+ session.turnOpenSince = null;
224856
225142
  this.touchSession(session);
224857
225143
  const clearPatch = ConversationPatch.clearAll();
224858
225144
  this.broadcastPatch(sessionId, clearPatch);
@@ -225013,6 +225299,8 @@ ${details}`;
225013
225299
  break;
225014
225300
  case "system":
225015
225301
  break;
225302
+ case "turn_end":
225303
+ break;
225016
225304
  }
225017
225305
  }
225018
225306
  if (lines.length === 0) return null;
@@ -225055,6 +225343,7 @@ ${details}`;
225055
225343
  content: userMessage,
225056
225344
  timestamp: Date.now()
225057
225345
  }, true, userId);
225346
+ session.turnOpenSince = Date.now();
225058
225347
  setTimeout(() => {
225059
225348
  const context2 = this.buildFullConversationContext(session.store.entries);
225060
225349
  if (context2) {
@@ -225105,6 +225394,34 @@ ${details}`;
225105
225394
  indexProvider.setIndex(maxIndex + 1);
225106
225395
  return store;
225107
225396
  }
225397
+ /**
225398
+ * Crash repair (restore path): if the previous process died mid-turn, the
225399
+ * history has no closing turn_end — append one with outcome
225400
+ * "server_restart" and no duration (the crash time is unknown; the UI
225401
+ * shows "interrupted" instead of a fabricated number). Runs BEFORE
225402
+ * rebuildStoreFromRows so the store is built from the repaired rows.
225403
+ * The other constructor of turn_end entries is endActiveTurn (live paths).
225404
+ */
225405
+ async repairInterruptedTurn(sessionId, rows) {
225406
+ let landingType = null;
225407
+ for (let i = rows.length - 1; i >= 0; i--) {
225408
+ try {
225409
+ const msg = JSON.parse(rows[i].data);
225410
+ if (msg.type === "system") continue;
225411
+ landingType = msg.type;
225412
+ } catch {
225413
+ landingType = "unparsable";
225414
+ }
225415
+ break;
225416
+ }
225417
+ if (landingType === null || landingType === "turn_end") return rows;
225418
+ const maxIndex = rows.reduce((m2, r) => Math.max(m2, r.entry_index), -1);
225419
+ const repair = { type: "turn_end", timestamp: Date.now(), outcome: "server_restart" };
225420
+ const data = JSON.stringify(repair);
225421
+ await this.storage.agentSessions.upsertEntry(sessionId, maxIndex + 1, data);
225422
+ console.log(`[AgentSession] Repaired interrupted turn for ${sessionId} (server_restart turn_end at ${maxIndex + 1})`);
225423
+ return [...rows, { entry_index: maxIndex + 1, data }];
225424
+ }
225108
225425
  /**
225109
225426
  * Restore sessions from database on startup.
225110
225427
  * Creates dormant RunningSession objects with process=null for sessions that have entries.
@@ -225114,8 +225431,11 @@ ${details}`;
225114
225431
  let restoredCount = 0;
225115
225432
  for (const dbSession of allSessions) {
225116
225433
  if (this.sessions.has(dbSession.id)) continue;
225117
- const entries = await this.storage.agentSessions.getEntries(dbSession.id);
225434
+ let entries = await this.storage.agentSessions.getEntries(dbSession.id);
225118
225435
  if (entries.length === 0) continue;
225436
+ if (dbSession.status === "running") {
225437
+ entries = await this.repairInterruptedTurn(dbSession.id, entries);
225438
+ }
225119
225439
  const store = this.rebuildStoreFromRows(entries, dbSession.id);
225120
225440
  const permissionMode = dbSession.permission_mode === "plan" ? "plan" : "edit";
225121
225441
  const runningSession = {
@@ -225136,7 +225456,8 @@ ${details}`;
225136
225456
  eventChain: Promise.resolve(),
225137
225457
  bgSpawnHintsThisTurn: 0,
225138
225458
  taskStartedThisTurn: 0,
225139
- lastActiveAt: Date.now()
225459
+ lastActiveAt: Date.now(),
225460
+ turnOpenSince: null
225140
225461
  };
225141
225462
  this.sessions.set(dbSession.id, runningSession);
225142
225463
  await this.storage.agentSessions.updateStatusPreservingTimestamp(dbSession.id, "stopped");
@@ -225154,17 +225475,38 @@ ${details}`;
225154
225475
  * message goes through wakeDormantSession, which replays the full copied
225155
225476
  * context to a fresh process, so a branch also works with a different
225156
225477
  * agent type than the source.
225157
- * Returns the new session id, or null when the source is unknown or has
225158
- * no persisted history to copy.
225478
+ *
225479
+ * `opts.upToEntryIndex`, when given, is an inclusive cutoff that must land
225480
+ * on a `turn_end` row — every branch then ends with its own tail divider.
225481
+ * With a cutoff the source's live process is never touched (no
225482
+ * finalizeStreamingEntry), so a historical branch is safe even while the
225483
+ * source is running. Without a cutoff (legacy full-copy callers) a running
225484
+ * source is refused outright — copying mid-turn would leave the branch
225485
+ * with a half-finished turn and no closing turn_end.
225159
225486
  */
225160
225487
  async branchSession(sourceSessionId, agentTypeOverride, opts = {}) {
225161
225488
  const source = this.sessions.get(sourceSessionId);
225162
225489
  const sourceRow = await this.storage.agentSessions.getById(sourceSessionId);
225163
- if (!source && !sourceRow) return null;
225164
- if (source?.skipDb) return null;
225165
- if (source) await this.finalizeStreamingEntry(source);
225166
- const entryRows = await this.storage.agentSessions.getEntries(sourceSessionId);
225167
- if (entryRows.length === 0) return null;
225490
+ if (!source && !sourceRow) return { ok: false, reason: "not-found" };
225491
+ if (source?.skipDb) return { ok: false, reason: "empty-history" };
225492
+ if (opts.upToEntryIndex === void 0) {
225493
+ if (source?.status === "running") return { ok: false, reason: "running-needs-cutoff" };
225494
+ if (source) await this.finalizeStreamingEntry(source);
225495
+ }
225496
+ let entryRows = await this.storage.agentSessions.getEntries(sourceSessionId);
225497
+ if (opts.upToEntryIndex !== void 0) {
225498
+ const cut = entryRows.find((r) => r.entry_index === opts.upToEntryIndex);
225499
+ let cutType = null;
225500
+ if (cut) {
225501
+ try {
225502
+ cutType = JSON.parse(cut.data).type;
225503
+ } catch {
225504
+ }
225505
+ }
225506
+ if (cutType !== "turn_end") return { ok: false, reason: "invalid-cutoff" };
225507
+ entryRows = entryRows.filter((r) => r.entry_index <= opts.upToEntryIndex);
225508
+ }
225509
+ if (entryRows.length === 0) return { ok: false, reason: "empty-history" };
225168
225510
  const projectId = source?.projectId ?? sourceRow.project_id;
225169
225511
  const branch = source?.branch ?? (sourceRow.branch || null);
225170
225512
  const permissionMode = source?.permissionMode ?? (sourceRow?.permission_mode === "plan" ? "plan" : "edit");
@@ -225216,12 +225558,13 @@ ${details}`;
225216
225558
  bgSpawnHintsThisTurn: 0,
225217
225559
  taskStartedThisTurn: 0,
225218
225560
  lastActiveAt: Date.now(),
225561
+ turnOpenSince: null,
225219
225562
  crossRemoteMcp: opts.crossRemoteMcp
225220
225563
  };
225221
225564
  this.sessions.set(newId, branched);
225222
225565
  await this.emitDerivedBranchActivity(projectId, branch);
225223
225566
  console.log(`[AgentSession] branchSession: ${sourceSessionId} \u2192 ${newId} (entries=${entryRows.length}, agentType=${agentType})`);
225224
- return newId;
225567
+ return { ok: true, sessionId: newId };
225225
225568
  }
225226
225569
  /**
225227
225570
  * Kill all active session processes and clear state for graceful shutdown
@@ -225907,12 +226250,12 @@ function extractLogText(logs, tailLines) {
225907
226250
  const lines = joined.split("\n");
225908
226251
  return stripAnsi(lines.slice(-tailLines).join("\n"));
225909
226252
  }
225910
- function parseDbTimestamp(s3) {
226253
+ function parseDbTimestamp2(s3) {
225911
226254
  if (s3.includes("T") || s3.endsWith("Z")) return new Date(s3).getTime();
225912
226255
  return (/* @__PURE__ */ new Date(s3.replace(" ", "T") + "Z")).getTime();
225913
226256
  }
225914
226257
  function formatRelativeAge(timestamp, nowMs) {
225915
- const t = parseDbTimestamp(timestamp);
226258
+ const t = parseDbTimestamp2(timestamp);
225916
226259
  if (Number.isNaN(t)) return "unknown";
225917
226260
  const sec = Math.max(0, Math.round((nowMs - t) / 1e3));
225918
226261
  if (sec < 60) return `${sec}s ago`;
@@ -227123,8 +227466,8 @@ var ChatSessionManager = class {
227123
227466
  };
227124
227467
  }
227125
227468
  const TURN_GRACE_MS = 1e3;
227126
- const startedThisTurn = turnStartedAt != null && parseDbTimestamp(lastRow.started_at) >= turnStartedAt - TURN_GRACE_MS;
227127
- const finishedThisTurn = turnStartedAt != null && lastRow.finished_at != null && parseDbTimestamp(lastRow.finished_at) >= turnStartedAt - TURN_GRACE_MS;
227469
+ const startedThisTurn = turnStartedAt != null && parseDbTimestamp2(lastRow.started_at) >= turnStartedAt - TURN_GRACE_MS;
227470
+ const finishedThisTurn = turnStartedAt != null && lastRow.finished_at != null && parseDbTimestamp2(lastRow.finished_at) >= turnStartedAt - TURN_GRACE_MS;
227128
227471
  return {
227129
227472
  name: executor.name,
227130
227473
  command: executor.command,
@@ -233149,14 +233492,21 @@ var routes11 = async (fastify2) => {
233149
233492
  if (!sourceRow || !await fastify2.storage.projects.getById(sourceRow.project_id, userId)) {
233150
233493
  return { ok: false, code: 404, error: "Session not found" };
233151
233494
  }
233152
- const newSessionId = await fastify2.agentSessionManager.branchSession(
233495
+ const result = await fastify2.agentSessionManager.branchSession(
233153
233496
  sourceSessionId,
233154
233497
  opts.agentType,
233155
- { sessionId: opts.sessionId, crossRemoteMcp: opts.crossRemoteMcp }
233498
+ { sessionId: opts.sessionId, crossRemoteMcp: opts.crossRemoteMcp, upToEntryIndex: opts.upToEntryIndex }
233156
233499
  );
233157
- if (!newSessionId) {
233500
+ if (!result.ok) {
233501
+ if (result.reason === "invalid-cutoff") {
233502
+ return { ok: false, code: 400, error: "upToEntryIndex must reference a turn_end stop point" };
233503
+ }
233504
+ if (result.reason === "running-needs-cutoff") {
233505
+ return { ok: false, code: 409, error: "Session is running; branching requires a stop-point cutoff" };
233506
+ }
233158
233507
  return { ok: false, code: 404, error: "Session not found or has no history to branch" };
233159
233508
  }
233509
+ const newSessionId = result.sessionId;
233160
233510
  const session = fastify2.agentSessionManager.getSession(newSessionId);
233161
233511
  const messages = fastify2.agentSessionManager.getMessages(newSessionId);
233162
233512
  const dbRow = await fastify2.storage.agentSessions.getById(newSessionId);
@@ -233906,7 +234256,10 @@ var routes11 = async (fastify2) => {
233906
234256
  fastify2.post(
233907
234257
  "/api/agent-sessions/:sessionId/branch",
233908
234258
  async (req, reply) => {
233909
- const { agentType } = req.body || {};
234259
+ const { agentType, upToEntryIndex } = req.body || {};
234260
+ if (upToEntryIndex !== void 0 && (!Number.isInteger(upToEntryIndex) || upToEntryIndex < 0)) {
234261
+ return reply.code(400).send({ error: "upToEntryIndex must be a non-negative integer" });
234262
+ }
233910
234263
  const userId = requireAuth(req, reply);
233911
234264
  if (userId === null) return;
233912
234265
  if (req.params.sessionId.startsWith("remote-")) {
@@ -233927,7 +234280,7 @@ var routes11 = async (fastify2) => {
233927
234280
  remoteInfo.remoteApiKey,
233928
234281
  "POST",
233929
234282
  `/api/path/agent-sessions/${remoteInfo.remoteSessionId}/branch`,
233930
- { agentType, sessionId: newRemoteSessionId, crossRemoteMcp: crossRemoteMcp2 }
234283
+ { agentType, sessionId: newRemoteSessionId, crossRemoteMcp: crossRemoteMcp2, upToEntryIndex }
233931
234284
  );
233932
234285
  if (!result.ok) {
233933
234286
  return reply.code(proxyStatus(result)).send(result.data);
@@ -233936,6 +234289,12 @@ var routes11 = async (fastify2) => {
233936
234289
  if (remoteData.session.id !== newRemoteSessionId) {
233937
234290
  return reply.code(409).send({ error: "Remote returned an unexpected session id; upgrade the remote" });
233938
234291
  }
234292
+ if (upToEntryIndex !== void 0 && remoteData.messages.length > upToEntryIndex + 1) {
234293
+ console.error(
234294
+ `[Branch] Remote ${remoteInfo.remoteServerId} ignored branch cutoff (${remoteData.messages.length} messages > cutoff ${upToEntryIndex}) \u2014 version drift, upgrade the remote`
234295
+ );
234296
+ return reply.code(409).send({ error: "Remote ignored branch cutoff; upgrade the remote" });
234297
+ }
233939
234298
  fastify2.remoteSessionMap.set(localSessionId, {
233940
234299
  remoteServerId: remoteInfo.remoteServerId,
233941
234300
  remoteUrl: remoteInfo.remoteUrl,
@@ -233979,7 +234338,8 @@ var routes11 = async (fastify2) => {
233979
234338
  const branched = await performLocalBranch(req.params.sessionId, userId, {
233980
234339
  agentType,
233981
234340
  sessionId: preSessionId,
233982
- crossRemoteMcp
234341
+ crossRemoteMcp,
234342
+ upToEntryIndex
233983
234343
  });
233984
234344
  if (!branched.ok) {
233985
234345
  return reply.code(branched.code).send({ error: branched.error });
@@ -233988,13 +234348,17 @@ var routes11 = async (fastify2) => {
233988
234348
  }
233989
234349
  );
233990
234350
  fastify2.post("/api/path/agent-sessions/:sessionId/branch", async (req, reply) => {
233991
- const { agentType, sessionId, crossRemoteMcp } = req.body || {};
234351
+ const { agentType, sessionId, crossRemoteMcp, upToEntryIndex } = req.body || {};
234352
+ if (upToEntryIndex !== void 0 && (!Number.isInteger(upToEntryIndex) || upToEntryIndex < 0)) {
234353
+ return reply.code(400).send({ error: "upToEntryIndex must be a non-negative integer" });
234354
+ }
233992
234355
  const userId = requireAuth(req, reply);
233993
234356
  if (userId === null) return;
233994
234357
  const branched = await performLocalBranch(req.params.sessionId, userId, {
233995
234358
  agentType,
233996
234359
  sessionId,
233997
- crossRemoteMcp
234360
+ crossRemoteMcp,
234361
+ upToEntryIndex
233998
234362
  });
233999
234363
  if (!branched.ok) {
234000
234364
  return reply.code(branched.code).send({ error: branched.error });
@@ -234180,6 +234544,13 @@ var routes11 = async (fastify2) => {
234180
234544
  remoteInfo.branch ?? null,
234181
234545
  normalizedTitle
234182
234546
  );
234547
+ if (normalizedTitle) {
234548
+ try {
234549
+ await fastify2.storage.searchCache.updateCachedSessionTitle(req.params.sessionId, normalizedTitle);
234550
+ } catch (err) {
234551
+ console.error("[API] searchCache.updateCachedSessionTitle failed:", err);
234552
+ }
234553
+ }
234183
234554
  }
234184
234555
  return reply.code(proxyStatus(result)).send(result.data);
234185
234556
  }
@@ -234309,6 +234680,77 @@ var branch_activity_routes_default = (0, import_fastify_plugin13.default)(routes
234309
234680
 
234310
234681
  // src/routes/merge-status-routes.ts
234311
234682
  var import_fastify_plugin14 = __toESM(require_plugin2(), 1);
234683
+ function resolveTargets(comparisons, storedTargets) {
234684
+ const sources = [];
234685
+ const effective = comparisons.map((comparison) => {
234686
+ if (comparison.target !== void 0) {
234687
+ sources.push("request");
234688
+ return comparison;
234689
+ }
234690
+ const storedTarget = storedTargets.get(comparison.branch);
234691
+ if (storedTarget !== void 0) {
234692
+ sources.push("stored");
234693
+ return { branch: comparison.branch, target: storedTarget };
234694
+ }
234695
+ sources.push("default");
234696
+ return comparison;
234697
+ });
234698
+ return { comparisons: effective, sources };
234699
+ }
234700
+ function annotateEntries(entries, comparisons, sources) {
234701
+ return entries.map((entry, index) => ({
234702
+ ...entry,
234703
+ targetSource: sources[index],
234704
+ requestedTarget: comparisons[index]?.target ?? entry.target
234705
+ }));
234706
+ }
234707
+ var MERGE_PAIR_ERRORS = /* @__PURE__ */ new Set([
234708
+ "target-not-found",
234709
+ "branch-not-found",
234710
+ "no-default-branch"
234711
+ ]);
234712
+ var MERGE_STATUS_VALUES = /* @__PURE__ */ new Set([
234713
+ "merged",
234714
+ "partial",
234715
+ "unmerged",
234716
+ "no-unique-commits"
234717
+ ]);
234718
+ function isRecord(value) {
234719
+ return value !== null && typeof value === "object" && !Array.isArray(value);
234720
+ }
234721
+ function isRemoteMergeStatusEntry(value, comparison) {
234722
+ if (!isRecord(value) || value.branch !== comparison.branch) return false;
234723
+ if (value.target !== null && typeof value.target !== "string") return false;
234724
+ if (value.error !== void 0) {
234725
+ if (!MERGE_PAIR_ERRORS.has(value.error)) return false;
234726
+ if (value.status !== void 0 || value.unmergedCount !== void 0 || value.dirty !== void 0) {
234727
+ return false;
234728
+ }
234729
+ if (value.error === "target-not-found") {
234730
+ return comparison.target !== void 0 && value.target === null;
234731
+ }
234732
+ if (value.error === "no-default-branch") {
234733
+ return comparison.target === void 0 && value.target === null;
234734
+ }
234735
+ return typeof value.target === "string" && (comparison.target === void 0 || value.target === comparison.target);
234736
+ }
234737
+ return typeof value.target === "string" && (comparison.target === void 0 || value.target === comparison.target) && MERGE_STATUS_VALUES.has(value.status) && Number.isInteger(value.unmergedCount) && value.unmergedCount >= 0 && typeof value.dirty === "boolean";
234738
+ }
234739
+ function parseRemoteEntries(data, comparisons) {
234740
+ if (!isRecord(data) || !Array.isArray(data.entries)) return null;
234741
+ if (data.entries.length !== comparisons.length) return null;
234742
+ if (!data.entries.every((entry, index) => isRemoteMergeStatusEntry(entry, comparisons[index]))) {
234743
+ return null;
234744
+ }
234745
+ const entries = data.entries;
234746
+ const implicitEntries = entries.filter((_entry, index) => comparisons[index].target === void 0);
234747
+ const hasNoDefault = implicitEntries.some((entry) => entry.error === "no-default-branch");
234748
+ if (hasNoDefault) {
234749
+ return implicitEntries.every((entry) => entry.error === "no-default-branch") ? entries : null;
234750
+ }
234751
+ const resolvedDefault = implicitEntries[0]?.target;
234752
+ return implicitEntries.every((entry) => entry.target === resolvedDefault) ? entries : null;
234753
+ }
234312
234754
  function legacyRemoteLabel(remoteUrl) {
234313
234755
  try {
234314
234756
  return new URL(remoteUrl).hostname;
@@ -234340,6 +234782,10 @@ async function getRemoteConfig4(fastify2, project) {
234340
234782
  return null;
234341
234783
  }
234342
234784
  var MAX_COMPARISONS = 50;
234785
+ var MAX_NAME_LENGTH = 256;
234786
+ function isValidName(value) {
234787
+ return typeof value === "string" && value.length > 0 && value.length <= MAX_NAME_LENGTH;
234788
+ }
234343
234789
  function parseComparisons(body) {
234344
234790
  if (!body || typeof body !== "object") return null;
234345
234791
  const comparisons = body.comparisons;
@@ -234395,6 +234841,11 @@ var routes13 = async (fastify2) => {
234395
234841
  if (!comparisons) {
234396
234842
  return reply.code(400).send({ error: "Invalid comparisons" });
234397
234843
  }
234844
+ const storedTargets = await fastify2.storage.mergeTargets.getForBranches(
234845
+ project.id,
234846
+ comparisons.map(({ branch }) => branch)
234847
+ );
234848
+ const resolved = resolveTargets(comparisons, storedTargets);
234398
234849
  if (!project.path) {
234399
234850
  const remoteConfig = await getRemoteConfig4(fastify2, project);
234400
234851
  if (!remoteConfig) {
@@ -234406,28 +234857,83 @@ var routes13 = async (fastify2) => {
234406
234857
  remoteConfig.apiKey,
234407
234858
  "POST",
234408
234859
  "/api/path/branches/merge-status",
234409
- { path: remoteConfig.remotePath, comparisons },
234860
+ { path: remoteConfig.remotePath, comparisons: resolved.comparisons },
234410
234861
  { reverseConnectManager: fastify2.reverseConnectManager }
234411
234862
  );
234412
234863
  if (!result.ok) {
234413
234864
  return reply.code(proxyStatus(result)).send(result.data);
234414
234865
  }
234415
- const data = result.data;
234866
+ const entries = parseRemoteEntries(result.data, resolved.comparisons);
234867
+ if (!entries) {
234868
+ return reply.code(502).send({ error: "Remote merge-status response invalid" });
234869
+ }
234416
234870
  return reply.code(200).send({
234417
234871
  repository: {
234418
234872
  kind: "remote",
234419
234873
  remoteServerId: remoteConfig.serverId,
234420
234874
  label: remoteConfig.serverName
234421
234875
  },
234422
- entries: data.entries ?? []
234876
+ entries: annotateEntries(
234877
+ entries,
234878
+ resolved.comparisons,
234879
+ resolved.sources
234880
+ )
234423
234881
  });
234424
234882
  }
234425
- return sendComputed(
234426
- reply,
234427
- project.path,
234428
- comparisons,
234429
- { kind: "local", label: "Local" }
234430
- );
234883
+ try {
234884
+ const entries = computeMergeStatusPairs(project.path, resolved.comparisons);
234885
+ return reply.code(200).send({
234886
+ repository: { kind: "local", label: "Local" },
234887
+ entries: annotateEntries(entries, resolved.comparisons, resolved.sources)
234888
+ });
234889
+ } catch (error48) {
234890
+ const statusCode = error48.statusCode ?? 500;
234891
+ const message = error48 instanceof Error ? error48.message : "Failed to compute merge status";
234892
+ return reply.code(statusCode).send({ error: message });
234893
+ }
234894
+ }
234895
+ );
234896
+ fastify2.put(
234897
+ "/api/projects/:id/branches/merge-target",
234898
+ async (req, reply) => {
234899
+ const userId = requireAuth(req, reply);
234900
+ if (userId === null) return;
234901
+ const project = await fastify2.storage.projects.getById(req.params.id, userId);
234902
+ if (!project) {
234903
+ return reply.code(404).send({ error: "Project not found" });
234904
+ }
234905
+ if (!req.body || typeof req.body !== "object") {
234906
+ return reply.code(400).send({ error: "Invalid merge target" });
234907
+ }
234908
+ const body = req.body;
234909
+ const { branch, target, ifAbsent } = body;
234910
+ if (!isValidName(branch) || target !== null && !isValidName(target) || ifAbsent !== void 0 && typeof ifAbsent !== "boolean" || ifAbsent === true && target === null) {
234911
+ return reply.code(400).send({ error: "Invalid merge target" });
234912
+ }
234913
+ let changed;
234914
+ let storedTarget;
234915
+ if (target === null) {
234916
+ changed = await fastify2.storage.mergeTargets.delete(project.id, branch);
234917
+ storedTarget = null;
234918
+ } else if (ifAbsent === true) {
234919
+ changed = await fastify2.storage.mergeTargets.insertIfAbsent(project.id, branch, target);
234920
+ if (changed) {
234921
+ storedTarget = target;
234922
+ } else {
234923
+ storedTarget = (await fastify2.storage.mergeTargets.getForBranches(project.id, [branch])).get(branch) ?? null;
234924
+ }
234925
+ } else {
234926
+ changed = await fastify2.storage.mergeTargets.upsert(project.id, branch, target);
234927
+ storedTarget = target;
234928
+ }
234929
+ if (changed) {
234930
+ fastify2.eventBus.emit({
234931
+ type: "merge-target:updated",
234932
+ projectId: project.id,
234933
+ branch
234934
+ });
234935
+ }
234936
+ return reply.code(200).send({ branch, target: storedTarget });
234431
234937
  }
234432
234938
  );
234433
234939
  };
@@ -234873,7 +235379,7 @@ var SCROLLBACK_MAX = 1e5;
234873
235379
  var FONT_SIZE_MIN = 8;
234874
235380
  var FONT_SIZE_MAX = 32;
234875
235381
  var DEFAULT_CONVERSATION_SETTINGS = {
234876
- agentFontSize: 15,
235382
+ agentFontSize: 16,
234877
235383
  chatFontSize: 15,
234878
235384
  filesTreeFontSize: 14,
234879
235385
  filesContentFontSize: 14
@@ -237639,6 +238145,244 @@ var routes28 = async (fastify2) => {
237639
238145
  };
237640
238146
  var schedule_routes_default = (0, import_fastify_plugin29.default)(routes28, { name: "schedule-routes" });
237641
238147
 
238148
+ // src/search/catalog.ts
238149
+ var parseDbTimestamp3 = (ts) => {
238150
+ if (!ts) return null;
238151
+ const ms = Date.parse(ts.replace(" ", "T") + "Z");
238152
+ return Number.isNaN(ms) ? null : ms;
238153
+ };
238154
+ async function buildSearchCatalog(deps, projectId, projectPath) {
238155
+ pruneWorktrees(projectPath);
238156
+ const workspaces = getWorktreeBranches(projectPath);
238157
+ const sessions = await deps.storage.agentSessions.getByProjectId(projectId);
238158
+ const counts = new Map(
238159
+ (await deps.storage.agentSessions.countEntries()).map((r) => [r.session_id, r.cnt])
238160
+ );
238161
+ return {
238162
+ snapshotAt: Date.now(),
238163
+ workspaces,
238164
+ sessions: sessions.map((s3) => ({ s: s3, entryCount: counts.get(s3.id) ?? 0 })).filter(({ s: s3, entryCount }) => shouldShowBranchSessionInList({
238165
+ entryCount,
238166
+ processAlive: deps.getProcessAlive?.(s3.id) ?? false
238167
+ })).map(({ s: s3, entryCount }) => ({
238168
+ id: s3.id,
238169
+ branch: s3.branch === "" ? null : s3.branch,
238170
+ title: s3.title ?? null,
238171
+ lastActiveAt: s3.last_user_message_at ?? parseDbTimestamp3(s3.updated_at),
238172
+ favoritedAt: s3.favorited_at ?? null,
238173
+ entryCount
238174
+ }))
238175
+ };
238176
+ }
238177
+
238178
+ // src/search/refresh.ts
238179
+ var DEFAULT_TTL_MS = 3e4;
238180
+ var DEFAULT_DEADLINE_MS = 5e3;
238181
+ var PER_WORKER_CONCURRENCY = 3;
238182
+ var LOCAL_CONCURRENCY = 4;
238183
+ async function listSearchTargets(storage, userId) {
238184
+ const projects = await storage.projects.getAll(userId);
238185
+ const targets = [];
238186
+ for (const p2 of projects) {
238187
+ if (p2.path) targets.push({ projectId: p2.id, targetId: "local", projectPath: p2.path });
238188
+ const remotes = await storage.projectRemotes.getByProject(p2.id);
238189
+ for (const r of remotes) {
238190
+ targets.push({
238191
+ projectId: p2.id,
238192
+ targetId: r.remote_server_id,
238193
+ remote: {
238194
+ serverId: r.remote_server_id,
238195
+ url: r.server_url ?? "",
238196
+ apiKey: r.server_api_key ?? "",
238197
+ remotePath: r.remote_path
238198
+ }
238199
+ });
238200
+ }
238201
+ }
238202
+ return targets;
238203
+ }
238204
+ function computeCacheState(states, targets, now2, ttlMs = DEFAULT_TTL_MS) {
238205
+ if (targets.length === 0) return "fresh";
238206
+ const stateByKey = new Map(states.map((s3) => [`${s3.project_id}:${s3.target_id}`, s3]));
238207
+ const matched = targets.map((t) => stateByKey.get(`${t.projectId}:${t.targetId}`));
238208
+ if (matched.some((s3) => s3?.last_success_at == null)) return "cold";
238209
+ return matched.every((s3) => now2 - (s3.last_success_at ?? 0) <= ttlMs) ? "fresh" : "stale";
238210
+ }
238211
+ async function runWithConcurrency(tasks, limit) {
238212
+ const queue = [...tasks];
238213
+ const workers = Array.from({ length: Math.max(1, Math.min(limit, queue.length)) }, async () => {
238214
+ for (let task = queue.shift(); task; task = queue.shift()) {
238215
+ await task();
238216
+ }
238217
+ });
238218
+ await Promise.all(workers);
238219
+ }
238220
+ function createSearchRefresher(deps) {
238221
+ const ttlMs = deps.ttlMs ?? DEFAULT_TTL_MS;
238222
+ const deadlineMs = deps.deadlineMs ?? DEFAULT_DEADLINE_MS;
238223
+ const now2 = deps.now ?? Date.now;
238224
+ const inflight = /* @__PURE__ */ new Map();
238225
+ function refreshTarget(t) {
238226
+ const key = `${t.projectId}:${t.targetId}`;
238227
+ const existing = inflight.get(key);
238228
+ if (existing) return existing;
238229
+ const run2 = (async () => {
238230
+ try {
238231
+ const snapshot = t.targetId === "local" ? await deps.buildLocalCatalog(t.projectId, t.projectPath ?? "") : await deps.fetchRemoteCatalog(t);
238232
+ await deps.storage.searchCache.applyCatalogSnapshot(t.projectId, t.targetId, snapshot);
238233
+ } catch (err) {
238234
+ await deps.storage.searchCache.recordSyncFailure(
238235
+ t.projectId,
238236
+ t.targetId,
238237
+ err instanceof Error ? err.message : String(err)
238238
+ ).catch(() => {
238239
+ });
238240
+ }
238241
+ })();
238242
+ inflight.set(key, run2);
238243
+ void run2.finally(() => inflight.delete(key));
238244
+ return run2;
238245
+ }
238246
+ async function refreshAll(userId) {
238247
+ const targets = await listSearchTargets(deps.storage, userId);
238248
+ const states = await deps.storage.searchCache.getSyncStates([...new Set(targets.map((t) => t.projectId))]);
238249
+ const stateByKey = new Map(states.map((s3) => [`${s3.project_id}:${s3.target_id}`, s3]));
238250
+ const due = targets.filter((t) => {
238251
+ const s3 = stateByKey.get(`${t.projectId}:${t.targetId}`);
238252
+ return !s3?.last_success_at || now2() - s3.last_success_at > ttlMs;
238253
+ });
238254
+ const byWorker = /* @__PURE__ */ new Map();
238255
+ for (const t of due) {
238256
+ const k2 = t.targetId;
238257
+ byWorker.set(k2, [...byWorker.get(k2) ?? [], t]);
238258
+ }
238259
+ const lanes = [...byWorker.entries()].map(
238260
+ ([workerId, ts]) => runWithConcurrency(
238261
+ ts.map((t) => () => refreshTarget(t)),
238262
+ workerId === "local" ? LOCAL_CONCURRENCY : PER_WORKER_CONCURRENCY
238263
+ )
238264
+ );
238265
+ const all = Promise.all(lanes).then(() => void 0);
238266
+ await Promise.race([
238267
+ all,
238268
+ new Promise((resolve3) => {
238269
+ const timer = setTimeout(resolve3, deadlineMs);
238270
+ timer.unref?.();
238271
+ })
238272
+ ]);
238273
+ }
238274
+ return { refreshAll };
238275
+ }
238276
+
238277
+ // src/routes/search-routes.ts
238278
+ var searchRoutes = async (fastify2) => {
238279
+ fastify2.get(
238280
+ "/api/path/search-catalog",
238281
+ async (req, reply) => {
238282
+ const projectPath = req.query.path;
238283
+ if (!projectPath) {
238284
+ return reply.code(400).send({ error: "path is required" });
238285
+ }
238286
+ const project = await fastify2.storage.projects.getByPath(projectPath);
238287
+ if (!project) {
238288
+ return reply.code(200).send({ snapshotAt: Date.now(), workspaces: [], sessions: [] });
238289
+ }
238290
+ try {
238291
+ const catalog = await buildSearchCatalog(
238292
+ {
238293
+ storage: fastify2.storage,
238294
+ getProcessAlive: (id) => fastify2.agentSessionManager.getSessionProcessAlive(id)
238295
+ },
238296
+ project.id,
238297
+ projectPath
238298
+ );
238299
+ return reply.code(200).send(catalog);
238300
+ } catch (error48) {
238301
+ return reply.code(500).send({ error: String(error48) });
238302
+ }
238303
+ }
238304
+ );
238305
+ const refresher = createSearchRefresher({
238306
+ storage: fastify2.storage,
238307
+ buildLocalCatalog: (projectId, projectPath) => buildSearchCatalog(
238308
+ {
238309
+ storage: fastify2.storage,
238310
+ getProcessAlive: (id) => fastify2.agentSessionManager.getSessionProcessAlive(id)
238311
+ },
238312
+ projectId,
238313
+ projectPath
238314
+ ),
238315
+ fetchRemoteCatalog: async (target) => {
238316
+ const r = target.remote;
238317
+ if (!r) throw new Error("remote target without remote config");
238318
+ const params = new URLSearchParams({ path: r.remotePath });
238319
+ const result = await proxyToRemoteAuto(
238320
+ r.serverId,
238321
+ r.url,
238322
+ r.apiKey,
238323
+ "GET",
238324
+ `/api/path/search-catalog?${params.toString()}`,
238325
+ void 0,
238326
+ { reverseConnectManager: fastify2.reverseConnectManager, timeoutMs: 2e3 }
238327
+ );
238328
+ if (!result.ok) {
238329
+ throw new Error(`catalog fetch failed: ${result.status} ${result.errorCode ?? ""}`);
238330
+ }
238331
+ const data = result.data;
238332
+ const sessions = await Promise.all(data.sessions.map(async (s3) => {
238333
+ const localSessionId = `remote-${target.targetId}-${target.projectId}-${s3.id}`;
238334
+ const mapBranch = s3.branch ?? "";
238335
+ if (!fastify2.remoteSessionMap.has(localSessionId)) {
238336
+ fastify2.remoteSessionMap.set(localSessionId, {
238337
+ remoteServerId: target.targetId,
238338
+ remoteUrl: r.url,
238339
+ remoteApiKey: r.apiKey,
238340
+ remoteSessionId: s3.id,
238341
+ branch: mapBranch
238342
+ });
238343
+ }
238344
+ await fastify2.storage.remoteSessionMappings.upsert(
238345
+ localSessionId,
238346
+ target.projectId,
238347
+ target.targetId,
238348
+ s3.id,
238349
+ mapBranch
238350
+ );
238351
+ return { ...s3, id: localSessionId };
238352
+ }));
238353
+ return { workspaces: data.workspaces, sessions };
238354
+ }
238355
+ });
238356
+ async function currentCacheState(userId) {
238357
+ const targets = await listSearchTargets(fastify2.storage, userId);
238358
+ const states = await fastify2.storage.searchCache.getSyncStates(
238359
+ [...new Set(targets.map((t) => t.projectId))]
238360
+ );
238361
+ return computeCacheState(states, targets, Date.now());
238362
+ }
238363
+ fastify2.get(
238364
+ "/api/search",
238365
+ async (req, reply) => {
238366
+ const userId = requireAuth(req, reply);
238367
+ if (userId === null) return;
238368
+ const query = (req.query.q ?? "").slice(0, 256);
238369
+ const parsed = parseInt(req.query.limitPerGroup ?? "10", 10);
238370
+ const limitPerGroup = Math.min(Math.max(Number.isNaN(parsed) ? 10 : parsed, 1), 50);
238371
+ const results = await fastify2.storage.searchCache.search({ userId, query, limitPerGroup });
238372
+ const cacheState = await currentCacheState(userId);
238373
+ return reply.code(200).send({ ...results, cacheState });
238374
+ }
238375
+ );
238376
+ fastify2.post("/api/search/refresh", async (req, reply) => {
238377
+ const userId = requireAuth(req, reply);
238378
+ if (userId === null) return;
238379
+ await refresher.refreshAll(userId);
238380
+ const cacheState = await currentCacheState(userId);
238381
+ return reply.code(200).send({ ok: true, cacheState });
238382
+ });
238383
+ };
238384
+ var search_routes_default = searchRoutes;
238385
+
237642
238386
  // ../../node_modules/.pnpm/@clerk+fastify@3.1.2_fastify@5.7.2_react-dom@19.2.3_react@19.2.3__react@19.2.3/node_modules/@clerk/fastify/dist/chunk-RY4T2JMQ.mjs
237643
238387
  import { Readable as Readable3 } from "stream";
237644
238388
  var fastifyRequestToRequest = (req) => {
@@ -238896,6 +239640,7 @@ var createServer = async (opts) => {
238896
239640
  server.register(remote_routes_default);
238897
239641
  server.register(remote_server_routes_default);
238898
239642
  server.register(project_remote_routes_default);
239643
+ server.register(search_routes_default);
238899
239644
  server.register(executor_group_routes_default);
238900
239645
  server.register(executor_routes_default);
238901
239646
  server.register(schedule_routes_default);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedeckx/linux-x64",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Vibedeckx platform binaries for Linux x64",
5
5
  "os": [
6
6
  "linux"