@vibedeckx/linux-x64 0.3.34 → 0.3.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +732 -161
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -186887,7 +186887,8 @@ var mapLocalActivity = (row) => {
|
|
|
186887
186887
|
parseActivityTimestamp(row.created_at) ?? 0
|
|
186888
186888
|
),
|
|
186889
186889
|
lastUserMessageAt: row.last_user_message_at,
|
|
186890
|
-
lastCompletedAt: row.last_completed_at
|
|
186890
|
+
lastCompletedAt: row.last_completed_at,
|
|
186891
|
+
favoritedAt: row.favorited_at
|
|
186891
186892
|
};
|
|
186892
186893
|
};
|
|
186893
186894
|
var observeLocalActivity = (consumer, row) => {
|
|
@@ -186915,6 +186916,7 @@ var localActivityBase = (kdb, projectId) => {
|
|
|
186915
186916
|
"s.updated_at",
|
|
186916
186917
|
"s.last_user_message_at",
|
|
186917
186918
|
"s.last_completed_at",
|
|
186919
|
+
"s.favorited_at",
|
|
186918
186920
|
"checkout.worktree_path",
|
|
186919
186921
|
"checkout.deleted_at as checkout_deleted_at",
|
|
186920
186922
|
"checkout.status as checkout_status",
|
|
@@ -187196,6 +187198,12 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
187196
187198
|
await observeDanglingLocalScope(kdb, consumer, projectId);
|
|
187197
187199
|
return rows.map(mapLocalActivity);
|
|
187198
187200
|
},
|
|
187201
|
+
listFavoritedActivityByProject: async (projectId, limit, consumer) => {
|
|
187202
|
+
const rows = await localActivityBase(kdb, projectId).where(visibleLifecycleOf("s")).where("s.favorited_at", "is not", null).orderBy("s.favorited_at", "desc").orderBy("s.id", "desc").limit(limit).execute();
|
|
187203
|
+
rows.forEach((row) => observeLocalActivity(consumer, row));
|
|
187204
|
+
await observeDanglingLocalScope(kdb, consumer, projectId);
|
|
187205
|
+
return rows.map(mapLocalActivity);
|
|
187206
|
+
},
|
|
187199
187207
|
countRunningByProject: async (projectId) => {
|
|
187200
187208
|
const row = await kdb.selectFrom("agent_sessions").select(kdb.fn.countAll().as("count")).where("project_id", "=", projectId).where("status", "=", "running").where(visibleLifecycle).executeTakeFirstOrThrow();
|
|
187201
187209
|
return Number(row.count);
|
|
@@ -187296,12 +187304,25 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
187296
187304
|
getEntries: async (sessionId) => {
|
|
187297
187305
|
return kdb.selectFrom("agent_session_entries").select(["entry_index", "data"]).where("session_id", "=", sessionId).orderBy("entry_index", "asc").execute();
|
|
187298
187306
|
},
|
|
187307
|
+
getEntriesBefore: async (sessionId, beforeIndex, limit) => {
|
|
187308
|
+
let query = kdb.selectFrom("agent_session_entries").select(["entry_index", "data"]).where("session_id", "=", sessionId);
|
|
187309
|
+
if (beforeIndex !== null) query = query.where("entry_index", "<", beforeIndex);
|
|
187310
|
+
return query.orderBy("entry_index", "desc").limit(limit).execute();
|
|
187311
|
+
},
|
|
187299
187312
|
deleteEntries: async (sessionId) => {
|
|
187300
187313
|
await kdb.deleteFrom("agent_session_entries").where("session_id", "=", sessionId).execute();
|
|
187301
187314
|
},
|
|
187302
187315
|
countEntries: async () => {
|
|
187303
187316
|
return kdb.selectFrom("agent_session_entries").select("session_id").select(kdb.fn.countAll().as("cnt")).groupBy("session_id").execute();
|
|
187304
187317
|
},
|
|
187318
|
+
getEntryMetaAll: async () => {
|
|
187319
|
+
const rows = await kdb.selectFrom("agent_session_entries").select("session_id").select(kdb.fn.countAll().as("cnt")).select(kdb.fn.max("entry_index").as("max_index")).groupBy("session_id").execute();
|
|
187320
|
+
return rows.map((row) => ({
|
|
187321
|
+
session_id: row.session_id,
|
|
187322
|
+
cnt: Number(row.cnt),
|
|
187323
|
+
max_index: row.max_index ?? -1
|
|
187324
|
+
}));
|
|
187325
|
+
},
|
|
187305
187326
|
listRetentionCandidates: async ({ cutoff, limit, after }) => {
|
|
187306
187327
|
let query = kdb.selectFrom("agent_sessions").select(["id", "project_id", "branch", "activity_at"]).where(retentionPredicate(cutoff));
|
|
187307
187328
|
if (after) {
|
|
@@ -188166,7 +188187,8 @@ var mapRemoteActivity = (row) => {
|
|
|
188166
188187
|
model: row.model,
|
|
188167
188188
|
lastActiveAt: row.last_active_at,
|
|
188168
188189
|
lastUserMessageAt: row.last_user_message_at,
|
|
188169
|
-
lastCompletedAt: row.last_completed_at
|
|
188190
|
+
lastCompletedAt: row.last_completed_at,
|
|
188191
|
+
favoritedAt: row.favorited_at
|
|
188170
188192
|
};
|
|
188171
188193
|
};
|
|
188172
188194
|
var observeRemoteActivity = (consumer, row) => {
|
|
@@ -188234,6 +188256,7 @@ var remoteActivityBase = (kdb, projectId) => remoteSessionScope(kdb, projectId).
|
|
|
188234
188256
|
"c.model",
|
|
188235
188257
|
"c.last_user_message_at",
|
|
188236
188258
|
"c.last_completed_at",
|
|
188259
|
+
"c.favorited_at",
|
|
188237
188260
|
"checkout.worktree_path",
|
|
188238
188261
|
"checkout.deleted_at as checkout_deleted_at",
|
|
188239
188262
|
"checkout.status as checkout_status",
|
|
@@ -188269,6 +188292,12 @@ var createSearchCacheRepos = (kdb, _h) => ({
|
|
|
188269
188292
|
await observeDanglingRemoteActivity(kdb, consumer, projectId);
|
|
188270
188293
|
return rows.map(mapRemoteActivity);
|
|
188271
188294
|
},
|
|
188295
|
+
listRemoteSessionFavoritesByProject: async (projectId, limit, consumer) => {
|
|
188296
|
+
const rows = await remoteActivityBase(kdb, projectId).where("c.favorited_at", "is not", null).orderBy("c.favorited_at", "desc").orderBy("c.local_session_id", "asc").limit(limit).execute();
|
|
188297
|
+
rows.forEach((row) => observeRemoteActivity(consumer, row));
|
|
188298
|
+
await observeDanglingRemoteActivity(kdb, consumer, projectId);
|
|
188299
|
+
return rows.map(mapRemoteActivity);
|
|
188300
|
+
},
|
|
188272
188301
|
countRemoteSessionActivityByProject: async (projectId) => {
|
|
188273
188302
|
const row = await remoteSessionScope(kdb, projectId).select([
|
|
188274
188303
|
sql`coalesce(sum(case when c.status = 'running' then 1 else 0 end), 0)`.as("running")
|
|
@@ -188450,6 +188479,39 @@ var createSearchCacheRepos = (kdb, _h) => ({
|
|
|
188450
188479
|
updateCachedSessionTitle: async (localSessionId, title) => {
|
|
188451
188480
|
await kdb.updateTable("session_search_cache").set({ title, written_at: Date.now() }).where("local_session_id", "=", localSessionId).execute();
|
|
188452
188481
|
},
|
|
188482
|
+
// Star write-through. Unlike the title and delete write-throughs this one
|
|
188483
|
+
// may CREATE the row: the remote favorite PATCH has already succeeded on
|
|
188484
|
+
// the worker, so the session provably exists, and the path that lists a
|
|
188485
|
+
// remote's sessions (where the star button lives) binds only a mapping —
|
|
188486
|
+
// no cache row. Before that target's first catalog snapshot there would be
|
|
188487
|
+
// nothing to update and the star would sit invisible, which is the exact
|
|
188488
|
+
// latency this write-through exists to remove. Only starring creates a
|
|
188489
|
+
// row; an unstar with no row has nothing to hide (see noteSessionDeleted).
|
|
188490
|
+
updateCachedSessionFavorited: async (localSessionId, favoritedAt) => {
|
|
188491
|
+
const now3 = Date.now();
|
|
188492
|
+
const updated = await kdb.updateTable("session_search_cache").set({ favorited_at: favoritedAt, written_at: now3 }).where("local_session_id", "=", localSessionId).executeTakeFirst();
|
|
188493
|
+
if (Number(updated?.numUpdatedRows ?? 0) > 0 || favoritedAt === null) return;
|
|
188494
|
+
const mapping = await kdb.selectFrom("remote_session_mappings").select(["project_id", "remote_server_id", "branch"]).where("local_session_id", "=", localSessionId).executeTakeFirst();
|
|
188495
|
+
if (!mapping || mapping.remote_server_id === "local") return;
|
|
188496
|
+
await kdb.insertInto("session_search_cache").values({
|
|
188497
|
+
local_session_id: localSessionId,
|
|
188498
|
+
project_id: mapping.project_id,
|
|
188499
|
+
target_id: mapping.remote_server_id,
|
|
188500
|
+
branch: toDbBranch(mapping.branch),
|
|
188501
|
+
title: null,
|
|
188502
|
+
last_active_at: null,
|
|
188503
|
+
favorited_at: favoritedAt,
|
|
188504
|
+
entry_count: 0,
|
|
188505
|
+
status: "unknown",
|
|
188506
|
+
agent_type: null,
|
|
188507
|
+
model: null,
|
|
188508
|
+
last_user_message_at: null,
|
|
188509
|
+
last_completed_at: null,
|
|
188510
|
+
generation: 0,
|
|
188511
|
+
deleted_at: null,
|
|
188512
|
+
written_at: now3
|
|
188513
|
+
}).onConflict((oc) => oc.column("local_session_id").doUpdateSet({ favorited_at: favoritedAt, written_at: now3 })).execute();
|
|
188514
|
+
},
|
|
188453
188515
|
// Create write-through: called where a remote session's creation transits
|
|
188454
188516
|
// the server (UI create proxy, commander spawn, branch-from-history). The
|
|
188455
188517
|
// written_at stamp keeps the row exempt from snapshot reconciliation until
|
|
@@ -208493,6 +208555,111 @@ var EntryTracker = class {
|
|
|
208493
208555
|
}
|
|
208494
208556
|
};
|
|
208495
208557
|
|
|
208558
|
+
// src/session-history-window.ts
|
|
208559
|
+
function historyHead(entries, historyEpoch) {
|
|
208560
|
+
let latestEntryIndex = null;
|
|
208561
|
+
let lastTurnEndEntryIndex = null;
|
|
208562
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
208563
|
+
const message = entries[index];
|
|
208564
|
+
if (!message) continue;
|
|
208565
|
+
latestEntryIndex ??= index;
|
|
208566
|
+
if (lastTurnEndEntryIndex === null && message.type === "turn_end") {
|
|
208567
|
+
lastTurnEndEntryIndex = index;
|
|
208568
|
+
}
|
|
208569
|
+
if (latestEntryIndex !== null && lastTurnEndEntryIndex !== null) break;
|
|
208570
|
+
}
|
|
208571
|
+
return { historyEpoch, latestEntryIndex, lastTurnEndEntryIndex };
|
|
208572
|
+
}
|
|
208573
|
+
function buildHistoryWindow(entries, historyEpoch, opts = {}) {
|
|
208574
|
+
const head = historyHead(entries, historyEpoch);
|
|
208575
|
+
const endExclusive = Math.max(0, Math.min(opts.before ?? entries.length, entries.length));
|
|
208576
|
+
const requestedTurns = Math.max(1, Math.min(opts.turns ?? 5, 20));
|
|
208577
|
+
const boundaries = [];
|
|
208578
|
+
for (let index = endExclusive - 1; index >= 0; index--) {
|
|
208579
|
+
if (entries[index]?.type === "turn_end") boundaries.push(index);
|
|
208580
|
+
if (boundaries.length >= requestedTurns + 2) break;
|
|
208581
|
+
}
|
|
208582
|
+
const startIndex = boundaries.length > requestedTurns + 1 ? boundaries[requestedTurns + 1] + 1 : 0;
|
|
208583
|
+
const dense = [];
|
|
208584
|
+
for (let index = startIndex; index < endExclusive; index++) {
|
|
208585
|
+
const message = entries[index];
|
|
208586
|
+
if (message) dense.push({ entryIndex: index, message });
|
|
208587
|
+
}
|
|
208588
|
+
const hasMore = startIndex > 0;
|
|
208589
|
+
return {
|
|
208590
|
+
...head,
|
|
208591
|
+
entries: dense,
|
|
208592
|
+
previousCursor: hasMore ? startIndex : null,
|
|
208593
|
+
hasMore
|
|
208594
|
+
};
|
|
208595
|
+
}
|
|
208596
|
+
|
|
208597
|
+
// src/session-history-reader.ts
|
|
208598
|
+
var SessionHistoryReader = class {
|
|
208599
|
+
storage;
|
|
208600
|
+
constructor(storage2) {
|
|
208601
|
+
this.storage = storage2;
|
|
208602
|
+
}
|
|
208603
|
+
/**
|
|
208604
|
+
* The whole transcript as a SPARSE array indexed by entry index — the exact
|
|
208605
|
+
* shape `MessageStore.entries` has, so callers can treat hot and cold
|
|
208606
|
+
* sessions identically. Unparsable rows become holes, matching
|
|
208607
|
+
* `rebuildStoreFromRows`.
|
|
208608
|
+
*/
|
|
208609
|
+
async readAll(sessionId) {
|
|
208610
|
+
const rows = await this.storage.agentSessions.getEntries(sessionId);
|
|
208611
|
+
return parseRows(rows, sessionId);
|
|
208612
|
+
}
|
|
208613
|
+
/** `readAll` with holes dropped — the `getMessages` shape. */
|
|
208614
|
+
async readDense(sessionId) {
|
|
208615
|
+
return (await this.readAll(sessionId)).filter(Boolean);
|
|
208616
|
+
}
|
|
208617
|
+
/**
|
|
208618
|
+
* Phase 1 reads the whole transcript and slices it in memory: correctness
|
|
208619
|
+
* first, and the slicing logic stays the single implementation shared with
|
|
208620
|
+
* hot sessions. Phase 2 replaces the body with paged `getEntriesBefore`
|
|
208621
|
+
* queries plus one turn_end count — the signature is chosen to allow that
|
|
208622
|
+
* without touching a single caller.
|
|
208623
|
+
*/
|
|
208624
|
+
async readWindow(sessionId, historyEpoch, opts = {}) {
|
|
208625
|
+
return buildHistoryWindow(await this.readAll(sessionId), historyEpoch, opts);
|
|
208626
|
+
}
|
|
208627
|
+
async readHead(sessionId, historyEpoch) {
|
|
208628
|
+
return historyHead(await this.readAll(sessionId), historyEpoch);
|
|
208629
|
+
}
|
|
208630
|
+
/**
|
|
208631
|
+
* One page of entries in descending index order, strictly before
|
|
208632
|
+
* `beforeIndex` (null = the tail). The backward walk crash repair uses to
|
|
208633
|
+
* find a turn boundary without reading the whole transcript.
|
|
208634
|
+
*/
|
|
208635
|
+
async readBefore(sessionId, beforeIndex, limit) {
|
|
208636
|
+
const rows = await this.storage.agentSessions.getEntriesBefore(sessionId, beforeIndex, limit);
|
|
208637
|
+
return rows.map((row) => ({
|
|
208638
|
+
entryIndex: row.entry_index,
|
|
208639
|
+
message: parseRow(row.data)
|
|
208640
|
+
}));
|
|
208641
|
+
}
|
|
208642
|
+
};
|
|
208643
|
+
function parseRow(data) {
|
|
208644
|
+
try {
|
|
208645
|
+
return JSON.parse(data);
|
|
208646
|
+
} catch {
|
|
208647
|
+
return void 0;
|
|
208648
|
+
}
|
|
208649
|
+
}
|
|
208650
|
+
function parseRows(rows, sessionIdForLog) {
|
|
208651
|
+
const entries = [];
|
|
208652
|
+
for (const row of rows) {
|
|
208653
|
+
const message = parseRow(row.data);
|
|
208654
|
+
if (message === void 0) {
|
|
208655
|
+
console.error(`[SessionHistoryReader] Failed to parse entry ${row.entry_index} for session ${sessionIdForLog}`);
|
|
208656
|
+
continue;
|
|
208657
|
+
}
|
|
208658
|
+
entries[row.entry_index] = message;
|
|
208659
|
+
}
|
|
208660
|
+
return entries;
|
|
208661
|
+
}
|
|
208662
|
+
|
|
208496
208663
|
// src/utils/worktree-paths.ts
|
|
208497
208664
|
import path7 from "path";
|
|
208498
208665
|
import { createHash } from "crypto";
|
|
@@ -230246,6 +230413,7 @@ ${details}`;
|
|
|
230246
230413
|
return msg;
|
|
230247
230414
|
}
|
|
230248
230415
|
var SUMMARY_TEXT_CAP = 1500;
|
|
230416
|
+
var REPAIR_SCAN_BATCH = 64;
|
|
230249
230417
|
function extractLastAssistantText(entries) {
|
|
230250
230418
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
230251
230419
|
const entry = entries[i];
|
|
@@ -230256,6 +230424,55 @@ function extractLastAssistantText(entries) {
|
|
|
230256
230424
|
}
|
|
230257
230425
|
return void 0;
|
|
230258
230426
|
}
|
|
230427
|
+
var WS_OPEN = 1;
|
|
230428
|
+
function isSocketOpen(ws) {
|
|
230429
|
+
return ws.readyState === void 0 || ws.readyState === WS_OPEN;
|
|
230430
|
+
}
|
|
230431
|
+
function replayPatchesFor(entries) {
|
|
230432
|
+
const patches = [];
|
|
230433
|
+
entries.forEach((message, index) => {
|
|
230434
|
+
if (message !== void 0) patches.push(ConversationPatch.addEntry(index, message));
|
|
230435
|
+
});
|
|
230436
|
+
return patches;
|
|
230437
|
+
}
|
|
230438
|
+
function denseMessagesFromRows(rows) {
|
|
230439
|
+
const messages = [];
|
|
230440
|
+
for (const row of rows) {
|
|
230441
|
+
try {
|
|
230442
|
+
messages.push(JSON.parse(row.data));
|
|
230443
|
+
} catch {
|
|
230444
|
+
}
|
|
230445
|
+
}
|
|
230446
|
+
return messages;
|
|
230447
|
+
}
|
|
230448
|
+
function metaFromStore(store) {
|
|
230449
|
+
let entryCount = 0;
|
|
230450
|
+
let maxEntryIndex = -1;
|
|
230451
|
+
store.entries.forEach((entry, index) => {
|
|
230452
|
+
if (entry === void 0) return;
|
|
230453
|
+
entryCount++;
|
|
230454
|
+
if (index > maxEntryIndex) maxEntryIndex = index;
|
|
230455
|
+
});
|
|
230456
|
+
return { entryCount, maxEntryIndex };
|
|
230457
|
+
}
|
|
230458
|
+
function coldStore(maxEntryIndex) {
|
|
230459
|
+
const indexProvider = new EntryIndexProvider(maxEntryIndex + 1);
|
|
230460
|
+
return {
|
|
230461
|
+
patches: [],
|
|
230462
|
+
entries: [],
|
|
230463
|
+
indexProvider,
|
|
230464
|
+
toolTracker: new EntryTracker(indexProvider),
|
|
230465
|
+
currentAssistantIndex: null
|
|
230466
|
+
};
|
|
230467
|
+
}
|
|
230468
|
+
var SpawnSupersededError = class extends Error {
|
|
230469
|
+
code = "spawn_superseded";
|
|
230470
|
+
statusCode = 409;
|
|
230471
|
+
constructor(sessionId) {
|
|
230472
|
+
super(`Session ${sessionId} was restarted while its history was loading`);
|
|
230473
|
+
this.name = "SpawnSupersededError";
|
|
230474
|
+
}
|
|
230475
|
+
};
|
|
230259
230476
|
var WorkspaceCheckoutUnavailableError = class extends Error {
|
|
230260
230477
|
code = "workspace_checkout_unavailable";
|
|
230261
230478
|
statusCode = 409;
|
|
@@ -230313,8 +230530,11 @@ var AgentSessionManager = class {
|
|
|
230313
230530
|
/** Bound on a parked completion (injectable for tests). */
|
|
230314
230531
|
parkTimeoutMs;
|
|
230315
230532
|
workflowSuppressionCheck = null;
|
|
230533
|
+
/** Every read of a cold session's transcript goes through here. */
|
|
230534
|
+
reader;
|
|
230316
230535
|
constructor(storage2, opts) {
|
|
230317
230536
|
this.storage = storage2;
|
|
230537
|
+
this.reader = new SessionHistoryReader(storage2);
|
|
230318
230538
|
this.completionGraceMs = opts?.completionGraceMs ?? COMPLETION_GRACE_MS;
|
|
230319
230539
|
this.parkTimeoutMs = opts?.parkTimeoutMs ?? PARK_TIMEOUT_MS;
|
|
230320
230540
|
}
|
|
@@ -230471,6 +230691,154 @@ var AgentSessionManager = class {
|
|
|
230471
230691
|
const session = this.sessions.get(sessionId);
|
|
230472
230692
|
if (session) this.emitSessionTitle(session.projectId, session.branch, sessionId, title);
|
|
230473
230693
|
}
|
|
230694
|
+
// ============ History hydration (plan B §2) ============
|
|
230695
|
+
/**
|
|
230696
|
+
* Guard for the paths that may only run against an in-memory transcript:
|
|
230697
|
+
* everything on the streaming write side (`stageEntry` and the stdout
|
|
230698
|
+
* parsing chain behind it) plus the turn-boundary scans that read
|
|
230699
|
+
* `store.entries` directly.
|
|
230700
|
+
*
|
|
230701
|
+
* They all sit behind invariant B1 — a session with a process is hot — so
|
|
230702
|
+
* this should be unreachable. It throws rather than silently degrading
|
|
230703
|
+
* because the failure mode it guards against is invisible: a cold session
|
|
230704
|
+
* reads as a transcript of length zero, which would make a turn boundary
|
|
230705
|
+
* resolve to a fabricated disposition instead of blowing up.
|
|
230706
|
+
*/
|
|
230707
|
+
assertHot(session, operation) {
|
|
230708
|
+
if (session.hot) return;
|
|
230709
|
+
throw new Error(
|
|
230710
|
+
`[AgentSession] ${operation} requires a hydrated session but ${session.id} is cold`
|
|
230711
|
+
);
|
|
230712
|
+
}
|
|
230713
|
+
/**
|
|
230714
|
+
* Load a cold session's transcript into memory ahead of spawning a process
|
|
230715
|
+
* for it. The ONLY hydration path.
|
|
230716
|
+
*
|
|
230717
|
+
* @throws SpawnSupersededError if a restart overtook the caller's operation.
|
|
230718
|
+
* The caller must abort: not spawn, not append, not touch the session.
|
|
230719
|
+
*/
|
|
230720
|
+
async hydrateForSpawn(session) {
|
|
230721
|
+
const generation = session.clearGeneration;
|
|
230722
|
+
if (!session.hot) {
|
|
230723
|
+
if (!session.hydrating) {
|
|
230724
|
+
session.hydrating = this.runOnHistoryChain(session, async () => {
|
|
230725
|
+
if (session.hot) return;
|
|
230726
|
+
const generationAtRead = session.clearGeneration;
|
|
230727
|
+
const rows = await this.storage.agentSessions.getEntries(session.id);
|
|
230728
|
+
if (session.clearGeneration !== generationAtRead || session.hot) return;
|
|
230729
|
+
const store = this.rebuildStoreFromRows(rows, session.id);
|
|
230730
|
+
session.store = store;
|
|
230731
|
+
session.historyMeta = metaFromStore(store);
|
|
230732
|
+
session.hot = true;
|
|
230733
|
+
}).finally(() => {
|
|
230734
|
+
session.hydrating = null;
|
|
230735
|
+
});
|
|
230736
|
+
}
|
|
230737
|
+
await session.hydrating;
|
|
230738
|
+
}
|
|
230739
|
+
if (session.clearGeneration !== generation) throw new SpawnSupersededError(session.id);
|
|
230740
|
+
}
|
|
230741
|
+
/**
|
|
230742
|
+
* Drop a session's transcript back to the database. Called at every point
|
|
230743
|
+
* where a session stops owning a process — the four in plan §2.3 (process
|
|
230744
|
+
* exit, Stop, hibernate, agent switch) plus `setModel` retiring an idle
|
|
230745
|
+
* process. This is the whole of the memory-reclaim story: no sweeper and no
|
|
230746
|
+
* idle threshold, because "does it have a process" is not a policy.
|
|
230747
|
+
*
|
|
230748
|
+
* A no-op for sessions that are already cold (a second Stop, a Stop on a
|
|
230749
|
+
* session restored from disk) and for `skipDb` mirrors, which have no rows
|
|
230750
|
+
* to read back.
|
|
230751
|
+
*/
|
|
230752
|
+
unloadHistory(session, reason) {
|
|
230753
|
+
if (!session.hot || session.skipDb) return;
|
|
230754
|
+
if (session.chainPending > 0) {
|
|
230755
|
+
this.enqueueSessionWork(
|
|
230756
|
+
session,
|
|
230757
|
+
async () => this.unloadHistoryNow(session, reason),
|
|
230758
|
+
`unload:${reason}`
|
|
230759
|
+
);
|
|
230760
|
+
return;
|
|
230761
|
+
}
|
|
230762
|
+
this.unloadHistoryNow(session, reason);
|
|
230763
|
+
}
|
|
230764
|
+
unloadHistoryNow(session, reason) {
|
|
230765
|
+
if (!session.hot || session.skipDb) return;
|
|
230766
|
+
if (session.process !== null) {
|
|
230767
|
+
console.warn(`[AgentSession] Refusing to unload ${session.id} (${reason}): process still attached`);
|
|
230768
|
+
return;
|
|
230769
|
+
}
|
|
230770
|
+
if (session.processStartsInFlight > 0) {
|
|
230771
|
+
return;
|
|
230772
|
+
}
|
|
230773
|
+
const meta3 = metaFromStore(session.store);
|
|
230774
|
+
session.historyMeta = meta3;
|
|
230775
|
+
session.store = coldStore(Math.max(meta3.maxEntryIndex, session.store.indexProvider.current() - 1));
|
|
230776
|
+
session.hot = false;
|
|
230777
|
+
}
|
|
230778
|
+
/**
|
|
230779
|
+
* Release the transcript when a spawn attempt ends without attaching a
|
|
230780
|
+
* process — `ensureResidentCapacity` refusing at the cap, a checkout that
|
|
230781
|
+
* vanished, a spawn that could not start.
|
|
230782
|
+
*
|
|
230783
|
+
* Without this, every rejected send on a dormant session loads a transcript
|
|
230784
|
+
* that nothing will ever unload (the four unload points all hang off a
|
|
230785
|
+
* process going away, and no process ever arrived). A user retrying against
|
|
230786
|
+
* a full resident pool would walk the memory bound back up one session at a
|
|
230787
|
+
* time — the exact failure this design exists to prevent.
|
|
230788
|
+
*
|
|
230789
|
+
* Must run AFTER the caller releases its `processStartsInFlight` claim: a
|
|
230790
|
+
* second, still-committed start means the transcript has a new owner.
|
|
230791
|
+
*/
|
|
230792
|
+
unloadIfSpawnAbandoned(session) {
|
|
230793
|
+
if (session.process !== null || session.processStartsInFlight > 0) return;
|
|
230794
|
+
this.unloadHistory(session, "spawn-abandoned");
|
|
230795
|
+
}
|
|
230796
|
+
/**
|
|
230797
|
+
* Aggregate hydration counters for `memory-stats`. Aggregate-only and
|
|
230798
|
+
* name-free by construction, like everything else on that endpoint.
|
|
230799
|
+
*
|
|
230800
|
+
* `hot_entries` is the number of transcript entries actually resident. It is
|
|
230801
|
+
* the curve this whole design exists to flatten: before it, that number was
|
|
230802
|
+
* every entry in the database; after it, it should track the number of live
|
|
230803
|
+
* agent processes. No byte estimate is reported — measuring it means
|
|
230804
|
+
* serializing the very heap we are trying not to touch, twelve times an hour.
|
|
230805
|
+
*/
|
|
230806
|
+
hydrationStats() {
|
|
230807
|
+
let hot = 0;
|
|
230808
|
+
let hotEntries = 0;
|
|
230809
|
+
for (const session of this.sessions.values()) {
|
|
230810
|
+
if (!session.hot) continue;
|
|
230811
|
+
hot++;
|
|
230812
|
+
hotEntries += session.store.entries.filter(Boolean).length;
|
|
230813
|
+
}
|
|
230814
|
+
return { total: this.sessions.size, hot, cold: this.sessions.size - hot, hot_entries: hotEntries };
|
|
230815
|
+
}
|
|
230816
|
+
/** Dense messages (holes dropped), from memory when hot and storage when cold. */
|
|
230817
|
+
async loadMessages(sessionId) {
|
|
230818
|
+
const session = this.sessions.get(sessionId);
|
|
230819
|
+
if (!session) return [];
|
|
230820
|
+
if (session.hot) return session.store.entries.filter(Boolean);
|
|
230821
|
+
return this.reader.readDense(sessionId);
|
|
230822
|
+
}
|
|
230823
|
+
/** Sparse entries (holes preserved) — index space matches entry indices. */
|
|
230824
|
+
async loadRawMessages(sessionId) {
|
|
230825
|
+
const session = this.sessions.get(sessionId);
|
|
230826
|
+
if (!session) return [];
|
|
230827
|
+
if (session.hot) return session.store.entries;
|
|
230828
|
+
return this.reader.readAll(sessionId);
|
|
230829
|
+
}
|
|
230830
|
+
async loadHistoryWindow(sessionId, opts = {}) {
|
|
230831
|
+
const session = this.sessions.get(sessionId);
|
|
230832
|
+
if (!session) return null;
|
|
230833
|
+
if (session.hot) return buildHistoryWindow(session.store.entries, session.historyEpoch, opts);
|
|
230834
|
+
return this.reader.readWindow(sessionId, session.historyEpoch, opts);
|
|
230835
|
+
}
|
|
230836
|
+
async loadHistoryHead(sessionId) {
|
|
230837
|
+
const session = this.sessions.get(sessionId);
|
|
230838
|
+
if (!session) return null;
|
|
230839
|
+
if (session.hot) return historyHead(session.store.entries, session.historyEpoch);
|
|
230840
|
+
return this.reader.readHead(sessionId, session.historyEpoch);
|
|
230841
|
+
}
|
|
230474
230842
|
isProcessAlive(session) {
|
|
230475
230843
|
return !!session.process && session.process.exitCode === null && !session.dormant;
|
|
230476
230844
|
}
|
|
@@ -230644,7 +231012,7 @@ var AgentSessionManager = class {
|
|
|
230644
231012
|
}
|
|
230645
231013
|
for (const session of this.sessions.values()) {
|
|
230646
231014
|
if (session.projectId === projectId && session.branch === branch) {
|
|
230647
|
-
console.log(`[findExisting] skipDb in-memory match: ${session.id} (entries=${
|
|
231015
|
+
console.log(`[findExisting] skipDb in-memory match: ${session.id} (entries=${this.historyEntryCount(session)})`);
|
|
230648
231016
|
return this.reuseExistingSession(session, projectPath);
|
|
230649
231017
|
}
|
|
230650
231018
|
}
|
|
@@ -230838,6 +231206,11 @@ var AgentSessionManager = class {
|
|
|
230838
231206
|
processStartsInFlight: 0,
|
|
230839
231207
|
historyEpoch: stored?.history_epoch ?? 0,
|
|
230840
231208
|
store,
|
|
231209
|
+
// Born hot: a process is spawned for it a few lines below (B1).
|
|
231210
|
+
hot: true,
|
|
231211
|
+
historyMeta: metaFromStore(store),
|
|
231212
|
+
hydrating: null,
|
|
231213
|
+
clearGeneration: 0,
|
|
230841
231214
|
subscribers: /* @__PURE__ */ new Set(),
|
|
230842
231215
|
status: "running",
|
|
230843
231216
|
buffer: "",
|
|
@@ -230851,6 +231224,8 @@ var AgentSessionManager = class {
|
|
|
230851
231224
|
graceTimer: null,
|
|
230852
231225
|
parkTimer: null,
|
|
230853
231226
|
eventChain: Promise.resolve(),
|
|
231227
|
+
chainPending: 0,
|
|
231228
|
+
historyChain: Promise.resolve(),
|
|
230854
231229
|
bgSpawnHintsThisTurn: 0,
|
|
230855
231230
|
taskStartedThisTurn: 0,
|
|
230856
231231
|
lastActiveAt: Date.now(),
|
|
@@ -230907,7 +231282,7 @@ var AgentSessionManager = class {
|
|
|
230907
231282
|
* switch-mode route, which carries actual user intent.
|
|
230908
231283
|
*/
|
|
230909
231284
|
async reuseExistingSession(session, projectPath) {
|
|
230910
|
-
const entriesCount =
|
|
231285
|
+
const entriesCount = this.historyEntryCount(session);
|
|
230911
231286
|
this.touchSession(session);
|
|
230912
231287
|
if (session.dormant) {
|
|
230913
231288
|
console.log(`[AgentSession] Returning dormant session ${session.id} (entries=${entriesCount})`);
|
|
@@ -230939,9 +231314,10 @@ var AgentSessionManager = class {
|
|
|
230939
231314
|
* Uses negative PID to signal the process group (requires detached: true at spawn).
|
|
230940
231315
|
*/
|
|
230941
231316
|
killProcess(proc, signal = "SIGTERM") {
|
|
230942
|
-
|
|
231317
|
+
const pid = proc?.pid;
|
|
231318
|
+
if (!proc || pid === void 0 || !Number.isSafeInteger(pid) || pid <= 1) return;
|
|
230943
231319
|
try {
|
|
230944
|
-
process.kill(-
|
|
231320
|
+
process.kill(-pid, signal);
|
|
230945
231321
|
} catch {
|
|
230946
231322
|
try {
|
|
230947
231323
|
proc.kill(signal);
|
|
@@ -231073,6 +231449,7 @@ var AgentSessionManager = class {
|
|
|
231073
231449
|
this.broadcastPatch(session.id, ConversationPatch.updateStatus(session.status));
|
|
231074
231450
|
this.eventBus?.emit({ type: "session:status", projectId: session.projectId, branch: session.branch, sessionId: session.id, status: session.status });
|
|
231075
231451
|
this.broadcastRaw(session.id, { finished: true });
|
|
231452
|
+
this.unloadHistory(session, "process-exit");
|
|
231076
231453
|
}, "process-close");
|
|
231077
231454
|
});
|
|
231078
231455
|
childProcess2.on("error", (error48) => {
|
|
@@ -231100,20 +231477,42 @@ var AgentSessionManager = class {
|
|
|
231100
231477
|
* runs through here so steps never interleave across await points.
|
|
231101
231478
|
*/
|
|
231102
231479
|
enqueueSessionWork(session, work, label) {
|
|
231480
|
+
session.chainPending += 1;
|
|
231103
231481
|
session.eventChain = session.eventChain.then(work).catch((err) => {
|
|
231104
231482
|
console.error(`[AgentSession] Error in ${label} handler for ${session.id}:`, err);
|
|
231483
|
+
}).then(() => {
|
|
231484
|
+
session.chainPending -= 1;
|
|
231105
231485
|
});
|
|
231106
231486
|
}
|
|
231487
|
+
/**
|
|
231488
|
+
* Run `work` with exclusive access to the session's history, i.e. serialized
|
|
231489
|
+
* against every other hydration and cold append for that session.
|
|
231490
|
+
*
|
|
231491
|
+
* Separate from `eventChain` on purpose — see `historyChain`'s declaration.
|
|
231492
|
+
*/
|
|
231493
|
+
runOnHistoryChain(session, work) {
|
|
231494
|
+
const result = session.historyChain.then(work);
|
|
231495
|
+
session.historyChain = result.then(
|
|
231496
|
+
() => void 0,
|
|
231497
|
+
() => void 0
|
|
231498
|
+
);
|
|
231499
|
+
return result;
|
|
231500
|
+
}
|
|
231107
231501
|
/**
|
|
231108
231502
|
* Same serial chain as `enqueueSessionWork`, for a caller that needs the
|
|
231109
231503
|
* result back. The chain itself absorbs the outcome (success or failure) so
|
|
231110
231504
|
* one queued step can never break the next; the caller gets the real promise.
|
|
231111
231505
|
*/
|
|
231112
231506
|
runSerialForResult(session, work) {
|
|
231507
|
+
session.chainPending += 1;
|
|
231113
231508
|
const result = session.eventChain.then(work);
|
|
231114
231509
|
session.eventChain = result.then(
|
|
231115
|
-
() =>
|
|
231116
|
-
|
|
231510
|
+
() => {
|
|
231511
|
+
session.chainPending -= 1;
|
|
231512
|
+
},
|
|
231513
|
+
() => {
|
|
231514
|
+
session.chainPending -= 1;
|
|
231515
|
+
}
|
|
231117
231516
|
);
|
|
231118
231517
|
return result;
|
|
231119
231518
|
}
|
|
@@ -231343,6 +231742,7 @@ var AgentSessionManager = class {
|
|
|
231343
231742
|
}
|
|
231344
231743
|
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")) {
|
|
231345
231744
|
session.turnOpenSince = timestamp;
|
|
231745
|
+
this.assertHot(session, "turn-open disposition");
|
|
231346
231746
|
session.turnDisposition = resolveNotificationDisposition(findLatestUserEntry(session.store.entries));
|
|
231347
231747
|
}
|
|
231348
231748
|
if (session.status !== "running" && !outOfTurn && (event.type === "text" || event.type === "thinking" || event.type === "tool_use" || event.type === "tool_result" || event.type === "approval_request")) {
|
|
@@ -231606,15 +232006,40 @@ var AgentSessionManager = class {
|
|
|
231606
232006
|
* the two persistence paths can't drift on index allocation or patch shape.
|
|
231607
232007
|
*/
|
|
231608
232008
|
stageEntry(session, message) {
|
|
232009
|
+
this.assertHot(session, "stageEntry");
|
|
231609
232010
|
const index = session.store.indexProvider.next();
|
|
231610
232011
|
session.store.entries[index] = message;
|
|
231611
232012
|
const patch = ConversationPatch.addEntry(index, message);
|
|
231612
232013
|
session.store.patches.push(patch);
|
|
231613
232014
|
return { index, patch };
|
|
231614
232015
|
}
|
|
232016
|
+
/**
|
|
232017
|
+
* Does this session have any transcript at all?
|
|
232018
|
+
*
|
|
232019
|
+
* Reads the store when hot and the metadata when cold, so it is correct in
|
|
232020
|
+
* both states AND — the load-bearing half — never triggers a read. Retention
|
|
232021
|
+
* and the discard compensator ask this of every candidate they consider; if
|
|
232022
|
+
* the question could hydrate, one sweep would pull the whole database back
|
|
232023
|
+
* into the heap that this design just emptied (§3.2).
|
|
232024
|
+
*
|
|
232025
|
+
* Deliberately NOT "historyMeta.entryCount > 0" in both states: keeping the
|
|
232026
|
+
* counter exact through the streaming write paths (which allocate indices
|
|
232027
|
+
* via `toolTracker.getOrCreate`, not only `stageEntry`) would be a new
|
|
232028
|
+
* invariant spread across a dozen sites. While hot, the store is already the
|
|
232029
|
+
* authority; `historyMeta` only has to be right at the moments it is read,
|
|
232030
|
+
* which are all cold.
|
|
232031
|
+
*/
|
|
232032
|
+
hasHistory(session) {
|
|
232033
|
+
return session.hot ? session.store.entries.some((entry) => entry !== void 0) : session.historyMeta.entryCount > 0;
|
|
232034
|
+
}
|
|
232035
|
+
/** Entry count for logging — same hot/cold split as `hasHistory`, never reads. */
|
|
232036
|
+
historyEntryCount(session) {
|
|
232037
|
+
return session.hot ? session.store.entries.filter(Boolean).length : session.historyMeta.entryCount;
|
|
232038
|
+
}
|
|
231615
232039
|
async pushEntry(sessionId, message, broadcast = true, userId = "local", pushOpts) {
|
|
231616
232040
|
const session = this.sessions.get(sessionId);
|
|
231617
232041
|
if (!session) return -1;
|
|
232042
|
+
if (!session.hot) return this.pushColdEntry(session, message, broadcast, userId, pushOpts);
|
|
231618
232043
|
const { index, patch } = this.stageEntry(session, message);
|
|
231619
232044
|
if (!session.skipDb && message.type !== "assistant") {
|
|
231620
232045
|
await this.persistEntry(session, index, message, userId, { strict: pushOpts?.strictPersist });
|
|
@@ -231624,6 +232049,48 @@ var AgentSessionManager = class {
|
|
|
231624
232049
|
}
|
|
231625
232050
|
return index;
|
|
231626
232051
|
}
|
|
232052
|
+
/**
|
|
232053
|
+
* Append to a session whose transcript is not in memory: allocate an index,
|
|
232054
|
+
* write the row, advance the metadata, broadcast the patch. No store is
|
|
232055
|
+
* built — a Stop note or an agent-switch note must not be a reason to pull a
|
|
232056
|
+
* whole transcript into the heap.
|
|
232057
|
+
*
|
|
232058
|
+
* Callers do not choose this path; `pushEntry` picks it. That is what makes
|
|
232059
|
+
* "stop a session restored from disk" and "switch the agent on a dormant
|
|
232060
|
+
* session" work unchanged, which the previous design missed by giving only
|
|
232061
|
+
* `switchAgentType` a bespoke cold-append helper.
|
|
232062
|
+
*
|
|
232063
|
+
* Runs on the session's history chain so it cannot interleave with a
|
|
232064
|
+
* `hydrateForSpawn` and be dropped when that installs its snapshot (§3.3).
|
|
232065
|
+
* It must NOT run on `eventChain`: an unload can land while a stdout chunk
|
|
232066
|
+
* is mid-flight on that chain, so the chunk resumes against a session that
|
|
232067
|
+
* went cold underneath it and appends from *inside* the chain. Queuing there
|
|
232068
|
+
* would make that task wait for itself — the chain wedges, and because
|
|
232069
|
+
* `hydrateForSpawn` used to queue on it too, the session could then never be
|
|
232070
|
+
* woken again.
|
|
232071
|
+
*/
|
|
232072
|
+
pushColdEntry(session, message, broadcast, userId, pushOpts) {
|
|
232073
|
+
return this.runOnHistoryChain(session, async () => {
|
|
232074
|
+
if (session.hot) {
|
|
232075
|
+
const { index: index2, patch } = this.stageEntry(session, message);
|
|
232076
|
+
if (!session.skipDb && message.type !== "assistant") {
|
|
232077
|
+
await this.persistEntry(session, index2, message, userId, { strict: pushOpts?.strictPersist });
|
|
232078
|
+
}
|
|
232079
|
+
if (broadcast) this.broadcastPatch(session.id, patch);
|
|
232080
|
+
return index2;
|
|
232081
|
+
}
|
|
232082
|
+
const index = session.store.indexProvider.next();
|
|
232083
|
+
if (!session.skipDb && message.type !== "assistant") {
|
|
232084
|
+
await this.persistEntry(session, index, message, userId, { strict: pushOpts?.strictPersist });
|
|
232085
|
+
}
|
|
232086
|
+
session.historyMeta = {
|
|
232087
|
+
entryCount: session.historyMeta.entryCount + 1,
|
|
232088
|
+
maxEntryIndex: Math.max(session.historyMeta.maxEntryIndex, index)
|
|
232089
|
+
};
|
|
232090
|
+
if (broadcast) this.broadcastPatch(session.id, ConversationPatch.addEntry(index, message));
|
|
232091
|
+
return index;
|
|
232092
|
+
});
|
|
232093
|
+
}
|
|
231627
232094
|
/**
|
|
231628
232095
|
* Persist a `turn_end` together with the attention milestone it earns.
|
|
231629
232096
|
*
|
|
@@ -231649,8 +232116,15 @@ var AgentSessionManager = class {
|
|
|
231649
232116
|
if (entryIndexOverride !== void 0) {
|
|
231650
232117
|
index = entryIndexOverride;
|
|
231651
232118
|
patch = ConversationPatch.addEntry(index, message);
|
|
231652
|
-
} else {
|
|
232119
|
+
} else if (session.hot) {
|
|
231653
232120
|
({ index, patch } = this.stageEntry(session, message));
|
|
232121
|
+
} else {
|
|
232122
|
+
index = session.store.indexProvider.next();
|
|
232123
|
+
session.historyMeta = {
|
|
232124
|
+
entryCount: session.historyMeta.entryCount + 1,
|
|
232125
|
+
maxEntryIndex: Math.max(session.historyMeta.maxEntryIndex, index)
|
|
232126
|
+
};
|
|
232127
|
+
patch = ConversationPatch.addEntry(index, message);
|
|
231654
232128
|
}
|
|
231655
232129
|
if (!session.skipDb) {
|
|
231656
232130
|
const activityReader = this.storage.agentSessions.getActivityById;
|
|
@@ -231736,9 +232210,12 @@ var AgentSessionManager = class {
|
|
|
231736
232210
|
if (session.turnOpenSince === null) return null;
|
|
231737
232211
|
const endedAt = Date.now();
|
|
231738
232212
|
const durationMs = endedAt - session.turnOpenSince;
|
|
231739
|
-
|
|
231740
|
-
|
|
231741
|
-
|
|
232213
|
+
let disposition = session.turnDisposition;
|
|
232214
|
+
if (disposition === null) {
|
|
232215
|
+
const entries = session.hot ? session.store.entries : await this.reader.readAll(session.id);
|
|
232216
|
+
const beforeIndex = session.hot ? entries.length : session.historyMeta.maxEntryIndex + 1;
|
|
232217
|
+
disposition = resolveNotificationDisposition(findTurnOpeningUserEntry(entries, beforeIndex));
|
|
232218
|
+
}
|
|
231742
232219
|
const index = await this.pushTurnEnd(session, outcome, disposition, endedAt, durationMs);
|
|
231743
232220
|
session.turnOpenSince = null;
|
|
231744
232221
|
session.turnDisposition = null;
|
|
@@ -231897,48 +232374,83 @@ var AgentSessionManager = class {
|
|
|
231897
232374
|
}
|
|
231898
232375
|
}
|
|
231899
232376
|
/**
|
|
231900
|
-
*
|
|
232377
|
+
* Attach a client to a session's live stream and replay its history to it.
|
|
232378
|
+
*
|
|
232379
|
+
* Async since lazy hydration: a dormant session's transcript is read from
|
|
232380
|
+
* storage here. The caller MUST register its `close` handler before
|
|
232381
|
+
* awaiting — see `websocket-routes.ts` — or a user who closes the tab during
|
|
232382
|
+
* that read leaves a dead socket in `subscribers` and a heartbeat running.
|
|
231901
232383
|
*/
|
|
231902
|
-
subscribe(sessionId, ws, opts = {}) {
|
|
232384
|
+
async subscribe(sessionId, ws, opts = {}) {
|
|
231903
232385
|
const session = this.sessions.get(sessionId);
|
|
231904
232386
|
if (!session) {
|
|
231905
232387
|
return null;
|
|
231906
232388
|
}
|
|
232389
|
+
if (!isSocketOpen(ws)) return null;
|
|
231907
232390
|
session.subscribers.add(ws);
|
|
231908
|
-
const
|
|
231909
|
-
ws.send(JSON.stringify({
|
|
231910
|
-
HistorySync: {
|
|
231911
|
-
historyEpoch: session.historyEpoch,
|
|
231912
|
-
reset: opts.historyEpoch !== void 0 && opts.historyEpoch !== session.historyEpoch
|
|
231913
|
-
}
|
|
231914
|
-
}));
|
|
231915
|
-
ws.send(JSON.stringify(this.backgroundTasksMessage(session)));
|
|
231916
|
-
for (const patch of session.store.patches) {
|
|
231917
|
-
const entryIndices = patch.flatMap((op) => {
|
|
231918
|
-
const match2 = op.path.match(/^\/entries\/(\d+)$/);
|
|
231919
|
-
return match2 ? [Number(match2[1])] : [];
|
|
231920
|
-
});
|
|
231921
|
-
if (entryIndices.length > 0 && entryIndices.every((index) => index <= after)) continue;
|
|
231922
|
-
const msg = { JsonPatch: patch };
|
|
231923
|
-
ws.send(JSON.stringify(msg));
|
|
231924
|
-
}
|
|
231925
|
-
ws.send(JSON.stringify({ Ready: true, historyEpoch: session.historyEpoch }));
|
|
231926
|
-
const statusPatch = ConversationPatch.updateStatus(session.status);
|
|
231927
|
-
ws.send(JSON.stringify({ JsonPatch: statusPatch }));
|
|
231928
|
-
return () => {
|
|
232391
|
+
const unsubscribe = () => {
|
|
231929
232392
|
session.subscribers.delete(ws);
|
|
231930
232393
|
};
|
|
232394
|
+
for (let attempt = 0; ; attempt++) {
|
|
232395
|
+
if (attempt >= 5) {
|
|
232396
|
+
console.error(`[AgentSession] subscribe to ${sessionId} kept being invalidated; giving up`);
|
|
232397
|
+
unsubscribe();
|
|
232398
|
+
return null;
|
|
232399
|
+
}
|
|
232400
|
+
const generation = session.clearGeneration;
|
|
232401
|
+
const after = opts.historyEpoch === void 0 || opts.historyEpoch === session.historyEpoch ? opts.afterEntryIndex ?? -1 : -1;
|
|
232402
|
+
ws.send(JSON.stringify({
|
|
232403
|
+
HistorySync: {
|
|
232404
|
+
historyEpoch: session.historyEpoch,
|
|
232405
|
+
reset: opts.historyEpoch !== void 0 && opts.historyEpoch !== session.historyEpoch
|
|
232406
|
+
}
|
|
232407
|
+
}));
|
|
232408
|
+
ws.send(JSON.stringify(this.backgroundTasksMessage(session)));
|
|
232409
|
+
const patches = session.hot ? session.store.patches : replayPatchesFor(await this.reader.readAll(sessionId));
|
|
232410
|
+
if (!isSocketOpen(ws)) {
|
|
232411
|
+
unsubscribe();
|
|
232412
|
+
return null;
|
|
232413
|
+
}
|
|
232414
|
+
if (session.clearGeneration !== generation) continue;
|
|
232415
|
+
for (const patch of patches) {
|
|
232416
|
+
const entryIndices = patch.flatMap((op) => {
|
|
232417
|
+
const match2 = op.path.match(/^\/entries\/(\d+)$/);
|
|
232418
|
+
return match2 ? [Number(match2[1])] : [];
|
|
232419
|
+
});
|
|
232420
|
+
if (entryIndices.length > 0 && entryIndices.every((index) => index <= after)) continue;
|
|
232421
|
+
const msg = { JsonPatch: patch };
|
|
232422
|
+
ws.send(JSON.stringify(msg));
|
|
232423
|
+
}
|
|
232424
|
+
ws.send(JSON.stringify({ Ready: true, historyEpoch: session.historyEpoch }));
|
|
232425
|
+
const statusPatch = ConversationPatch.updateStatus(session.status);
|
|
232426
|
+
ws.send(JSON.stringify({ JsonPatch: statusPatch }));
|
|
232427
|
+
return unsubscribe;
|
|
232428
|
+
}
|
|
231931
232429
|
}
|
|
231932
232430
|
/**
|
|
231933
|
-
* Get all messages for a session (reconstructed from patches)
|
|
232431
|
+
* Get all messages for a session (reconstructed from patches).
|
|
232432
|
+
*
|
|
232433
|
+
* HOT SESSIONS ONLY — use `loadMessages` unless you know a process is
|
|
232434
|
+
* attached. Throwing beats returning `[]` for a cold session: an empty
|
|
232435
|
+
* transcript is a plausible-looking answer that would quietly corrupt
|
|
232436
|
+
* whatever the caller does next.
|
|
231934
232437
|
*/
|
|
231935
232438
|
getMessages(sessionId) {
|
|
231936
232439
|
const session = this.sessions.get(sessionId);
|
|
231937
|
-
|
|
232440
|
+
if (!session) return [];
|
|
232441
|
+
this.assertHot(session, "getMessages");
|
|
232442
|
+
return session.store.entries.filter(Boolean);
|
|
231938
232443
|
}
|
|
231939
|
-
/**
|
|
232444
|
+
/**
|
|
232445
|
+
* Raw sparse entries (holes preserved) — index space matches entry indices.
|
|
232446
|
+
* Hot sessions only, same as `getMessages`; cold callers want
|
|
232447
|
+
* `loadRawMessages`.
|
|
232448
|
+
*/
|
|
231940
232449
|
getRawMessages(sessionId) {
|
|
231941
|
-
|
|
232450
|
+
const session = this.sessions.get(sessionId);
|
|
232451
|
+
if (!session) return [];
|
|
232452
|
+
this.assertHot(session, "getRawMessages");
|
|
232453
|
+
return session.store.entries;
|
|
231942
232454
|
}
|
|
231943
232455
|
getHistoryEpoch(sessionId) {
|
|
231944
232456
|
return this.sessions.get(sessionId)?.historyEpoch;
|
|
@@ -232043,6 +232555,7 @@ var AgentSessionManager = class {
|
|
|
232043
232555
|
});
|
|
232044
232556
|
}
|
|
232045
232557
|
}
|
|
232558
|
+
this.unloadHistory(session, "stop");
|
|
232046
232559
|
return true;
|
|
232047
232560
|
} catch (error48) {
|
|
232048
232561
|
console.error(`[AgentSession] Failed to stop session:`, error48);
|
|
@@ -232113,6 +232626,7 @@ var AgentSessionManager = class {
|
|
|
232113
232626
|
sessionId: session.id,
|
|
232114
232627
|
status: "stopped"
|
|
232115
232628
|
});
|
|
232629
|
+
this.unloadHistory(session, "hibernate");
|
|
232116
232630
|
return true;
|
|
232117
232631
|
} catch (error48) {
|
|
232118
232632
|
console.error(`[AgentSession] Failed to hibernate session:`, error48);
|
|
@@ -232202,7 +232716,7 @@ var AgentSessionManager = class {
|
|
|
232202
232716
|
if (session?.skipDb) return retained("retained_skip_db");
|
|
232203
232717
|
if ((this.userMessagesInFlight.get(sessionId) ?? 0) > 0) return retained("retained_in_flight");
|
|
232204
232718
|
if (this.retentionDeleting.has(sessionId)) return retained("retained_deleting");
|
|
232205
|
-
if (session
|
|
232719
|
+
if (session && this.hasHistory(session)) return retained("retained_has_entries");
|
|
232206
232720
|
this.retentionDeleting.add(sessionId);
|
|
232207
232721
|
try {
|
|
232208
232722
|
const deleted = await this.storage.agentSessions.deleteIfEmpty(sessionId);
|
|
@@ -232251,19 +232765,18 @@ var AgentSessionManager = class {
|
|
|
232251
232765
|
this.killProcess(proc);
|
|
232252
232766
|
this.emitProcessAlive(session, false);
|
|
232253
232767
|
this.resetCompletion(session);
|
|
232768
|
+
session.clearGeneration += 1;
|
|
232769
|
+
session.store = coldStore(-1);
|
|
232770
|
+
session.historyMeta = { entryCount: 0, maxEntryIndex: -1 };
|
|
232771
|
+
session.hot = true;
|
|
232772
|
+
session.buffer = "";
|
|
232773
|
+
session.dormant = false;
|
|
232254
232774
|
if (!session.skipDb) {
|
|
232255
232775
|
await this.storage.agentSessions.deleteEntries(sessionId);
|
|
232256
232776
|
session.historyEpoch = await this.storage.agentSessions.incrementHistoryEpoch(sessionId);
|
|
232257
232777
|
} else {
|
|
232258
232778
|
session.historyEpoch += 1;
|
|
232259
232779
|
}
|
|
232260
|
-
session.store.patches = [];
|
|
232261
|
-
session.store.entries = [];
|
|
232262
|
-
session.store.indexProvider.reset();
|
|
232263
|
-
session.store.toolTracker.clear();
|
|
232264
|
-
session.store.currentAssistantIndex = null;
|
|
232265
|
-
session.buffer = "";
|
|
232266
|
-
session.dormant = false;
|
|
232267
232780
|
session.turnOpenSince = null;
|
|
232268
232781
|
this.touchSession(session);
|
|
232269
232782
|
this.broadcastRaw(sessionId, {
|
|
@@ -232304,8 +232817,7 @@ var AgentSessionManager = class {
|
|
|
232304
232817
|
const session = this.sessions.get(sessionId);
|
|
232305
232818
|
if (!session) return "not_found";
|
|
232306
232819
|
if (session.agentType === agentType) return "ok";
|
|
232307
|
-
|
|
232308
|
-
if (session.status === "running" && hasHistory) return "busy";
|
|
232820
|
+
if (session.status === "running" && this.hasHistory(session)) return "busy";
|
|
232309
232821
|
console.log(`[AgentSession] Switching session ${sessionId} agent ${session.agentType} \u2192 ${agentType} (dormant, history preserved)`);
|
|
232310
232822
|
const proc = session.process;
|
|
232311
232823
|
session.process = null;
|
|
@@ -232337,6 +232849,7 @@ var AgentSessionManager = class {
|
|
|
232337
232849
|
this.broadcastPatch(sessionId, ConversationPatch.updateStatus("stopped"));
|
|
232338
232850
|
this.eventBus?.emit({ type: "session:status", projectId: session.projectId, branch: session.branch, sessionId: session.id, status: "stopped" });
|
|
232339
232851
|
}
|
|
232852
|
+
this.unloadHistory(session, "agent-switch");
|
|
232340
232853
|
return "ok";
|
|
232341
232854
|
}
|
|
232342
232855
|
/**
|
|
@@ -232406,6 +232919,7 @@ var AgentSessionManager = class {
|
|
|
232406
232919
|
this.broadcastPatch(sessionId, ConversationPatch.updateStatus("stopped"));
|
|
232407
232920
|
this.eventBus?.emit({ type: "session:status", projectId: session.projectId, branch: session.branch, sessionId: session.id, status: "stopped" });
|
|
232408
232921
|
}
|
|
232922
|
+
this.unloadHistory(session, "model-change");
|
|
232409
232923
|
}
|
|
232410
232924
|
return "ok";
|
|
232411
232925
|
});
|
|
@@ -232417,7 +232931,7 @@ var AgentSessionManager = class {
|
|
|
232417
232931
|
* chip and the agent chip lock at different moments in the same header row.
|
|
232418
232932
|
*/
|
|
232419
232933
|
isModelChangeTooLate(session) {
|
|
232420
|
-
return session.status === "running" &&
|
|
232934
|
+
return session.status === "running" && this.hasHistory(session);
|
|
232421
232935
|
}
|
|
232422
232936
|
/**
|
|
232423
232937
|
* Switch permission mode for a session (preserves conversation history)
|
|
@@ -232427,7 +232941,21 @@ var AgentSessionManager = class {
|
|
|
232427
232941
|
if (!session) {
|
|
232428
232942
|
return false;
|
|
232429
232943
|
}
|
|
232430
|
-
const
|
|
232944
|
+
const release = this.beginProcessStart(session);
|
|
232945
|
+
if (!release) {
|
|
232946
|
+
console.log(`[AgentSession] Refusing to switch mode on ${sessionId}: retention is deleting it`);
|
|
232947
|
+
return false;
|
|
232948
|
+
}
|
|
232949
|
+
try {
|
|
232950
|
+
const absoluteWorktreePath = await this.resolveSessionWorktreePath(session, projectPath);
|
|
232951
|
+
await this.hydrateForSpawn(session);
|
|
232952
|
+
return await this.switchModeInner(session, sessionId, absoluteWorktreePath, newMode, initialMessage);
|
|
232953
|
+
} finally {
|
|
232954
|
+
release();
|
|
232955
|
+
this.unloadIfSpawnAbandoned(session);
|
|
232956
|
+
}
|
|
232957
|
+
}
|
|
232958
|
+
async switchModeInner(session, sessionId, absoluteWorktreePath, newMode, initialMessage) {
|
|
232431
232959
|
console.log(`[AgentSession] Switching session ${sessionId} from ${session.permissionMode} to ${newMode}`);
|
|
232432
232960
|
this.resetCompletion(session);
|
|
232433
232961
|
const proc = session.process;
|
|
@@ -232561,10 +233089,12 @@ var AgentSessionManager = class {
|
|
|
232561
233089
|
return true;
|
|
232562
233090
|
} finally {
|
|
232563
233091
|
release();
|
|
233092
|
+
this.unloadIfSpawnAbandoned(session);
|
|
232564
233093
|
}
|
|
232565
233094
|
}
|
|
232566
233095
|
async wakeDormantSessionInner(session, projectPath, userMessage, userId, origin, notificationDisposition) {
|
|
232567
233096
|
const absoluteWorktreePath = await this.resolveSessionWorktreePath(session, projectPath);
|
|
233097
|
+
await this.hydrateForSpawn(session);
|
|
232568
233098
|
await this.ensureResidentCapacity(
|
|
232569
233099
|
{ projectId: session.projectId, branch: session.branch },
|
|
232570
233100
|
{ excludeSessionId: session.id }
|
|
@@ -232642,9 +233172,10 @@ var AgentSessionManager = class {
|
|
|
232642
233172
|
* Crash repair (restore path): if the previous process died mid-turn, the
|
|
232643
233173
|
* history has no closing turn_end — append one with outcome
|
|
232644
233174
|
* "server_restart" and no duration (the crash time is unknown; the UI
|
|
232645
|
-
* shows "interrupted" instead of a fabricated number).
|
|
232646
|
-
*
|
|
232647
|
-
* The other constructor of turn_end entries is
|
|
233175
|
+
* shows "interrupted" instead of a fabricated number). Returns the session's
|
|
233176
|
+
* updated history metadata, so restore accounts for the entry it wrote
|
|
233177
|
+
* without re-reading. The other constructor of turn_end entries is
|
|
233178
|
+
* endActiveTurn (live paths).
|
|
232648
233179
|
*
|
|
232649
233180
|
* Also records a turn_snapshots row at the repair index (mirrors
|
|
232650
233181
|
* endActiveTurn's hook), capturing the worktree exactly as the crash left
|
|
@@ -232654,32 +233185,39 @@ var AgentSessionManager = class {
|
|
|
232654
233185
|
* this runs on server boot for every restored session and must never
|
|
232655
233186
|
* throw into the restore path.
|
|
232656
233187
|
*/
|
|
232657
|
-
async repairInterruptedTurn(dbSession,
|
|
233188
|
+
async repairInterruptedTurn(dbSession, meta3) {
|
|
232658
233189
|
const sessionId = dbSession.id;
|
|
232659
|
-
|
|
232660
|
-
|
|
232661
|
-
|
|
232662
|
-
|
|
232663
|
-
|
|
232664
|
-
|
|
232665
|
-
|
|
232666
|
-
|
|
232667
|
-
}
|
|
232668
|
-
|
|
232669
|
-
|
|
232670
|
-
|
|
232671
|
-
|
|
232672
|
-
|
|
232673
|
-
|
|
232674
|
-
|
|
232675
|
-
|
|
232676
|
-
|
|
232677
|
-
|
|
233190
|
+
if (meta3.entryCount === 0) return meta3;
|
|
233191
|
+
let landingType;
|
|
233192
|
+
let opening;
|
|
233193
|
+
let cursor = null;
|
|
233194
|
+
let atBoundary = false;
|
|
233195
|
+
while (!atBoundary) {
|
|
233196
|
+
const batch = await this.reader.readBefore(sessionId, cursor, REPAIR_SCAN_BATCH);
|
|
233197
|
+
if (batch.length === 0) break;
|
|
233198
|
+
for (const { message } of batch) {
|
|
233199
|
+
if (message === void 0) {
|
|
233200
|
+
landingType ??= "unparsable";
|
|
233201
|
+
continue;
|
|
233202
|
+
}
|
|
233203
|
+
if (landingType === void 0) {
|
|
233204
|
+
if (message.type === "system") continue;
|
|
233205
|
+
landingType = message.type;
|
|
233206
|
+
if (landingType === "turn_end") return meta3;
|
|
233207
|
+
if (message.type === "user") opening = message;
|
|
233208
|
+
continue;
|
|
233209
|
+
}
|
|
233210
|
+
if (message.type === "turn_end") {
|
|
233211
|
+
atBoundary = true;
|
|
233212
|
+
break;
|
|
233213
|
+
}
|
|
233214
|
+
if (message.type === "user") opening = message;
|
|
232678
233215
|
}
|
|
233216
|
+
cursor = batch[batch.length - 1].entryIndex;
|
|
232679
233217
|
}
|
|
232680
|
-
|
|
232681
|
-
|
|
232682
|
-
);
|
|
233218
|
+
if (landingType === void 0) return meta3;
|
|
233219
|
+
const repairIndex = meta3.maxEntryIndex + 1;
|
|
233220
|
+
const disposition = resolveNotificationDisposition(opening);
|
|
232683
233221
|
const repair = {
|
|
232684
233222
|
type: "turn_end",
|
|
232685
233223
|
timestamp: Date.now(),
|
|
@@ -232719,27 +233257,42 @@ var AgentSessionManager = class {
|
|
|
232719
233257
|
} catch (error48) {
|
|
232720
233258
|
console.warn(`[AgentSession] Turn snapshot lookup failed for ${sessionId}@${repairIndex}:`, error48);
|
|
232721
233259
|
}
|
|
232722
|
-
return
|
|
233260
|
+
return { entryCount: meta3.entryCount + 1, maxEntryIndex: repairIndex };
|
|
232723
233261
|
}
|
|
232724
233262
|
/**
|
|
232725
233263
|
* Restore sessions from database on startup.
|
|
232726
233264
|
* Creates dormant RunningSession objects with process=null for sessions that have entries.
|
|
233265
|
+
*
|
|
233266
|
+
* Restores IDENTITY AND METADATA ONLY — no transcripts. One aggregate query
|
|
233267
|
+
* gives every session its entry count and highest index; the entries
|
|
233268
|
+
* themselves stay in the database until something actually spawns a process
|
|
233269
|
+
* for the session (`hydrateForSpawn`) or reads its history
|
|
233270
|
+
* (`SessionHistoryReader`). That is what turns startup from O(all history)
|
|
233271
|
+
* into O(session count): on the worker this plan was written for, 1385
|
|
233272
|
+
* sessions were carrying 474 MiB of transcript that boot used to parse into
|
|
233273
|
+
* the heap before the server would answer its first request.
|
|
232727
233274
|
*/
|
|
232728
233275
|
async restoreSessionsFromDb() {
|
|
232729
233276
|
const allSessions = await this.storage.agentSessions.getAll();
|
|
233277
|
+
const entryMeta = new Map(
|
|
233278
|
+
(await this.storage.agentSessions.getEntryMetaAll()).map((row) => [
|
|
233279
|
+
row.session_id,
|
|
233280
|
+
{ entryCount: row.cnt, maxEntryIndex: row.max_index }
|
|
233281
|
+
])
|
|
233282
|
+
);
|
|
232730
233283
|
let restoredCount = 0;
|
|
232731
233284
|
let zeroEntryRows = 0;
|
|
232732
233285
|
for (const dbSession of allSessions) {
|
|
232733
233286
|
if (this.sessions.has(dbSession.id)) continue;
|
|
232734
|
-
let
|
|
232735
|
-
if (
|
|
233287
|
+
let meta3 = entryMeta.get(dbSession.id);
|
|
233288
|
+
if (!meta3 || meta3.entryCount === 0) {
|
|
232736
233289
|
zeroEntryRows++;
|
|
232737
233290
|
continue;
|
|
232738
233291
|
}
|
|
232739
233292
|
if (dbSession.status === "running") {
|
|
232740
|
-
|
|
233293
|
+
meta3 = await this.repairInterruptedTurn(dbSession, meta3);
|
|
232741
233294
|
}
|
|
232742
|
-
const store =
|
|
233295
|
+
const store = coldStore(meta3.maxEntryIndex);
|
|
232743
233296
|
const permissionMode = dbSession.permission_mode === "plan" ? "plan" : "edit";
|
|
232744
233297
|
const restoredCheckout = dbSession.workspace_checkout_id ? await this.storage.workspaceRegistry.getCheckoutById(dbSession.workspace_checkout_id) : void 0;
|
|
232745
233298
|
const activityReader = this.storage.agentSessions.getActivityById;
|
|
@@ -232758,6 +233311,11 @@ var AgentSessionManager = class {
|
|
|
232758
233311
|
processStartsInFlight: 0,
|
|
232759
233312
|
historyEpoch: dbSession.history_epoch ?? 0,
|
|
232760
233313
|
store,
|
|
233314
|
+
// Restored cold: no process, so no transcript in memory (B1).
|
|
233315
|
+
hot: false,
|
|
233316
|
+
historyMeta: meta3,
|
|
233317
|
+
hydrating: null,
|
|
233318
|
+
clearGeneration: 0,
|
|
232761
233319
|
subscribers: /* @__PURE__ */ new Set(),
|
|
232762
233320
|
status: "stopped",
|
|
232763
233321
|
buffer: "",
|
|
@@ -232771,6 +233329,8 @@ var AgentSessionManager = class {
|
|
|
232771
233329
|
graceTimer: null,
|
|
232772
233330
|
parkTimer: null,
|
|
232773
233331
|
eventChain: Promise.resolve(),
|
|
233332
|
+
chainPending: 0,
|
|
233333
|
+
historyChain: Promise.resolve(),
|
|
232774
233334
|
bgSpawnHintsThisTurn: 0,
|
|
232775
233335
|
taskStartedThisTurn: 0,
|
|
232776
233336
|
lastActiveAt: Date.now(),
|
|
@@ -232902,7 +233462,7 @@ var AgentSessionManager = class {
|
|
|
232902
233462
|
existingRuntime.branchedFromEntryIndex = repairedEntryIndex;
|
|
232903
233463
|
}
|
|
232904
233464
|
}
|
|
232905
|
-
return { ok: true, sessionId: newId };
|
|
233465
|
+
return { ok: true, sessionId: newId, messages: denseMessagesFromRows(entryRows) };
|
|
232906
233466
|
}
|
|
232907
233467
|
}
|
|
232908
233468
|
let inheritedCheckoutId = source?.workspaceCheckoutId ?? sourceRow?.workspace_checkout_id ?? void 0;
|
|
@@ -232952,7 +233512,11 @@ var AgentSessionManager = class {
|
|
|
232952
233512
|
}
|
|
232953
233513
|
await this.storage.agentSessions.updateTitle(newId, `Branch - ${baseTitle || "Conversation"}`);
|
|
232954
233514
|
this.markTitleResolved(newId);
|
|
232955
|
-
const
|
|
233515
|
+
const branchedMeta = {
|
|
233516
|
+
entryCount: entryRows.length,
|
|
233517
|
+
maxEntryIndex: entryRows[entryRows.length - 1].entry_index
|
|
233518
|
+
};
|
|
233519
|
+
const store = coldStore(branchedMeta.maxEntryIndex);
|
|
232956
233520
|
const branched = {
|
|
232957
233521
|
id: newId,
|
|
232958
233522
|
projectId,
|
|
@@ -232964,6 +233528,10 @@ var AgentSessionManager = class {
|
|
|
232964
233528
|
processStartsInFlight: 0,
|
|
232965
233529
|
historyEpoch: 0,
|
|
232966
233530
|
store,
|
|
233531
|
+
hot: false,
|
|
233532
|
+
historyMeta: branchedMeta,
|
|
233533
|
+
hydrating: null,
|
|
233534
|
+
clearGeneration: 0,
|
|
232967
233535
|
subscribers: /* @__PURE__ */ new Set(),
|
|
232968
233536
|
status: "stopped",
|
|
232969
233537
|
buffer: "",
|
|
@@ -232975,6 +233543,8 @@ var AgentSessionManager = class {
|
|
|
232975
233543
|
graceTimer: null,
|
|
232976
233544
|
parkTimer: null,
|
|
232977
233545
|
eventChain: Promise.resolve(),
|
|
233546
|
+
chainPending: 0,
|
|
233547
|
+
historyChain: Promise.resolve(),
|
|
232978
233548
|
bgSpawnHintsThisTurn: 0,
|
|
232979
233549
|
taskStartedThisTurn: 0,
|
|
232980
233550
|
lastActiveAt: Date.now(),
|
|
@@ -232988,7 +233558,7 @@ var AgentSessionManager = class {
|
|
|
232988
233558
|
this.sessions.set(newId, branched);
|
|
232989
233559
|
await this.emitDerivedBranchActivity(projectId, branch);
|
|
232990
233560
|
console.log(`[AgentSession] branchSession: ${sourceSessionId} \u2192 ${newId} (entries=${entryRows.length}, agentType=${agentType})`);
|
|
232991
|
-
return { ok: true, sessionId: newId };
|
|
233561
|
+
return { ok: true, sessionId: newId, messages: denseMessagesFromRows(entryRows) };
|
|
232992
233562
|
}
|
|
232993
233563
|
/**
|
|
232994
233564
|
* Kill all active session processes and clear state for graceful shutdown
|
|
@@ -235755,7 +236325,7 @@ var ChatSessionManager = class {
|
|
|
235755
236325
|
agentSession = projectSessions.find((s3) => s3.status === "running") ?? projectSessions[0] ?? null;
|
|
235756
236326
|
}
|
|
235757
236327
|
if (agentSession) {
|
|
235758
|
-
const allMessages = agentSessionManager.
|
|
236328
|
+
const allMessages = await agentSessionManager.loadMessages(agentSession.id);
|
|
235759
236329
|
const recent = allMessages.slice(-tailMessages);
|
|
235760
236330
|
localResult = {
|
|
235761
236331
|
sessionId: agentSession.id,
|
|
@@ -238231,7 +238801,7 @@ async function createProjectChatTools(options) {
|
|
|
238231
238801
|
agentType: nullablePreview(local.agent_type, ENUM_CHAR_LIMIT),
|
|
238232
238802
|
model: nullablePreview(local.model, MODEL_CHAR_LIMIT),
|
|
238233
238803
|
processAlive: agentSessionManager.getSessionProcessAlive(local.id),
|
|
238234
|
-
transcript: transcriptPreview(agentSessionManager.
|
|
238804
|
+
transcript: transcriptPreview(await agentSessionManager.loadMessages(local.id))
|
|
238235
238805
|
};
|
|
238236
238806
|
await touch("agent_session", local.id);
|
|
238237
238807
|
return detail2;
|
|
@@ -240761,7 +241331,7 @@ var WorkflowEngine = class {
|
|
|
240761
241331
|
if (sourceSession?.status === "running") {
|
|
240762
241332
|
throw new WorkflowError("source-running", "source session \u6B63\u5728\u8FD0\u884C\uFF0C\u8BF7\u7B49\u5F85\u5F53\u524D turn \u5B8C\u6210\u540E\u518D\u53D1\u8D77 review");
|
|
240763
241333
|
}
|
|
240764
|
-
const entries = this.agentOps.getRawMessages(opts.sourceSessionId);
|
|
241334
|
+
const entries = await this.agentOps.getRawMessages(opts.sourceSessionId);
|
|
240765
241335
|
const turnEndIndex = opts.sourceTurnEndIndex ?? extractLatestTurnEndIndex(entries);
|
|
240766
241336
|
if (turnEndIndex === null) {
|
|
240767
241337
|
throw new WorkflowError("no-completed-turn", "source session \u8FD8\u6CA1\u6709\u5DF2\u5B8C\u6210\u7684 turn \u53EF\u4F9B review");
|
|
@@ -240945,7 +241515,7 @@ var WorkflowEngine = class {
|
|
|
240945
241515
|
const pending = this.pendingActivations.get(runId) ?? parsePreparedContext(run2.prepared_context);
|
|
240946
241516
|
let outcome;
|
|
240947
241517
|
try {
|
|
240948
|
-
const entries = pending ? null : this.agentOps.getRawMessages(run2.source_session_id);
|
|
241518
|
+
const entries = pending ? null : await this.agentOps.getRawMessages(run2.source_session_id);
|
|
240949
241519
|
const prompt = buildReviewerPrompt({
|
|
240950
241520
|
taskContext: pending ? pending.taskContext : extractTaskContextBefore(entries, run2.source_turn_end_index),
|
|
240951
241521
|
originalIntent: pending ? pending.originalIntent : extractFirstUserMessage(entries),
|
|
@@ -241002,7 +241572,7 @@ var WorkflowEngine = class {
|
|
|
241002
241572
|
if (!p2 || p2.role !== "reviewer") return;
|
|
241003
241573
|
const run2 = await this.storage.workflowRuns.getById(p2.runId);
|
|
241004
241574
|
if (!run2 || run2.status !== "waiting_reviewer") return;
|
|
241005
|
-
const entries = this.agentOps.getRawMessages(event.sessionId);
|
|
241575
|
+
const entries = await this.agentOps.getRawMessages(event.sessionId);
|
|
241006
241576
|
const boundary = event.turnEndEntryIndex ?? extractLatestTurnEndIndex(entries) ?? entries.length;
|
|
241007
241577
|
const feedback = extractLastAssistantInTurn(entries, boundary) ?? "(reviewer \u6CA1\u6709\u8F93\u51FA\u53EF\u7528\u7684\u53CD\u9988\u6587\u672C)";
|
|
241008
241578
|
let driftNote = null;
|
|
@@ -245234,7 +245804,8 @@ function collectMemoryStats(deps) {
|
|
|
245234
245804
|
uptime_s: Math.round(process.uptime())
|
|
245235
245805
|
},
|
|
245236
245806
|
patch_cache: deps.remotePatchCache.stats(),
|
|
245237
|
-
process_manager: deps.processManager.logBufferStats()
|
|
245807
|
+
process_manager: deps.processManager.logBufferStats(),
|
|
245808
|
+
...deps.sessionHydration ? { agent_sessions: deps.sessionHydration.hydrationStats() } : {}
|
|
245238
245809
|
};
|
|
245239
245810
|
}
|
|
245240
245811
|
var MemoryStatsReporter = class {
|
|
@@ -245617,7 +246188,7 @@ var sharedServices = async (fastify2, opts) => {
|
|
|
245617
246188
|
sendUserMessage: (...args) => agentSessionManager.sendUserMessage(...args),
|
|
245618
246189
|
setFinalSessionTitle: (sessionId, title) => agentSessionManager.setFinalSessionTitle(sessionId, title),
|
|
245619
246190
|
switchMode: (sessionId, projectPath, mode) => agentSessionManager.switchMode(sessionId, projectPath, mode),
|
|
245620
|
-
getRawMessages: (sessionId) => agentSessionManager.
|
|
246191
|
+
getRawMessages: (sessionId) => agentSessionManager.loadRawMessages(sessionId),
|
|
245621
246192
|
broadcastRawToSession: (sessionId, payload) => agentSessionManager.broadcastRawToSession(sessionId, payload)
|
|
245622
246193
|
};
|
|
245623
246194
|
const workflowEngine = new WorkflowEngine(opts.storage, reviewAgentOps);
|
|
@@ -245675,7 +246246,8 @@ var sharedServices = async (fastify2, opts) => {
|
|
|
245675
246246
|
remoteNotificationSync.enqueue(() => remoteNotificationSync.syncAll({ includeExpired: true }));
|
|
245676
246247
|
const memoryStatsReporter = new MemoryStatsReporter({
|
|
245677
246248
|
remotePatchCache,
|
|
245678
|
-
processManager
|
|
246249
|
+
processManager,
|
|
246250
|
+
sessionHydration: agentSessionManager
|
|
245679
246251
|
});
|
|
245680
246252
|
memoryStatsReporter.start();
|
|
245681
246253
|
fastify2.addHook("onClose", async () => {
|
|
@@ -246267,7 +246839,8 @@ var routes4 = async (fastify2) => {
|
|
|
246267
246839
|
if (!isOperatorRequest(request)) return reply.code(404).send({ error: "Not found" });
|
|
246268
246840
|
return reply.send(collectMemoryStats({
|
|
246269
246841
|
remotePatchCache: fastify2.remotePatchCache,
|
|
246270
|
-
processManager: fastify2.processManager
|
|
246842
|
+
processManager: fastify2.processManager,
|
|
246843
|
+
sessionHydration: fastify2.agentSessionManager
|
|
246271
246844
|
}));
|
|
246272
246845
|
});
|
|
246273
246846
|
fastify2.get("/api/admin/worker-version-stats", async (request, reply) => {
|
|
@@ -249401,48 +249974,9 @@ import { createHash as createHash7, randomUUID as randomUUID16 } from "crypto";
|
|
|
249401
249974
|
// src/protocol/model-suggestions.ts
|
|
249402
249975
|
var MODEL_SUGGESTIONS = {
|
|
249403
249976
|
"claude-code": ["opus", "sonnet", "haiku", "fable"],
|
|
249404
|
-
codex: ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"]
|
|
249977
|
+
codex: ["gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"]
|
|
249405
249978
|
};
|
|
249406
249979
|
|
|
249407
|
-
// src/session-history-window.ts
|
|
249408
|
-
function historyHead(entries, historyEpoch) {
|
|
249409
|
-
let latestEntryIndex = null;
|
|
249410
|
-
let lastTurnEndEntryIndex = null;
|
|
249411
|
-
for (let index = entries.length - 1; index >= 0; index--) {
|
|
249412
|
-
const message = entries[index];
|
|
249413
|
-
if (!message) continue;
|
|
249414
|
-
latestEntryIndex ??= index;
|
|
249415
|
-
if (lastTurnEndEntryIndex === null && message.type === "turn_end") {
|
|
249416
|
-
lastTurnEndEntryIndex = index;
|
|
249417
|
-
}
|
|
249418
|
-
if (latestEntryIndex !== null && lastTurnEndEntryIndex !== null) break;
|
|
249419
|
-
}
|
|
249420
|
-
return { historyEpoch, latestEntryIndex, lastTurnEndEntryIndex };
|
|
249421
|
-
}
|
|
249422
|
-
function buildHistoryWindow(entries, historyEpoch, opts = {}) {
|
|
249423
|
-
const head = historyHead(entries, historyEpoch);
|
|
249424
|
-
const endExclusive = Math.max(0, Math.min(opts.before ?? entries.length, entries.length));
|
|
249425
|
-
const requestedTurns = Math.max(1, Math.min(opts.turns ?? 5, 20));
|
|
249426
|
-
const boundaries = [];
|
|
249427
|
-
for (let index = endExclusive - 1; index >= 0; index--) {
|
|
249428
|
-
if (entries[index]?.type === "turn_end") boundaries.push(index);
|
|
249429
|
-
if (boundaries.length >= requestedTurns + 2) break;
|
|
249430
|
-
}
|
|
249431
|
-
const startIndex = boundaries.length > requestedTurns + 1 ? boundaries[requestedTurns + 1] + 1 : 0;
|
|
249432
|
-
const dense = [];
|
|
249433
|
-
for (let index = startIndex; index < endExclusive; index++) {
|
|
249434
|
-
const message = entries[index];
|
|
249435
|
-
if (message) dense.push({ entryIndex: index, message });
|
|
249436
|
-
}
|
|
249437
|
-
const hasMore = startIndex > 0;
|
|
249438
|
-
return {
|
|
249439
|
-
...head,
|
|
249440
|
-
entries: dense,
|
|
249441
|
-
previousCursor: hasMore ? startIndex : null,
|
|
249442
|
-
hasMore
|
|
249443
|
-
};
|
|
249444
|
-
}
|
|
249445
|
-
|
|
249446
249980
|
// src/routes/agent-session-routes.ts
|
|
249447
249981
|
async function resolveProjectPath(projectId, storage2) {
|
|
249448
249982
|
if (projectId.startsWith("path:")) {
|
|
@@ -249621,7 +250155,7 @@ var routes11 = async (fastify2) => {
|
|
|
249621
250155
|
}
|
|
249622
250156
|
const newSessionId = result.sessionId;
|
|
249623
250157
|
const session = fastify2.agentSessionManager.getSession(newSessionId);
|
|
249624
|
-
const messages =
|
|
250158
|
+
const messages = result.messages;
|
|
249625
250159
|
const dbRow = await fastify2.storage.agentSessions.getById(newSessionId);
|
|
249626
250160
|
return {
|
|
249627
250161
|
ok: true,
|
|
@@ -249698,9 +250232,10 @@ var routes11 = async (fastify2) => {
|
|
|
249698
250232
|
return reply.code(200).send({ session: null, messages: [] });
|
|
249699
250233
|
}
|
|
249700
250234
|
const session = fastify2.agentSessionManager.getSession(sessionId);
|
|
249701
|
-
const messages = fastify2.agentSessionManager.getMessages(sessionId);
|
|
249702
250235
|
const epoch = fastify2.agentSessionManager.getHistoryEpoch(sessionId) ?? 0;
|
|
249703
|
-
const
|
|
250236
|
+
const rawMessages = await fastify2.agentSessionManager.loadRawMessages(sessionId);
|
|
250237
|
+
const messages = rawMessages.filter(Boolean);
|
|
250238
|
+
const historyWindow = historyTurns ? buildHistoryWindow(rawMessages, epoch, { turns: historyTurns }) : void 0;
|
|
249704
250239
|
const projection = await fastify2.storage.agentSessions.getActivityById(sessionId, "session-detail");
|
|
249705
250240
|
if (session?.workspaceCheckoutId && !projection) {
|
|
249706
250241
|
return reply.code(409).send({
|
|
@@ -249866,7 +250401,7 @@ var routes11 = async (fastify2) => {
|
|
|
249866
250401
|
worktreePath: recovered?.checkoutPath ?? null,
|
|
249867
250402
|
processAlive: recovered ? fastify2.agentSessionManager.getSessionProcessAlive(recoveredSessionId) : false
|
|
249868
250403
|
},
|
|
249869
|
-
messages: fastify2.agentSessionManager.
|
|
250404
|
+
messages: await fastify2.agentSessionManager.loadMessages(recoveredSessionId)
|
|
249870
250405
|
});
|
|
249871
250406
|
}
|
|
249872
250407
|
return reply.code(200).send({
|
|
@@ -249883,7 +250418,7 @@ var routes11 = async (fastify2) => {
|
|
|
249883
250418
|
processAlive: fastify2.agentSessionManager.getSessionProcessAlive(sessionId),
|
|
249884
250419
|
...sendBackFields(active)
|
|
249885
250420
|
},
|
|
249886
|
-
messages: fastify2.agentSessionManager.
|
|
250421
|
+
messages: await fastify2.agentSessionManager.loadMessages(sessionId)
|
|
249887
250422
|
});
|
|
249888
250423
|
}
|
|
249889
250424
|
}
|
|
@@ -250234,9 +250769,10 @@ var routes11 = async (fastify2) => {
|
|
|
250234
250769
|
return reply.code(200).send({ session: null, messages: [] });
|
|
250235
250770
|
}
|
|
250236
250771
|
const session = fastify2.agentSessionManager.getSession(sessionId);
|
|
250237
|
-
const messages = fastify2.agentSessionManager.getMessages(sessionId);
|
|
250238
250772
|
const epoch = fastify2.agentSessionManager.getHistoryEpoch(sessionId) ?? 0;
|
|
250239
|
-
const
|
|
250773
|
+
const rawMessages = await fastify2.agentSessionManager.loadRawMessages(sessionId);
|
|
250774
|
+
const messages = rawMessages.filter(Boolean);
|
|
250775
|
+
const historyWindow = historyTurns ? buildHistoryWindow(rawMessages, epoch, { turns: historyTurns }) : void 0;
|
|
250240
250776
|
const effectiveStatus = session?.status || "stopped";
|
|
250241
250777
|
return reply.code(200).send({
|
|
250242
250778
|
session: {
|
|
@@ -250420,7 +250956,7 @@ var routes11 = async (fastify2) => {
|
|
|
250420
250956
|
error: "The workspace checkout binding for this session is unavailable"
|
|
250421
250957
|
});
|
|
250422
250958
|
}
|
|
250423
|
-
const messages = fastify2.agentSessionManager.
|
|
250959
|
+
const messages = await fastify2.agentSessionManager.loadMessages(req.params.sessionId);
|
|
250424
250960
|
return reply.code(200).send({
|
|
250425
250961
|
session: {
|
|
250426
250962
|
id: session.id,
|
|
@@ -250492,9 +251028,10 @@ var routes11 = async (fastify2) => {
|
|
|
250492
251028
|
}
|
|
250493
251029
|
const session = fastify2.agentSessionManager.getSession(req.params.sessionId);
|
|
250494
251030
|
if (!session) return reply.code(404).send({ error: "Session not found" });
|
|
250495
|
-
const
|
|
251031
|
+
const window2 = await fastify2.agentSessionManager.loadHistoryWindow(req.params.sessionId, { before, turns });
|
|
251032
|
+
if (!window2) return reply.code(404).send({ error: "Session not found" });
|
|
250496
251033
|
return reply.code(200).send({
|
|
250497
|
-
...
|
|
251034
|
+
...window2,
|
|
250498
251035
|
status: session.status,
|
|
250499
251036
|
session: {
|
|
250500
251037
|
id: session.id,
|
|
@@ -250560,8 +251097,8 @@ var routes11 = async (fastify2) => {
|
|
|
250560
251097
|
}
|
|
250561
251098
|
const session = fastify2.agentSessionManager.getSession(req.params.sessionId);
|
|
250562
251099
|
if (!session) return reply.code(404).send({ error: "Session not found" });
|
|
250563
|
-
const
|
|
250564
|
-
|
|
251100
|
+
const head = await fastify2.agentSessionManager.loadHistoryHead(req.params.sessionId);
|
|
251101
|
+
if (!head) return reply.code(404).send({ error: "Session not found" });
|
|
250565
251102
|
return reply.code(200).send({
|
|
250566
251103
|
historyEpoch: head.historyEpoch,
|
|
250567
251104
|
latestEntryIndex: head.latestEntryIndex,
|
|
@@ -250576,7 +251113,7 @@ var routes11 = async (fastify2) => {
|
|
|
250576
251113
|
const session = fastify2.agentSessionManager.getSession(req.params.sessionId);
|
|
250577
251114
|
if (!session) return reply.code(404).send({ error: "Session not found" });
|
|
250578
251115
|
return reply.code(200).send({
|
|
250579
|
-
messages: projectMessagesForBrief(fastify2.agentSessionManager.
|
|
251116
|
+
messages: projectMessagesForBrief(await fastify2.agentSessionManager.loadMessages(req.params.sessionId))
|
|
250580
251117
|
});
|
|
250581
251118
|
}
|
|
250582
251119
|
);
|
|
@@ -251385,6 +251922,16 @@ var routes11 = async (fastify2) => {
|
|
|
251385
251922
|
`/api/agent-sessions/${remoteInfo.remoteSessionId}/favorite`,
|
|
251386
251923
|
{ favorited }
|
|
251387
251924
|
);
|
|
251925
|
+
if (result.ok) {
|
|
251926
|
+
try {
|
|
251927
|
+
await fastify2.storage.searchCache.updateCachedSessionFavorited(
|
|
251928
|
+
req.params.sessionId,
|
|
251929
|
+
favorited ? Date.now() : null
|
|
251930
|
+
);
|
|
251931
|
+
} catch (err) {
|
|
251932
|
+
console.error("[API] searchCache.updateCachedSessionFavorited failed:", err);
|
|
251933
|
+
}
|
|
251934
|
+
}
|
|
251388
251935
|
return reply.code(proxyStatus(result)).send(result.data);
|
|
251389
251936
|
}
|
|
251390
251937
|
const session = await fastify2.storage.agentSessions.getById(req.params.sessionId);
|
|
@@ -252467,17 +253014,19 @@ var RECENT_SESSION_LIMIT = 8;
|
|
|
252467
253014
|
var RECENT_RUN_LIMIT = 5;
|
|
252468
253015
|
var PRIORITY_TASK_LIMIT = 5;
|
|
252469
253016
|
var ATTENTION_LIMIT = 10;
|
|
253017
|
+
var STARRED_LIMIT = 50;
|
|
252470
253018
|
var parseDbTimestamp4 = (value) => {
|
|
252471
253019
|
if (!value) return null;
|
|
252472
253020
|
const explicitZone = /(?:Z|[+-]\d\d:\d\d)$/i.test(value);
|
|
252473
253021
|
const parsed = Date.parse(explicitZone ? value : `${value.replace(" ", "T")}Z`);
|
|
252474
253022
|
return Number.isNaN(parsed) ? null : parsed;
|
|
252475
253023
|
};
|
|
252476
|
-
var
|
|
253024
|
+
var mergeBy = (local, remote, limit, rank) => {
|
|
252477
253025
|
const byId = /* @__PURE__ */ new Map();
|
|
252478
253026
|
for (const row of [...local, ...remote]) if (!byId.has(row.id)) byId.set(row.id, row);
|
|
252479
|
-
return [...byId.values()].sort((left, right) => (right
|
|
253027
|
+
return [...byId.values()].sort((left, right) => rank(right) - rank(left) || left.id.localeCompare(right.id)).slice(0, limit);
|
|
252480
253028
|
};
|
|
253029
|
+
var mergeActivity = (local, remote, limit) => mergeBy(local, remote, limit, (row) => row.lastActiveAt ?? 0);
|
|
252481
253030
|
async function getProjectActivity(storage2, projectId, userId) {
|
|
252482
253031
|
const [
|
|
252483
253032
|
recentThreads,
|
|
@@ -252487,6 +253036,8 @@ async function getProjectActivity(storage2, projectId, userId) {
|
|
|
252487
253036
|
priorityTasks,
|
|
252488
253037
|
localAttentionSessions,
|
|
252489
253038
|
remoteAttentionSessions,
|
|
253039
|
+
localStarredSessions,
|
|
253040
|
+
remoteStarredSessions,
|
|
252490
253041
|
attentionRuns,
|
|
252491
253042
|
runningSessions,
|
|
252492
253043
|
runningRuns,
|
|
@@ -252500,6 +253051,8 @@ async function getProjectActivity(storage2, projectId, userId) {
|
|
|
252500
253051
|
storage2.tasks.listPriorityByProject(projectId, PRIORITY_TASK_LIMIT),
|
|
252501
253052
|
storage2.agentSessions.listAttentionActivityByProject(projectId, ATTENTION_LIMIT, "project-activity"),
|
|
252502
253053
|
storage2.searchCache.listRemoteSessionAttentionByProject(projectId, ATTENTION_LIMIT, "project-activity"),
|
|
253054
|
+
storage2.agentSessions.listFavoritedActivityByProject(projectId, STARRED_LIMIT + 1, "project-activity"),
|
|
253055
|
+
storage2.searchCache.listRemoteSessionFavoritesByProject(projectId, STARRED_LIMIT + 1, "project-activity"),
|
|
252503
253056
|
storage2.scheduledTaskRuns.getAttentionByProject(projectId, ATTENTION_LIMIT),
|
|
252504
253057
|
storage2.agentSessions.countRunningActivityByProject(projectId),
|
|
252505
253058
|
storage2.scheduledTaskRuns.countByProjectStatuses(projectId, ["starting", "running"]),
|
|
@@ -252516,6 +253069,14 @@ async function getProjectActivity(storage2, projectId, userId) {
|
|
|
252516
253069
|
remoteAttentionSessions,
|
|
252517
253070
|
ATTENTION_LIMIT
|
|
252518
253071
|
);
|
|
253072
|
+
const starredProbe = mergeBy(
|
|
253073
|
+
localStarredSessions,
|
|
253074
|
+
remoteStarredSessions,
|
|
253075
|
+
STARRED_LIMIT + 1,
|
|
253076
|
+
(row) => row.favoritedAt ?? 0
|
|
253077
|
+
);
|
|
253078
|
+
const starredHasMore = starredProbe.length > STARRED_LIMIT;
|
|
253079
|
+
const starredSessions = starredProbe.slice(0, STARRED_LIMIT);
|
|
252519
253080
|
const attention = [
|
|
252520
253081
|
...attentionSessions.map((session) => ({
|
|
252521
253082
|
type: "agent_session",
|
|
@@ -252540,6 +253101,8 @@ async function getProjectActivity(storage2, projectId, userId) {
|
|
|
252540
253101
|
return {
|
|
252541
253102
|
recentThreads,
|
|
252542
253103
|
recentAgentSessions,
|
|
253104
|
+
starredSessions,
|
|
253105
|
+
starredHasMore,
|
|
252543
253106
|
recentScheduleRuns,
|
|
252544
253107
|
priorityTasks,
|
|
252545
253108
|
attention,
|
|
@@ -253113,7 +253676,7 @@ async function routes21(fastify2) {
|
|
|
253113
253676
|
if (!historyResult.ok) return void 0;
|
|
253114
253677
|
messages = historyResult.data.messages ?? [];
|
|
253115
253678
|
} else {
|
|
253116
|
-
messages = fastify2.agentSessionManager.
|
|
253679
|
+
messages = await fastify2.agentSessionManager.loadMessages(sourceSessionId);
|
|
253117
253680
|
}
|
|
253118
253681
|
return await generateIntentBrief(fastify2.storage, resolveUserId(userId), messages) ?? void 0;
|
|
253119
253682
|
} catch (err) {
|
|
@@ -254854,14 +255417,12 @@ var routes24 = async (fastify2) => {
|
|
|
254854
255417
|
});
|
|
254855
255418
|
return;
|
|
254856
255419
|
}
|
|
254857
|
-
|
|
254858
|
-
|
|
254859
|
-
console.log(`[AgentWS]
|
|
255420
|
+
let unsubscribe = null;
|
|
255421
|
+
socket.on("close", () => {
|
|
255422
|
+
console.log(`[AgentWS] Client disconnected from session ${sessionId}`);
|
|
254860
255423
|
stopHeartbeat();
|
|
254861
|
-
|
|
254862
|
-
|
|
254863
|
-
return;
|
|
254864
|
-
}
|
|
255424
|
+
unsubscribe?.();
|
|
255425
|
+
});
|
|
254865
255426
|
socket.on("message", (data) => {
|
|
254866
255427
|
try {
|
|
254867
255428
|
const message = JSON.parse(data.toString());
|
|
@@ -254874,11 +255435,21 @@ var routes24 = async (fastify2) => {
|
|
|
254874
255435
|
console.error("[AgentWS] Failed to parse message:", error48);
|
|
254875
255436
|
}
|
|
254876
255437
|
});
|
|
254877
|
-
|
|
254878
|
-
|
|
255438
|
+
unsubscribe = await fastify2.agentSessionManager.subscribe(
|
|
255439
|
+
sessionId,
|
|
255440
|
+
socket,
|
|
255441
|
+
{ afterEntryIndex, historyEpoch }
|
|
255442
|
+
);
|
|
255443
|
+
if (!unsubscribe) {
|
|
255444
|
+
console.log(`[AgentWS] Session ${sessionId} unavailable for subscribe`);
|
|
254879
255445
|
stopHeartbeat();
|
|
254880
|
-
|
|
254881
|
-
|
|
255446
|
+
try {
|
|
255447
|
+
socket.send(JSON.stringify({ error: "Session not found" }));
|
|
255448
|
+
socket.close();
|
|
255449
|
+
} catch {
|
|
255450
|
+
}
|
|
255451
|
+
return;
|
|
255452
|
+
}
|
|
254882
255453
|
}
|
|
254883
255454
|
);
|
|
254884
255455
|
fastify2.get(
|