@vibedeckx/linux-x64 0.3.14 → 0.3.17

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 +513 -55
  2. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -186724,7 +186724,8 @@ var mapAgentSession = (row) => ({
186724
186724
  last_user_message_at: row.last_user_message_at,
186725
186725
  last_completed_at: row.last_completed_at,
186726
186726
  favorited_at: row.favorited_at,
186727
- native_session_id: row.native_session_id
186727
+ native_session_id: row.native_session_id,
186728
+ history_epoch: row.history_epoch
186728
186729
  });
186729
186730
  var parseActivityTimestamp = (value) => {
186730
186731
  const explicitZone = /(?:Z|[+-]\d\d:\d\d)$/i.test(value);
@@ -187123,6 +187124,11 @@ var createAgentSessionRepos = (kdb, h) => ({
187123
187124
  markCompleted: async (id, timestampMs) => {
187124
187125
  await kdb.updateTable("agent_sessions").set({ last_completed_at: timestampMs, activity_at: sql`max(activity_at, ${timestampMs})` }).where("id", "=", id).execute();
187125
187126
  },
187127
+ incrementHistoryEpoch: async (id) => {
187128
+ await kdb.updateTable("agent_sessions").set({ history_epoch: sql`history_epoch + 1` }).where("id", "=", id).execute();
187129
+ const row = await kdb.selectFrom("agent_sessions").select("history_epoch").where("id", "=", id).executeTakeFirstOrThrow();
187130
+ return row.history_epoch;
187131
+ },
187126
187132
  delete: async (id) => {
187127
187133
  await kdb.deleteFrom("agent_sessions").where("id", "=", id).execute();
187128
187134
  },
@@ -203632,6 +203638,7 @@ var tightenWorkspaceCheckoutForeignKeys = (db) => {
203632
203638
  last_completed_at INTEGER DEFAULT NULL,
203633
203639
  favorited_at INTEGER DEFAULT NULL,
203634
203640
  native_session_id TEXT DEFAULT NULL,
203641
+ history_epoch INTEGER NOT NULL DEFAULT 0,
203635
203642
  FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
203636
203643
  FOREIGN KEY (workspace_checkout_id) REFERENCES workspace_checkouts(id)
203637
203644
  DEFERRABLE INITIALLY DEFERRED
@@ -203639,10 +203646,10 @@ var tightenWorkspaceCheckoutForeignKeys = (db) => {
203639
203646
  INSERT INTO agent_sessions_fk_new
203640
203647
  (id, project_id, branch, workspace_checkout_id, status, permission_mode, agent_type,
203641
203648
  title, model, created_at, updated_at, activity_at, last_user_message_at,
203642
- last_completed_at, favorited_at, native_session_id)
203649
+ last_completed_at, favorited_at, native_session_id, history_epoch)
203643
203650
  SELECT id, project_id, branch, workspace_checkout_id, status, permission_mode, agent_type,
203644
203651
  title, model, created_at, updated_at, activity_at, last_user_message_at,
203645
- last_completed_at, favorited_at, native_session_id
203652
+ last_completed_at, favorited_at, native_session_id, history_epoch
203646
203653
  FROM agent_sessions;
203647
203654
  DROP TABLE agent_sessions;
203648
203655
  ALTER TABLE agent_sessions_fk_new RENAME TO agent_sessions;
@@ -203916,6 +203923,7 @@ var initializeSchema = (db) => {
203916
203923
  -- Drives the workspace-status derivation; see plans/branch-activity-refactor.md.
203917
203924
  last_user_message_at INTEGER DEFAULT NULL,
203918
203925
  last_completed_at INTEGER DEFAULT NULL,
203926
+ history_epoch INTEGER NOT NULL DEFAULT 0,
203919
203927
  FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
203920
203928
  );
203921
203929
 
@@ -204450,6 +204458,10 @@ var initializeSchema = (db) => {
204450
204458
  if (!sessionInfoNativeId.some((col) => col.name === "native_session_id")) {
204451
204459
  db.exec("ALTER TABLE agent_sessions ADD COLUMN native_session_id TEXT DEFAULT NULL");
204452
204460
  }
204461
+ const sessionHistoryEpochInfo = db.prepare("PRAGMA table_info(agent_sessions)").all();
204462
+ if (!sessionHistoryEpochInfo.some((col) => col.name === "history_epoch")) {
204463
+ db.exec("ALTER TABLE agent_sessions ADD COLUMN history_epoch INTEGER NOT NULL DEFAULT 0");
204464
+ }
204453
204465
  const sessionCheckoutInfo = db.prepare("PRAGMA table_info(agent_sessions)").all();
204454
204466
  if (!sessionCheckoutInfo.some((col) => col.name === "workspace_checkout_id")) {
204455
204467
  db.exec("ALTER TABLE agent_sessions ADD COLUMN workspace_checkout_id TEXT DEFAULT NULL");
@@ -207067,6 +207079,7 @@ function buildApprovalResponse(requestId, decision) {
207067
207079
 
207068
207080
  // src/providers/codex-provider.ts
207069
207081
  var ITEM_PAYLOAD_SUMMARY_LIMIT = 500;
207082
+ var MAX_COMPLETED_TURN_IDS = 32;
207070
207083
  function firstText(...candidates) {
207071
207084
  for (const c of candidates) {
207072
207085
  if (typeof c === "string" && c.trim()) return c.trim();
@@ -207200,14 +207213,23 @@ var CodexProvider = class _CodexProvider {
207200
207213
  handleItemCompleted(params, sessionId) {
207201
207214
  const item = params?.item;
207202
207215
  if (!item?.type) return [];
207203
- if (params?.turnId != null) {
207204
- this.getSessionState(sessionId).currentTurnId = String(params.turnId);
207205
- }
207216
+ const state = this.getSessionState(sessionId);
207217
+ const itemTurnId = params?.turnId != null ? String(params.turnId) : null;
207218
+ const outOfTurn = itemTurnId !== null && state.completedTurnIds.has(itemTurnId);
207219
+ if (itemTurnId !== null && !outOfTurn) {
207220
+ state.currentTurnId = itemTurnId;
207221
+ }
207222
+ const events = this.itemEvents(item, itemTurnId, outOfTurn, state);
207223
+ if (!outOfTurn) return events;
207224
+ return events.map(
207225
+ (e) => e.type === "tool_use" || e.type === "tool_result" ? { ...e, outOfTurn: true } : e
207226
+ );
207227
+ }
207228
+ itemEvents(item, itemTurnId, outOfTurn, state) {
207206
207229
  switch (item.type) {
207207
207230
  case "agentMessage": {
207208
- if (params?.turnId && (item.phase === "final_answer" || item.text)) {
207209
- const state = this.getSessionState(sessionId);
207210
- state.turnsWithFinalMessage.add(String(params.turnId));
207231
+ if (itemTurnId && !outOfTurn && (item.phase === "final_answer" || item.text)) {
207232
+ state.turnsWithFinalMessage.add(itemTurnId);
207211
207233
  }
207212
207234
  return [{ type: "text", content: item.text ?? "" }];
207213
207235
  }
@@ -207300,6 +207322,10 @@ var CodexProvider = class _CodexProvider {
207300
207322
  const hadFinalMessage = turnId != null && state.turnsWithFinalMessage.has(turnId);
207301
207323
  if (turnId != null) {
207302
207324
  state.turnsWithFinalMessage.delete(turnId);
207325
+ state.completedTurnIds.add(turnId);
207326
+ while (state.completedTurnIds.size > MAX_COMPLETED_TURN_IDS) {
207327
+ state.completedTurnIds.delete(state.completedTurnIds.values().next().value);
207328
+ }
207303
207329
  }
207304
207330
  state.currentTurnId = null;
207305
207331
  if (turn.status === "completed" && !hadFinalMessage) {
@@ -207424,7 +207450,8 @@ var CodexProvider = class _CodexProvider {
207424
207450
  pendingTurnContent: null,
207425
207451
  lastTokenUsage: {},
207426
207452
  turnsWithFinalMessage: /* @__PURE__ */ new Set(),
207427
- currentTurnId: null
207453
+ currentTurnId: null,
207454
+ completedTurnIds: /* @__PURE__ */ new Set()
207428
207455
  });
207429
207456
  }
207430
207457
  onSessionDestroyed(sessionId) {
@@ -207443,7 +207470,8 @@ var CodexProvider = class _CodexProvider {
207443
207470
  pendingTurnContent: null,
207444
207471
  lastTokenUsage: {},
207445
207472
  turnsWithFinalMessage: /* @__PURE__ */ new Set(),
207446
- currentTurnId: null
207473
+ currentTurnId: null,
207474
+ completedTurnIds: /* @__PURE__ */ new Set()
207447
207475
  };
207448
207476
  this.sessions.set(sessionId, state);
207449
207477
  }
@@ -229656,6 +229684,7 @@ var AgentSessionManager = class {
229656
229684
  process: null,
229657
229685
  dormant: false,
229658
229686
  processStartsInFlight: 0,
229687
+ historyEpoch: stored?.history_epoch ?? 0,
229659
229688
  store,
229660
229689
  subscribers: /* @__PURE__ */ new Set(),
229661
229690
  status: "running",
@@ -230001,14 +230030,15 @@ var AgentSessionManager = class {
230001
230030
  if (!session) return;
230002
230031
  const timestamp = Date.now();
230003
230032
  session.lastActiveAt = timestamp;
230004
- if (event.type === "turn_started" || event.type === "text" || event.type === "thinking" || event.type === "tool_use" || event.type === "tool_result" || event.type === "approval_request") {
230033
+ const outOfTurn = (event.type === "tool_use" || event.type === "tool_result") && event.outOfTurn === true;
230034
+ if (!outOfTurn && (event.type === "turn_started" || event.type === "text" || event.type === "thinking" || event.type === "tool_use" || event.type === "tool_result" || event.type === "approval_request")) {
230005
230035
  this.applyCompletionTimerAction(session, session.completion.noteTurnActivity());
230006
230036
  }
230007
- if (session.turnOpenSince === null && (event.type === "turn_started" || event.type === "text" || event.type === "thinking" || event.type === "tool_use" || event.type === "tool_result" || event.type === "approval_request")) {
230037
+ if (session.turnOpenSince === null && !outOfTurn && (event.type === "turn_started" || event.type === "text" || event.type === "thinking" || event.type === "tool_use" || event.type === "tool_result" || event.type === "approval_request")) {
230008
230038
  session.turnOpenSince = timestamp;
230009
230039
  session.turnDisposition = resolveNotificationDisposition(findLatestUserEntry(session.store.entries));
230010
230040
  }
230011
- if (session.status !== "running" && (event.type === "text" || event.type === "thinking" || event.type === "tool_use" || event.type === "tool_result" || event.type === "approval_request")) {
230041
+ if (session.status !== "running" && !outOfTurn && (event.type === "text" || event.type === "thinking" || event.type === "tool_use" || event.type === "tool_result" || event.type === "approval_request")) {
230012
230042
  session.status = "running";
230013
230043
  if (!session.skipDb) await this.storage.agentSessions.updateStatus(sessionId, "running");
230014
230044
  this.broadcastPatch(sessionId, ConversationPatch.updateStatus("running"));
@@ -230497,17 +230527,29 @@ var AgentSessionManager = class {
230497
230527
  /**
230498
230528
  * Subscribe to session updates (WebSocket connection)
230499
230529
  */
230500
- subscribe(sessionId, ws) {
230530
+ subscribe(sessionId, ws, opts = {}) {
230501
230531
  const session = this.sessions.get(sessionId);
230502
230532
  if (!session) {
230503
230533
  return null;
230504
230534
  }
230505
230535
  session.subscribers.add(ws);
230536
+ const after = opts.historyEpoch === void 0 || opts.historyEpoch === session.historyEpoch ? opts.afterEntryIndex ?? -1 : -1;
230537
+ ws.send(JSON.stringify({
230538
+ HistorySync: {
230539
+ historyEpoch: session.historyEpoch,
230540
+ reset: opts.historyEpoch !== void 0 && opts.historyEpoch !== session.historyEpoch
230541
+ }
230542
+ }));
230506
230543
  for (const patch of session.store.patches) {
230544
+ const entryIndices = patch.flatMap((op) => {
230545
+ const match2 = op.path.match(/^\/entries\/(\d+)$/);
230546
+ return match2 ? [Number(match2[1])] : [];
230547
+ });
230548
+ if (entryIndices.length > 0 && entryIndices.every((index) => index <= after)) continue;
230507
230549
  const msg = { JsonPatch: patch };
230508
230550
  ws.send(JSON.stringify(msg));
230509
230551
  }
230510
- ws.send(JSON.stringify({ Ready: true }));
230552
+ ws.send(JSON.stringify({ Ready: true, historyEpoch: session.historyEpoch }));
230511
230553
  const statusPatch = ConversationPatch.updateStatus(session.status);
230512
230554
  ws.send(JSON.stringify({ JsonPatch: statusPatch }));
230513
230555
  return () => {
@@ -230525,6 +230567,9 @@ var AgentSessionManager = class {
230525
230567
  getRawMessages(sessionId) {
230526
230568
  return this.sessions.get(sessionId)?.store.entries ?? [];
230527
230569
  }
230570
+ getHistoryEpoch(sessionId) {
230571
+ return this.sessions.get(sessionId)?.historyEpoch;
230572
+ }
230528
230573
  /**
230529
230574
  * Public wrapper over broadcastRaw for the WorkflowEngine: mirror a raw WS
230530
230575
  * frame to a session's stream subscribers (a front server subscribed to
@@ -230796,6 +230841,9 @@ var AgentSessionManager = class {
230796
230841
  this.emitProcessAlive(session, false);
230797
230842
  if (!session.skipDb) {
230798
230843
  await this.storage.agentSessions.deleteEntries(sessionId);
230844
+ session.historyEpoch = await this.storage.agentSessions.incrementHistoryEpoch(sessionId);
230845
+ } else {
230846
+ session.historyEpoch += 1;
230799
230847
  }
230800
230848
  session.store.patches = [];
230801
230849
  session.store.entries = [];
@@ -230806,6 +230854,9 @@ var AgentSessionManager = class {
230806
230854
  session.dormant = false;
230807
230855
  session.turnOpenSince = null;
230808
230856
  this.touchSession(session);
230857
+ this.broadcastRaw(sessionId, {
230858
+ HistorySync: { historyEpoch: session.historyEpoch, reset: true }
230859
+ });
230809
230860
  const clearPatch = ConversationPatch.clearAll();
230810
230861
  this.broadcastPatch(sessionId, clearPatch);
230811
230862
  session.status = "running";
@@ -231289,6 +231340,7 @@ var AgentSessionManager = class {
231289
231340
  process: null,
231290
231341
  dormant: true,
231291
231342
  processStartsInFlight: 0,
231343
+ historyEpoch: dbSession.history_epoch ?? 0,
231292
231344
  store,
231293
231345
  subscribers: /* @__PURE__ */ new Set(),
231294
231346
  status: "stopped",
@@ -231310,10 +231362,59 @@ var AgentSessionManager = class {
231310
231362
  await this.storage.agentSessions.updateStatusPreservingTimestamp(dbSession.id, "stopped");
231311
231363
  restoredCount++;
231312
231364
  }
231365
+ await this.repairOrphanedRunningRows(allSessions);
231313
231366
  if (restoredCount > 0) {
231314
231367
  console.log(`[AgentSession] Restored ${restoredCount} dormant session(s) from database`);
231315
231368
  }
231316
231369
  }
231370
+ /**
231371
+ * Reconcile rows the database still calls "running" against what this
231372
+ * process actually owns.
231373
+ *
231374
+ * `create` writes `status='running'` BEFORE the process is spawned, so any
231375
+ * path that dies between the INSERT and the first entry leaves a row
231376
+ * claiming to run forever. The restore loop above is supposed to be the
231377
+ * backstop, but it skips zero-entry rows before it resets crashed statuses
231378
+ * — so precisely the sessions that died earliest are the ones nothing ever
231379
+ * repairs. Measured on a real worker (2026-08-10): 63 such rows, the oldest
231380
+ * four months old, every one of them permanently exempt from session
231381
+ * retention (`status <> 'running'`) and inflating the project dashboard's
231382
+ * running count (46 reported, 1 real).
231383
+ *
231384
+ * Reconciliation rather than prevention, deliberately: a DB row and an OS
231385
+ * process are two resources with no atomic commit between them, so a kill
231386
+ * landing between the INSERT and the spawn leaves the same row no matter
231387
+ * how careful `create` becomes. Prevention narrows the window; only
231388
+ * reconciliation closes it. Same argument the retention plan's §3.1 makes
231389
+ * for snapshot reconciliation over delete events.
231390
+ *
231391
+ * The ownership test is "not in `this.sessions`", NOT `process === null`: a
231392
+ * session that is spawning or waking sits in the map with a null process,
231393
+ * and resetting it would be the same class of bug as the wake/retention
231394
+ * race in `deleteDormantSessionIfExpired`.
231395
+ *
231396
+ * STARTUP ONLY. `createNewSession` INSERTs the row before it puts the
231397
+ * session in the map, so a few milliseconds exist in which a perfectly
231398
+ * healthy session looks orphaned. That window is unreachable from here (no
231399
+ * requests are served yet); a periodic caller would first have to add an
231400
+ * age threshold.
231401
+ *
231402
+ * @param snapshot the row list read at the top of `restoreSessionsFromDb`.
231403
+ * Rows the loop already reset read as stale "running" here — the map check
231404
+ * is what excludes them, which is why it must come after the loop.
231405
+ */
231406
+ async repairOrphanedRunningRows(snapshot) {
231407
+ let repaired = 0;
231408
+ for (const row of snapshot) {
231409
+ if (row.status !== "running") continue;
231410
+ if (this.sessions.has(row.id)) continue;
231411
+ await this.storage.agentSessions.updateStatusPreservingTimestamp(row.id, "stopped");
231412
+ repaired++;
231413
+ }
231414
+ if (repaired > 0) {
231415
+ console.log(`[AgentSession] Reset ${repaired} orphaned session row(s) left as "running"`);
231416
+ }
231417
+ }
231317
231418
  /**
231318
231419
  * Create a new dormant session that copies another session's conversation
231319
231420
  * history ("branch"). The new session gets its own DB row, copied entry
@@ -231431,6 +231532,7 @@ var AgentSessionManager = class {
231431
231532
  process: null,
231432
231533
  dormant: true,
231433
231534
  processStartsInFlight: 0,
231535
+ historyEpoch: 0,
231434
231536
  store,
231435
231537
  subscribers: /* @__PURE__ */ new Set(),
231436
231538
  status: "stopped",
@@ -232742,7 +232844,7 @@ async function persistRemoteSessionActivityFrame(storage, sessionId, remoteInfo,
232742
232844
  }
232743
232845
  return false;
232744
232846
  }
232745
- function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnectManager, eventBus, agentSessionManager, storage) {
232847
+ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnectManager, eventBus, agentSessionManager, storage, requestedHistory) {
232746
232848
  const hasCachedData = cache2.hasData(sessionId);
232747
232849
  console.log(`[AgentWS] Opening persistent remote WS for ${sessionId} (cached=${hasCachedData})`);
232748
232850
  if (!reverseConnectManager || !reverseConnectManager.isConnected(remoteInfo.remoteServerId)) {
@@ -232752,7 +232854,16 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232752
232854
  return;
232753
232855
  }
232754
232856
  const channelId = randomUUID2();
232755
- const wsPath = `/api/agent-sessions/${remoteInfo.remoteSessionId}/stream`;
232857
+ const cachedHead = cache2.get(sessionId);
232858
+ const upstreamEpoch = requestedHistory?.historyEpoch ?? cachedHead?.historyEpoch ?? void 0;
232859
+ const upstreamAfter = requestedHistory?.afterEntryIndex ?? (upstreamEpoch !== void 0 ? cachedHead?.lastTurnEndEntryIndex ?? void 0 : void 0);
232860
+ const upstreamParams = new URLSearchParams();
232861
+ if (upstreamEpoch !== void 0 && upstreamAfter !== void 0) {
232862
+ upstreamParams.set("after", String(upstreamAfter));
232863
+ upstreamParams.set("epoch", String(upstreamEpoch));
232864
+ }
232865
+ const usesBoundedReplay = upstreamParams.size > 0;
232866
+ const wsPath = `/api/agent-sessions/${encodeURIComponent(remoteInfo.remoteSessionId)}/stream${usesBoundedReplay ? `?${upstreamParams}` : ""}`;
232756
232867
  const adapter = new VirtualWsAdapter(
232757
232868
  (data) => reverseConnectManager.sendChannelData(remoteInfo.remoteServerId, channelId, data),
232758
232869
  () => reverseConnectManager.closeChannel(remoteInfo.remoteServerId, channelId)
@@ -232786,6 +232897,7 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232786
232897
  cache2.broadcast(sessionId, raw);
232787
232898
  const statusEvent = statusEventFromRemotePatch(parsed, sessionId, remoteInfo);
232788
232899
  if (statusEvent) {
232900
+ cache2.setSessionStatus(sessionId, statusEvent.status);
232789
232901
  let activityReady = true;
232790
232902
  if (storage) {
232791
232903
  activityReady = await persistRemoteSessionActivityFrame(storage, sessionId, remoteInfo, parsed).catch((error48) => {
@@ -232802,6 +232914,11 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232802
232914
  cache2.setFinished(sessionId);
232803
232915
  cache2.broadcast(sessionId, raw);
232804
232916
  } else if ("taskCompleted" in parsed) {
232917
+ cache2.setSessionStatus(sessionId, "stopped");
232918
+ const turnEndIndex = parsed.taskCompleted.turnEndEntryIndex;
232919
+ if (typeof turnEndIndex === "number" && Number.isInteger(turnEndIndex)) {
232920
+ cache2.setLastTurnEndEntryIndex(sessionId, turnEndIndex);
232921
+ }
232805
232922
  cache2.appendMessage(sessionId, raw, false);
232806
232923
  cache2.broadcast(sessionId, raw);
232807
232924
  let activityReady = true;
@@ -232857,6 +232974,7 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232857
232974
  }
232858
232975
  }
232859
232976
  } else if ("error" in parsed) {
232977
+ cache2.setSessionStatus(sessionId, "error");
232860
232978
  cache2.appendMessage(sessionId, raw, false);
232861
232979
  cache2.broadcast(sessionId, raw);
232862
232980
  let activityReady = true;
@@ -232879,6 +232997,15 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232879
232997
  cache2.setFinished(sessionId);
232880
232998
  }
232881
232999
  } else if ("Ready" in parsed) {
233000
+ const epoch = parsed.historyEpoch;
233001
+ if (typeof epoch === "number" && Number.isInteger(epoch)) cache2.setHistoryEpoch(sessionId, epoch);
233002
+ cache2.broadcast(sessionId, raw);
233003
+ } else if ("HistorySync" in parsed) {
233004
+ const sync = parsed.HistorySync;
233005
+ if (typeof sync.historyEpoch === "number" && Number.isInteger(sync.historyEpoch)) {
233006
+ if (sync.reset === true) cache2.resetHistory(sessionId, sync.historyEpoch);
233007
+ else cache2.setHistoryEpoch(sessionId, sync.historyEpoch);
233008
+ }
232882
233009
  cache2.broadcast(sessionId, raw);
232883
233010
  }
232884
233011
  };
@@ -232917,8 +233044,12 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232917
233044
  syncing = false;
232918
233045
  const currentEntry = cache2.get(sessionId);
232919
233046
  const cachedSeq = entryPatchFrames(currentEntry.messages);
232920
- const extendsCache = isSequencePrefix(cachedSeq, replayBuffer);
232921
- if (extendsCache && replayBuffer.length > cachedSeq.length) {
233047
+ const extendsCache = !usesBoundedReplay && isSequencePrefix(cachedSeq, replayBuffer);
233048
+ if (usesBoundedReplay) {
233049
+ cache2.replaceEntryTail(sessionId, upstreamAfter, replayBuffer);
233050
+ for (const msg of replayBuffer) cache2.broadcast(sessionId, msg);
233051
+ cache2.broadcast(sessionId, raw);
233052
+ } else if (extendsCache && replayBuffer.length > cachedSeq.length) {
232922
233053
  const delta = replayBuffer.slice(cachedSeq.length);
232923
233054
  console.log(`[AgentWS] Sync delta: ${delta.length} new entry patches for ${sessionId} (remote=${replayBuffer.length}, cached=${cachedSeq.length})`);
232924
233055
  for (const msg of delta) {
@@ -239313,6 +239444,24 @@ var EventBus = class {
239313
239444
  init_proxy_manager();
239314
239445
 
239315
239446
  // src/remote-patch-cache.ts
239447
+ function patchEntryMetadata(raw) {
239448
+ let latest = null;
239449
+ let lastTurnEnd = null;
239450
+ try {
239451
+ const parsed = JSON.parse(raw);
239452
+ for (const op of parsed.JsonPatch ?? []) {
239453
+ const match2 = op.path?.match(/^\/entries\/(\d+)$/);
239454
+ if (!match2) continue;
239455
+ const index = Number(match2[1]);
239456
+ latest = Math.max(latest ?? -1, index);
239457
+ if (op.value?.type === "ENTRY" && op.value.content?.type === "turn_end") {
239458
+ lastTurnEnd = Math.max(lastTurnEnd ?? -1, index);
239459
+ }
239460
+ }
239461
+ } catch {
239462
+ }
239463
+ return { latest, lastTurnEnd };
239464
+ }
239316
239465
  var RemotePatchCache = class {
239317
239466
  cache = /* @__PURE__ */ new Map();
239318
239467
  getOrCreate(sessionId) {
@@ -239326,7 +239475,11 @@ var RemotePatchCache = class {
239326
239475
  subscribers: /* @__PURE__ */ new Set(),
239327
239476
  reconnecting: false,
239328
239477
  reconnectTimer: null,
239329
- reconnectAttempt: 0
239478
+ reconnectAttempt: 0,
239479
+ historyEpoch: null,
239480
+ latestEntryIndex: null,
239481
+ lastTurnEndEntryIndex: null,
239482
+ sessionStatus: null
239330
239483
  };
239331
239484
  this.cache.set(sessionId, entry);
239332
239485
  }
@@ -239349,6 +239502,11 @@ var RemotePatchCache = class {
239349
239502
  entry.messages.push(raw);
239350
239503
  if (isJsonPatch) {
239351
239504
  entry.patchCount++;
239505
+ const metadata = patchEntryMetadata(raw);
239506
+ if (metadata.latest !== null) entry.latestEntryIndex = Math.max(entry.latestEntryIndex ?? -1, metadata.latest);
239507
+ if (metadata.lastTurnEnd !== null) {
239508
+ entry.lastTurnEndEntryIndex = Math.max(entry.lastTurnEndEntryIndex ?? -1, metadata.lastTurnEnd);
239509
+ }
239352
239510
  }
239353
239511
  }
239354
239512
  /** Full cache replacement (used when cache is detected as stale). */
@@ -239359,6 +239517,15 @@ var RemotePatchCache = class {
239359
239517
  const reconnecting = existing?.reconnecting ?? false;
239360
239518
  const reconnectTimer = existing?.reconnectTimer ?? null;
239361
239519
  const reconnectAttempt = existing?.reconnectAttempt ?? 0;
239520
+ const historyEpoch = existing?.historyEpoch ?? null;
239521
+ let latestEntryIndex = null;
239522
+ let lastTurnEndEntryIndex = null;
239523
+ for (const raw of messages) {
239524
+ const metadata = patchEntryMetadata(raw);
239525
+ if (metadata.latest !== null) latestEntryIndex = Math.max(latestEntryIndex ?? -1, metadata.latest);
239526
+ if (metadata.lastTurnEnd !== null) lastTurnEndEntryIndex = Math.max(lastTurnEndEntryIndex ?? -1, metadata.lastTurnEnd);
239527
+ }
239528
+ const sessionStatus = existing?.sessionStatus ?? null;
239362
239529
  this.cache.set(sessionId, {
239363
239530
  messages,
239364
239531
  patchCount,
@@ -239367,8 +239534,43 @@ var RemotePatchCache = class {
239367
239534
  subscribers,
239368
239535
  reconnecting,
239369
239536
  reconnectTimer,
239370
- reconnectAttempt
239537
+ reconnectAttempt,
239538
+ historyEpoch,
239539
+ latestEntryIndex,
239540
+ lastTurnEndEntryIndex,
239541
+ sessionStatus
239542
+ });
239543
+ }
239544
+ /**
239545
+ * Replace only the unsealed entry tail. Completed entries at or before the
239546
+ * cursor and non-entry lifecycle frames stay cached; replayed tail frames
239547
+ * become the authoritative copy after a tunnel reconnect.
239548
+ */
239549
+ replaceEntryTail(sessionId, afterEntryIndex, tail) {
239550
+ const entry = this.getOrCreate(sessionId);
239551
+ const kept = entry.messages.filter((raw) => {
239552
+ try {
239553
+ const parsed = JSON.parse(raw);
239554
+ if (!Array.isArray(parsed.JsonPatch)) return true;
239555
+ const indices = parsed.JsonPatch.flatMap((op) => {
239556
+ const match2 = op.path?.match(/^\/entries\/(\d+)$/);
239557
+ return match2 ? [Number(match2[1])] : [];
239558
+ });
239559
+ return indices.length === 0 || indices.some((index) => index <= afterEntryIndex);
239560
+ } catch {
239561
+ return true;
239562
+ }
239371
239563
  });
239564
+ const messages = [...kept, ...tail];
239565
+ const patchCount = messages.reduce((count, raw) => {
239566
+ try {
239567
+ const parsed = JSON.parse(raw);
239568
+ return count + (Array.isArray(parsed.JsonPatch) ? 1 : 0);
239569
+ } catch {
239570
+ return count;
239571
+ }
239572
+ }, 0);
239573
+ this.replaceAll(sessionId, messages, patchCount);
239372
239574
  }
239373
239575
  setFinished(sessionId) {
239374
239576
  const entry = this.cache.get(sessionId);
@@ -239376,6 +239578,25 @@ var RemotePatchCache = class {
239376
239578
  entry.finished = true;
239377
239579
  }
239378
239580
  }
239581
+ setHistoryEpoch(sessionId, epoch) {
239582
+ this.getOrCreate(sessionId).historyEpoch = epoch;
239583
+ }
239584
+ /** Start a fresh entry-index namespace without dropping live connections. */
239585
+ resetHistory(sessionId, epoch) {
239586
+ const entry = this.getOrCreate(sessionId);
239587
+ entry.messages = [];
239588
+ entry.patchCount = 0;
239589
+ entry.finished = false;
239590
+ entry.historyEpoch = epoch;
239591
+ entry.latestEntryIndex = null;
239592
+ entry.lastTurnEndEntryIndex = null;
239593
+ }
239594
+ setLastTurnEndEntryIndex(sessionId, index) {
239595
+ this.getOrCreate(sessionId).lastTurnEndEntryIndex = index;
239596
+ }
239597
+ setSessionStatus(sessionId, status) {
239598
+ this.getOrCreate(sessionId).sessionStatus = status;
239599
+ }
239379
239600
  /** Store a persistent remote WebSocket connection. */
239380
239601
  setRemoteWs(sessionId, ws) {
239381
239602
  const entry = this.getOrCreate(sessionId);
@@ -239953,18 +240174,36 @@ var RemoteExecutorMonitor = class {
239953
240174
  this.eventBus = eventBus;
239954
240175
  this.storage = storage;
239955
240176
  this.remoteExecutorMap = remoteExecutorMap;
240177
+ this.reverseConnectManager.setStatusChangeHandler((_remoteServerId, status) => {
240178
+ if (status !== "online") return;
240179
+ for (const [localProcessId, info] of this.watched) {
240180
+ if (this.reverseConnectManager.isConnected(info.remoteServerId)) {
240181
+ this.attach(localProcessId, info);
240182
+ }
240183
+ }
240184
+ });
239956
240185
  }
239957
240186
  reverseConnectManager;
239958
240187
  eventBus;
239959
240188
  storage;
239960
240189
  remoteExecutorMap;
239961
- /** localProcessId → cleanup function */
239962
- monitors = /* @__PURE__ */ new Map();
240190
+ /**
240191
+ * Processes that still need a terminal observation. This registry is
240192
+ * deliberately separate from the currently-open virtual channels: a control
240193
+ * connection can disappear while the remote process keeps running.
240194
+ */
240195
+ watched = /* @__PURE__ */ new Map();
240196
+ /** localProcessId → cleanup for the currently-open virtual channel */
240197
+ activeMonitors = /* @__PURE__ */ new Map();
239963
240198
  watch(localProcessId, remoteInfo) {
239964
- if (this.monitors.has(localProcessId)) return;
240199
+ this.watched.set(localProcessId, remoteInfo);
240200
+ this.attach(localProcessId, remoteInfo);
240201
+ }
240202
+ attach(localProcessId, remoteInfo) {
240203
+ if (this.activeMonitors.has(localProcessId)) return;
239965
240204
  const rcm = this.reverseConnectManager;
239966
240205
  if (!rcm.isConnected(remoteInfo.remoteServerId)) {
239967
- console.log(`[RemoteExecutorMonitor] remote ${remoteInfo.remoteServerId} not connected for ${localProcessId}, skipping`);
240206
+ console.log(`[RemoteExecutorMonitor] remote ${remoteInfo.remoteServerId} not connected for ${localProcessId}, deferring`);
239968
240207
  return;
239969
240208
  }
239970
240209
  const channelId = randomUUID8();
@@ -239977,11 +240216,16 @@ var RemoteExecutorMonitor = class {
239977
240216
  rcm.openVirtualChannel(remoteInfo.remoteServerId, channelId, wsPath);
239978
240217
  const remoteWs = adapter;
239979
240218
  setTimeout(() => adapter.emit("open"), 0);
239980
- const cleanup = () => {
239981
- this.monitors.delete(localProcessId);
239982
- try {
239983
- remoteWs.close();
239984
- } catch {
240219
+ let cleanedUp = false;
240220
+ const cleanupActive = (closeChannel) => {
240221
+ if (cleanedUp) return;
240222
+ cleanedUp = true;
240223
+ this.activeMonitors.delete(localProcessId);
240224
+ if (closeChannel) {
240225
+ try {
240226
+ remoteWs.close();
240227
+ } catch {
240228
+ }
239985
240229
  }
239986
240230
  };
239987
240231
  const outputChunks = [];
@@ -239992,7 +240236,10 @@ var RemoteExecutorMonitor = class {
239992
240236
  outputChunks.push(parsed.data);
239993
240237
  }
239994
240238
  if (parsed.type === "finished") {
240239
+ this.watched.delete(localProcessId);
239995
240240
  const info = this.remoteExecutorMap.get(localProcessId);
240241
+ const hasKnownExitCode = typeof parsed.exitCode === "number";
240242
+ const eventExitCode = hasKnownExitCode ? parsed.exitCode : 1;
239996
240243
  if (info && !info.stoppedEmitted) {
239997
240244
  info.stoppedEmitted = true;
239998
240245
  let raw = outputChunks.join("");
@@ -240003,7 +240250,7 @@ var RemoteExecutorMonitor = class {
240003
240250
  projectId: info.projectId ?? "",
240004
240251
  executorId: info.executorId,
240005
240252
  processId: localProcessId,
240006
- exitCode: parsed.exitCode ?? 0,
240253
+ exitCode: eventExitCode,
240007
240254
  target: info.remoteServerId,
240008
240255
  tailOutput,
240009
240256
  // Structured final message forwarded by the remote's finished
@@ -240012,33 +240259,33 @@ var RemoteExecutorMonitor = class {
240012
240259
  });
240013
240260
  }
240014
240261
  this.remoteExecutorMap.delete(localProcessId);
240015
- this.storage.remoteExecutorProcesses.markFinished(
240016
- localProcessId,
240017
- typeof parsed.exitCode === "number" ? parsed.exitCode : 0
240018
- ).catch((err) => {
240262
+ const markFinished = hasKnownExitCode ? this.storage.remoteExecutorProcesses.markFinished(localProcessId, parsed.exitCode) : this.storage.remoteExecutorProcesses.markFinished(localProcessId, void 0, "killed");
240263
+ markFinished.catch((err) => {
240019
240264
  console.error(`[RemoteExecutorMonitor] Failed to mark process ${localProcessId} finished:`, err);
240020
240265
  });
240021
- cleanup();
240266
+ cleanupActive(true);
240022
240267
  }
240023
240268
  } catch {
240024
240269
  }
240025
240270
  });
240026
240271
  remoteWs.on("close", () => {
240027
- cleanup();
240272
+ cleanupActive(false);
240028
240273
  });
240029
240274
  remoteWs.on("error", (error48) => {
240030
240275
  console.error(`[RemoteExecutorMonitor] error for ${localProcessId}:`, error48);
240031
- cleanup();
240276
+ cleanupActive(true);
240032
240277
  });
240033
- this.monitors.set(localProcessId, cleanup);
240278
+ this.activeMonitors.set(localProcessId, () => cleanupActive(true));
240034
240279
  console.log(`[RemoteExecutorMonitor] watching ${localProcessId}`);
240035
240280
  }
240036
240281
  unwatch(localProcessId) {
240037
- this.monitors.get(localProcessId)?.();
240282
+ this.watched.delete(localProcessId);
240283
+ this.activeMonitors.get(localProcessId)?.();
240038
240284
  }
240039
240285
  shutdown() {
240040
- for (const cleanup of this.monitors.values()) cleanup();
240041
- this.monitors.clear();
240286
+ this.watched.clear();
240287
+ for (const cleanup of [...this.activeMonitors.values()]) cleanup();
240288
+ this.activeMonitors.clear();
240042
240289
  }
240043
240290
  };
240044
240291
 
@@ -245367,6 +245614,45 @@ var MODEL_SUGGESTIONS = {
245367
245614
  codex: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"]
245368
245615
  };
245369
245616
 
245617
+ // src/session-history-window.ts
245618
+ function historyHead(entries, historyEpoch) {
245619
+ let latestEntryIndex = null;
245620
+ let lastTurnEndEntryIndex = null;
245621
+ for (let index = entries.length - 1; index >= 0; index--) {
245622
+ const message = entries[index];
245623
+ if (!message) continue;
245624
+ latestEntryIndex ??= index;
245625
+ if (lastTurnEndEntryIndex === null && message.type === "turn_end") {
245626
+ lastTurnEndEntryIndex = index;
245627
+ }
245628
+ if (latestEntryIndex !== null && lastTurnEndEntryIndex !== null) break;
245629
+ }
245630
+ return { historyEpoch, latestEntryIndex, lastTurnEndEntryIndex };
245631
+ }
245632
+ function buildHistoryWindow(entries, historyEpoch, opts = {}) {
245633
+ const head = historyHead(entries, historyEpoch);
245634
+ const endExclusive = Math.max(0, Math.min(opts.before ?? entries.length, entries.length));
245635
+ const requestedTurns = Math.max(1, Math.min(opts.turns ?? 5, 20));
245636
+ const boundaries = [];
245637
+ for (let index = endExclusive - 1; index >= 0; index--) {
245638
+ if (entries[index]?.type === "turn_end") boundaries.push(index);
245639
+ if (boundaries.length >= requestedTurns + 2) break;
245640
+ }
245641
+ const startIndex = boundaries.length > requestedTurns + 1 ? boundaries[requestedTurns + 1] + 1 : 0;
245642
+ const dense = [];
245643
+ for (let index = startIndex; index < endExclusive; index++) {
245644
+ const message = entries[index];
245645
+ if (message) dense.push({ entryIndex: index, message });
245646
+ }
245647
+ const hasMore = startIndex > 0;
245648
+ return {
245649
+ ...head,
245650
+ entries: dense,
245651
+ previousCursor: hasMore ? startIndex : null,
245652
+ hasMore
245653
+ };
245654
+ }
245655
+
245370
245656
  // src/routes/agent-session-routes.ts
245371
245657
  async function resolveProjectPath(projectId, storage) {
245372
245658
  if (projectId.startsWith("path:")) {
@@ -245517,7 +245803,7 @@ var routes11 = async (fastify2) => {
245517
245803
  return reply.code(200).send({ providers: providers2 });
245518
245804
  });
245519
245805
  fastify2.post("/api/path/agent-sessions", async (req, reply) => {
245520
- const { path: projectPath, branch, permissionMode, agentType, force, model } = req.body;
245806
+ const { path: projectPath, branch, permissionMode, agentType, force, model, historyTurns } = req.body;
245521
245807
  if (!projectPath) {
245522
245808
  return reply.code(400).send({ error: "Path is required" });
245523
245809
  }
@@ -245555,6 +245841,8 @@ var routes11 = async (fastify2) => {
245555
245841
  }
245556
245842
  const session = fastify2.agentSessionManager.getSession(sessionId);
245557
245843
  const messages = fastify2.agentSessionManager.getMessages(sessionId);
245844
+ const epoch = fastify2.agentSessionManager.getHistoryEpoch(sessionId) ?? 0;
245845
+ const historyWindow = historyTurns ? buildHistoryWindow(fastify2.agentSessionManager.getRawMessages(sessionId), epoch, { turns: historyTurns }) : void 0;
245558
245846
  const projection = await fastify2.storage.agentSessions.getActivityById(sessionId, "session-detail");
245559
245847
  if (session?.workspaceCheckoutId && !projection) {
245560
245848
  return reply.code(409).send({
@@ -245579,7 +245867,8 @@ var routes11 = async (fastify2) => {
245579
245867
  checkoutDeletedAt: projection?.checkoutDeletedAt ?? null,
245580
245868
  processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
245581
245869
  },
245582
- messages
245870
+ messages: historyWindow ? historyWindow.entries.map((entry) => entry.message) : messages,
245871
+ ...historyWindow ? { historyWindow } : {}
245583
245872
  });
245584
245873
  } catch (error48) {
245585
245874
  console.error("[API] Failed to load path-based agent session:", error48);
@@ -245866,7 +246155,7 @@ var routes11 = async (fastify2) => {
245866
246155
  if (!project) {
245867
246156
  return reply.code(404).send({ error: "Project not found" });
245868
246157
  }
245869
- const { branch, permissionMode, agentType, model } = req.body;
246158
+ const { branch, permissionMode, agentType, model, historyTurns } = req.body;
245870
246159
  let agentMode = project.agent_mode;
245871
246160
  let useRemoteAgent = agentMode !== "local";
245872
246161
  if (useRemoteAgent && agentMode === "remote") {
@@ -245899,7 +246188,7 @@ var routes11 = async (fastify2) => {
245899
246188
  agentMode,
245900
246189
  "POST",
245901
246190
  `/api/path/agent-sessions`,
245902
- { path: remoteConfig.remote_path, branch, permissionMode, agentType, model }
246191
+ { path: remoteConfig.remote_path, branch, permissionMode, agentType, model, historyTurns }
245903
246192
  );
245904
246193
  console.log(`[API] Remote proxy result: ok=${result.ok}, status=${result.status}, data=${JSON.stringify(result.data).substring(0, 500)}`);
245905
246194
  if (result.ok) {
@@ -245926,10 +246215,21 @@ var routes11 = async (fastify2) => {
245926
246215
  if (remoteData.messages && remoteData.messages.length > 0) {
245927
246216
  const cacheEntry = fastify2.remotePatchCache.getOrCreate(localSessionId);
245928
246217
  if (cacheEntry.messages.length === 0) {
245929
- for (let i = 0; i < remoteData.messages.length; i++) {
245930
- const patch = ConversationPatch.addEntry(i, remoteData.messages[i]);
246218
+ const seededEntries = remoteData.historyWindow?.entries ?? remoteData.messages.map((message, entryIndex) => ({ entryIndex, message }));
246219
+ for (const { entryIndex, message } of seededEntries) {
246220
+ const patch = ConversationPatch.addEntry(entryIndex, message);
245931
246221
  fastify2.remotePatchCache.appendMessage(localSessionId, JSON.stringify({ JsonPatch: patch }), true);
245932
246222
  }
246223
+ if (remoteData.historyWindow) {
246224
+ fastify2.remotePatchCache.setHistoryEpoch(localSessionId, remoteData.historyWindow.historyEpoch);
246225
+ if (remoteData.historyWindow.lastTurnEndEntryIndex !== null) {
246226
+ fastify2.remotePatchCache.setLastTurnEndEntryIndex(localSessionId, remoteData.historyWindow.lastTurnEndEntryIndex);
246227
+ }
246228
+ }
246229
+ const seededStatus = remoteData.session.status;
246230
+ if (seededStatus === "running" || seededStatus === "stopped" || seededStatus === "error") {
246231
+ fastify2.remotePatchCache.setSessionStatus(localSessionId, seededStatus);
246232
+ }
245933
246233
  console.log(`[API] findExisting proxy: seeded cache with ${remoteData.messages.length} msgs for ${localSessionId}`);
245934
246234
  } else {
245935
246235
  console.log(`[API] findExisting proxy: cache already has ${cacheEntry.messages.length} msgs for ${localSessionId} (remote returned ${remoteData.messages.length}), skipping seed`);
@@ -245943,7 +246243,8 @@ var routes11 = async (fastify2) => {
245943
246243
  id: localSessionId,
245944
246244
  projectId: req.params.projectId
245945
246245
  },
245946
- messages: remoteData.messages
246246
+ messages: remoteData.messages,
246247
+ ...remoteData.historyWindow ? { historyWindow: remoteData.historyWindow } : {}
245947
246248
  });
245948
246249
  }
245949
246250
  return reply.code(proxyStatus(result)).send(result.data);
@@ -245968,6 +246269,8 @@ var routes11 = async (fastify2) => {
245968
246269
  }
245969
246270
  const session = fastify2.agentSessionManager.getSession(sessionId);
245970
246271
  const messages = fastify2.agentSessionManager.getMessages(sessionId);
246272
+ const epoch = fastify2.agentSessionManager.getHistoryEpoch(sessionId) ?? 0;
246273
+ const historyWindow = historyTurns ? buildHistoryWindow(fastify2.agentSessionManager.getRawMessages(sessionId), epoch, { turns: historyTurns }) : void 0;
245971
246274
  const effectiveStatus = session?.status || "stopped";
245972
246275
  return reply.code(200).send({
245973
246276
  session: {
@@ -245980,7 +246283,8 @@ var routes11 = async (fastify2) => {
245980
246283
  model: session?.model ?? null,
245981
246284
  processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
245982
246285
  },
245983
- messages
246286
+ messages: historyWindow ? historyWindow.entries.map((entry) => entry.message) : messages,
246287
+ ...historyWindow ? { historyWindow } : {}
245984
246288
  });
245985
246289
  } catch (error48) {
245986
246290
  console.error("[API] Failed to load agent session:", error48);
@@ -246165,6 +246469,121 @@ var routes11 = async (fastify2) => {
246165
246469
  });
246166
246470
  }
246167
246471
  );
246472
+ fastify2.get("/api/agent-sessions/:sessionId/history-window", async (req, reply) => {
246473
+ const beforeRaw = req.query.before === void 0 ? void 0 : Number(req.query.before);
246474
+ const turnsRaw = req.query.turns === void 0 ? void 0 : Number(req.query.turns);
246475
+ const before = Number.isInteger(beforeRaw) && beforeRaw >= 0 ? beforeRaw : void 0;
246476
+ const turns = Number.isInteger(turnsRaw) ? Math.max(1, Math.min(turnsRaw, 20)) : 5;
246477
+ if (req.params.sessionId.startsWith("remote-")) {
246478
+ const userId = requireUserFacingUserId(req, reply);
246479
+ if (userId === null) return;
246480
+ const remoteInfo = await getAuthorizedRemoteSessionInfo(req.params.sessionId, userId);
246481
+ if (!remoteInfo) return reply.code(404).send({ error: "Remote session not found" });
246482
+ const params = new URLSearchParams({ turns: String(turns) });
246483
+ if (before !== void 0) params.set("before", String(before));
246484
+ const result = await proxyAuto(
246485
+ remoteInfo.remoteServerId,
246486
+ "GET",
246487
+ `/api/agent-sessions/${encodeURIComponent(remoteInfo.remoteSessionId)}/history-window?${params}`
246488
+ );
246489
+ if (result.ok) {
246490
+ const data = result.data;
246491
+ return reply.code(200).send({
246492
+ ...data,
246493
+ session: data.session ? {
246494
+ ...data.session,
246495
+ id: req.params.sessionId,
246496
+ projectId: projectIdFromRemoteSessionId(req.params.sessionId, remoteInfo),
246497
+ branch: remoteInfo.branch ?? null
246498
+ } : void 0
246499
+ });
246500
+ }
246501
+ if (result.status === 404) {
246502
+ const legacy = await proxyAuto(
246503
+ remoteInfo.remoteServerId,
246504
+ "GET",
246505
+ `/api/agent-sessions/${encodeURIComponent(remoteInfo.remoteSessionId)}`
246506
+ );
246507
+ if (!legacy.ok) return reply.code(proxyStatus(legacy)).send(legacy.data);
246508
+ const data = legacy.data;
246509
+ const messages = data.messages ?? [];
246510
+ return reply.code(200).send({
246511
+ ...buildHistoryWindow(messages, 0, { before, turns }),
246512
+ status: data.session?.status ?? "stopped",
246513
+ legacyFallback: true
246514
+ });
246515
+ }
246516
+ return reply.code(proxyStatus(result)).send(result.data);
246517
+ }
246518
+ const session = fastify2.agentSessionManager.getSession(req.params.sessionId);
246519
+ if (!session) return reply.code(404).send({ error: "Session not found" });
246520
+ const epoch = fastify2.agentSessionManager.getHistoryEpoch(req.params.sessionId) ?? 0;
246521
+ return reply.code(200).send({
246522
+ ...buildHistoryWindow(fastify2.agentSessionManager.getRawMessages(req.params.sessionId), epoch, { before, turns }),
246523
+ status: session.status,
246524
+ session: {
246525
+ id: session.id,
246526
+ projectId: session.projectId,
246527
+ branch: session.branch,
246528
+ status: session.status,
246529
+ permissionMode: session.permissionMode,
246530
+ agentType: session.agentType,
246531
+ model: session.model,
246532
+ processAlive: fastify2.agentSessionManager.getSessionProcessAlive(session.id)
246533
+ }
246534
+ });
246535
+ });
246536
+ fastify2.get(
246537
+ "/api/agent-sessions/:sessionId/history-head",
246538
+ async (req, reply) => {
246539
+ if (req.params.sessionId.startsWith("remote-")) {
246540
+ const userId = requireUserFacingUserId(req, reply);
246541
+ if (userId === null) return;
246542
+ const remoteInfo = await getAuthorizedRemoteSessionInfo(req.params.sessionId, userId);
246543
+ if (!remoteInfo) return reply.code(404).send({ error: "Remote session not found" });
246544
+ const cached2 = fastify2.remotePatchCache.get(req.params.sessionId);
246545
+ if (cached2?.historyEpoch !== null && cached2?.lastTurnEndEntryIndex !== null && cached2?.sessionStatus) {
246546
+ return reply.code(200).send({
246547
+ historyEpoch: cached2.historyEpoch,
246548
+ latestEntryIndex: cached2.latestEntryIndex,
246549
+ lastTurnEndEntryIndex: cached2.lastTurnEndEntryIndex,
246550
+ status: cached2.sessionStatus
246551
+ });
246552
+ }
246553
+ const result = await proxyAuto(
246554
+ remoteInfo.remoteServerId,
246555
+ "GET",
246556
+ `/api/agent-sessions/${encodeURIComponent(remoteInfo.remoteSessionId)}/history-head`
246557
+ );
246558
+ if (result.ok) return reply.code(200).send(result.data);
246559
+ if (result.status !== 404) return reply.code(proxyStatus(result)).send(result.data);
246560
+ const legacy = await proxyAuto(
246561
+ remoteInfo.remoteServerId,
246562
+ "GET",
246563
+ `/api/agent-sessions/${encodeURIComponent(remoteInfo.remoteSessionId)}`
246564
+ );
246565
+ if (!legacy.ok) return reply.code(proxyStatus(legacy)).send(legacy.data);
246566
+ const data = legacy.data;
246567
+ const head2 = buildHistoryWindow(data.messages ?? [], 0, { turns: 1 });
246568
+ return reply.code(200).send({
246569
+ historyEpoch: head2.historyEpoch,
246570
+ latestEntryIndex: head2.latestEntryIndex,
246571
+ lastTurnEndEntryIndex: head2.lastTurnEndEntryIndex,
246572
+ status: data.session?.status ?? "stopped"
246573
+ });
246574
+ }
246575
+ const session = fastify2.agentSessionManager.getSession(req.params.sessionId);
246576
+ if (!session) return reply.code(404).send({ error: "Session not found" });
246577
+ const epoch = fastify2.agentSessionManager.getHistoryEpoch(req.params.sessionId) ?? 0;
246578
+ const head = buildHistoryWindow(fastify2.agentSessionManager.getRawMessages(req.params.sessionId), epoch, { turns: 1 });
246579
+ return reply.code(200).send({
246580
+ historyEpoch: head.historyEpoch,
246581
+ latestEntryIndex: head.latestEntryIndex,
246582
+ lastTurnEndEntryIndex: head.lastTurnEndEntryIndex,
246583
+ status: session.status
246584
+ });
246585
+ }
246586
+ );
246168
246587
  fastify2.get(
246169
246588
  "/api/agent-sessions/:sessionId/brief-source",
246170
246589
  async (req, reply) => {
@@ -249434,6 +249853,13 @@ function attachWsHeartbeat(socket, { label, intervalMs = DEFAULT_INTERVAL_MS, ke
249434
249853
  }
249435
249854
 
249436
249855
  // src/routes/websocket-routes.ts
249856
+ function resolveRemoteReplayCursor(clientEpoch, cachedEpoch, afterEntryIndex) {
249857
+ const epochMatches = clientEpoch === void 0 || cachedEpoch !== null && clientEpoch === cachedEpoch;
249858
+ return {
249859
+ epochMatches,
249860
+ replayAfter: epochMatches ? afterEntryIndex ?? -1 : -1
249861
+ };
249862
+ }
249437
249863
  var routes23 = async (fastify2) => {
249438
249864
  fastify2.reverseConnectManager.setStatusChangeHandler((remoteServerId, status) => {
249439
249865
  if (status !== "online") return;
@@ -249645,6 +250071,8 @@ var routes23 = async (fastify2) => {
249645
250071
  { websocket: true },
249646
250072
  async (socket, req) => {
249647
250073
  const { sessionId } = req.params;
250074
+ const afterEntryIndex = Number.isInteger(Number(req.query.after)) ? Number(req.query.after) : void 0;
250075
+ const historyEpoch = Number.isInteger(Number(req.query.epoch)) ? Number(req.query.epoch) : void 0;
249648
250076
  console.log(`[AgentWS] Connection attempt for session ${sessionId} (auth=${fastify2.authEnabled})`);
249649
250077
  let principalUserId = fastify2.authEnabled ? null : "local";
249650
250078
  if (fastify2.authEnabled) {
@@ -249694,15 +250122,42 @@ var routes23 = async (fastify2) => {
249694
250122
  const cacheEntry = cache2.getOrCreate(sessionId);
249695
250123
  console.log(`[AgentWS] WS connect: cacheEntry for ${sessionId} has messages.length=${cacheEntry.messages.length} finished=${cacheEntry.finished} remoteWsOpen=${!!cache2.getRemoteWs(sessionId)}`);
249696
250124
  if (cacheEntry.messages.length > 0) {
249697
- console.log(`[AgentWS] Replaying ${cacheEntry.messages.length} cached msgs for ${sessionId}`);
250125
+ const { epochMatches, replayAfter } = resolveRemoteReplayCursor(
250126
+ historyEpoch,
250127
+ cacheEntry.historyEpoch,
250128
+ afterEntryIndex
250129
+ );
250130
+ console.log(`[AgentWS] Replaying cached msgs for ${sessionId} after=${replayAfter}`);
250131
+ try {
250132
+ socket.send(JSON.stringify({
250133
+ HistorySync: {
250134
+ historyEpoch: cacheEntry.historyEpoch ?? historyEpoch ?? 0,
250135
+ reset: !epochMatches
250136
+ }
250137
+ }));
250138
+ } catch {
250139
+ }
249698
250140
  for (const raw of cacheEntry.messages) {
250141
+ if (replayAfter >= 0) {
250142
+ try {
250143
+ const parsed = JSON.parse(raw);
250144
+ if (Array.isArray(parsed.JsonPatch)) {
250145
+ const indices = parsed.JsonPatch.flatMap((op) => {
250146
+ const match2 = op.path?.match(/^\/entries\/(\d+)$/);
250147
+ return match2 ? [Number(match2[1])] : [];
250148
+ });
250149
+ if (indices.length > 0 && indices.every((index) => index <= replayAfter)) continue;
250150
+ }
250151
+ } catch {
250152
+ }
250153
+ }
249699
250154
  try {
249700
250155
  socket.send(raw);
249701
250156
  } catch {
249702
250157
  }
249703
250158
  }
249704
250159
  try {
249705
- socket.send(JSON.stringify({ Ready: true }));
250160
+ socket.send(JSON.stringify({ Ready: true, historyEpoch: cacheEntry.historyEpoch ?? void 0 }));
249706
250161
  } catch {
249707
250162
  }
249708
250163
  if (cacheEntry.finished) {
@@ -249728,7 +250183,8 @@ var routes23 = async (fastify2) => {
249728
250183
  fastify2.reverseConnectManager,
249729
250184
  fastify2.eventBus,
249730
250185
  fastify2.agentSessionManager,
249731
- fastify2.storage
250186
+ fastify2.storage,
250187
+ { afterEntryIndex, historyEpoch }
249732
250188
  );
249733
250189
  }
249734
250190
  if (cache2.getRemoteWs(sessionId)) {
@@ -249758,7 +250214,7 @@ var routes23 = async (fastify2) => {
249758
250214
  });
249759
250215
  return;
249760
250216
  }
249761
- const unsubscribe = fastify2.agentSessionManager.subscribe(sessionId, socket);
250217
+ const unsubscribe = fastify2.agentSessionManager.subscribe(sessionId, socket, { afterEntryIndex, historyEpoch });
249762
250218
  if (!unsubscribe) {
249763
250219
  console.log(`[AgentWS] Session ${sessionId} not found`);
249764
250220
  stopHeartbeat();
@@ -253437,6 +253893,8 @@ var WORKER_CAPABILITIES = {
253437
253893
  "http:POST /api/path/agent-sessions": { since: "0.2.0", summary: "\u521B\u5EFA\u4F1A\u8BDD" },
253438
253894
  "http:POST /api/path/agent-sessions/new": { since: "0.2.0", summary: "\u521B\u5EFA\u4F1A\u8BDD(\u6307\u5B9A ID)" },
253439
253895
  "http:GET /api/agent-sessions/:param": { since: "0.2.0", summary: "\u8BFB\u4F1A\u8BDD\u8BE6\u60C5/\u5BF9\u8BDD" },
253896
+ "http:GET /api/agent-sessions/:param/history-window": { since: "0.4.0", summary: "\u6309 turn \u8FB9\u754C\u8BFB\u53D6\u4F1A\u8BDD\u7A97\u53E3" },
253897
+ "http:GET /api/agent-sessions/:param/history-head": { since: "0.4.0", summary: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2\u6E38\u6807" },
253440
253898
  // Additive: a worker below 0.3.6 404s it and the hub's intent-brief
253441
253899
  // distillation degrades to the deterministic excerpt (tier 2).
253442
253900
  "http:GET /api/agent-sessions/:param/brief-source": { since: "0.3.6", summary: "\u8BFB\u4F1A\u8BDD\u5BF9\u8BDD(\u4EC5\u84B8\u998F\u7528\u6587\u672C)" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedeckx/linux-x64",
3
- "version": "0.3.14",
3
+ "version": "0.3.17",
4
4
  "description": "Vibedeckx platform binaries for Linux x64",
5
5
  "os": [
6
6
  "linux"