@vybestack/llxprt-code-core 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7
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/src/config/config.d.ts +20 -0
- package/dist/src/config/config.js +143 -23
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/configBaseCore.d.ts +6 -2
- package/dist/src/config/configBaseCore.js +8 -0
- package/dist/src/config/configBaseCore.js.map +1 -1
- package/dist/src/config/configConstructor.d.ts +2 -0
- package/dist/src/config/configConstructor.js +9 -1
- package/dist/src/config/configConstructor.js.map +1 -1
- package/dist/src/config/configTestHarness.d.ts +8 -6
- package/dist/src/config/configTestHarness.js +10 -4
- package/dist/src/config/configTestHarness.js.map +1 -1
- package/dist/src/config/configTypes.d.ts +7 -2
- package/dist/src/config/configTypes.js +2 -0
- package/dist/src/config/configTypes.js.map +1 -1
- package/dist/src/config/liveTrustTransitionLifecycle.d.ts +35 -0
- package/dist/src/config/liveTrustTransitionLifecycle.js +180 -0
- package/dist/src/config/liveTrustTransitionLifecycle.js.map +1 -0
- package/dist/src/config/schedulerSingleton.d.ts +2 -2
- package/dist/src/config/schedulerSingleton.js.map +1 -1
- package/dist/src/core/compression/types.d.ts +48 -1
- package/dist/src/core/compression/types.js +41 -0
- package/dist/src/core/compression/types.js.map +1 -1
- package/dist/src/core/model-limits.json +96 -0
- package/dist/src/core/model-limits.schema.d.ts +123 -0
- package/dist/src/core/model-limits.schema.js +75 -0
- package/dist/src/core/model-limits.schema.js.map +1 -0
- package/dist/src/core/tokenLimits.d.ts +6 -8
- package/dist/src/core/tokenLimits.js +56 -132
- package/dist/src/core/tokenLimits.js.map +1 -1
- package/dist/src/core/turn.d.ts +20 -3
- package/dist/src/core/turn.js +20 -3
- package/dist/src/core/turn.js.map +1 -1
- package/dist/src/hooks/hookRegistry.d.ts +1 -1
- package/dist/src/hooks/hookRegistry.js +3 -1
- package/dist/src/hooks/hookRegistry.js.map +1 -1
- package/dist/src/hooks/hookRunner.d.ts +6 -0
- package/dist/src/hooks/hookRunner.js +22 -6
- package/dist/src/hooks/hookRunner.js.map +1 -1
- package/dist/src/hooks/hookSystem.d.ts +10 -1
- package/dist/src/hooks/hookSystem.js +69 -12
- package/dist/src/hooks/hookSystem.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/llm-types/geminiContent.d.ts +3 -0
- package/dist/src/policy/config.d.ts +4 -2
- package/dist/src/policy/config.js +22 -18
- package/dist/src/policy/config.js.map +1 -1
- package/dist/src/recording/ReplayEngine.js +39 -2
- package/dist/src/recording/ReplayEngine.js.map +1 -1
- package/dist/src/recording/SessionDiscovery.d.ts +8 -0
- package/dist/src/recording/SessionDiscovery.js +88 -3
- package/dist/src/recording/SessionDiscovery.js.map +1 -1
- package/dist/src/recording/SessionLockManager.d.ts +3 -0
- package/dist/src/recording/SessionLockManager.js +9 -3
- package/dist/src/recording/SessionLockManager.js.map +1 -1
- package/dist/src/recording/SessionRecordingService.d.ts +14 -1
- package/dist/src/recording/SessionRecordingService.js +27 -3
- package/dist/src/recording/SessionRecordingService.js.map +1 -1
- package/dist/src/recording/index.d.ts +4 -3
- package/dist/src/recording/index.js +4 -3
- package/dist/src/recording/index.js.map +1 -1
- package/dist/src/recording/replay-test-helpers.d.ts +5 -0
- package/dist/src/recording/replay-test-helpers.js +13 -0
- package/dist/src/recording/replay-test-helpers.js.map +1 -1
- package/dist/src/recording/resumeNotFoundMessages.d.ts +56 -0
- package/dist/src/recording/resumeNotFoundMessages.js +64 -0
- package/dist/src/recording/resumeNotFoundMessages.js.map +1 -0
- package/dist/src/recording/resumeSession.js +3 -2
- package/dist/src/recording/resumeSession.js.map +1 -1
- package/dist/src/recording/sessionManagement.d.ts +3 -0
- package/dist/src/recording/sessionManagement.js +38 -19
- package/dist/src/recording/sessionManagement.js.map +1 -1
- package/dist/src/recording/types.d.ts +42 -3
- package/dist/src/recording/types.js +1 -1
- package/dist/src/recording/types.js.map +1 -1
- package/dist/src/runtime/AgentRuntimeLoader.js +2 -1
- package/dist/src/runtime/AgentRuntimeLoader.js.map +1 -1
- package/dist/src/scheduler/liveOutput.d.ts +10 -19
- package/dist/src/scheduler/liveOutput.js +21 -21
- package/dist/src/scheduler/liveOutput.js.map +1 -1
- package/dist/src/scheduler/types.d.ts +2 -2
- package/dist/src/services/history/ContentConverters.d.ts +4 -0
- package/dist/src/services/history/ContentConverters.js +77 -59
- package/dist/src/services/history/ContentConverters.js.map +1 -1
- package/dist/src/services/history/HistoryService.d.ts +13 -1
- package/dist/src/services/history/HistoryService.js +71 -0
- package/dist/src/services/history/HistoryService.js.map +1 -1
- package/dist/src/services/history/IContent.d.ts +10 -0
- package/dist/src/services/history/IContent.js.map +1 -1
- package/dist/src/skills/skillManager.d.ts +11 -1
- package/dist/src/skills/skillManager.js +21 -1
- package/dist/src/skills/skillManager.js.map +1 -1
- package/dist/src/test-utils/mock-tool.d.ts +3 -3
- package/dist/src/test-utils/mock-tool.js.map +1 -1
- package/dist/src/test-utils/tools.d.ts +2 -2
- package/dist/src/test-utils/tools.js.map +1 -1
- package/dist/src/tools-adapters/CoreShellToolHostAdapter.js +1 -6
- package/dist/src/tools-adapters/CoreShellToolHostAdapter.js.map +1 -1
- package/dist/src/utils/browser-profile-discovery.d.ts +40 -0
- package/dist/src/utils/browser-profile-discovery.js +251 -0
- package/dist/src/utils/browser-profile-discovery.js.map +1 -0
- package/dist/src/utils/env.d.ts +12 -0
- package/dist/src/utils/env.js +15 -0
- package/dist/src/utils/env.js.map +1 -0
- package/dist/src/utils/events.d.ts +10 -1
- package/dist/src/utils/events.js +22 -0
- package/dist/src/utils/events.js.map +1 -1
- package/dist/src/utils/fileUtils.js +8 -1
- package/dist/src/utils/fileUtils.js.map +1 -1
- package/dist/src/utils/retry.js +3 -4
- package/dist/src/utils/retry.js.map +1 -1
- package/dist/src/utils/secure-browser-launcher.d.ts +35 -1
- package/dist/src/utils/secure-browser-launcher.js +341 -1
- package/dist/src/utils/secure-browser-launcher.js.map +1 -1
- package/dist/src/utils/terminalSerializer.d.ts +12 -0
- package/dist/src/utils/terminalSerializer.js.map +1 -1
- package/dist/src/utils/thoughtUtils.d.ts +2 -0
- package/dist/src/utils/thoughtUtils.js.map +1 -1
- package/package.json +13 -9
- package/src/config/config.ts +161 -22
- package/src/config/configBaseCore.ts +10 -2
- package/src/config/configConstructor.ts +11 -0
- package/src/config/configTestHarness.ts +10 -4
- package/src/config/configTypes.ts +6 -1
- package/src/config/liveTrustTransitionLifecycle.ts +250 -0
- package/src/config/schedulerSingleton.ts +2 -5
- package/src/core/compression/types.ts +81 -1
- package/src/core/legacy-model-limits.expected.txt +96 -0
- package/src/core/model-limits.json +96 -0
- package/src/core/model-limits.schema.ts +87 -0
- package/src/core/tokenLimits.ts +73 -147
- package/src/core/turn.ts +20 -3
- package/src/hooks/hookRegistry.ts +3 -1
- package/src/hooks/hookRunner.ts +24 -7
- package/src/hooks/hookSystem.ts +73 -13
- package/src/index.ts +1 -0
- package/src/llm-types/geminiContent.ts +3 -0
- package/src/policy/config.ts +32 -19
- package/src/recording/ReplayEngine.ts +51 -2
- package/src/recording/SessionDiscovery.ts +99 -4
- package/src/recording/SessionLockManager.ts +10 -3
- package/src/recording/SessionRecordingService.ts +36 -3
- package/src/recording/index.ts +19 -2
- package/src/recording/replay-test-helpers.ts +14 -0
- package/src/recording/resumeNotFoundMessages.ts +75 -0
- package/src/recording/resumeSession.ts +3 -1
- package/src/recording/sessionManagement.ts +51 -21
- package/src/recording/types.ts +44 -2
- package/src/runtime/AgentRuntimeLoader.ts +3 -1
- package/src/scheduler/liveOutput.ts +25 -22
- package/src/scheduler/types.ts +5 -2
- package/src/services/history/ContentConverters.ts +91 -68
- package/src/services/history/HistoryService.ts +92 -1
- package/src/services/history/IContent.ts +13 -0
- package/src/skills/skillManager.ts +26 -1
- package/src/test-utils/mock-tool.ts +4 -3
- package/src/test-utils/tools.ts +4 -3
- package/src/tools-adapters/CoreShellToolHostAdapter.ts +1 -7
- package/src/utils/browser-profile-discovery.ts +358 -0
- package/src/utils/env.ts +15 -0
- package/src/utils/events.ts +31 -0
- package/src/utils/fileUtils.ts +11 -1
- package/src/utils/retry.ts +3 -4
- package/src/utils/secure-browser-launcher.ts +500 -2
- package/src/utils/terminalSerializer.ts +9 -0
- package/src/utils/thoughtUtils.ts +2 -0
|
@@ -147,10 +147,21 @@ function handleSessionStart(
|
|
|
147
147
|
return undefined;
|
|
148
148
|
}
|
|
149
149
|
const startPayload = payload as unknown as SessionStartPayload;
|
|
150
|
-
|
|
150
|
+
const requiredStrings = [
|
|
151
|
+
startPayload.sessionId,
|
|
152
|
+
startPayload.projectHash,
|
|
153
|
+
startPayload.provider,
|
|
154
|
+
startPayload.model,
|
|
155
|
+
startPayload.startTime,
|
|
156
|
+
];
|
|
157
|
+
if (
|
|
158
|
+
requiredStrings.some(
|
|
159
|
+
(value) => typeof value !== 'string' || value.length === 0,
|
|
160
|
+
)
|
|
161
|
+
) {
|
|
151
162
|
return {
|
|
152
163
|
ok: false,
|
|
153
|
-
error: 'Invalid session_start: missing required fields',
|
|
164
|
+
error: 'Invalid session_start: missing or malformed required fields',
|
|
154
165
|
warnings: acc.warnings,
|
|
155
166
|
};
|
|
156
167
|
}
|
|
@@ -167,6 +178,7 @@ function handleSessionStart(
|
|
|
167
178
|
provider: startPayload.provider,
|
|
168
179
|
model: startPayload.model,
|
|
169
180
|
workspaceDirs: resolveWorkspaceDirs(startPayload.workspaceDirs),
|
|
181
|
+
...(typeof startPayload.cwd === 'string' ? { cwd: startPayload.cwd } : {}),
|
|
170
182
|
startTime: startPayload.startTime,
|
|
171
183
|
};
|
|
172
184
|
return undefined;
|
|
@@ -289,6 +301,40 @@ function handleSessionEvent(
|
|
|
289
301
|
}
|
|
290
302
|
}
|
|
291
303
|
|
|
304
|
+
/**
|
|
305
|
+
* @requirement issue #1611: session_metadata — update metadata.title (tri-state).
|
|
306
|
+
*
|
|
307
|
+
* The title is tri-state: undefined (field absent) is legacy and does NOT
|
|
308
|
+
* modify metadata.title; null sets it to null (explicit untitled); string sets
|
|
309
|
+
* it to that string. Malformed title values (non-string, non-null, non-
|
|
310
|
+
* undefined) are recorded as malformed.
|
|
311
|
+
*/
|
|
312
|
+
function handleSessionMetadata(
|
|
313
|
+
payload: Record<string, unknown>,
|
|
314
|
+
acc: ReplayAccumulators,
|
|
315
|
+
lineNumber: number,
|
|
316
|
+
): void {
|
|
317
|
+
if (acc.metadata === null) {
|
|
318
|
+
acc.malformedCount++;
|
|
319
|
+
acc.warnings.push(
|
|
320
|
+
`Line ${lineNumber}: session_metadata before session_start, skipping`,
|
|
321
|
+
);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!('title' in payload)) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const title = payload.title;
|
|
328
|
+
if (title === null || typeof title === 'string') {
|
|
329
|
+
acc.metadata.title = title;
|
|
330
|
+
} else if (title !== undefined) {
|
|
331
|
+
acc.malformedCount++;
|
|
332
|
+
acc.warnings.push(
|
|
333
|
+
`Line ${lineNumber}: malformed session_metadata title, skipping`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
292
338
|
/** @pseudocode line 126-131: directories_changed — update metadata or record malformed. */
|
|
293
339
|
function handleDirectoriesChanged(
|
|
294
340
|
payload: Record<string, unknown>,
|
|
@@ -381,6 +427,9 @@ function dispatchEvent(
|
|
|
381
427
|
case 'session_event':
|
|
382
428
|
handleSessionEvent(payload, acc, lineNumber);
|
|
383
429
|
break;
|
|
430
|
+
case 'session_metadata':
|
|
431
|
+
handleSessionMetadata(payload, acc, lineNumber);
|
|
432
|
+
break;
|
|
384
433
|
case 'directories_changed':
|
|
385
434
|
handleDirectoriesChanged(payload, acc, lineNumber);
|
|
386
435
|
break;
|
|
@@ -24,10 +24,21 @@
|
|
|
24
24
|
* for the resume flow.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import { createReadStream } from 'node:fs';
|
|
27
28
|
import * as fs from 'node:fs/promises';
|
|
28
29
|
import * as path from 'node:path';
|
|
29
|
-
import
|
|
30
|
+
import * as readline from 'node:readline';
|
|
31
|
+
import {
|
|
32
|
+
SESSION_TITLE_MAX_LENGTH,
|
|
33
|
+
type SessionSummary,
|
|
34
|
+
type SessionStartPayload,
|
|
35
|
+
} from './types.js';
|
|
30
36
|
import { readSessionHeader } from './ReplayEngine.js';
|
|
37
|
+
import {
|
|
38
|
+
resumeSessionIndexOutOfRangeMessage,
|
|
39
|
+
resumeSessionNotFoundMessage,
|
|
40
|
+
} from './resumeNotFoundMessages.js';
|
|
41
|
+
import { debugLogger } from '../utils/debugLogger.js';
|
|
31
42
|
|
|
32
43
|
/**
|
|
33
44
|
* Result of successfully resolving a session reference.
|
|
@@ -163,7 +174,7 @@ export class SessionDiscovery {
|
|
|
163
174
|
return { session: sessions[indexNum - 1] };
|
|
164
175
|
}
|
|
165
176
|
return {
|
|
166
|
-
error:
|
|
177
|
+
error: resumeSessionIndexOutOfRangeMessage(indexNum, sessions.length),
|
|
167
178
|
};
|
|
168
179
|
}
|
|
169
180
|
|
|
@@ -176,7 +187,7 @@ export class SessionDiscovery {
|
|
|
176
187
|
};
|
|
177
188
|
}
|
|
178
189
|
|
|
179
|
-
return { error:
|
|
190
|
+
return { error: resumeSessionNotFoundMessage(ref) };
|
|
180
191
|
}
|
|
181
192
|
|
|
182
193
|
/**
|
|
@@ -268,7 +279,7 @@ export class SessionDiscovery {
|
|
|
268
279
|
*/
|
|
269
280
|
static async readFirstUserMessage(
|
|
270
281
|
filePath: string,
|
|
271
|
-
maxLength: number =
|
|
282
|
+
maxLength: number = SESSION_TITLE_MAX_LENGTH,
|
|
272
283
|
): Promise<string | null> {
|
|
273
284
|
try {
|
|
274
285
|
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
@@ -284,6 +295,44 @@ export class SessionDiscovery {
|
|
|
284
295
|
return null;
|
|
285
296
|
}
|
|
286
297
|
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Read the tri-state title from the last valid `session_metadata` event in a
|
|
301
|
+
* session file (issue #1611). Returns:
|
|
302
|
+
* - `undefined` when no `session_metadata` event exists (legacy fallback).
|
|
303
|
+
* - `null` when the event explicitly asserts untitled.
|
|
304
|
+
* - `string` when the event carries a concrete title.
|
|
305
|
+
*/
|
|
306
|
+
static async readSessionMetadataTitle(
|
|
307
|
+
filePath: string,
|
|
308
|
+
): Promise<string | null | undefined> {
|
|
309
|
+
let stream: ReturnType<typeof createReadStream> | undefined;
|
|
310
|
+
let reader: readline.Interface | undefined;
|
|
311
|
+
try {
|
|
312
|
+
stream = createReadStream(filePath, { encoding: 'utf8' });
|
|
313
|
+
reader = readline.createInterface({
|
|
314
|
+
input: stream,
|
|
315
|
+
crlfDelay: Infinity,
|
|
316
|
+
});
|
|
317
|
+
let title: string | null | undefined;
|
|
318
|
+
for await (const line of reader) {
|
|
319
|
+
const result = extractSessionMetadataTitle(line);
|
|
320
|
+
if (result !== undefined) {
|
|
321
|
+
title = result;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return title;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
debugLogger.debug(
|
|
327
|
+
() =>
|
|
328
|
+
`readSessionMetadataTitle failed for ${filePath}: ${String(error)}`,
|
|
329
|
+
);
|
|
330
|
+
return undefined;
|
|
331
|
+
} finally {
|
|
332
|
+
reader?.close();
|
|
333
|
+
stream?.destroy();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
287
336
|
}
|
|
288
337
|
|
|
289
338
|
async function readSessionSummary(
|
|
@@ -310,6 +359,10 @@ async function readSessionSummary(
|
|
|
310
359
|
fileSize: stat.size,
|
|
311
360
|
provider: header.provider,
|
|
312
361
|
model: header.model,
|
|
362
|
+
...(typeof header.cwd === 'string' ? { cwd: header.cwd } : {}),
|
|
363
|
+
...(typeof header.startTime === 'string'
|
|
364
|
+
? { createdAt: header.startTime }
|
|
365
|
+
: {}),
|
|
313
366
|
};
|
|
314
367
|
}
|
|
315
368
|
|
|
@@ -367,3 +420,45 @@ function extractUserMessageText(line: string): string | null {
|
|
|
367
420
|
|
|
368
421
|
return text || null;
|
|
369
422
|
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Extract the title from a `session_metadata` JSONL line.
|
|
426
|
+
* Returns:
|
|
427
|
+
* - `undefined` when the line is not a valid `session_metadata` event (so
|
|
428
|
+
* callers can continue scanning).
|
|
429
|
+
* - `null` when title is explicitly null (untitled).
|
|
430
|
+
* - `string` when a concrete title is present.
|
|
431
|
+
*/
|
|
432
|
+
function extractSessionMetadataTitle(line: string): string | null | undefined {
|
|
433
|
+
if (!line.trim()) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
let event: unknown;
|
|
437
|
+
try {
|
|
438
|
+
event = JSON.parse(line);
|
|
439
|
+
} catch {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
if (event === null || typeof event !== 'object') {
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
const record = event as Record<string, unknown>;
|
|
446
|
+
if (record.type !== 'session_metadata') {
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
const payload = record.payload as Record<string, unknown> | undefined;
|
|
450
|
+
if (!payload || typeof payload !== 'object') {
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
if (!('title' in payload)) {
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
const title = payload.title;
|
|
457
|
+
if (title === null) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
if (typeof title === 'string') {
|
|
461
|
+
return title;
|
|
462
|
+
}
|
|
463
|
+
return undefined;
|
|
464
|
+
}
|
|
@@ -37,6 +37,13 @@ export interface LockHandle {
|
|
|
37
37
|
release(): Promise<void>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export class SessionLockedError extends Error {
|
|
41
|
+
constructor() {
|
|
42
|
+
super('Session is in use by another process');
|
|
43
|
+
this.name = 'SessionLockedError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
/** @pseudocode concurrency-lifecycle.md lines 10-134 */
|
|
41
48
|
export class SessionLockManager {
|
|
42
49
|
/** @pseudocode concurrency-lifecycle.md lines 12-14 */
|
|
@@ -108,7 +115,7 @@ export class SessionLockManager {
|
|
|
108
115
|
await fs.writeFile(lockPath, lockContent, { flag: 'wx' });
|
|
109
116
|
} catch (writeErr: unknown) {
|
|
110
117
|
if ((writeErr as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
111
|
-
throw new
|
|
118
|
+
throw new SessionLockedError();
|
|
112
119
|
}
|
|
113
120
|
throw writeErr;
|
|
114
121
|
}
|
|
@@ -121,7 +128,7 @@ export class SessionLockManager {
|
|
|
121
128
|
): Promise<void> {
|
|
122
129
|
const isStale = await SessionLockManager.checkStale(lockPath);
|
|
123
130
|
if (!isStale) {
|
|
124
|
-
throw new
|
|
131
|
+
throw new SessionLockedError();
|
|
125
132
|
}
|
|
126
133
|
try {
|
|
127
134
|
await fs.unlink(lockPath);
|
|
@@ -135,7 +142,7 @@ export class SessionLockManager {
|
|
|
135
142
|
} catch (writeErr: unknown) {
|
|
136
143
|
const code = (writeErr as NodeJS.ErrnoException).code;
|
|
137
144
|
if (code === 'EEXIST') {
|
|
138
|
-
throw new
|
|
145
|
+
throw new SessionLockedError();
|
|
139
146
|
}
|
|
140
147
|
if (code === 'ENOENT') {
|
|
141
148
|
await SessionLockManager.createLockAfterMkdir(lockPath, lockContent);
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
type SessionRecordLine,
|
|
36
36
|
} from './types.js';
|
|
37
37
|
|
|
38
|
+
export const SESSION_FILE_ID_PREFIX_LENGTH = 12;
|
|
39
|
+
|
|
38
40
|
/**
|
|
39
41
|
* Core service for recording session events to a JSONL file.
|
|
40
42
|
*
|
|
@@ -55,6 +57,7 @@ export class SessionRecordingService {
|
|
|
55
57
|
private readonly chatsDir: string;
|
|
56
58
|
private preContentBuffer: SessionRecordLine[] = [];
|
|
57
59
|
private chatsDirWatcher: FSWatcher | null = null;
|
|
60
|
+
private sessionTitle: string | null | undefined;
|
|
58
61
|
|
|
59
62
|
/**
|
|
60
63
|
* @plan PLAN-20260211-SESSIONRECORDING.P05
|
|
@@ -69,6 +72,7 @@ export class SessionRecordingService {
|
|
|
69
72
|
sessionId: config.sessionId,
|
|
70
73
|
projectHash: config.projectHash,
|
|
71
74
|
workspaceDirs: config.workspaceDirs,
|
|
75
|
+
...(config.cwd === undefined ? {} : { cwd: config.cwd }),
|
|
72
76
|
provider: config.provider,
|
|
73
77
|
model: config.model,
|
|
74
78
|
startTime: new Date().toISOString(),
|
|
@@ -106,7 +110,10 @@ export class SessionRecordingService {
|
|
|
106
110
|
enqueue(type: SessionEventType, payload: unknown): void {
|
|
107
111
|
if (!this.active) return;
|
|
108
112
|
|
|
109
|
-
if (
|
|
113
|
+
if (
|
|
114
|
+
(type === 'content' || type === 'session_metadata') &&
|
|
115
|
+
!this.materialized
|
|
116
|
+
) {
|
|
110
117
|
this.materialize();
|
|
111
118
|
for (const buffered of this.preContentBuffer) {
|
|
112
119
|
this.queue.push(buffered);
|
|
@@ -142,7 +149,7 @@ export class SessionRecordingService {
|
|
|
142
149
|
private materialize(): void {
|
|
143
150
|
const now = new Date();
|
|
144
151
|
const timestamp = now.toISOString().slice(0, 19).replace(/:/g, '-');
|
|
145
|
-
const prefix = this.sessionId.substring(0,
|
|
152
|
+
const prefix = this.sessionId.substring(0, SESSION_FILE_ID_PREFIX_LENGTH);
|
|
146
153
|
const fileName = `session-${timestamp}-${prefix}.jsonl`;
|
|
147
154
|
this.filePath = path.join(this.chatsDir, fileName);
|
|
148
155
|
mkdirSync(this.chatsDir, { recursive: true });
|
|
@@ -307,11 +314,16 @@ export class SessionRecordingService {
|
|
|
307
314
|
* @requirement REQ-REC-008
|
|
308
315
|
* @pseudocode session-recording-service.md lines 174-179
|
|
309
316
|
*/
|
|
310
|
-
initializeForResume(
|
|
317
|
+
initializeForResume(
|
|
318
|
+
filePath: string,
|
|
319
|
+
lastSeq: number,
|
|
320
|
+
title?: string | null,
|
|
321
|
+
): void {
|
|
311
322
|
this.filePath = filePath;
|
|
312
323
|
this.seq = lastSeq;
|
|
313
324
|
this.materialized = true;
|
|
314
325
|
this.preContentBuffer = [];
|
|
326
|
+
this.sessionTitle = title;
|
|
315
327
|
this.startChatsDirWatcher();
|
|
316
328
|
}
|
|
317
329
|
|
|
@@ -468,4 +480,25 @@ export class SessionRecordingService {
|
|
|
468
480
|
recordDirectoriesChanged(directories: string[]): void {
|
|
469
481
|
this.enqueue('directories_changed', { directories });
|
|
470
482
|
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Record a session_metadata event — persisted human-readable title.
|
|
486
|
+
* The title is tri-state: `string` for a concrete title, `null` for explicit
|
|
487
|
+
* untitled, `undefined` for legacy (field absent). Like `content`, this event
|
|
488
|
+
* materializes the file so slash/failure sessions persist metadata even
|
|
489
|
+
* without a content event.
|
|
490
|
+
*
|
|
491
|
+
* @requirement REQ-REC-002
|
|
492
|
+
*/
|
|
493
|
+
recordSessionMetadata(title: string | null): void {
|
|
494
|
+
if (!this.active) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
this.enqueue('session_metadata', { title });
|
|
498
|
+
this.sessionTitle = title;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
getSessionMetadataTitle(): string | null | undefined {
|
|
502
|
+
return this.sessionTitle;
|
|
503
|
+
}
|
|
471
504
|
}
|
package/src/recording/index.ts
CHANGED
|
@@ -21,9 +21,16 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
export * from './types.js';
|
|
24
|
-
export {
|
|
24
|
+
export {
|
|
25
|
+
SessionRecordingService,
|
|
26
|
+
SESSION_FILE_ID_PREFIX_LENGTH,
|
|
27
|
+
} from './SessionRecordingService.js';
|
|
25
28
|
export { replaySession, readSessionHeader } from './ReplayEngine.js';
|
|
26
|
-
export {
|
|
29
|
+
export {
|
|
30
|
+
SessionLockManager,
|
|
31
|
+
SessionLockedError,
|
|
32
|
+
type LockHandle,
|
|
33
|
+
} from './SessionLockManager.js';
|
|
27
34
|
export { RecordingIntegration } from './RecordingIntegration.js';
|
|
28
35
|
export {
|
|
29
36
|
type JsonlSessionFileEntry,
|
|
@@ -43,9 +50,19 @@ export {
|
|
|
43
50
|
type ResumeResult,
|
|
44
51
|
type ResumeError,
|
|
45
52
|
} from './resumeSession.js';
|
|
53
|
+
export {
|
|
54
|
+
RESUME_NO_SESSIONS_FOUND,
|
|
55
|
+
RESUME_SESSION_NOT_FOUND_PREFIX,
|
|
56
|
+
RESUME_SESSION_INDEX_OUT_OF_RANGE_PREFIX,
|
|
57
|
+
RESUME_SESSION_INDEX_OUT_OF_RANGE_RE,
|
|
58
|
+
resumeSessionNotFoundMessage,
|
|
59
|
+
resumeSessionIndexOutOfRangeMessage,
|
|
60
|
+
} from './resumeNotFoundMessages.js';
|
|
46
61
|
export {
|
|
47
62
|
listSessions,
|
|
48
63
|
deleteSession,
|
|
64
|
+
deleteSessionById,
|
|
65
|
+
SESSION_NOT_FOUND_PREFIX,
|
|
49
66
|
type ListSessionsResult,
|
|
50
67
|
type DeleteSessionResult,
|
|
51
68
|
type DeleteSessionError,
|
|
@@ -199,6 +199,20 @@ export function _directoriesChangedLine(
|
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Build a valid JSONL line for a session_metadata event (issue #1611).
|
|
204
|
+
* The title is either a concrete string or explicit null.
|
|
205
|
+
*/
|
|
206
|
+
export function sessionMetadataLine(seq: number, title: string | null): string {
|
|
207
|
+
return JSON.stringify({
|
|
208
|
+
v: 1,
|
|
209
|
+
seq,
|
|
210
|
+
ts: new Date().toISOString(),
|
|
211
|
+
type: 'session_metadata',
|
|
212
|
+
payload: { title },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
202
216
|
/**
|
|
203
217
|
* Write raw JSONL lines to a file.
|
|
204
218
|
*/
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2025 Vybestack LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The "session not found"-style messages the resume/discovery flow emits, as
|
|
19
|
+
* shared constants so downstream classifiers (e.g. the Zed ACP loadSession
|
|
20
|
+
* error mapper) can match them WITHOUT string-literal coupling that would
|
|
21
|
+
* silently break if the wording changed. The emitting sites
|
|
22
|
+
* (resumeSession.ts, SessionDiscovery.ts) and every matcher must reference
|
|
23
|
+
* these — never inline copies of the sentences.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Emitted by resumeSession when the project has no recorded sessions at all. */
|
|
27
|
+
export const RESUME_NO_SESSIONS_FOUND = 'No sessions found for this project';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Prefix (including the trailing colon) of the
|
|
31
|
+
* SessionDiscovery.resolveSessionRef message for an unknown session reference;
|
|
32
|
+
* the full message appends ` <ref>` (space + ref) after it — see
|
|
33
|
+
* {@link resumeSessionNotFoundMessage}.
|
|
34
|
+
*/
|
|
35
|
+
export const RESUME_SESSION_NOT_FOUND_PREFIX =
|
|
36
|
+
'Session not found for this project:';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds the full SessionDiscovery.resolveSessionRef not-found message for a
|
|
40
|
+
* specific reference.
|
|
41
|
+
*/
|
|
42
|
+
export function resumeSessionNotFoundMessage(ref: string): string {
|
|
43
|
+
return `${RESUME_SESSION_NOT_FOUND_PREFIX} ${ref}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Prefix of the SessionDiscovery.resolveSessionRef message for a numeric
|
|
48
|
+
* session index outside the listing range; the full message is
|
|
49
|
+
* `Session index <n> out of range (1-<m>)`.
|
|
50
|
+
*/
|
|
51
|
+
export const RESUME_SESSION_INDEX_OUT_OF_RANGE_PREFIX = 'Session index ';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Builds the full SessionDiscovery.resolveSessionRef out-of-range message for
|
|
55
|
+
* a specific index and session count. `ref` must render as digits (a number,
|
|
56
|
+
* or the digit-only string the caller already validated with /^\d+$/) so the
|
|
57
|
+
* produced message stays matchable by
|
|
58
|
+
* {@link RESUME_SESSION_INDEX_OUT_OF_RANGE_RE}.
|
|
59
|
+
*/
|
|
60
|
+
export function resumeSessionIndexOutOfRangeMessage(
|
|
61
|
+
ref: number | `${number}`,
|
|
62
|
+
count: number,
|
|
63
|
+
): string {
|
|
64
|
+
if (count <= 0) {
|
|
65
|
+
return RESUME_NO_SESSIONS_FOUND;
|
|
66
|
+
}
|
|
67
|
+
return `${RESUME_SESSION_INDEX_OUT_OF_RANGE_PREFIX}${ref} out of range (1-${count})`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Matches the out-of-range message shape for classifiers that only have the
|
|
72
|
+
* final (envelope-wrapped) detail string to inspect.
|
|
73
|
+
*/
|
|
74
|
+
export const RESUME_SESSION_INDEX_OUT_OF_RANGE_RE =
|
|
75
|
+
/^(?:Failed to resume session: )?Session index \d+ out of range \(1-\d+\)$/;
|
|
@@ -30,6 +30,7 @@ import { SessionRecordingService } from './SessionRecordingService.js';
|
|
|
30
30
|
import { SessionDiscovery } from './SessionDiscovery.js';
|
|
31
31
|
import { SessionLockManager, type LockHandle } from './SessionLockManager.js';
|
|
32
32
|
import { replaySession } from './ReplayEngine.js';
|
|
33
|
+
import { RESUME_NO_SESSIONS_FOUND } from './resumeNotFoundMessages.js';
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Sentinel constant for "resume most recent session".
|
|
@@ -138,6 +139,7 @@ function initializeRecordingForResume(
|
|
|
138
139
|
recording.initializeForResume(
|
|
139
140
|
lockedSession.targetFilePath,
|
|
140
141
|
replayResult.lastSeq,
|
|
142
|
+
replayResult.metadata.title,
|
|
141
143
|
);
|
|
142
144
|
|
|
143
145
|
if (
|
|
@@ -180,7 +182,7 @@ export async function resumeSession(
|
|
|
180
182
|
);
|
|
181
183
|
|
|
182
184
|
if (sessions.length === 0) {
|
|
183
|
-
return { ok: false, error:
|
|
185
|
+
return { ok: false, error: RESUME_NO_SESSIONS_FOUND };
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
// Step 2: Resolve which session to resume
|
|
@@ -25,7 +25,12 @@
|
|
|
25
25
|
|
|
26
26
|
import { type SessionSummary } from './types.js';
|
|
27
27
|
import { SessionDiscovery } from './SessionDiscovery.js';
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
SessionLockManager,
|
|
30
|
+
SessionLockedError,
|
|
31
|
+
type LockHandle,
|
|
32
|
+
} from './SessionLockManager.js';
|
|
33
|
+
import { RESUME_NO_SESSIONS_FOUND } from './resumeNotFoundMessages.js';
|
|
29
34
|
|
|
30
35
|
/**
|
|
31
36
|
* Result of listing sessions for a project.
|
|
@@ -77,7 +82,7 @@ export async function deleteSession(
|
|
|
77
82
|
const sessions = await SessionDiscovery.listSessions(chatsDir, projectHash);
|
|
78
83
|
|
|
79
84
|
if (sessions.length === 0) {
|
|
80
|
-
return { ok: false, error:
|
|
85
|
+
return { ok: false, error: RESUME_NO_SESSIONS_FOUND };
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
const resolved = SessionDiscovery.resolveSessionRef(ref, sessions);
|
|
@@ -85,38 +90,63 @@ export async function deleteSession(
|
|
|
85
90
|
return { ok: false, error: resolved.error };
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
|
|
93
|
+
return deleteResolvedSession(resolved.session, chatsDir);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const SESSION_NOT_FOUND_PREFIX = 'Session not found:';
|
|
97
|
+
|
|
98
|
+
/** Delete a session only when the supplied identifier is an exact match. */
|
|
99
|
+
export async function deleteSessionById(
|
|
100
|
+
sessionId: string,
|
|
101
|
+
chatsDir: string,
|
|
102
|
+
projectHash: string,
|
|
103
|
+
): Promise<DeleteSessionResult | DeleteSessionError> {
|
|
104
|
+
const sessions = await SessionDiscovery.listSessions(chatsDir, projectHash);
|
|
105
|
+
const target = sessions.find((session) => session.sessionId === sessionId);
|
|
106
|
+
if (target === undefined) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: `${SESSION_NOT_FOUND_PREFIX} ${sessionId}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return deleteResolvedSession(target, chatsDir);
|
|
114
|
+
}
|
|
89
115
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
116
|
+
async function deleteResolvedSession(
|
|
117
|
+
target: SessionSummary,
|
|
118
|
+
chatsDir: string,
|
|
119
|
+
): Promise<DeleteSessionResult | DeleteSessionError> {
|
|
120
|
+
let lock: LockHandle;
|
|
121
|
+
try {
|
|
122
|
+
lock = await SessionLockManager.acquire(chatsDir, target.sessionId);
|
|
123
|
+
} catch (error: unknown) {
|
|
124
|
+
if (error instanceof SessionLockedError) {
|
|
96
125
|
return {
|
|
97
126
|
ok: false,
|
|
98
127
|
error: 'Cannot delete: session is in use by another process',
|
|
99
128
|
};
|
|
100
129
|
}
|
|
130
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
error: `Failed to lock session for deletion: ${detail}`,
|
|
134
|
+
};
|
|
101
135
|
}
|
|
102
136
|
|
|
103
137
|
const { unlink } = await import('node:fs/promises');
|
|
104
|
-
|
|
138
|
+
let result: DeleteSessionResult | DeleteSessionError;
|
|
105
139
|
try {
|
|
106
140
|
await unlink(target.filePath);
|
|
141
|
+
result = { ok: true, deletedSessionId: target.sessionId };
|
|
107
142
|
} catch (error: unknown) {
|
|
108
|
-
|
|
143
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
144
|
+
result = {
|
|
109
145
|
ok: false,
|
|
110
|
-
error: `Failed to delete session: ${
|
|
146
|
+
error: `Failed to delete session: ${detail}`,
|
|
111
147
|
};
|
|
148
|
+
} finally {
|
|
149
|
+
await lock.release().catch(() => undefined);
|
|
112
150
|
}
|
|
113
|
-
|
|
114
|
-
const lockPath = SessionLockManager.getLockPath(chatsDir, target.sessionId);
|
|
115
|
-
try {
|
|
116
|
-
await unlink(lockPath);
|
|
117
|
-
} catch {
|
|
118
|
-
// Lock file may not exist
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return { ok: true, deletedSessionId: target.sessionId };
|
|
151
|
+
return result;
|
|
122
152
|
}
|