@vibedeckx/linux-x64 0.3.35 → 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 +653 -157
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -187304,12 +187304,25 @@ var createAgentSessionRepos = (kdb, h) => ({
|
|
|
187304
187304
|
getEntries: async (sessionId) => {
|
|
187305
187305
|
return kdb.selectFrom("agent_session_entries").select(["entry_index", "data"]).where("session_id", "=", sessionId).orderBy("entry_index", "asc").execute();
|
|
187306
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
|
+
},
|
|
187307
187312
|
deleteEntries: async (sessionId) => {
|
|
187308
187313
|
await kdb.deleteFrom("agent_session_entries").where("session_id", "=", sessionId).execute();
|
|
187309
187314
|
},
|
|
187310
187315
|
countEntries: async () => {
|
|
187311
187316
|
return kdb.selectFrom("agent_session_entries").select("session_id").select(kdb.fn.countAll().as("cnt")).groupBy("session_id").execute();
|
|
187312
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
|
+
},
|
|
187313
187326
|
listRetentionCandidates: async ({ cutoff, limit, after }) => {
|
|
187314
187327
|
let query = kdb.selectFrom("agent_sessions").select(["id", "project_id", "branch", "activity_at"]).where(retentionPredicate(cutoff));
|
|
187315
187328
|
if (after) {
|
|
@@ -208542,6 +208555,111 @@ var EntryTracker = class {
|
|
|
208542
208555
|
}
|
|
208543
208556
|
};
|
|
208544
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
|
+
|
|
208545
208663
|
// src/utils/worktree-paths.ts
|
|
208546
208664
|
import path7 from "path";
|
|
208547
208665
|
import { createHash } from "crypto";
|
|
@@ -230295,6 +230413,7 @@ ${details}`;
|
|
|
230295
230413
|
return msg;
|
|
230296
230414
|
}
|
|
230297
230415
|
var SUMMARY_TEXT_CAP = 1500;
|
|
230416
|
+
var REPAIR_SCAN_BATCH = 64;
|
|
230298
230417
|
function extractLastAssistantText(entries) {
|
|
230299
230418
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
230300
230419
|
const entry = entries[i];
|
|
@@ -230305,6 +230424,55 @@ function extractLastAssistantText(entries) {
|
|
|
230305
230424
|
}
|
|
230306
230425
|
return void 0;
|
|
230307
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
|
+
};
|
|
230308
230476
|
var WorkspaceCheckoutUnavailableError = class extends Error {
|
|
230309
230477
|
code = "workspace_checkout_unavailable";
|
|
230310
230478
|
statusCode = 409;
|
|
@@ -230362,8 +230530,11 @@ var AgentSessionManager = class {
|
|
|
230362
230530
|
/** Bound on a parked completion (injectable for tests). */
|
|
230363
230531
|
parkTimeoutMs;
|
|
230364
230532
|
workflowSuppressionCheck = null;
|
|
230533
|
+
/** Every read of a cold session's transcript goes through here. */
|
|
230534
|
+
reader;
|
|
230365
230535
|
constructor(storage2, opts) {
|
|
230366
230536
|
this.storage = storage2;
|
|
230537
|
+
this.reader = new SessionHistoryReader(storage2);
|
|
230367
230538
|
this.completionGraceMs = opts?.completionGraceMs ?? COMPLETION_GRACE_MS;
|
|
230368
230539
|
this.parkTimeoutMs = opts?.parkTimeoutMs ?? PARK_TIMEOUT_MS;
|
|
230369
230540
|
}
|
|
@@ -230520,6 +230691,154 @@ var AgentSessionManager = class {
|
|
|
230520
230691
|
const session = this.sessions.get(sessionId);
|
|
230521
230692
|
if (session) this.emitSessionTitle(session.projectId, session.branch, sessionId, title);
|
|
230522
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
|
+
}
|
|
230523
230842
|
isProcessAlive(session) {
|
|
230524
230843
|
return !!session.process && session.process.exitCode === null && !session.dormant;
|
|
230525
230844
|
}
|
|
@@ -230693,7 +231012,7 @@ var AgentSessionManager = class {
|
|
|
230693
231012
|
}
|
|
230694
231013
|
for (const session of this.sessions.values()) {
|
|
230695
231014
|
if (session.projectId === projectId && session.branch === branch) {
|
|
230696
|
-
console.log(`[findExisting] skipDb in-memory match: ${session.id} (entries=${
|
|
231015
|
+
console.log(`[findExisting] skipDb in-memory match: ${session.id} (entries=${this.historyEntryCount(session)})`);
|
|
230697
231016
|
return this.reuseExistingSession(session, projectPath);
|
|
230698
231017
|
}
|
|
230699
231018
|
}
|
|
@@ -230887,6 +231206,11 @@ var AgentSessionManager = class {
|
|
|
230887
231206
|
processStartsInFlight: 0,
|
|
230888
231207
|
historyEpoch: stored?.history_epoch ?? 0,
|
|
230889
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,
|
|
230890
231214
|
subscribers: /* @__PURE__ */ new Set(),
|
|
230891
231215
|
status: "running",
|
|
230892
231216
|
buffer: "",
|
|
@@ -230900,6 +231224,8 @@ var AgentSessionManager = class {
|
|
|
230900
231224
|
graceTimer: null,
|
|
230901
231225
|
parkTimer: null,
|
|
230902
231226
|
eventChain: Promise.resolve(),
|
|
231227
|
+
chainPending: 0,
|
|
231228
|
+
historyChain: Promise.resolve(),
|
|
230903
231229
|
bgSpawnHintsThisTurn: 0,
|
|
230904
231230
|
taskStartedThisTurn: 0,
|
|
230905
231231
|
lastActiveAt: Date.now(),
|
|
@@ -230956,7 +231282,7 @@ var AgentSessionManager = class {
|
|
|
230956
231282
|
* switch-mode route, which carries actual user intent.
|
|
230957
231283
|
*/
|
|
230958
231284
|
async reuseExistingSession(session, projectPath) {
|
|
230959
|
-
const entriesCount =
|
|
231285
|
+
const entriesCount = this.historyEntryCount(session);
|
|
230960
231286
|
this.touchSession(session);
|
|
230961
231287
|
if (session.dormant) {
|
|
230962
231288
|
console.log(`[AgentSession] Returning dormant session ${session.id} (entries=${entriesCount})`);
|
|
@@ -230988,9 +231314,10 @@ var AgentSessionManager = class {
|
|
|
230988
231314
|
* Uses negative PID to signal the process group (requires detached: true at spawn).
|
|
230989
231315
|
*/
|
|
230990
231316
|
killProcess(proc, signal = "SIGTERM") {
|
|
230991
|
-
|
|
231317
|
+
const pid = proc?.pid;
|
|
231318
|
+
if (!proc || pid === void 0 || !Number.isSafeInteger(pid) || pid <= 1) return;
|
|
230992
231319
|
try {
|
|
230993
|
-
process.kill(-
|
|
231320
|
+
process.kill(-pid, signal);
|
|
230994
231321
|
} catch {
|
|
230995
231322
|
try {
|
|
230996
231323
|
proc.kill(signal);
|
|
@@ -231122,6 +231449,7 @@ var AgentSessionManager = class {
|
|
|
231122
231449
|
this.broadcastPatch(session.id, ConversationPatch.updateStatus(session.status));
|
|
231123
231450
|
this.eventBus?.emit({ type: "session:status", projectId: session.projectId, branch: session.branch, sessionId: session.id, status: session.status });
|
|
231124
231451
|
this.broadcastRaw(session.id, { finished: true });
|
|
231452
|
+
this.unloadHistory(session, "process-exit");
|
|
231125
231453
|
}, "process-close");
|
|
231126
231454
|
});
|
|
231127
231455
|
childProcess2.on("error", (error48) => {
|
|
@@ -231149,20 +231477,42 @@ var AgentSessionManager = class {
|
|
|
231149
231477
|
* runs through here so steps never interleave across await points.
|
|
231150
231478
|
*/
|
|
231151
231479
|
enqueueSessionWork(session, work, label) {
|
|
231480
|
+
session.chainPending += 1;
|
|
231152
231481
|
session.eventChain = session.eventChain.then(work).catch((err) => {
|
|
231153
231482
|
console.error(`[AgentSession] Error in ${label} handler for ${session.id}:`, err);
|
|
231483
|
+
}).then(() => {
|
|
231484
|
+
session.chainPending -= 1;
|
|
231154
231485
|
});
|
|
231155
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
|
+
}
|
|
231156
231501
|
/**
|
|
231157
231502
|
* Same serial chain as `enqueueSessionWork`, for a caller that needs the
|
|
231158
231503
|
* result back. The chain itself absorbs the outcome (success or failure) so
|
|
231159
231504
|
* one queued step can never break the next; the caller gets the real promise.
|
|
231160
231505
|
*/
|
|
231161
231506
|
runSerialForResult(session, work) {
|
|
231507
|
+
session.chainPending += 1;
|
|
231162
231508
|
const result = session.eventChain.then(work);
|
|
231163
231509
|
session.eventChain = result.then(
|
|
231164
|
-
() =>
|
|
231165
|
-
|
|
231510
|
+
() => {
|
|
231511
|
+
session.chainPending -= 1;
|
|
231512
|
+
},
|
|
231513
|
+
() => {
|
|
231514
|
+
session.chainPending -= 1;
|
|
231515
|
+
}
|
|
231166
231516
|
);
|
|
231167
231517
|
return result;
|
|
231168
231518
|
}
|
|
@@ -231392,6 +231742,7 @@ var AgentSessionManager = class {
|
|
|
231392
231742
|
}
|
|
231393
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")) {
|
|
231394
231744
|
session.turnOpenSince = timestamp;
|
|
231745
|
+
this.assertHot(session, "turn-open disposition");
|
|
231395
231746
|
session.turnDisposition = resolveNotificationDisposition(findLatestUserEntry(session.store.entries));
|
|
231396
231747
|
}
|
|
231397
231748
|
if (session.status !== "running" && !outOfTurn && (event.type === "text" || event.type === "thinking" || event.type === "tool_use" || event.type === "tool_result" || event.type === "approval_request")) {
|
|
@@ -231655,15 +232006,40 @@ var AgentSessionManager = class {
|
|
|
231655
232006
|
* the two persistence paths can't drift on index allocation or patch shape.
|
|
231656
232007
|
*/
|
|
231657
232008
|
stageEntry(session, message) {
|
|
232009
|
+
this.assertHot(session, "stageEntry");
|
|
231658
232010
|
const index = session.store.indexProvider.next();
|
|
231659
232011
|
session.store.entries[index] = message;
|
|
231660
232012
|
const patch = ConversationPatch.addEntry(index, message);
|
|
231661
232013
|
session.store.patches.push(patch);
|
|
231662
232014
|
return { index, patch };
|
|
231663
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
|
+
}
|
|
231664
232039
|
async pushEntry(sessionId, message, broadcast = true, userId = "local", pushOpts) {
|
|
231665
232040
|
const session = this.sessions.get(sessionId);
|
|
231666
232041
|
if (!session) return -1;
|
|
232042
|
+
if (!session.hot) return this.pushColdEntry(session, message, broadcast, userId, pushOpts);
|
|
231667
232043
|
const { index, patch } = this.stageEntry(session, message);
|
|
231668
232044
|
if (!session.skipDb && message.type !== "assistant") {
|
|
231669
232045
|
await this.persistEntry(session, index, message, userId, { strict: pushOpts?.strictPersist });
|
|
@@ -231673,6 +232049,48 @@ var AgentSessionManager = class {
|
|
|
231673
232049
|
}
|
|
231674
232050
|
return index;
|
|
231675
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
|
+
}
|
|
231676
232094
|
/**
|
|
231677
232095
|
* Persist a `turn_end` together with the attention milestone it earns.
|
|
231678
232096
|
*
|
|
@@ -231698,8 +232116,15 @@ var AgentSessionManager = class {
|
|
|
231698
232116
|
if (entryIndexOverride !== void 0) {
|
|
231699
232117
|
index = entryIndexOverride;
|
|
231700
232118
|
patch = ConversationPatch.addEntry(index, message);
|
|
231701
|
-
} else {
|
|
232119
|
+
} else if (session.hot) {
|
|
231702
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);
|
|
231703
232128
|
}
|
|
231704
232129
|
if (!session.skipDb) {
|
|
231705
232130
|
const activityReader = this.storage.agentSessions.getActivityById;
|
|
@@ -231785,9 +232210,12 @@ var AgentSessionManager = class {
|
|
|
231785
232210
|
if (session.turnOpenSince === null) return null;
|
|
231786
232211
|
const endedAt = Date.now();
|
|
231787
232212
|
const durationMs = endedAt - session.turnOpenSince;
|
|
231788
|
-
|
|
231789
|
-
|
|
231790
|
-
|
|
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
|
+
}
|
|
231791
232219
|
const index = await this.pushTurnEnd(session, outcome, disposition, endedAt, durationMs);
|
|
231792
232220
|
session.turnOpenSince = null;
|
|
231793
232221
|
session.turnDisposition = null;
|
|
@@ -231946,48 +232374,83 @@ var AgentSessionManager = class {
|
|
|
231946
232374
|
}
|
|
231947
232375
|
}
|
|
231948
232376
|
/**
|
|
231949
|
-
*
|
|
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.
|
|
231950
232383
|
*/
|
|
231951
|
-
subscribe(sessionId, ws, opts = {}) {
|
|
232384
|
+
async subscribe(sessionId, ws, opts = {}) {
|
|
231952
232385
|
const session = this.sessions.get(sessionId);
|
|
231953
232386
|
if (!session) {
|
|
231954
232387
|
return null;
|
|
231955
232388
|
}
|
|
232389
|
+
if (!isSocketOpen(ws)) return null;
|
|
231956
232390
|
session.subscribers.add(ws);
|
|
231957
|
-
const
|
|
231958
|
-
ws.send(JSON.stringify({
|
|
231959
|
-
HistorySync: {
|
|
231960
|
-
historyEpoch: session.historyEpoch,
|
|
231961
|
-
reset: opts.historyEpoch !== void 0 && opts.historyEpoch !== session.historyEpoch
|
|
231962
|
-
}
|
|
231963
|
-
}));
|
|
231964
|
-
ws.send(JSON.stringify(this.backgroundTasksMessage(session)));
|
|
231965
|
-
for (const patch of session.store.patches) {
|
|
231966
|
-
const entryIndices = patch.flatMap((op) => {
|
|
231967
|
-
const match2 = op.path.match(/^\/entries\/(\d+)$/);
|
|
231968
|
-
return match2 ? [Number(match2[1])] : [];
|
|
231969
|
-
});
|
|
231970
|
-
if (entryIndices.length > 0 && entryIndices.every((index) => index <= after)) continue;
|
|
231971
|
-
const msg = { JsonPatch: patch };
|
|
231972
|
-
ws.send(JSON.stringify(msg));
|
|
231973
|
-
}
|
|
231974
|
-
ws.send(JSON.stringify({ Ready: true, historyEpoch: session.historyEpoch }));
|
|
231975
|
-
const statusPatch = ConversationPatch.updateStatus(session.status);
|
|
231976
|
-
ws.send(JSON.stringify({ JsonPatch: statusPatch }));
|
|
231977
|
-
return () => {
|
|
232391
|
+
const unsubscribe = () => {
|
|
231978
232392
|
session.subscribers.delete(ws);
|
|
231979
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
|
+
}
|
|
231980
232429
|
}
|
|
231981
232430
|
/**
|
|
231982
|
-
* 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.
|
|
231983
232437
|
*/
|
|
231984
232438
|
getMessages(sessionId) {
|
|
231985
232439
|
const session = this.sessions.get(sessionId);
|
|
231986
|
-
|
|
232440
|
+
if (!session) return [];
|
|
232441
|
+
this.assertHot(session, "getMessages");
|
|
232442
|
+
return session.store.entries.filter(Boolean);
|
|
231987
232443
|
}
|
|
231988
|
-
/**
|
|
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
|
+
*/
|
|
231989
232449
|
getRawMessages(sessionId) {
|
|
231990
|
-
|
|
232450
|
+
const session = this.sessions.get(sessionId);
|
|
232451
|
+
if (!session) return [];
|
|
232452
|
+
this.assertHot(session, "getRawMessages");
|
|
232453
|
+
return session.store.entries;
|
|
231991
232454
|
}
|
|
231992
232455
|
getHistoryEpoch(sessionId) {
|
|
231993
232456
|
return this.sessions.get(sessionId)?.historyEpoch;
|
|
@@ -232092,6 +232555,7 @@ var AgentSessionManager = class {
|
|
|
232092
232555
|
});
|
|
232093
232556
|
}
|
|
232094
232557
|
}
|
|
232558
|
+
this.unloadHistory(session, "stop");
|
|
232095
232559
|
return true;
|
|
232096
232560
|
} catch (error48) {
|
|
232097
232561
|
console.error(`[AgentSession] Failed to stop session:`, error48);
|
|
@@ -232162,6 +232626,7 @@ var AgentSessionManager = class {
|
|
|
232162
232626
|
sessionId: session.id,
|
|
232163
232627
|
status: "stopped"
|
|
232164
232628
|
});
|
|
232629
|
+
this.unloadHistory(session, "hibernate");
|
|
232165
232630
|
return true;
|
|
232166
232631
|
} catch (error48) {
|
|
232167
232632
|
console.error(`[AgentSession] Failed to hibernate session:`, error48);
|
|
@@ -232251,7 +232716,7 @@ var AgentSessionManager = class {
|
|
|
232251
232716
|
if (session?.skipDb) return retained("retained_skip_db");
|
|
232252
232717
|
if ((this.userMessagesInFlight.get(sessionId) ?? 0) > 0) return retained("retained_in_flight");
|
|
232253
232718
|
if (this.retentionDeleting.has(sessionId)) return retained("retained_deleting");
|
|
232254
|
-
if (session
|
|
232719
|
+
if (session && this.hasHistory(session)) return retained("retained_has_entries");
|
|
232255
232720
|
this.retentionDeleting.add(sessionId);
|
|
232256
232721
|
try {
|
|
232257
232722
|
const deleted = await this.storage.agentSessions.deleteIfEmpty(sessionId);
|
|
@@ -232300,19 +232765,18 @@ var AgentSessionManager = class {
|
|
|
232300
232765
|
this.killProcess(proc);
|
|
232301
232766
|
this.emitProcessAlive(session, false);
|
|
232302
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;
|
|
232303
232774
|
if (!session.skipDb) {
|
|
232304
232775
|
await this.storage.agentSessions.deleteEntries(sessionId);
|
|
232305
232776
|
session.historyEpoch = await this.storage.agentSessions.incrementHistoryEpoch(sessionId);
|
|
232306
232777
|
} else {
|
|
232307
232778
|
session.historyEpoch += 1;
|
|
232308
232779
|
}
|
|
232309
|
-
session.store.patches = [];
|
|
232310
|
-
session.store.entries = [];
|
|
232311
|
-
session.store.indexProvider.reset();
|
|
232312
|
-
session.store.toolTracker.clear();
|
|
232313
|
-
session.store.currentAssistantIndex = null;
|
|
232314
|
-
session.buffer = "";
|
|
232315
|
-
session.dormant = false;
|
|
232316
232780
|
session.turnOpenSince = null;
|
|
232317
232781
|
this.touchSession(session);
|
|
232318
232782
|
this.broadcastRaw(sessionId, {
|
|
@@ -232353,8 +232817,7 @@ var AgentSessionManager = class {
|
|
|
232353
232817
|
const session = this.sessions.get(sessionId);
|
|
232354
232818
|
if (!session) return "not_found";
|
|
232355
232819
|
if (session.agentType === agentType) return "ok";
|
|
232356
|
-
|
|
232357
|
-
if (session.status === "running" && hasHistory) return "busy";
|
|
232820
|
+
if (session.status === "running" && this.hasHistory(session)) return "busy";
|
|
232358
232821
|
console.log(`[AgentSession] Switching session ${sessionId} agent ${session.agentType} \u2192 ${agentType} (dormant, history preserved)`);
|
|
232359
232822
|
const proc = session.process;
|
|
232360
232823
|
session.process = null;
|
|
@@ -232386,6 +232849,7 @@ var AgentSessionManager = class {
|
|
|
232386
232849
|
this.broadcastPatch(sessionId, ConversationPatch.updateStatus("stopped"));
|
|
232387
232850
|
this.eventBus?.emit({ type: "session:status", projectId: session.projectId, branch: session.branch, sessionId: session.id, status: "stopped" });
|
|
232388
232851
|
}
|
|
232852
|
+
this.unloadHistory(session, "agent-switch");
|
|
232389
232853
|
return "ok";
|
|
232390
232854
|
}
|
|
232391
232855
|
/**
|
|
@@ -232455,6 +232919,7 @@ var AgentSessionManager = class {
|
|
|
232455
232919
|
this.broadcastPatch(sessionId, ConversationPatch.updateStatus("stopped"));
|
|
232456
232920
|
this.eventBus?.emit({ type: "session:status", projectId: session.projectId, branch: session.branch, sessionId: session.id, status: "stopped" });
|
|
232457
232921
|
}
|
|
232922
|
+
this.unloadHistory(session, "model-change");
|
|
232458
232923
|
}
|
|
232459
232924
|
return "ok";
|
|
232460
232925
|
});
|
|
@@ -232466,7 +232931,7 @@ var AgentSessionManager = class {
|
|
|
232466
232931
|
* chip and the agent chip lock at different moments in the same header row.
|
|
232467
232932
|
*/
|
|
232468
232933
|
isModelChangeTooLate(session) {
|
|
232469
|
-
return session.status === "running" &&
|
|
232934
|
+
return session.status === "running" && this.hasHistory(session);
|
|
232470
232935
|
}
|
|
232471
232936
|
/**
|
|
232472
232937
|
* Switch permission mode for a session (preserves conversation history)
|
|
@@ -232476,7 +232941,21 @@ var AgentSessionManager = class {
|
|
|
232476
232941
|
if (!session) {
|
|
232477
232942
|
return false;
|
|
232478
232943
|
}
|
|
232479
|
-
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) {
|
|
232480
232959
|
console.log(`[AgentSession] Switching session ${sessionId} from ${session.permissionMode} to ${newMode}`);
|
|
232481
232960
|
this.resetCompletion(session);
|
|
232482
232961
|
const proc = session.process;
|
|
@@ -232610,10 +233089,12 @@ var AgentSessionManager = class {
|
|
|
232610
233089
|
return true;
|
|
232611
233090
|
} finally {
|
|
232612
233091
|
release();
|
|
233092
|
+
this.unloadIfSpawnAbandoned(session);
|
|
232613
233093
|
}
|
|
232614
233094
|
}
|
|
232615
233095
|
async wakeDormantSessionInner(session, projectPath, userMessage, userId, origin, notificationDisposition) {
|
|
232616
233096
|
const absoluteWorktreePath = await this.resolveSessionWorktreePath(session, projectPath);
|
|
233097
|
+
await this.hydrateForSpawn(session);
|
|
232617
233098
|
await this.ensureResidentCapacity(
|
|
232618
233099
|
{ projectId: session.projectId, branch: session.branch },
|
|
232619
233100
|
{ excludeSessionId: session.id }
|
|
@@ -232691,9 +233172,10 @@ var AgentSessionManager = class {
|
|
|
232691
233172
|
* Crash repair (restore path): if the previous process died mid-turn, the
|
|
232692
233173
|
* history has no closing turn_end — append one with outcome
|
|
232693
233174
|
* "server_restart" and no duration (the crash time is unknown; the UI
|
|
232694
|
-
* shows "interrupted" instead of a fabricated number).
|
|
232695
|
-
*
|
|
232696
|
-
* 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).
|
|
232697
233179
|
*
|
|
232698
233180
|
* Also records a turn_snapshots row at the repair index (mirrors
|
|
232699
233181
|
* endActiveTurn's hook), capturing the worktree exactly as the crash left
|
|
@@ -232703,32 +233185,39 @@ var AgentSessionManager = class {
|
|
|
232703
233185
|
* this runs on server boot for every restored session and must never
|
|
232704
233186
|
* throw into the restore path.
|
|
232705
233187
|
*/
|
|
232706
|
-
async repairInterruptedTurn(dbSession,
|
|
233188
|
+
async repairInterruptedTurn(dbSession, meta3) {
|
|
232707
233189
|
const sessionId = dbSession.id;
|
|
232708
|
-
|
|
232709
|
-
|
|
232710
|
-
|
|
232711
|
-
|
|
232712
|
-
|
|
232713
|
-
|
|
232714
|
-
|
|
232715
|
-
|
|
232716
|
-
}
|
|
232717
|
-
|
|
232718
|
-
|
|
232719
|
-
|
|
232720
|
-
|
|
232721
|
-
|
|
232722
|
-
|
|
232723
|
-
|
|
232724
|
-
|
|
232725
|
-
|
|
232726
|
-
|
|
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;
|
|
232727
233215
|
}
|
|
233216
|
+
cursor = batch[batch.length - 1].entryIndex;
|
|
232728
233217
|
}
|
|
232729
|
-
|
|
232730
|
-
|
|
232731
|
-
);
|
|
233218
|
+
if (landingType === void 0) return meta3;
|
|
233219
|
+
const repairIndex = meta3.maxEntryIndex + 1;
|
|
233220
|
+
const disposition = resolveNotificationDisposition(opening);
|
|
232732
233221
|
const repair = {
|
|
232733
233222
|
type: "turn_end",
|
|
232734
233223
|
timestamp: Date.now(),
|
|
@@ -232768,27 +233257,42 @@ var AgentSessionManager = class {
|
|
|
232768
233257
|
} catch (error48) {
|
|
232769
233258
|
console.warn(`[AgentSession] Turn snapshot lookup failed for ${sessionId}@${repairIndex}:`, error48);
|
|
232770
233259
|
}
|
|
232771
|
-
return
|
|
233260
|
+
return { entryCount: meta3.entryCount + 1, maxEntryIndex: repairIndex };
|
|
232772
233261
|
}
|
|
232773
233262
|
/**
|
|
232774
233263
|
* Restore sessions from database on startup.
|
|
232775
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.
|
|
232776
233274
|
*/
|
|
232777
233275
|
async restoreSessionsFromDb() {
|
|
232778
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
|
+
);
|
|
232779
233283
|
let restoredCount = 0;
|
|
232780
233284
|
let zeroEntryRows = 0;
|
|
232781
233285
|
for (const dbSession of allSessions) {
|
|
232782
233286
|
if (this.sessions.has(dbSession.id)) continue;
|
|
232783
|
-
let
|
|
232784
|
-
if (
|
|
233287
|
+
let meta3 = entryMeta.get(dbSession.id);
|
|
233288
|
+
if (!meta3 || meta3.entryCount === 0) {
|
|
232785
233289
|
zeroEntryRows++;
|
|
232786
233290
|
continue;
|
|
232787
233291
|
}
|
|
232788
233292
|
if (dbSession.status === "running") {
|
|
232789
|
-
|
|
233293
|
+
meta3 = await this.repairInterruptedTurn(dbSession, meta3);
|
|
232790
233294
|
}
|
|
232791
|
-
const store =
|
|
233295
|
+
const store = coldStore(meta3.maxEntryIndex);
|
|
232792
233296
|
const permissionMode = dbSession.permission_mode === "plan" ? "plan" : "edit";
|
|
232793
233297
|
const restoredCheckout = dbSession.workspace_checkout_id ? await this.storage.workspaceRegistry.getCheckoutById(dbSession.workspace_checkout_id) : void 0;
|
|
232794
233298
|
const activityReader = this.storage.agentSessions.getActivityById;
|
|
@@ -232807,6 +233311,11 @@ var AgentSessionManager = class {
|
|
|
232807
233311
|
processStartsInFlight: 0,
|
|
232808
233312
|
historyEpoch: dbSession.history_epoch ?? 0,
|
|
232809
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,
|
|
232810
233319
|
subscribers: /* @__PURE__ */ new Set(),
|
|
232811
233320
|
status: "stopped",
|
|
232812
233321
|
buffer: "",
|
|
@@ -232820,6 +233329,8 @@ var AgentSessionManager = class {
|
|
|
232820
233329
|
graceTimer: null,
|
|
232821
233330
|
parkTimer: null,
|
|
232822
233331
|
eventChain: Promise.resolve(),
|
|
233332
|
+
chainPending: 0,
|
|
233333
|
+
historyChain: Promise.resolve(),
|
|
232823
233334
|
bgSpawnHintsThisTurn: 0,
|
|
232824
233335
|
taskStartedThisTurn: 0,
|
|
232825
233336
|
lastActiveAt: Date.now(),
|
|
@@ -232951,7 +233462,7 @@ var AgentSessionManager = class {
|
|
|
232951
233462
|
existingRuntime.branchedFromEntryIndex = repairedEntryIndex;
|
|
232952
233463
|
}
|
|
232953
233464
|
}
|
|
232954
|
-
return { ok: true, sessionId: newId };
|
|
233465
|
+
return { ok: true, sessionId: newId, messages: denseMessagesFromRows(entryRows) };
|
|
232955
233466
|
}
|
|
232956
233467
|
}
|
|
232957
233468
|
let inheritedCheckoutId = source?.workspaceCheckoutId ?? sourceRow?.workspace_checkout_id ?? void 0;
|
|
@@ -233001,7 +233512,11 @@ var AgentSessionManager = class {
|
|
|
233001
233512
|
}
|
|
233002
233513
|
await this.storage.agentSessions.updateTitle(newId, `Branch - ${baseTitle || "Conversation"}`);
|
|
233003
233514
|
this.markTitleResolved(newId);
|
|
233004
|
-
const
|
|
233515
|
+
const branchedMeta = {
|
|
233516
|
+
entryCount: entryRows.length,
|
|
233517
|
+
maxEntryIndex: entryRows[entryRows.length - 1].entry_index
|
|
233518
|
+
};
|
|
233519
|
+
const store = coldStore(branchedMeta.maxEntryIndex);
|
|
233005
233520
|
const branched = {
|
|
233006
233521
|
id: newId,
|
|
233007
233522
|
projectId,
|
|
@@ -233013,6 +233528,10 @@ var AgentSessionManager = class {
|
|
|
233013
233528
|
processStartsInFlight: 0,
|
|
233014
233529
|
historyEpoch: 0,
|
|
233015
233530
|
store,
|
|
233531
|
+
hot: false,
|
|
233532
|
+
historyMeta: branchedMeta,
|
|
233533
|
+
hydrating: null,
|
|
233534
|
+
clearGeneration: 0,
|
|
233016
233535
|
subscribers: /* @__PURE__ */ new Set(),
|
|
233017
233536
|
status: "stopped",
|
|
233018
233537
|
buffer: "",
|
|
@@ -233024,6 +233543,8 @@ var AgentSessionManager = class {
|
|
|
233024
233543
|
graceTimer: null,
|
|
233025
233544
|
parkTimer: null,
|
|
233026
233545
|
eventChain: Promise.resolve(),
|
|
233546
|
+
chainPending: 0,
|
|
233547
|
+
historyChain: Promise.resolve(),
|
|
233027
233548
|
bgSpawnHintsThisTurn: 0,
|
|
233028
233549
|
taskStartedThisTurn: 0,
|
|
233029
233550
|
lastActiveAt: Date.now(),
|
|
@@ -233037,7 +233558,7 @@ var AgentSessionManager = class {
|
|
|
233037
233558
|
this.sessions.set(newId, branched);
|
|
233038
233559
|
await this.emitDerivedBranchActivity(projectId, branch);
|
|
233039
233560
|
console.log(`[AgentSession] branchSession: ${sourceSessionId} \u2192 ${newId} (entries=${entryRows.length}, agentType=${agentType})`);
|
|
233040
|
-
return { ok: true, sessionId: newId };
|
|
233561
|
+
return { ok: true, sessionId: newId, messages: denseMessagesFromRows(entryRows) };
|
|
233041
233562
|
}
|
|
233042
233563
|
/**
|
|
233043
233564
|
* Kill all active session processes and clear state for graceful shutdown
|
|
@@ -235804,7 +236325,7 @@ var ChatSessionManager = class {
|
|
|
235804
236325
|
agentSession = projectSessions.find((s3) => s3.status === "running") ?? projectSessions[0] ?? null;
|
|
235805
236326
|
}
|
|
235806
236327
|
if (agentSession) {
|
|
235807
|
-
const allMessages = agentSessionManager.
|
|
236328
|
+
const allMessages = await agentSessionManager.loadMessages(agentSession.id);
|
|
235808
236329
|
const recent = allMessages.slice(-tailMessages);
|
|
235809
236330
|
localResult = {
|
|
235810
236331
|
sessionId: agentSession.id,
|
|
@@ -238280,7 +238801,7 @@ async function createProjectChatTools(options) {
|
|
|
238280
238801
|
agentType: nullablePreview(local.agent_type, ENUM_CHAR_LIMIT),
|
|
238281
238802
|
model: nullablePreview(local.model, MODEL_CHAR_LIMIT),
|
|
238282
238803
|
processAlive: agentSessionManager.getSessionProcessAlive(local.id),
|
|
238283
|
-
transcript: transcriptPreview(agentSessionManager.
|
|
238804
|
+
transcript: transcriptPreview(await agentSessionManager.loadMessages(local.id))
|
|
238284
238805
|
};
|
|
238285
238806
|
await touch("agent_session", local.id);
|
|
238286
238807
|
return detail2;
|
|
@@ -240810,7 +241331,7 @@ var WorkflowEngine = class {
|
|
|
240810
241331
|
if (sourceSession?.status === "running") {
|
|
240811
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");
|
|
240812
241333
|
}
|
|
240813
|
-
const entries = this.agentOps.getRawMessages(opts.sourceSessionId);
|
|
241334
|
+
const entries = await this.agentOps.getRawMessages(opts.sourceSessionId);
|
|
240814
241335
|
const turnEndIndex = opts.sourceTurnEndIndex ?? extractLatestTurnEndIndex(entries);
|
|
240815
241336
|
if (turnEndIndex === null) {
|
|
240816
241337
|
throw new WorkflowError("no-completed-turn", "source session \u8FD8\u6CA1\u6709\u5DF2\u5B8C\u6210\u7684 turn \u53EF\u4F9B review");
|
|
@@ -240994,7 +241515,7 @@ var WorkflowEngine = class {
|
|
|
240994
241515
|
const pending = this.pendingActivations.get(runId) ?? parsePreparedContext(run2.prepared_context);
|
|
240995
241516
|
let outcome;
|
|
240996
241517
|
try {
|
|
240997
|
-
const entries = pending ? null : this.agentOps.getRawMessages(run2.source_session_id);
|
|
241518
|
+
const entries = pending ? null : await this.agentOps.getRawMessages(run2.source_session_id);
|
|
240998
241519
|
const prompt = buildReviewerPrompt({
|
|
240999
241520
|
taskContext: pending ? pending.taskContext : extractTaskContextBefore(entries, run2.source_turn_end_index),
|
|
241000
241521
|
originalIntent: pending ? pending.originalIntent : extractFirstUserMessage(entries),
|
|
@@ -241051,7 +241572,7 @@ var WorkflowEngine = class {
|
|
|
241051
241572
|
if (!p2 || p2.role !== "reviewer") return;
|
|
241052
241573
|
const run2 = await this.storage.workflowRuns.getById(p2.runId);
|
|
241053
241574
|
if (!run2 || run2.status !== "waiting_reviewer") return;
|
|
241054
|
-
const entries = this.agentOps.getRawMessages(event.sessionId);
|
|
241575
|
+
const entries = await this.agentOps.getRawMessages(event.sessionId);
|
|
241055
241576
|
const boundary = event.turnEndEntryIndex ?? extractLatestTurnEndIndex(entries) ?? entries.length;
|
|
241056
241577
|
const feedback = extractLastAssistantInTurn(entries, boundary) ?? "(reviewer \u6CA1\u6709\u8F93\u51FA\u53EF\u7528\u7684\u53CD\u9988\u6587\u672C)";
|
|
241057
241578
|
let driftNote = null;
|
|
@@ -245283,7 +245804,8 @@ function collectMemoryStats(deps) {
|
|
|
245283
245804
|
uptime_s: Math.round(process.uptime())
|
|
245284
245805
|
},
|
|
245285
245806
|
patch_cache: deps.remotePatchCache.stats(),
|
|
245286
|
-
process_manager: deps.processManager.logBufferStats()
|
|
245807
|
+
process_manager: deps.processManager.logBufferStats(),
|
|
245808
|
+
...deps.sessionHydration ? { agent_sessions: deps.sessionHydration.hydrationStats() } : {}
|
|
245287
245809
|
};
|
|
245288
245810
|
}
|
|
245289
245811
|
var MemoryStatsReporter = class {
|
|
@@ -245666,7 +246188,7 @@ var sharedServices = async (fastify2, opts) => {
|
|
|
245666
246188
|
sendUserMessage: (...args) => agentSessionManager.sendUserMessage(...args),
|
|
245667
246189
|
setFinalSessionTitle: (sessionId, title) => agentSessionManager.setFinalSessionTitle(sessionId, title),
|
|
245668
246190
|
switchMode: (sessionId, projectPath, mode) => agentSessionManager.switchMode(sessionId, projectPath, mode),
|
|
245669
|
-
getRawMessages: (sessionId) => agentSessionManager.
|
|
246191
|
+
getRawMessages: (sessionId) => agentSessionManager.loadRawMessages(sessionId),
|
|
245670
246192
|
broadcastRawToSession: (sessionId, payload) => agentSessionManager.broadcastRawToSession(sessionId, payload)
|
|
245671
246193
|
};
|
|
245672
246194
|
const workflowEngine = new WorkflowEngine(opts.storage, reviewAgentOps);
|
|
@@ -245724,7 +246246,8 @@ var sharedServices = async (fastify2, opts) => {
|
|
|
245724
246246
|
remoteNotificationSync.enqueue(() => remoteNotificationSync.syncAll({ includeExpired: true }));
|
|
245725
246247
|
const memoryStatsReporter = new MemoryStatsReporter({
|
|
245726
246248
|
remotePatchCache,
|
|
245727
|
-
processManager
|
|
246249
|
+
processManager,
|
|
246250
|
+
sessionHydration: agentSessionManager
|
|
245728
246251
|
});
|
|
245729
246252
|
memoryStatsReporter.start();
|
|
245730
246253
|
fastify2.addHook("onClose", async () => {
|
|
@@ -246316,7 +246839,8 @@ var routes4 = async (fastify2) => {
|
|
|
246316
246839
|
if (!isOperatorRequest(request)) return reply.code(404).send({ error: "Not found" });
|
|
246317
246840
|
return reply.send(collectMemoryStats({
|
|
246318
246841
|
remotePatchCache: fastify2.remotePatchCache,
|
|
246319
|
-
processManager: fastify2.processManager
|
|
246842
|
+
processManager: fastify2.processManager,
|
|
246843
|
+
sessionHydration: fastify2.agentSessionManager
|
|
246320
246844
|
}));
|
|
246321
246845
|
});
|
|
246322
246846
|
fastify2.get("/api/admin/worker-version-stats", async (request, reply) => {
|
|
@@ -249450,48 +249974,9 @@ import { createHash as createHash7, randomUUID as randomUUID16 } from "crypto";
|
|
|
249450
249974
|
// src/protocol/model-suggestions.ts
|
|
249451
249975
|
var MODEL_SUGGESTIONS = {
|
|
249452
249976
|
"claude-code": ["opus", "sonnet", "haiku", "fable"],
|
|
249453
|
-
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"]
|
|
249454
249978
|
};
|
|
249455
249979
|
|
|
249456
|
-
// src/session-history-window.ts
|
|
249457
|
-
function historyHead(entries, historyEpoch) {
|
|
249458
|
-
let latestEntryIndex = null;
|
|
249459
|
-
let lastTurnEndEntryIndex = null;
|
|
249460
|
-
for (let index = entries.length - 1; index >= 0; index--) {
|
|
249461
|
-
const message = entries[index];
|
|
249462
|
-
if (!message) continue;
|
|
249463
|
-
latestEntryIndex ??= index;
|
|
249464
|
-
if (lastTurnEndEntryIndex === null && message.type === "turn_end") {
|
|
249465
|
-
lastTurnEndEntryIndex = index;
|
|
249466
|
-
}
|
|
249467
|
-
if (latestEntryIndex !== null && lastTurnEndEntryIndex !== null) break;
|
|
249468
|
-
}
|
|
249469
|
-
return { historyEpoch, latestEntryIndex, lastTurnEndEntryIndex };
|
|
249470
|
-
}
|
|
249471
|
-
function buildHistoryWindow(entries, historyEpoch, opts = {}) {
|
|
249472
|
-
const head = historyHead(entries, historyEpoch);
|
|
249473
|
-
const endExclusive = Math.max(0, Math.min(opts.before ?? entries.length, entries.length));
|
|
249474
|
-
const requestedTurns = Math.max(1, Math.min(opts.turns ?? 5, 20));
|
|
249475
|
-
const boundaries = [];
|
|
249476
|
-
for (let index = endExclusive - 1; index >= 0; index--) {
|
|
249477
|
-
if (entries[index]?.type === "turn_end") boundaries.push(index);
|
|
249478
|
-
if (boundaries.length >= requestedTurns + 2) break;
|
|
249479
|
-
}
|
|
249480
|
-
const startIndex = boundaries.length > requestedTurns + 1 ? boundaries[requestedTurns + 1] + 1 : 0;
|
|
249481
|
-
const dense = [];
|
|
249482
|
-
for (let index = startIndex; index < endExclusive; index++) {
|
|
249483
|
-
const message = entries[index];
|
|
249484
|
-
if (message) dense.push({ entryIndex: index, message });
|
|
249485
|
-
}
|
|
249486
|
-
const hasMore = startIndex > 0;
|
|
249487
|
-
return {
|
|
249488
|
-
...head,
|
|
249489
|
-
entries: dense,
|
|
249490
|
-
previousCursor: hasMore ? startIndex : null,
|
|
249491
|
-
hasMore
|
|
249492
|
-
};
|
|
249493
|
-
}
|
|
249494
|
-
|
|
249495
249980
|
// src/routes/agent-session-routes.ts
|
|
249496
249981
|
async function resolveProjectPath(projectId, storage2) {
|
|
249497
249982
|
if (projectId.startsWith("path:")) {
|
|
@@ -249670,7 +250155,7 @@ var routes11 = async (fastify2) => {
|
|
|
249670
250155
|
}
|
|
249671
250156
|
const newSessionId = result.sessionId;
|
|
249672
250157
|
const session = fastify2.agentSessionManager.getSession(newSessionId);
|
|
249673
|
-
const messages =
|
|
250158
|
+
const messages = result.messages;
|
|
249674
250159
|
const dbRow = await fastify2.storage.agentSessions.getById(newSessionId);
|
|
249675
250160
|
return {
|
|
249676
250161
|
ok: true,
|
|
@@ -249747,9 +250232,10 @@ var routes11 = async (fastify2) => {
|
|
|
249747
250232
|
return reply.code(200).send({ session: null, messages: [] });
|
|
249748
250233
|
}
|
|
249749
250234
|
const session = fastify2.agentSessionManager.getSession(sessionId);
|
|
249750
|
-
const messages = fastify2.agentSessionManager.getMessages(sessionId);
|
|
249751
250235
|
const epoch = fastify2.agentSessionManager.getHistoryEpoch(sessionId) ?? 0;
|
|
249752
|
-
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;
|
|
249753
250239
|
const projection = await fastify2.storage.agentSessions.getActivityById(sessionId, "session-detail");
|
|
249754
250240
|
if (session?.workspaceCheckoutId && !projection) {
|
|
249755
250241
|
return reply.code(409).send({
|
|
@@ -249915,7 +250401,7 @@ var routes11 = async (fastify2) => {
|
|
|
249915
250401
|
worktreePath: recovered?.checkoutPath ?? null,
|
|
249916
250402
|
processAlive: recovered ? fastify2.agentSessionManager.getSessionProcessAlive(recoveredSessionId) : false
|
|
249917
250403
|
},
|
|
249918
|
-
messages: fastify2.agentSessionManager.
|
|
250404
|
+
messages: await fastify2.agentSessionManager.loadMessages(recoveredSessionId)
|
|
249919
250405
|
});
|
|
249920
250406
|
}
|
|
249921
250407
|
return reply.code(200).send({
|
|
@@ -249932,7 +250418,7 @@ var routes11 = async (fastify2) => {
|
|
|
249932
250418
|
processAlive: fastify2.agentSessionManager.getSessionProcessAlive(sessionId),
|
|
249933
250419
|
...sendBackFields(active)
|
|
249934
250420
|
},
|
|
249935
|
-
messages: fastify2.agentSessionManager.
|
|
250421
|
+
messages: await fastify2.agentSessionManager.loadMessages(sessionId)
|
|
249936
250422
|
});
|
|
249937
250423
|
}
|
|
249938
250424
|
}
|
|
@@ -250283,9 +250769,10 @@ var routes11 = async (fastify2) => {
|
|
|
250283
250769
|
return reply.code(200).send({ session: null, messages: [] });
|
|
250284
250770
|
}
|
|
250285
250771
|
const session = fastify2.agentSessionManager.getSession(sessionId);
|
|
250286
|
-
const messages = fastify2.agentSessionManager.getMessages(sessionId);
|
|
250287
250772
|
const epoch = fastify2.agentSessionManager.getHistoryEpoch(sessionId) ?? 0;
|
|
250288
|
-
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;
|
|
250289
250776
|
const effectiveStatus = session?.status || "stopped";
|
|
250290
250777
|
return reply.code(200).send({
|
|
250291
250778
|
session: {
|
|
@@ -250469,7 +250956,7 @@ var routes11 = async (fastify2) => {
|
|
|
250469
250956
|
error: "The workspace checkout binding for this session is unavailable"
|
|
250470
250957
|
});
|
|
250471
250958
|
}
|
|
250472
|
-
const messages = fastify2.agentSessionManager.
|
|
250959
|
+
const messages = await fastify2.agentSessionManager.loadMessages(req.params.sessionId);
|
|
250473
250960
|
return reply.code(200).send({
|
|
250474
250961
|
session: {
|
|
250475
250962
|
id: session.id,
|
|
@@ -250541,9 +251028,10 @@ var routes11 = async (fastify2) => {
|
|
|
250541
251028
|
}
|
|
250542
251029
|
const session = fastify2.agentSessionManager.getSession(req.params.sessionId);
|
|
250543
251030
|
if (!session) return reply.code(404).send({ error: "Session not found" });
|
|
250544
|
-
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" });
|
|
250545
251033
|
return reply.code(200).send({
|
|
250546
|
-
...
|
|
251034
|
+
...window2,
|
|
250547
251035
|
status: session.status,
|
|
250548
251036
|
session: {
|
|
250549
251037
|
id: session.id,
|
|
@@ -250609,8 +251097,8 @@ var routes11 = async (fastify2) => {
|
|
|
250609
251097
|
}
|
|
250610
251098
|
const session = fastify2.agentSessionManager.getSession(req.params.sessionId);
|
|
250611
251099
|
if (!session) return reply.code(404).send({ error: "Session not found" });
|
|
250612
|
-
const
|
|
250613
|
-
|
|
251100
|
+
const head = await fastify2.agentSessionManager.loadHistoryHead(req.params.sessionId);
|
|
251101
|
+
if (!head) return reply.code(404).send({ error: "Session not found" });
|
|
250614
251102
|
return reply.code(200).send({
|
|
250615
251103
|
historyEpoch: head.historyEpoch,
|
|
250616
251104
|
latestEntryIndex: head.latestEntryIndex,
|
|
@@ -250625,7 +251113,7 @@ var routes11 = async (fastify2) => {
|
|
|
250625
251113
|
const session = fastify2.agentSessionManager.getSession(req.params.sessionId);
|
|
250626
251114
|
if (!session) return reply.code(404).send({ error: "Session not found" });
|
|
250627
251115
|
return reply.code(200).send({
|
|
250628
|
-
messages: projectMessagesForBrief(fastify2.agentSessionManager.
|
|
251116
|
+
messages: projectMessagesForBrief(await fastify2.agentSessionManager.loadMessages(req.params.sessionId))
|
|
250629
251117
|
});
|
|
250630
251118
|
}
|
|
250631
251119
|
);
|
|
@@ -253188,7 +253676,7 @@ async function routes21(fastify2) {
|
|
|
253188
253676
|
if (!historyResult.ok) return void 0;
|
|
253189
253677
|
messages = historyResult.data.messages ?? [];
|
|
253190
253678
|
} else {
|
|
253191
|
-
messages = fastify2.agentSessionManager.
|
|
253679
|
+
messages = await fastify2.agentSessionManager.loadMessages(sourceSessionId);
|
|
253192
253680
|
}
|
|
253193
253681
|
return await generateIntentBrief(fastify2.storage, resolveUserId(userId), messages) ?? void 0;
|
|
253194
253682
|
} catch (err) {
|
|
@@ -254929,14 +255417,12 @@ var routes24 = async (fastify2) => {
|
|
|
254929
255417
|
});
|
|
254930
255418
|
return;
|
|
254931
255419
|
}
|
|
254932
|
-
|
|
254933
|
-
|
|
254934
|
-
console.log(`[AgentWS]
|
|
255420
|
+
let unsubscribe = null;
|
|
255421
|
+
socket.on("close", () => {
|
|
255422
|
+
console.log(`[AgentWS] Client disconnected from session ${sessionId}`);
|
|
254935
255423
|
stopHeartbeat();
|
|
254936
|
-
|
|
254937
|
-
|
|
254938
|
-
return;
|
|
254939
|
-
}
|
|
255424
|
+
unsubscribe?.();
|
|
255425
|
+
});
|
|
254940
255426
|
socket.on("message", (data) => {
|
|
254941
255427
|
try {
|
|
254942
255428
|
const message = JSON.parse(data.toString());
|
|
@@ -254949,11 +255435,21 @@ var routes24 = async (fastify2) => {
|
|
|
254949
255435
|
console.error("[AgentWS] Failed to parse message:", error48);
|
|
254950
255436
|
}
|
|
254951
255437
|
});
|
|
254952
|
-
|
|
254953
|
-
|
|
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`);
|
|
254954
255445
|
stopHeartbeat();
|
|
254955
|
-
|
|
254956
|
-
|
|
255446
|
+
try {
|
|
255447
|
+
socket.send(JSON.stringify({ error: "Session not found" }));
|
|
255448
|
+
socket.close();
|
|
255449
|
+
} catch {
|
|
255450
|
+
}
|
|
255451
|
+
return;
|
|
255452
|
+
}
|
|
254957
255453
|
}
|
|
254958
255454
|
);
|
|
254959
255455
|
fastify2.get(
|