@vibedeckx/linux-x64 0.3.15 → 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 +464 -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",
@@ -231480,6 +231532,7 @@ var AgentSessionManager = class {
231480
231532
  process: null,
231481
231533
  dormant: true,
231482
231534
  processStartsInFlight: 0,
231535
+ historyEpoch: 0,
231483
231536
  store,
231484
231537
  subscribers: /* @__PURE__ */ new Set(),
231485
231538
  status: "stopped",
@@ -232791,7 +232844,7 @@ async function persistRemoteSessionActivityFrame(storage, sessionId, remoteInfo,
232791
232844
  }
232792
232845
  return false;
232793
232846
  }
232794
- function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnectManager, eventBus, agentSessionManager, storage) {
232847
+ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnectManager, eventBus, agentSessionManager, storage, requestedHistory) {
232795
232848
  const hasCachedData = cache2.hasData(sessionId);
232796
232849
  console.log(`[AgentWS] Opening persistent remote WS for ${sessionId} (cached=${hasCachedData})`);
232797
232850
  if (!reverseConnectManager || !reverseConnectManager.isConnected(remoteInfo.remoteServerId)) {
@@ -232801,7 +232854,16 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232801
232854
  return;
232802
232855
  }
232803
232856
  const channelId = randomUUID2();
232804
- 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}` : ""}`;
232805
232867
  const adapter = new VirtualWsAdapter(
232806
232868
  (data) => reverseConnectManager.sendChannelData(remoteInfo.remoteServerId, channelId, data),
232807
232869
  () => reverseConnectManager.closeChannel(remoteInfo.remoteServerId, channelId)
@@ -232835,6 +232897,7 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232835
232897
  cache2.broadcast(sessionId, raw);
232836
232898
  const statusEvent = statusEventFromRemotePatch(parsed, sessionId, remoteInfo);
232837
232899
  if (statusEvent) {
232900
+ cache2.setSessionStatus(sessionId, statusEvent.status);
232838
232901
  let activityReady = true;
232839
232902
  if (storage) {
232840
232903
  activityReady = await persistRemoteSessionActivityFrame(storage, sessionId, remoteInfo, parsed).catch((error48) => {
@@ -232851,6 +232914,11 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232851
232914
  cache2.setFinished(sessionId);
232852
232915
  cache2.broadcast(sessionId, raw);
232853
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
+ }
232854
232922
  cache2.appendMessage(sessionId, raw, false);
232855
232923
  cache2.broadcast(sessionId, raw);
232856
232924
  let activityReady = true;
@@ -232906,6 +232974,7 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232906
232974
  }
232907
232975
  }
232908
232976
  } else if ("error" in parsed) {
232977
+ cache2.setSessionStatus(sessionId, "error");
232909
232978
  cache2.appendMessage(sessionId, raw, false);
232910
232979
  cache2.broadcast(sessionId, raw);
232911
232980
  let activityReady = true;
@@ -232928,6 +232997,15 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232928
232997
  cache2.setFinished(sessionId);
232929
232998
  }
232930
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
+ }
232931
233009
  cache2.broadcast(sessionId, raw);
232932
233010
  }
232933
233011
  };
@@ -232966,8 +233044,12 @@ function connectPersistentRemoteWs(sessionId, remoteInfo, cache2, reverseConnect
232966
233044
  syncing = false;
232967
233045
  const currentEntry = cache2.get(sessionId);
232968
233046
  const cachedSeq = entryPatchFrames(currentEntry.messages);
232969
- const extendsCache = isSequencePrefix(cachedSeq, replayBuffer);
232970
- 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) {
232971
233053
  const delta = replayBuffer.slice(cachedSeq.length);
232972
233054
  console.log(`[AgentWS] Sync delta: ${delta.length} new entry patches for ${sessionId} (remote=${replayBuffer.length}, cached=${cachedSeq.length})`);
232973
233055
  for (const msg of delta) {
@@ -239362,6 +239444,24 @@ var EventBus = class {
239362
239444
  init_proxy_manager();
239363
239445
 
239364
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
+ }
239365
239465
  var RemotePatchCache = class {
239366
239466
  cache = /* @__PURE__ */ new Map();
239367
239467
  getOrCreate(sessionId) {
@@ -239375,7 +239475,11 @@ var RemotePatchCache = class {
239375
239475
  subscribers: /* @__PURE__ */ new Set(),
239376
239476
  reconnecting: false,
239377
239477
  reconnectTimer: null,
239378
- reconnectAttempt: 0
239478
+ reconnectAttempt: 0,
239479
+ historyEpoch: null,
239480
+ latestEntryIndex: null,
239481
+ lastTurnEndEntryIndex: null,
239482
+ sessionStatus: null
239379
239483
  };
239380
239484
  this.cache.set(sessionId, entry);
239381
239485
  }
@@ -239398,6 +239502,11 @@ var RemotePatchCache = class {
239398
239502
  entry.messages.push(raw);
239399
239503
  if (isJsonPatch) {
239400
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
+ }
239401
239510
  }
239402
239511
  }
239403
239512
  /** Full cache replacement (used when cache is detected as stale). */
@@ -239408,6 +239517,15 @@ var RemotePatchCache = class {
239408
239517
  const reconnecting = existing?.reconnecting ?? false;
239409
239518
  const reconnectTimer = existing?.reconnectTimer ?? null;
239410
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;
239411
239529
  this.cache.set(sessionId, {
239412
239530
  messages,
239413
239531
  patchCount,
@@ -239416,15 +239534,69 @@ var RemotePatchCache = class {
239416
239534
  subscribers,
239417
239535
  reconnecting,
239418
239536
  reconnectTimer,
239419
- reconnectAttempt
239537
+ reconnectAttempt,
239538
+ historyEpoch,
239539
+ latestEntryIndex,
239540
+ lastTurnEndEntryIndex,
239541
+ sessionStatus
239420
239542
  });
239421
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
+ }
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);
239574
+ }
239422
239575
  setFinished(sessionId) {
239423
239576
  const entry = this.cache.get(sessionId);
239424
239577
  if (entry) {
239425
239578
  entry.finished = true;
239426
239579
  }
239427
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
+ }
239428
239600
  /** Store a persistent remote WebSocket connection. */
239429
239601
  setRemoteWs(sessionId, ws) {
239430
239602
  const entry = this.getOrCreate(sessionId);
@@ -240002,18 +240174,36 @@ var RemoteExecutorMonitor = class {
240002
240174
  this.eventBus = eventBus;
240003
240175
  this.storage = storage;
240004
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
+ });
240005
240185
  }
240006
240186
  reverseConnectManager;
240007
240187
  eventBus;
240008
240188
  storage;
240009
240189
  remoteExecutorMap;
240010
- /** localProcessId → cleanup function */
240011
- 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();
240012
240198
  watch(localProcessId, remoteInfo) {
240013
- 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;
240014
240204
  const rcm = this.reverseConnectManager;
240015
240205
  if (!rcm.isConnected(remoteInfo.remoteServerId)) {
240016
- console.log(`[RemoteExecutorMonitor] remote ${remoteInfo.remoteServerId} not connected for ${localProcessId}, skipping`);
240206
+ console.log(`[RemoteExecutorMonitor] remote ${remoteInfo.remoteServerId} not connected for ${localProcessId}, deferring`);
240017
240207
  return;
240018
240208
  }
240019
240209
  const channelId = randomUUID8();
@@ -240026,11 +240216,16 @@ var RemoteExecutorMonitor = class {
240026
240216
  rcm.openVirtualChannel(remoteInfo.remoteServerId, channelId, wsPath);
240027
240217
  const remoteWs = adapter;
240028
240218
  setTimeout(() => adapter.emit("open"), 0);
240029
- const cleanup = () => {
240030
- this.monitors.delete(localProcessId);
240031
- try {
240032
- remoteWs.close();
240033
- } 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
+ }
240034
240229
  }
240035
240230
  };
240036
240231
  const outputChunks = [];
@@ -240041,7 +240236,10 @@ var RemoteExecutorMonitor = class {
240041
240236
  outputChunks.push(parsed.data);
240042
240237
  }
240043
240238
  if (parsed.type === "finished") {
240239
+ this.watched.delete(localProcessId);
240044
240240
  const info = this.remoteExecutorMap.get(localProcessId);
240241
+ const hasKnownExitCode = typeof parsed.exitCode === "number";
240242
+ const eventExitCode = hasKnownExitCode ? parsed.exitCode : 1;
240045
240243
  if (info && !info.stoppedEmitted) {
240046
240244
  info.stoppedEmitted = true;
240047
240245
  let raw = outputChunks.join("");
@@ -240052,7 +240250,7 @@ var RemoteExecutorMonitor = class {
240052
240250
  projectId: info.projectId ?? "",
240053
240251
  executorId: info.executorId,
240054
240252
  processId: localProcessId,
240055
- exitCode: parsed.exitCode ?? 0,
240253
+ exitCode: eventExitCode,
240056
240254
  target: info.remoteServerId,
240057
240255
  tailOutput,
240058
240256
  // Structured final message forwarded by the remote's finished
@@ -240061,33 +240259,33 @@ var RemoteExecutorMonitor = class {
240061
240259
  });
240062
240260
  }
240063
240261
  this.remoteExecutorMap.delete(localProcessId);
240064
- this.storage.remoteExecutorProcesses.markFinished(
240065
- localProcessId,
240066
- typeof parsed.exitCode === "number" ? parsed.exitCode : 0
240067
- ).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) => {
240068
240264
  console.error(`[RemoteExecutorMonitor] Failed to mark process ${localProcessId} finished:`, err);
240069
240265
  });
240070
- cleanup();
240266
+ cleanupActive(true);
240071
240267
  }
240072
240268
  } catch {
240073
240269
  }
240074
240270
  });
240075
240271
  remoteWs.on("close", () => {
240076
- cleanup();
240272
+ cleanupActive(false);
240077
240273
  });
240078
240274
  remoteWs.on("error", (error48) => {
240079
240275
  console.error(`[RemoteExecutorMonitor] error for ${localProcessId}:`, error48);
240080
- cleanup();
240276
+ cleanupActive(true);
240081
240277
  });
240082
- this.monitors.set(localProcessId, cleanup);
240278
+ this.activeMonitors.set(localProcessId, () => cleanupActive(true));
240083
240279
  console.log(`[RemoteExecutorMonitor] watching ${localProcessId}`);
240084
240280
  }
240085
240281
  unwatch(localProcessId) {
240086
- this.monitors.get(localProcessId)?.();
240282
+ this.watched.delete(localProcessId);
240283
+ this.activeMonitors.get(localProcessId)?.();
240087
240284
  }
240088
240285
  shutdown() {
240089
- for (const cleanup of this.monitors.values()) cleanup();
240090
- this.monitors.clear();
240286
+ this.watched.clear();
240287
+ for (const cleanup of [...this.activeMonitors.values()]) cleanup();
240288
+ this.activeMonitors.clear();
240091
240289
  }
240092
240290
  };
240093
240291
 
@@ -245416,6 +245614,45 @@ var MODEL_SUGGESTIONS = {
245416
245614
  codex: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"]
245417
245615
  };
245418
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
+
245419
245656
  // src/routes/agent-session-routes.ts
245420
245657
  async function resolveProjectPath(projectId, storage) {
245421
245658
  if (projectId.startsWith("path:")) {
@@ -245566,7 +245803,7 @@ var routes11 = async (fastify2) => {
245566
245803
  return reply.code(200).send({ providers: providers2 });
245567
245804
  });
245568
245805
  fastify2.post("/api/path/agent-sessions", async (req, reply) => {
245569
- const { path: projectPath, branch, permissionMode, agentType, force, model } = req.body;
245806
+ const { path: projectPath, branch, permissionMode, agentType, force, model, historyTurns } = req.body;
245570
245807
  if (!projectPath) {
245571
245808
  return reply.code(400).send({ error: "Path is required" });
245572
245809
  }
@@ -245604,6 +245841,8 @@ var routes11 = async (fastify2) => {
245604
245841
  }
245605
245842
  const session = fastify2.agentSessionManager.getSession(sessionId);
245606
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;
245607
245846
  const projection = await fastify2.storage.agentSessions.getActivityById(sessionId, "session-detail");
245608
245847
  if (session?.workspaceCheckoutId && !projection) {
245609
245848
  return reply.code(409).send({
@@ -245628,7 +245867,8 @@ var routes11 = async (fastify2) => {
245628
245867
  checkoutDeletedAt: projection?.checkoutDeletedAt ?? null,
245629
245868
  processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
245630
245869
  },
245631
- messages
245870
+ messages: historyWindow ? historyWindow.entries.map((entry) => entry.message) : messages,
245871
+ ...historyWindow ? { historyWindow } : {}
245632
245872
  });
245633
245873
  } catch (error48) {
245634
245874
  console.error("[API] Failed to load path-based agent session:", error48);
@@ -245915,7 +246155,7 @@ var routes11 = async (fastify2) => {
245915
246155
  if (!project) {
245916
246156
  return reply.code(404).send({ error: "Project not found" });
245917
246157
  }
245918
- const { branch, permissionMode, agentType, model } = req.body;
246158
+ const { branch, permissionMode, agentType, model, historyTurns } = req.body;
245919
246159
  let agentMode = project.agent_mode;
245920
246160
  let useRemoteAgent = agentMode !== "local";
245921
246161
  if (useRemoteAgent && agentMode === "remote") {
@@ -245948,7 +246188,7 @@ var routes11 = async (fastify2) => {
245948
246188
  agentMode,
245949
246189
  "POST",
245950
246190
  `/api/path/agent-sessions`,
245951
- { path: remoteConfig.remote_path, branch, permissionMode, agentType, model }
246191
+ { path: remoteConfig.remote_path, branch, permissionMode, agentType, model, historyTurns }
245952
246192
  );
245953
246193
  console.log(`[API] Remote proxy result: ok=${result.ok}, status=${result.status}, data=${JSON.stringify(result.data).substring(0, 500)}`);
245954
246194
  if (result.ok) {
@@ -245975,10 +246215,21 @@ var routes11 = async (fastify2) => {
245975
246215
  if (remoteData.messages && remoteData.messages.length > 0) {
245976
246216
  const cacheEntry = fastify2.remotePatchCache.getOrCreate(localSessionId);
245977
246217
  if (cacheEntry.messages.length === 0) {
245978
- for (let i = 0; i < remoteData.messages.length; i++) {
245979
- 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);
245980
246221
  fastify2.remotePatchCache.appendMessage(localSessionId, JSON.stringify({ JsonPatch: patch }), true);
245981
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
+ }
245982
246233
  console.log(`[API] findExisting proxy: seeded cache with ${remoteData.messages.length} msgs for ${localSessionId}`);
245983
246234
  } else {
245984
246235
  console.log(`[API] findExisting proxy: cache already has ${cacheEntry.messages.length} msgs for ${localSessionId} (remote returned ${remoteData.messages.length}), skipping seed`);
@@ -245992,7 +246243,8 @@ var routes11 = async (fastify2) => {
245992
246243
  id: localSessionId,
245993
246244
  projectId: req.params.projectId
245994
246245
  },
245995
- messages: remoteData.messages
246246
+ messages: remoteData.messages,
246247
+ ...remoteData.historyWindow ? { historyWindow: remoteData.historyWindow } : {}
245996
246248
  });
245997
246249
  }
245998
246250
  return reply.code(proxyStatus(result)).send(result.data);
@@ -246017,6 +246269,8 @@ var routes11 = async (fastify2) => {
246017
246269
  }
246018
246270
  const session = fastify2.agentSessionManager.getSession(sessionId);
246019
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;
246020
246274
  const effectiveStatus = session?.status || "stopped";
246021
246275
  return reply.code(200).send({
246022
246276
  session: {
@@ -246029,7 +246283,8 @@ var routes11 = async (fastify2) => {
246029
246283
  model: session?.model ?? null,
246030
246284
  processAlive: session ? fastify2.agentSessionManager.getSessionProcessAlive(sessionId) : false
246031
246285
  },
246032
- messages
246286
+ messages: historyWindow ? historyWindow.entries.map((entry) => entry.message) : messages,
246287
+ ...historyWindow ? { historyWindow } : {}
246033
246288
  });
246034
246289
  } catch (error48) {
246035
246290
  console.error("[API] Failed to load agent session:", error48);
@@ -246214,6 +246469,121 @@ var routes11 = async (fastify2) => {
246214
246469
  });
246215
246470
  }
246216
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
+ );
246217
246587
  fastify2.get(
246218
246588
  "/api/agent-sessions/:sessionId/brief-source",
246219
246589
  async (req, reply) => {
@@ -249483,6 +249853,13 @@ function attachWsHeartbeat(socket, { label, intervalMs = DEFAULT_INTERVAL_MS, ke
249483
249853
  }
249484
249854
 
249485
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
+ }
249486
249863
  var routes23 = async (fastify2) => {
249487
249864
  fastify2.reverseConnectManager.setStatusChangeHandler((remoteServerId, status) => {
249488
249865
  if (status !== "online") return;
@@ -249694,6 +250071,8 @@ var routes23 = async (fastify2) => {
249694
250071
  { websocket: true },
249695
250072
  async (socket, req) => {
249696
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;
249697
250076
  console.log(`[AgentWS] Connection attempt for session ${sessionId} (auth=${fastify2.authEnabled})`);
249698
250077
  let principalUserId = fastify2.authEnabled ? null : "local";
249699
250078
  if (fastify2.authEnabled) {
@@ -249743,15 +250122,42 @@ var routes23 = async (fastify2) => {
249743
250122
  const cacheEntry = cache2.getOrCreate(sessionId);
249744
250123
  console.log(`[AgentWS] WS connect: cacheEntry for ${sessionId} has messages.length=${cacheEntry.messages.length} finished=${cacheEntry.finished} remoteWsOpen=${!!cache2.getRemoteWs(sessionId)}`);
249745
250124
  if (cacheEntry.messages.length > 0) {
249746
- 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
+ }
249747
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
+ }
249748
250154
  try {
249749
250155
  socket.send(raw);
249750
250156
  } catch {
249751
250157
  }
249752
250158
  }
249753
250159
  try {
249754
- socket.send(JSON.stringify({ Ready: true }));
250160
+ socket.send(JSON.stringify({ Ready: true, historyEpoch: cacheEntry.historyEpoch ?? void 0 }));
249755
250161
  } catch {
249756
250162
  }
249757
250163
  if (cacheEntry.finished) {
@@ -249777,7 +250183,8 @@ var routes23 = async (fastify2) => {
249777
250183
  fastify2.reverseConnectManager,
249778
250184
  fastify2.eventBus,
249779
250185
  fastify2.agentSessionManager,
249780
- fastify2.storage
250186
+ fastify2.storage,
250187
+ { afterEntryIndex, historyEpoch }
249781
250188
  );
249782
250189
  }
249783
250190
  if (cache2.getRemoteWs(sessionId)) {
@@ -249807,7 +250214,7 @@ var routes23 = async (fastify2) => {
249807
250214
  });
249808
250215
  return;
249809
250216
  }
249810
- const unsubscribe = fastify2.agentSessionManager.subscribe(sessionId, socket);
250217
+ const unsubscribe = fastify2.agentSessionManager.subscribe(sessionId, socket, { afterEntryIndex, historyEpoch });
249811
250218
  if (!unsubscribe) {
249812
250219
  console.log(`[AgentWS] Session ${sessionId} not found`);
249813
250220
  stopHeartbeat();
@@ -253486,6 +253893,8 @@ var WORKER_CAPABILITIES = {
253486
253893
  "http:POST /api/path/agent-sessions": { since: "0.2.0", summary: "\u521B\u5EFA\u4F1A\u8BDD" },
253487
253894
  "http:POST /api/path/agent-sessions/new": { since: "0.2.0", summary: "\u521B\u5EFA\u4F1A\u8BDD(\u6307\u5B9A ID)" },
253488
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" },
253489
253898
  // Additive: a worker below 0.3.6 404s it and the hub's intent-brief
253490
253899
  // distillation degrades to the deterministic excerpt (tier 2).
253491
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.15",
3
+ "version": "0.3.17",
4
4
  "description": "Vibedeckx platform binaries for Linux x64",
5
5
  "os": [
6
6
  "linux"