@pikiloom/kernel 0.3.5 → 0.3.6
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/drivers/claude.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare function claudeBgAgentHoldCapMs(): number;
|
|
|
24
24
|
export declare function claudeTurnHasAgentBackground(s: any): boolean;
|
|
25
25
|
export declare function claudeBgHoldRecheckMs(): number;
|
|
26
26
|
export declare function claudeBgSettleQuietMs(): number;
|
|
27
|
-
export declare function claudeModelStallMs(
|
|
27
|
+
export declare function claudeModelStallMs(): number;
|
|
28
28
|
export declare const CLAUDE_TRUNCATED_RECOVERY_PROMPT: string;
|
|
29
29
|
export declare function claudeTruncatedRecoveryEnabled(): boolean;
|
|
30
30
|
export declare function isClaudeSyntheticResumeNoise(text: string): boolean;
|
package/dist/drivers/claude.js
CHANGED
|
@@ -193,7 +193,7 @@ export class ClaudeDriver {
|
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
195
|
settleResult({ stopReason: 'stalled', kill: false, ok: false });
|
|
196
|
-
}, claudeModelStallMs(
|
|
196
|
+
}, claudeModelStallMs());
|
|
197
197
|
unref(modelStallTimer);
|
|
198
198
|
};
|
|
199
199
|
try {
|
|
@@ -801,22 +801,15 @@ export function claudeBgSettleQuietMs() {
|
|
|
801
801
|
return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_BG_SETTLE_QUIET_DEFAULT_MS;
|
|
802
802
|
}
|
|
803
803
|
// How long the model may stay COMPLETELY silent after a tool_result (control handed back to it,
|
|
804
|
-
// no background pending) before the driver gives up and settles the turn as 'stalled'.
|
|
805
|
-
// generous: subscription accounts stream
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
//
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
// tool_result yet). Override with PIKILOOM_CLAUDE_MODEL_STALL_MS (wins over the ladder).
|
|
812
|
-
const CLAUDE_MODEL_STALL_DEFAULT_MS = 300_000;
|
|
813
|
-
const CLAUDE_MODEL_STALL_DEEP_MS = 600_000;
|
|
814
|
-
const CLAUDE_DEEP_REASONING_EFFORTS = new Set(['high', 'xhigh', 'max', 'ultra']);
|
|
815
|
-
export function claudeModelStallMs(effort) {
|
|
804
|
+
// no background pending) before the driver gives up and settles the turn as 'stalled'. Deliberately
|
|
805
|
+
// generous: legitimate silent extended-thinking (subscription accounts stream no thinking text) and
|
|
806
|
+
// slow providers must not trip it, and a still-running tool never trips it (it has no tool_result
|
|
807
|
+
// yet). This is the backstop for a turn that would otherwise hang forever with the answer never
|
|
808
|
+
// delivered. Override with PIKILOOM_CLAUDE_MODEL_STALL_MS.
|
|
809
|
+
const CLAUDE_MODEL_STALL_DEFAULT_MS = 120_000;
|
|
810
|
+
export function claudeModelStallMs() {
|
|
816
811
|
const raw = Number(process.env.PIKILOOM_CLAUDE_MODEL_STALL_MS);
|
|
817
|
-
|
|
818
|
-
return raw;
|
|
819
|
-
return effort && CLAUDE_DEEP_REASONING_EFFORTS.has(effort) ? CLAUDE_MODEL_STALL_DEEP_MS : CLAUDE_MODEL_STALL_DEFAULT_MS;
|
|
812
|
+
return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_MODEL_STALL_DEFAULT_MS;
|
|
820
813
|
}
|
|
821
814
|
// In-process self-heal for a truncated turn: when a clean result lands while the tool loop is
|
|
822
815
|
// still dangling (the model's closing round came back empty), the stdin is still open — inject
|
package/dist/drivers/native.js
CHANGED
|
@@ -21,6 +21,53 @@ function statSafe(p) {
|
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
// A transcript's title/preview/model/effort/turns all live in a BOUNDED slice of the file: the HEAD
|
|
25
|
+
// (first prompt, model, ai-title) and the TAIL (latest reply, last turn_context). We read only those
|
|
26
|
+
// slices — never the whole file — so discovery stays O(bounded) per session even when a rollout grows
|
|
27
|
+
// to hundreds of MB. Results are memoized by the file's (mtime,size): an unchanged transcript is read
|
|
28
|
+
// once and every later list call is free, so adding the tail read costs nothing on repeat. Any real
|
|
29
|
+
// change moves mtime+size and re-reads, so the cache can never go stale.
|
|
30
|
+
const HEAD_BYTES = 256 * 1024;
|
|
31
|
+
const TAIL_BYTES = 256 * 1024;
|
|
32
|
+
/** Read a bounded byte region as UTF-8. A partial leading/trailing line is expected; callers skip unparseable lines. */
|
|
33
|
+
function readRegion(filePath, start, length) {
|
|
34
|
+
const fd = fs.openSync(filePath, 'r');
|
|
35
|
+
try {
|
|
36
|
+
const buf = Buffer.alloc(Math.max(0, length));
|
|
37
|
+
const n = fs.readSync(fd, buf, 0, buf.length, Math.max(0, start));
|
|
38
|
+
return buf.toString('utf8', 0, n);
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
fs.closeSync(fd);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function memoized(cache, filePath, stat, compute) {
|
|
45
|
+
const hit = cache.get(filePath);
|
|
46
|
+
if (hit && hit.mtimeMs === stat.mtimeMs && hit.size === stat.size)
|
|
47
|
+
return hit.value;
|
|
48
|
+
const value = compute();
|
|
49
|
+
cache.set(filePath, { mtimeMs: stat.mtimeMs, size: stat.size, value });
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
/** Extract plain text from a Codex `response_item` message content (array of `{type,text}` blocks). */
|
|
53
|
+
function codexText(content) {
|
|
54
|
+
if (typeof content === 'string')
|
|
55
|
+
return content;
|
|
56
|
+
if (!Array.isArray(content))
|
|
57
|
+
return '';
|
|
58
|
+
return content
|
|
59
|
+
.filter((b) => b && typeof b.text === 'string' && (b.type === 'text' || (b.type ?? '').endsWith('_text')))
|
|
60
|
+
.map((b) => b.text)
|
|
61
|
+
.join('');
|
|
62
|
+
}
|
|
63
|
+
/** Reasoning effort from a Codex `turn_context` payload: top-level `effort`, else the nested collaboration setting. */
|
|
64
|
+
function codexEffortOf(payload) {
|
|
65
|
+
const top = payload?.effort;
|
|
66
|
+
if (typeof top === 'string' && top.trim())
|
|
67
|
+
return top.trim();
|
|
68
|
+
const re = payload?.collaboration_mode?.settings?.reasoning_effort;
|
|
69
|
+
return typeof re === 'string' && re.trim() ? re.trim() : null;
|
|
70
|
+
}
|
|
24
71
|
// ---- Claude: ~/.claude/projects/<encoded-workdir>/<sessionId>.jsonl ----
|
|
25
72
|
/** Claude encodes a workdir into a project dir name by replacing non-alphanumerics with '-'. */
|
|
26
73
|
export function encodeClaudeProjectDir(workdir) {
|
|
@@ -40,52 +87,80 @@ function claudeText(content) {
|
|
|
40
87
|
}
|
|
41
88
|
return parts.join(' ');
|
|
42
89
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
54
|
-
fs.closeSync(fd);
|
|
55
|
-
head = buf.toString('utf8', 0, n);
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return { title: null, model: null, preview: null, turns: 0 };
|
|
59
|
-
}
|
|
60
|
-
for (const line of head.split('\n')) {
|
|
61
|
-
if (!line || line[0] !== '{')
|
|
62
|
-
continue;
|
|
63
|
-
let ev;
|
|
90
|
+
const claudeMetaCache = new Map();
|
|
91
|
+
function readClaudeMeta(filePath, stat) {
|
|
92
|
+
return memoized(claudeMetaCache, filePath, stat, () => {
|
|
93
|
+
const size = stat.size;
|
|
94
|
+
let title = null;
|
|
95
|
+
let headModel = null;
|
|
96
|
+
let lastUser = null;
|
|
97
|
+
let headAssistant = null;
|
|
98
|
+
let turns = 0;
|
|
99
|
+
let head = '';
|
|
64
100
|
try {
|
|
65
|
-
|
|
101
|
+
head = readRegion(filePath, 0, Math.min(HEAD_BYTES, Math.max(65536, size)));
|
|
66
102
|
}
|
|
67
103
|
catch {
|
|
68
|
-
|
|
104
|
+
return { title: null, model: null, preview: null, turns: 0 };
|
|
69
105
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
106
|
+
for (const line of head.split('\n')) {
|
|
107
|
+
if (!line || line[0] !== '{')
|
|
108
|
+
continue;
|
|
109
|
+
let ev;
|
|
110
|
+
try {
|
|
111
|
+
ev = JSON.parse(line);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (ev.type === 'user' && ev.isMeta !== true) {
|
|
117
|
+
const text = claudeText(ev.message?.content).trim();
|
|
118
|
+
if (text && !text.startsWith('<') && !text.startsWith('[Image:')) {
|
|
119
|
+
if (!title)
|
|
120
|
+
title = cleanTitle(text);
|
|
121
|
+
lastUser = text;
|
|
122
|
+
turns++;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (ev.type === 'assistant') {
|
|
126
|
+
if (!headModel && ev.message?.model && ev.message.model !== '<synthetic>')
|
|
127
|
+
headModel = ev.message.model;
|
|
128
|
+
const text = claudeText(ev.message?.content).trim();
|
|
129
|
+
if (text)
|
|
130
|
+
headAssistant = text;
|
|
77
131
|
}
|
|
78
132
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
133
|
+
// The LATEST reply lives at the end of a long transcript, not in the head — read a bounded tail
|
|
134
|
+
// slice for an accurate preview (and the most recent model). Skipped when the file already fits
|
|
135
|
+
// in the head window (then the head scan above already saw the whole thing).
|
|
136
|
+
let tailAssistant = null;
|
|
137
|
+
let tailModel = null;
|
|
138
|
+
if (size > HEAD_BYTES) {
|
|
139
|
+
try {
|
|
140
|
+
for (const line of readRegion(filePath, size - TAIL_BYTES, TAIL_BYTES).split('\n')) {
|
|
141
|
+
if (!line || line[0] !== '{')
|
|
142
|
+
continue;
|
|
143
|
+
let ev;
|
|
144
|
+
try {
|
|
145
|
+
ev = JSON.parse(line);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (ev.type !== 'assistant')
|
|
151
|
+
continue;
|
|
152
|
+
const text = claudeText(ev.message?.content).trim();
|
|
153
|
+
if (text)
|
|
154
|
+
tailAssistant = text;
|
|
155
|
+
if (ev.message?.model && ev.message.model !== '<synthetic>')
|
|
156
|
+
tailModel = ev.message.model;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch { /* keep head-derived values */ }
|
|
85
160
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
161
|
+
const preview = cleanTitle(tailAssistant || headAssistant || lastUser, 200);
|
|
162
|
+
return { title, model: tailModel || headModel, preview, turns };
|
|
163
|
+
});
|
|
89
164
|
}
|
|
90
165
|
export function discoverClaudeNativeSessions(workdir, opts = {}) {
|
|
91
166
|
const home = homeOf(opts);
|
|
@@ -111,7 +186,7 @@ export function discoverClaudeNativeSessions(workdir, opts = {}) {
|
|
|
111
186
|
const selected = typeof opts.limit === 'number' ? files.slice(0, Math.max(0, opts.limit)) : files;
|
|
112
187
|
const now = Date.now();
|
|
113
188
|
return selected.map(({ sessionId, filePath, stat }) => {
|
|
114
|
-
const { title, model, preview, turns } =
|
|
189
|
+
const { title, model, preview, turns } = readClaudeMeta(filePath, stat);
|
|
115
190
|
return {
|
|
116
191
|
sessionId,
|
|
117
192
|
title,
|
|
@@ -148,6 +223,18 @@ function loadCodexTitleIndex(home) {
|
|
|
148
223
|
}
|
|
149
224
|
return map;
|
|
150
225
|
}
|
|
226
|
+
// A rollout's `session_meta` (id/cwd/timestamp) is written once at creation and never changes, so its
|
|
227
|
+
// parsed head is cached by path permanently — the per-list cwd filter then costs one 8 KB read per file
|
|
228
|
+
// only the FIRST time it is ever seen, not on every list call.
|
|
229
|
+
const codexHeadCache = new Map();
|
|
230
|
+
function readCodexHeadCached(filePath) {
|
|
231
|
+
const hit = codexHeadCache.get(filePath);
|
|
232
|
+
if (hit !== undefined)
|
|
233
|
+
return hit;
|
|
234
|
+
const v = readCodexHead(filePath);
|
|
235
|
+
codexHeadCache.set(filePath, v);
|
|
236
|
+
return v;
|
|
237
|
+
}
|
|
151
238
|
function readCodexHead(filePath) {
|
|
152
239
|
try {
|
|
153
240
|
const fd = fs.openSync(filePath, 'r');
|
|
@@ -173,6 +260,81 @@ function readCodexHead(filePath) {
|
|
|
173
260
|
return null;
|
|
174
261
|
}
|
|
175
262
|
}
|
|
263
|
+
const codexMetaCache = new Map();
|
|
264
|
+
/**
|
|
265
|
+
* Bounded per-row metadata for a Codex rollout: the first user prompt (title fallback when the thread
|
|
266
|
+
* has no index name) from a HEAD slice, and the latest reply + last model/effort from a TAIL slice.
|
|
267
|
+
* Memoized by (mtime,size); never reads the whole file.
|
|
268
|
+
*/
|
|
269
|
+
function readCodexBoundedMeta(filePath, stat) {
|
|
270
|
+
return memoized(codexMetaCache, filePath, stat, () => {
|
|
271
|
+
let firstPrompt = null;
|
|
272
|
+
let firstAny = null;
|
|
273
|
+
let preview = null;
|
|
274
|
+
let model = null;
|
|
275
|
+
let effort = null;
|
|
276
|
+
try {
|
|
277
|
+
// HEAD: the first real user turn (skip a leading <handover> injected by an agent switch).
|
|
278
|
+
for (const line of readRegion(filePath, 0, Math.min(HEAD_BYTES, stat.size)).split('\n')) {
|
|
279
|
+
const t = line.trim();
|
|
280
|
+
if (!t || t[0] !== '{' || !t.includes('user_message'))
|
|
281
|
+
continue;
|
|
282
|
+
let ev;
|
|
283
|
+
try {
|
|
284
|
+
ev = JSON.parse(t);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (ev.type !== 'event_msg' || ev.payload?.type !== 'user_message')
|
|
290
|
+
continue;
|
|
291
|
+
const text = String(ev.payload.message ?? '').trim();
|
|
292
|
+
if (!text)
|
|
293
|
+
continue;
|
|
294
|
+
if (!firstAny)
|
|
295
|
+
firstAny = text;
|
|
296
|
+
if (!String(text).toLowerCase().startsWith('<handover')) {
|
|
297
|
+
firstPrompt = text;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch { /* best-effort */ }
|
|
303
|
+
try {
|
|
304
|
+
// TAIL: the latest assistant reply + last turn_context (model/effort).
|
|
305
|
+
const start = Math.max(0, stat.size - TAIL_BYTES);
|
|
306
|
+
for (const line of readRegion(filePath, start, Math.min(TAIL_BYTES, stat.size)).split('\n')) {
|
|
307
|
+
const t = line.trim();
|
|
308
|
+
if (!t || t[0] !== '{')
|
|
309
|
+
continue;
|
|
310
|
+
let ev;
|
|
311
|
+
try {
|
|
312
|
+
ev = JSON.parse(t);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const p = ev.payload;
|
|
318
|
+
if (!p)
|
|
319
|
+
continue;
|
|
320
|
+
if (ev.type === 'turn_context') {
|
|
321
|
+
if (typeof p.model === 'string' && p.model.trim())
|
|
322
|
+
model = p.model.trim();
|
|
323
|
+
const e = codexEffortOf(p);
|
|
324
|
+
if (e)
|
|
325
|
+
effort = e;
|
|
326
|
+
}
|
|
327
|
+
else if (ev.type === 'response_item' && p.type === 'message' && p.role === 'assistant') {
|
|
328
|
+
const text = codexText(p.content).trim();
|
|
329
|
+
if (text)
|
|
330
|
+
preview = text;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch { /* best-effort */ }
|
|
335
|
+
return { firstPrompt: cleanTitle(firstPrompt ?? firstAny), preview: cleanTitle(preview, 200), model, effort };
|
|
336
|
+
});
|
|
337
|
+
}
|
|
176
338
|
export function discoverCodexNativeSessions(workdir, opts = {}) {
|
|
177
339
|
const home = homeOf(opts);
|
|
178
340
|
const sessionsDir = path.join(home, '.codex', 'sessions');
|
|
@@ -206,7 +368,7 @@ export function discoverCodexNativeSessions(workdir, opts = {}) {
|
|
|
206
368
|
const out = [];
|
|
207
369
|
const seen = new Set();
|
|
208
370
|
for (const { filePath, stat } of files) {
|
|
209
|
-
const meta =
|
|
371
|
+
const meta = readCodexHeadCached(filePath);
|
|
210
372
|
if (!meta || meta.isSubagent || path.resolve(meta.cwd) !== resolvedWorkdir)
|
|
211
373
|
continue;
|
|
212
374
|
if (seen.has(meta.sessionId))
|
|
@@ -214,12 +376,17 @@ export function discoverCodexNativeSessions(workdir, opts = {}) {
|
|
|
214
376
|
seen.add(meta.sessionId);
|
|
215
377
|
const idx = titleIndex.get(meta.sessionId);
|
|
216
378
|
const updatedAt = idx?.updatedAt || stat.mtime.toISOString();
|
|
379
|
+
// Only files that passed the cwd/subagent filter above pay for the bounded head+tail read (first
|
|
380
|
+
// prompt / preview / model / effort) — scoped to THIS workdir's own rollouts, and memoized per file.
|
|
381
|
+
const bounded = readCodexBoundedMeta(filePath, stat);
|
|
217
382
|
out.push({
|
|
218
383
|
sessionId: meta.sessionId,
|
|
219
|
-
|
|
220
|
-
|
|
384
|
+
// The agent's own thread name wins; otherwise fall back to the first prompt so the row is named + searchable.
|
|
385
|
+
title: cleanTitle(idx?.threadName || null) ?? bounded.firstPrompt,
|
|
386
|
+
preview: bounded.preview,
|
|
221
387
|
cwd: meta.cwd,
|
|
222
|
-
model:
|
|
388
|
+
model: bounded.model,
|
|
389
|
+
effort: bounded.effort,
|
|
223
390
|
createdAt: meta.timestamp || stat.birthtime.toISOString(),
|
|
224
391
|
updatedAt,
|
|
225
392
|
running: Date.now() - Date.parse(updatedAt) < threshold,
|
|
@@ -16,6 +16,7 @@ export interface ManagedSessionInfo {
|
|
|
16
16
|
source: SessionSource;
|
|
17
17
|
createdAt: string | null;
|
|
18
18
|
updatedAt: string | null;
|
|
19
|
+
messageCount?: number | null;
|
|
19
20
|
}
|
|
20
21
|
export interface ListSessionsOptions {
|
|
21
22
|
/**
|
|
@@ -28,6 +29,8 @@ export interface ListSessionsOptions {
|
|
|
28
29
|
agent?: string;
|
|
29
30
|
includeNative?: boolean;
|
|
30
31
|
limit?: number;
|
|
32
|
+
/** Skip this many of the most-recent rows before taking `limit` — for paginated "load more". */
|
|
33
|
+
offset?: number;
|
|
31
34
|
}
|
|
32
35
|
export interface SearchSessionsOptions extends ListSessionsOptions {
|
|
33
36
|
query: string;
|
|
@@ -24,6 +24,7 @@ function managedToInfo(agent, rec) {
|
|
|
24
24
|
source: 'managed',
|
|
25
25
|
createdAt: rec.createdAt ?? null,
|
|
26
26
|
updatedAt: rec.updatedAt ?? null,
|
|
27
|
+
messageCount: null, // managed records don't track a turn count; native rows carry the head-approx one
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
function nativeToInfo(agent, n) {
|
|
@@ -35,12 +36,13 @@ function nativeToInfo(agent, n) {
|
|
|
35
36
|
preview: n.preview,
|
|
36
37
|
workdir: n.cwd,
|
|
37
38
|
model: n.model,
|
|
38
|
-
effort: null,
|
|
39
|
+
effort: n.effort ?? null,
|
|
39
40
|
runState: n.running ? 'running' : 'completed',
|
|
40
41
|
running: n.running,
|
|
41
42
|
source: 'native',
|
|
42
43
|
createdAt: n.createdAt,
|
|
43
44
|
updatedAt: n.updatedAt,
|
|
45
|
+
messageCount: n.messageCount ?? null,
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
48
|
export class SessionsManager {
|
|
@@ -55,6 +57,15 @@ export class SessionsManager {
|
|
|
55
57
|
const drivers = this.deps.drivers();
|
|
56
58
|
const agentIds = opts.agent ? [opts.agent] : [...drivers.keys()];
|
|
57
59
|
const byKey = new Map();
|
|
60
|
+
// Native discovery must surface enough of the newest rows to satisfy offset+limit paging, since
|
|
61
|
+
// the final page is sliced AFTER merging managed + native. `undefined` limit = discover all.
|
|
62
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
63
|
+
const nativeLimit = typeof opts.limit === 'number' ? offset + opts.limit : undefined;
|
|
64
|
+
// Native session keys a managed record already represents because the agent minted a DIVERGENT
|
|
65
|
+
// transcript id (rec.sessionId ≠ rec.nativeSessionId — the new-chat case). Those native rows are
|
|
66
|
+
// dropped below so they don't double up with their managed row. Deduping HERE (before paging) keeps
|
|
67
|
+
// page sizes and "has more" correct — doing it in a host wrapper after slicing shrinks pages.
|
|
68
|
+
const coveredNativeKeys = new Set();
|
|
58
69
|
// Managed sessions (the kernel's own store).
|
|
59
70
|
for (const agent of agentIds) {
|
|
60
71
|
let records = [];
|
|
@@ -65,6 +76,9 @@ export class SessionsManager {
|
|
|
65
76
|
this.deps.log?.(`[sessions] store.list(${agent}) failed: ${e?.message || e}`);
|
|
66
77
|
}
|
|
67
78
|
for (const rec of records) {
|
|
79
|
+
if (rec.nativeSessionId && rec.nativeSessionId !== rec.sessionId) {
|
|
80
|
+
coveredNativeKeys.add(makeSessionKey(agent, rec.nativeSessionId));
|
|
81
|
+
}
|
|
68
82
|
if (scope === 'workspace') {
|
|
69
83
|
if (!rec.workdir || path.resolve(rec.workdir) !== workdir)
|
|
70
84
|
continue;
|
|
@@ -80,13 +94,15 @@ export class SessionsManager {
|
|
|
80
94
|
continue;
|
|
81
95
|
let natives = [];
|
|
82
96
|
try {
|
|
83
|
-
natives = await driver.listNativeSessions({ workdir, limit:
|
|
97
|
+
natives = await driver.listNativeSessions({ workdir, limit: nativeLimit });
|
|
84
98
|
}
|
|
85
99
|
catch (e) {
|
|
86
100
|
this.deps.log?.(`[sessions] ${agent}.listNativeSessions failed: ${e?.message || e}`);
|
|
87
101
|
}
|
|
88
102
|
for (const n of natives) {
|
|
89
103
|
const key = makeSessionKey(agent, n.sessionId);
|
|
104
|
+
if (coveredNativeKeys.has(key))
|
|
105
|
+
continue; // a managed record already represents this transcript
|
|
90
106
|
const existing = byKey.get(key);
|
|
91
107
|
if (existing) {
|
|
92
108
|
// Same identity discovered both ways: managed record wins, but adopt the newer
|
|
@@ -104,15 +120,16 @@ export class SessionsManager {
|
|
|
104
120
|
}
|
|
105
121
|
}
|
|
106
122
|
const out = [...byKey.values()].sort((a, b) => ts(b.updatedAt) - ts(a.updatedAt));
|
|
107
|
-
return typeof opts.limit === 'number' ? out.slice(
|
|
123
|
+
return typeof opts.limit === 'number' ? out.slice(offset, offset + opts.limit) : offset ? out.slice(offset) : out;
|
|
108
124
|
}
|
|
109
125
|
async search(opts) {
|
|
110
126
|
const q = (opts.query || '').trim().toLowerCase();
|
|
111
|
-
const all = await this.list({ ...opts, limit: undefined });
|
|
127
|
+
const all = await this.list({ ...opts, limit: undefined, offset: 0 });
|
|
112
128
|
const matched = q
|
|
113
129
|
? all.filter(s => [s.title, s.preview, s.sessionId, s.model, s.agent].some(v => v != null && v.toLowerCase().includes(q)))
|
|
114
130
|
: all;
|
|
115
|
-
|
|
131
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
132
|
+
return typeof opts.limit === 'number' ? matched.slice(offset, offset + opts.limit) : offset ? matched.slice(offset) : matched;
|
|
116
133
|
}
|
|
117
134
|
async get(sessionKey, opts = {}) {
|
|
118
135
|
const { agent, sessionId } = splitSessionKey(sessionKey);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikiloom/kernel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Heterogeneous coding agents (Claude / Codex / Gemini / ACP) -> an interaction-friendly, accumulating session snapshot + control handle, over pluggable surfaces (IM, Web, tunnel, TUI). The reusable core pikiloom itself is built on.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|