@kynetic-ai/spec 0.7.0 → 0.9.0
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/agents/adapters.d.ts +2 -0
- package/dist/agents/adapters.d.ts.map +1 -1
- package/dist/agents/adapters.js +18 -0
- package/dist/agents/adapters.js.map +1 -1
- package/dist/agents/spawner.d.ts +2 -0
- package/dist/agents/spawner.d.ts.map +1 -1
- package/dist/agents/spawner.js +4 -2
- package/dist/agents/spawner.js.map +1 -1
- package/dist/cli/commands/ralph.d.ts +48 -0
- package/dist/cli/commands/ralph.d.ts.map +1 -1
- package/dist/cli/commands/ralph.js +344 -86
- package/dist/cli/commands/ralph.js.map +1 -1
- package/dist/cli/commands/session/commands.d.ts.map +1 -1
- package/dist/cli/commands/session/commands.js +8 -0
- package/dist/cli/commands/session/commands.js.map +1 -1
- package/dist/cli/commands/session/compact.d.ts +13 -0
- package/dist/cli/commands/session/compact.d.ts.map +1 -0
- package/dist/cli/commands/session/compact.js +207 -0
- package/dist/cli/commands/session/compact.js.map +1 -0
- package/dist/cli/commands/session/log.d.ts +2 -0
- package/dist/cli/commands/session/log.d.ts.map +1 -1
- package/dist/cli/commands/session/log.js +12 -2
- package/dist/cli/commands/session/log.js.map +1 -1
- package/dist/cli/commands/setup-seeding.d.ts +6 -3
- package/dist/cli/commands/setup-seeding.d.ts.map +1 -1
- package/dist/cli/commands/setup-seeding.js +20 -4
- package/dist/cli/commands/setup-seeding.js.map +1 -1
- package/dist/cli/commands/setup.d.ts +3 -2
- package/dist/cli/commands/setup.d.ts.map +1 -1
- package/dist/cli/commands/setup.js +10 -90
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/skill-install.d.ts.map +1 -1
- package/dist/cli/commands/skill-install.js +104 -1
- package/dist/cli/commands/skill-install.js.map +1 -1
- package/dist/lib/codex-config.d.ts +14 -0
- package/dist/lib/codex-config.d.ts.map +1 -0
- package/dist/lib/codex-config.js +88 -0
- package/dist/lib/codex-config.js.map +1 -0
- package/dist/parser/agent-detection.d.ts +14 -0
- package/dist/parser/agent-detection.d.ts.map +1 -0
- package/dist/parser/agent-detection.js +118 -0
- package/dist/parser/agent-detection.js.map +1 -0
- package/dist/parser/setup-status.d.ts +4 -3
- package/dist/parser/setup-status.d.ts.map +1 -1
- package/dist/parser/setup-status.js +4 -10
- package/dist/parser/setup-status.js.map +1 -1
- package/dist/parser/shadow.d.ts.map +1 -1
- package/dist/parser/shadow.js +22 -31
- package/dist/parser/shadow.js.map +1 -1
- package/dist/parser/skill-render.d.ts +23 -1
- package/dist/parser/skill-render.d.ts.map +1 -1
- package/dist/parser/skill-render.js +126 -17
- package/dist/parser/skill-render.js.map +1 -1
- package/dist/ralph/subagent.d.ts +2 -0
- package/dist/ralph/subagent.d.ts.map +1 -1
- package/dist/ralph/subagent.js +2 -0
- package/dist/ralph/subagent.js.map +1 -1
- package/dist/ralph/wrap-up.d.ts +2 -0
- package/dist/ralph/wrap-up.d.ts.map +1 -1
- package/dist/ralph/wrap-up.js +1 -0
- package/dist/ralph/wrap-up.js.map +1 -1
- package/dist/sessions/store.d.ts +67 -0
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +396 -16
- package/dist/sessions/store.js.map +1 -1
- package/package.json +2 -1
- package/plugin/.claude-plugin/marketplace.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/kspec/skills/create-workflow/SKILL.md +10 -0
- package/plugin/plugins/kspec/skills/plan/SKILL.md +10 -0
- package/plugin/plugins/kspec/skills/review/SKILL.md +2 -0
- package/plugin/plugins/kspec/skills/task-work/SKILL.md +10 -0
- package/plugin/plugins/kspec/skills/triage-automation/SKILL.md +1 -0
- package/templates/skills/create-workflow/SKILL.md +12 -2
- package/templates/skills/manifest.yaml +11 -0
- package/templates/skills/observations/SKILL.md +2 -2
- package/templates/skills/plan/SKILL.md +15 -5
- package/templates/skills/reflect/SKILL.md +1 -1
- package/templates/skills/review/SKILL.md +4 -2
- package/templates/skills/task-work/SKILL.md +16 -6
- package/templates/skills/triage/SKILL.md +1 -1
- package/templates/skills/triage/docs/inbox.md +1 -1
- package/templates/skills/triage/docs/observations.md +1 -1
- package/templates/skills/triage-automation/SKILL.md +2 -1
- package/templates/skills/triage-inbox/SKILL.md +3 -3
- package/templates/skills/writing-specs/SKILL.md +6 -6
package/dist/sessions/store.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import * as fs from "node:fs";
|
|
15
15
|
import * as fsPromises from "node:fs/promises";
|
|
16
16
|
import * as path from "node:path";
|
|
17
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
18
|
+
import { parse as parseTOML, stringify as stringifyTOML } from "smol-toml";
|
|
17
19
|
import * as YAML from "yaml";
|
|
18
20
|
import { SessionEventSchema, SessionMetadataSchema, TaskBudgetSchema, } from "./types.js";
|
|
19
21
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
@@ -21,6 +23,12 @@ const SESSIONS_DIR = "sessions";
|
|
|
21
23
|
const METADATA_FILE = "session.yaml";
|
|
22
24
|
const EVENTS_FILE = "events.jsonl";
|
|
23
25
|
const BUDGET_FILE = "budget.json";
|
|
26
|
+
const BLOBS_DIR = "blobs";
|
|
27
|
+
// Event persistence guardrails: keep single-line events bounded in size while
|
|
28
|
+
// preserving full payloads via externalized blob files.
|
|
29
|
+
const EVENT_LINE_MAX_BYTES = 256 * 1024;
|
|
30
|
+
const EVENT_FIELD_EXTERNALIZE_BYTES = 16 * 1024;
|
|
31
|
+
const EVENT_PREVIEW_MAX_BYTES = 512;
|
|
24
32
|
// ─── Path Helpers ────────────────────────────────────────────────────────────
|
|
25
33
|
/**
|
|
26
34
|
* Get the sessions directory path within a spec directory.
|
|
@@ -59,6 +67,12 @@ export function getSessionContextPath(specDir, sessionId, iteration) {
|
|
|
59
67
|
export function getSessionBudgetPath(specDir, sessionId) {
|
|
60
68
|
return path.join(getSessionDir(specDir, sessionId), BUDGET_FILE);
|
|
61
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Get the path to a session's blob directory.
|
|
72
|
+
*/
|
|
73
|
+
export function getSessionBlobDir(specDir, sessionId) {
|
|
74
|
+
return path.join(getSessionDir(specDir, sessionId), BLOBS_DIR);
|
|
75
|
+
}
|
|
62
76
|
// ─── Session CRUD ────────────────────────────────────────────────────────────
|
|
63
77
|
/**
|
|
64
78
|
* Create a new session with metadata.
|
|
@@ -264,7 +278,171 @@ export async function closeSession(specDir, sessionId, status, reason) {
|
|
|
264
278
|
await fsPromises.writeFile(metadataPath, content, "utf-8");
|
|
265
279
|
return updated;
|
|
266
280
|
}
|
|
267
|
-
|
|
281
|
+
function isRecord(value) {
|
|
282
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
283
|
+
}
|
|
284
|
+
export function isSessionBlobPointer(value) {
|
|
285
|
+
if (!isRecord(value))
|
|
286
|
+
return false;
|
|
287
|
+
return (typeof value.path === "string" &&
|
|
288
|
+
typeof value.bytes === "number" &&
|
|
289
|
+
typeof value.sha256 === "string" &&
|
|
290
|
+
value.truncated === true &&
|
|
291
|
+
typeof value.preview === "string");
|
|
292
|
+
}
|
|
293
|
+
function stringifyPayload(value) {
|
|
294
|
+
if (typeof value === "string")
|
|
295
|
+
return value;
|
|
296
|
+
try {
|
|
297
|
+
return JSON.stringify(value);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return String(value);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function payloadBytes(value) {
|
|
304
|
+
return Buffer.byteLength(stringifyPayload(value), "utf-8");
|
|
305
|
+
}
|
|
306
|
+
function toPreview(content) {
|
|
307
|
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
308
|
+
if (totalBytes <= EVENT_PREVIEW_MAX_BYTES) {
|
|
309
|
+
return content;
|
|
310
|
+
}
|
|
311
|
+
let preview = "";
|
|
312
|
+
let usedBytes = 0;
|
|
313
|
+
for (const char of content) {
|
|
314
|
+
const charBytes = Buffer.byteLength(char, "utf-8");
|
|
315
|
+
if (usedBytes + charBytes > EVENT_PREVIEW_MAX_BYTES)
|
|
316
|
+
break;
|
|
317
|
+
preview += char;
|
|
318
|
+
usedBytes += charBytes;
|
|
319
|
+
}
|
|
320
|
+
return `${preview}...`;
|
|
321
|
+
}
|
|
322
|
+
function normalizeFieldLabel(pathSegments) {
|
|
323
|
+
const joined = pathSegments.length > 0 ? pathSegments.join("-") : "event-data";
|
|
324
|
+
const cleaned = joined
|
|
325
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
326
|
+
.replace(/-+/g, "-")
|
|
327
|
+
.replace(/^-|-$/g, "")
|
|
328
|
+
.slice(0, 48);
|
|
329
|
+
return cleaned || "event-data";
|
|
330
|
+
}
|
|
331
|
+
function shouldExternalizeField(pathSegments, value) {
|
|
332
|
+
if (value === null || value === undefined)
|
|
333
|
+
return false;
|
|
334
|
+
const keyPath = pathSegments.join(".");
|
|
335
|
+
if (keyPath === "update.rawOutput") {
|
|
336
|
+
return payloadBytes(value) > EVENT_FIELD_EXTERNALIZE_BYTES;
|
|
337
|
+
}
|
|
338
|
+
if (keyPath === "update._meta.claudeCode.toolResponse.stdout" ||
|
|
339
|
+
keyPath === "update._meta.claudeCode.toolResponse.stderr") {
|
|
340
|
+
return payloadBytes(value) > EVENT_FIELD_EXTERNALIZE_BYTES;
|
|
341
|
+
}
|
|
342
|
+
if (pathSegments[pathSegments.length - 1] === "text") {
|
|
343
|
+
const hasChunkContext = pathSegments.includes("content") ||
|
|
344
|
+
pathSegments.includes("chunk") ||
|
|
345
|
+
pathSegments.includes("delta");
|
|
346
|
+
if (hasChunkContext) {
|
|
347
|
+
return payloadBytes(value) > EVENT_FIELD_EXTERNALIZE_BYTES;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
async function createBlobPointer(specDir, sessionId, seq, pathSegments, value, context) {
|
|
353
|
+
const content = stringifyPayload(value);
|
|
354
|
+
const bytes = Buffer.byteLength(content, "utf-8");
|
|
355
|
+
const sha256 = createHash("sha256").update(content).digest("hex");
|
|
356
|
+
const fieldLabel = normalizeFieldLabel(pathSegments);
|
|
357
|
+
const dryRunCounter = context.dryRunCounter ?? 0;
|
|
358
|
+
const fileName = context.dryRun
|
|
359
|
+
? `${String(seq).padStart(6, "0")}-${fieldLabel}-dry-run-${String(dryRunCounter).padStart(4, "0")}.blob`
|
|
360
|
+
: `${String(seq).padStart(6, "0")}-${fieldLabel}-${randomUUID()}.blob`;
|
|
361
|
+
const relativePath = path.posix.join(BLOBS_DIR, fileName);
|
|
362
|
+
if (!context.dryRun) {
|
|
363
|
+
if (!context.ensuredDir) {
|
|
364
|
+
await fsPromises.mkdir(context.blobDir, { recursive: true });
|
|
365
|
+
context.ensuredDir = true;
|
|
366
|
+
}
|
|
367
|
+
const absolutePath = path.join(getSessionDir(specDir, sessionId), relativePath);
|
|
368
|
+
await fsPromises.writeFile(absolutePath, content, "utf-8");
|
|
369
|
+
}
|
|
370
|
+
context.createdBlobs = (context.createdBlobs ?? 0) + 1;
|
|
371
|
+
context.dryRunCounter = dryRunCounter + 1;
|
|
372
|
+
return {
|
|
373
|
+
path: relativePath,
|
|
374
|
+
bytes,
|
|
375
|
+
sha256,
|
|
376
|
+
truncated: true,
|
|
377
|
+
preview: toPreview(content),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
async function externalizeOversizedPayloads(specDir, sessionId, seq, value, pathSegments, context) {
|
|
381
|
+
if (isSessionBlobPointer(value)) {
|
|
382
|
+
return value;
|
|
383
|
+
}
|
|
384
|
+
if (shouldExternalizeField(pathSegments, value)) {
|
|
385
|
+
return createBlobPointer(specDir, sessionId, seq, pathSegments, value, context);
|
|
386
|
+
}
|
|
387
|
+
if (Array.isArray(value)) {
|
|
388
|
+
return Promise.all(value.map((entry, idx) => externalizeOversizedPayloads(specDir, sessionId, seq, entry, [
|
|
389
|
+
...pathSegments,
|
|
390
|
+
String(idx),
|
|
391
|
+
], context)));
|
|
392
|
+
}
|
|
393
|
+
if (isRecord(value)) {
|
|
394
|
+
const next = {};
|
|
395
|
+
for (const [key, child] of Object.entries(value)) {
|
|
396
|
+
next[key] = await externalizeOversizedPayloads(specDir, sessionId, seq, child, [...pathSegments, key], context);
|
|
397
|
+
}
|
|
398
|
+
return next;
|
|
399
|
+
}
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
function resolveBlobAbsolutePath(specDir, sessionId, relativePath) {
|
|
403
|
+
const sessionDir = path.resolve(getSessionDir(specDir, sessionId));
|
|
404
|
+
const absolutePath = path.resolve(sessionDir, relativePath);
|
|
405
|
+
if (absolutePath === sessionDir ||
|
|
406
|
+
absolutePath.startsWith(`${sessionDir}${path.sep}`)) {
|
|
407
|
+
return absolutePath;
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
async function resolveBlobPointer(specDir, sessionId, pointer) {
|
|
412
|
+
const absolutePath = resolveBlobAbsolutePath(specDir, sessionId, pointer.path);
|
|
413
|
+
if (!absolutePath) {
|
|
414
|
+
return { ...pointer, content: pointer.preview };
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
const content = await fsPromises.readFile(absolutePath, "utf-8");
|
|
418
|
+
return { ...pointer, content };
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
return { ...pointer, content: pointer.preview };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Resolve all blob pointers in a value tree to include full payload content.
|
|
426
|
+
*
|
|
427
|
+
* Default flows keep compact pointer objects (preview-only). This helper powers
|
|
428
|
+
* explicit on-demand blob resolution in session log commands.
|
|
429
|
+
*/
|
|
430
|
+
export async function resolveSessionBlobPointers(specDir, sessionId, value) {
|
|
431
|
+
if (isSessionBlobPointer(value)) {
|
|
432
|
+
return resolveBlobPointer(specDir, sessionId, value);
|
|
433
|
+
}
|
|
434
|
+
if (Array.isArray(value)) {
|
|
435
|
+
return Promise.all(value.map((entry) => resolveSessionBlobPointers(specDir, sessionId, entry)));
|
|
436
|
+
}
|
|
437
|
+
if (isRecord(value)) {
|
|
438
|
+
const next = {};
|
|
439
|
+
for (const [key, child] of Object.entries(value)) {
|
|
440
|
+
next[key] = await resolveSessionBlobPointers(specDir, sessionId, child);
|
|
441
|
+
}
|
|
442
|
+
return next;
|
|
443
|
+
}
|
|
444
|
+
return value;
|
|
445
|
+
}
|
|
268
446
|
/**
|
|
269
447
|
* Get the current event count for a session (for seq assignment).
|
|
270
448
|
*
|
|
@@ -320,14 +498,159 @@ export async function appendEvent(specDir, input) {
|
|
|
320
498
|
trace_id: input.trace_id,
|
|
321
499
|
data: input.data,
|
|
322
500
|
};
|
|
501
|
+
// AC: @session-events ac-8, ac-9 - Externalize oversized payload fields
|
|
502
|
+
// before writing to events.jsonl.
|
|
503
|
+
const externalizedData = await externalizeOversizedPayloads(specDir, input.session_id, seq, event.data, [], {
|
|
504
|
+
blobDir: getSessionBlobDir(specDir, input.session_id),
|
|
505
|
+
ensuredDir: false,
|
|
506
|
+
});
|
|
507
|
+
const eventWithGuardrails = {
|
|
508
|
+
...event,
|
|
509
|
+
data: externalizedData,
|
|
510
|
+
};
|
|
323
511
|
// Validate event
|
|
324
|
-
|
|
512
|
+
let validated = SessionEventSchema.parse(eventWithGuardrails);
|
|
513
|
+
// Event-line safety cap: if a single JSONL line is still too large after
|
|
514
|
+
// targeted field externalization, externalize the entire data payload.
|
|
515
|
+
let line = JSON.stringify(validated);
|
|
516
|
+
if (Buffer.byteLength(line, "utf-8") > EVENT_LINE_MAX_BYTES) {
|
|
517
|
+
const blobContext = {
|
|
518
|
+
blobDir: getSessionBlobDir(specDir, input.session_id),
|
|
519
|
+
ensuredDir: false,
|
|
520
|
+
};
|
|
521
|
+
const fullDataPointer = await createBlobPointer(specDir, input.session_id, seq, [], validated.data, blobContext);
|
|
522
|
+
validated = SessionEventSchema.parse({
|
|
523
|
+
...validated,
|
|
524
|
+
data: fullDataPointer,
|
|
525
|
+
});
|
|
526
|
+
line = JSON.stringify(validated);
|
|
527
|
+
}
|
|
325
528
|
// AC: @session-events ac-3 - Use synchronous append for crash safety
|
|
326
529
|
// This ensures the line is fully written before returning
|
|
327
|
-
|
|
328
|
-
fs.appendFileSync(eventsPath, line, "utf-8");
|
|
530
|
+
fs.appendFileSync(eventsPath, `${line}\n`, "utf-8");
|
|
329
531
|
return validated;
|
|
330
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Retroactively compact a session event log by externalizing oversized payloads.
|
|
535
|
+
*
|
|
536
|
+
* Reuses the same two-stage externalization pipeline as appendEvent():
|
|
537
|
+
* 1) Field-level externalization for known large payload fields
|
|
538
|
+
* 2) Full-data externalization if a line still exceeds EVENT_LINE_MAX_BYTES
|
|
539
|
+
*
|
|
540
|
+
* Writes are atomic (temp-file then rename). When dryRun is enabled, no files
|
|
541
|
+
* are modified and no blob files are written.
|
|
542
|
+
*/
|
|
543
|
+
export async function compactSessionEvents(specDir, sessionId, options = {}) {
|
|
544
|
+
const dryRun = options.dryRun === true;
|
|
545
|
+
const renameFn = options.renameFn ?? fsPromises.rename;
|
|
546
|
+
const eventsPath = getSessionEventsPath(specDir, sessionId);
|
|
547
|
+
let content;
|
|
548
|
+
try {
|
|
549
|
+
content = await fsPromises.readFile(eventsPath, "utf-8");
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
553
|
+
return {
|
|
554
|
+
events_processed: 0,
|
|
555
|
+
blobs_created: 0,
|
|
556
|
+
bytes_before: 0,
|
|
557
|
+
bytes_after: 0,
|
|
558
|
+
bytes_reclaimed: 0,
|
|
559
|
+
changed: false,
|
|
560
|
+
reason: "missing_events_file",
|
|
561
|
+
dry_run: dryRun,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
throw err;
|
|
565
|
+
}
|
|
566
|
+
const bytesBefore = Buffer.byteLength(content, "utf-8");
|
|
567
|
+
const sourceLines = content
|
|
568
|
+
.split("\n")
|
|
569
|
+
.filter((line) => line.trim().length > 0);
|
|
570
|
+
if (sourceLines.length === 0) {
|
|
571
|
+
return {
|
|
572
|
+
events_processed: 0,
|
|
573
|
+
blobs_created: 0,
|
|
574
|
+
bytes_before: bytesBefore,
|
|
575
|
+
bytes_after: bytesBefore,
|
|
576
|
+
bytes_reclaimed: 0,
|
|
577
|
+
changed: false,
|
|
578
|
+
reason: "empty_events_file",
|
|
579
|
+
dry_run: dryRun,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const blobContext = {
|
|
583
|
+
blobDir: getSessionBlobDir(specDir, sessionId),
|
|
584
|
+
ensuredDir: false,
|
|
585
|
+
dryRun,
|
|
586
|
+
createdBlobs: 0,
|
|
587
|
+
dryRunCounter: 0,
|
|
588
|
+
};
|
|
589
|
+
const compactedLines = [];
|
|
590
|
+
for (let i = 0; i < sourceLines.length; i += 1) {
|
|
591
|
+
const line = sourceLines[i];
|
|
592
|
+
let parsed;
|
|
593
|
+
try {
|
|
594
|
+
parsed = JSON.parse(line);
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
throw new Error(`Invalid JSON in events log at line ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
|
|
598
|
+
}
|
|
599
|
+
const event = SessionEventSchema.parse(parsed);
|
|
600
|
+
const externalizedData = await externalizeOversizedPayloads(specDir, sessionId, event.seq, event.data, [], blobContext);
|
|
601
|
+
let validated = SessionEventSchema.parse({
|
|
602
|
+
...event,
|
|
603
|
+
data: externalizedData,
|
|
604
|
+
});
|
|
605
|
+
let compactedLine = JSON.stringify(validated);
|
|
606
|
+
if (Buffer.byteLength(compactedLine, "utf-8") > EVENT_LINE_MAX_BYTES) {
|
|
607
|
+
const fullDataPointer = await createBlobPointer(specDir, sessionId, event.seq, [], validated.data, blobContext);
|
|
608
|
+
validated = SessionEventSchema.parse({
|
|
609
|
+
...validated,
|
|
610
|
+
data: fullDataPointer,
|
|
611
|
+
});
|
|
612
|
+
compactedLine = JSON.stringify(validated);
|
|
613
|
+
}
|
|
614
|
+
compactedLines.push(compactedLine);
|
|
615
|
+
}
|
|
616
|
+
const compactedContent = `${compactedLines.join("\n")}\n`;
|
|
617
|
+
const changed = compactedContent !== content;
|
|
618
|
+
const bytesAfter = changed ? Buffer.byteLength(compactedContent, "utf-8") : bytesBefore;
|
|
619
|
+
if (!changed) {
|
|
620
|
+
return {
|
|
621
|
+
events_processed: sourceLines.length,
|
|
622
|
+
blobs_created: 0,
|
|
623
|
+
bytes_before: bytesBefore,
|
|
624
|
+
bytes_after: bytesBefore,
|
|
625
|
+
bytes_reclaimed: 0,
|
|
626
|
+
changed: false,
|
|
627
|
+
reason: "already_compacted",
|
|
628
|
+
dry_run: dryRun,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
if (!dryRun) {
|
|
632
|
+
const sessionDir = getSessionDir(specDir, sessionId);
|
|
633
|
+
const tmpPath = path.join(sessionDir, `.${EVENTS_FILE}.${process.pid}.${Date.now()}.tmp`);
|
|
634
|
+
try {
|
|
635
|
+
await fsPromises.writeFile(tmpPath, compactedContent, "utf-8");
|
|
636
|
+
await renameFn(tmpPath, eventsPath);
|
|
637
|
+
}
|
|
638
|
+
catch (err) {
|
|
639
|
+
await fsPromises.unlink(tmpPath).catch(() => undefined);
|
|
640
|
+
throw err;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
events_processed: sourceLines.length,
|
|
645
|
+
blobs_created: blobContext.createdBlobs ?? 0,
|
|
646
|
+
bytes_before: bytesBefore,
|
|
647
|
+
bytes_after: bytesAfter,
|
|
648
|
+
bytes_reclaimed: Math.max(0, bytesBefore - bytesAfter),
|
|
649
|
+
changed: true,
|
|
650
|
+
reason: dryRun ? "would_compact" : "compacted",
|
|
651
|
+
dry_run: dryRun,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
331
654
|
/**
|
|
332
655
|
* Read all events from a session.
|
|
333
656
|
*
|
|
@@ -1119,6 +1442,7 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
|
|
|
1119
1442
|
const rawLimit = options.limit ?? 50;
|
|
1120
1443
|
const limit = Number.isNaN(rawLimit) || rawLimit <= 0 ? 50 : rawLimit;
|
|
1121
1444
|
const lowerPattern = pattern.toLowerCase();
|
|
1445
|
+
const resolveBlobs = options.resolveBlobs ?? false;
|
|
1122
1446
|
// Get all session summaries for metadata filtering
|
|
1123
1447
|
const allSummaries = await getAllSessionLogSummaries(specDir);
|
|
1124
1448
|
// AC: @session-log-search ac-3 - Pre-filter by --since
|
|
@@ -1152,8 +1476,9 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
|
|
|
1152
1476
|
for (const line of lines) {
|
|
1153
1477
|
if (totalMatches >= limit)
|
|
1154
1478
|
break;
|
|
1155
|
-
// Quick substring pre-filter before parsing JSON
|
|
1156
|
-
|
|
1479
|
+
// Quick substring pre-filter before parsing JSON.
|
|
1480
|
+
// Disabled when resolving blobs because full content lives outside line.
|
|
1481
|
+
if (!resolveBlobs && !line.toLowerCase().includes(lowerPattern))
|
|
1157
1482
|
continue;
|
|
1158
1483
|
try {
|
|
1159
1484
|
const event = JSON.parse(line);
|
|
@@ -1172,8 +1497,12 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
|
|
|
1172
1497
|
}
|
|
1173
1498
|
}
|
|
1174
1499
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1500
|
+
const searchableData = resolveBlobs
|
|
1501
|
+
? await resolveSessionBlobPointers(specDir, summary.id, event.data)
|
|
1502
|
+
: event.data;
|
|
1503
|
+
// Verify match in stringified data (not just line, in case pattern
|
|
1504
|
+
// appears in metadata)
|
|
1505
|
+
const dataStr = JSON.stringify(searchableData);
|
|
1177
1506
|
if (!dataStr.toLowerCase().includes(lowerPattern))
|
|
1178
1507
|
continue;
|
|
1179
1508
|
// AC: @session-log-search ac-4 - Create match with excerpt
|
|
@@ -1181,7 +1510,7 @@ export async function searchSessionEvents(specDir, pattern, options = {}) {
|
|
|
1181
1510
|
session_id: summary.id,
|
|
1182
1511
|
timestamp: event.ts,
|
|
1183
1512
|
event_type: event.type,
|
|
1184
|
-
content_excerpt: extractContentExcerpt(
|
|
1513
|
+
content_excerpt: extractContentExcerpt(searchableData, pattern, 200),
|
|
1185
1514
|
});
|
|
1186
1515
|
totalMatches++;
|
|
1187
1516
|
}
|
|
@@ -1411,12 +1740,12 @@ async function removeDotenvSessionId(filePath) {
|
|
|
1411
1740
|
*/
|
|
1412
1741
|
export async function injectCodexEnv(sessionId) {
|
|
1413
1742
|
const configDir = path.join(process.env.HOME || process.env.USERPROFILE || "", ".codex");
|
|
1414
|
-
const configPath = path.join(configDir, "config.
|
|
1743
|
+
const configPath = path.join(configDir, "config.toml");
|
|
1415
1744
|
await fsPromises.mkdir(configDir, { recursive: true });
|
|
1416
1745
|
let config = {};
|
|
1417
1746
|
try {
|
|
1418
1747
|
const content = await fsPromises.readFile(configPath, "utf-8");
|
|
1419
|
-
config =
|
|
1748
|
+
config = parseTOML(content);
|
|
1420
1749
|
}
|
|
1421
1750
|
catch (err) {
|
|
1422
1751
|
// Only start fresh for ENOENT; throw on parse errors to avoid overwriting
|
|
@@ -1424,10 +1753,17 @@ export async function injectCodexEnv(sessionId) {
|
|
|
1424
1753
|
// File doesn't exist, start fresh
|
|
1425
1754
|
}
|
|
1426
1755
|
else {
|
|
1427
|
-
throw new Error(`Cannot inject env: ~/.codex/config.
|
|
1756
|
+
throw new Error(`Cannot inject env: ~/.codex/config.toml exists but is not valid TOML. ` +
|
|
1428
1757
|
`Fix the file manually or remove it, then retry.`);
|
|
1429
1758
|
}
|
|
1430
1759
|
}
|
|
1760
|
+
// Capture previous value before overwriting
|
|
1761
|
+
const previousValue = config.shell_environment_policy &&
|
|
1762
|
+
typeof config.shell_environment_policy === "object" &&
|
|
1763
|
+
config.shell_environment_policy.set &&
|
|
1764
|
+
typeof config.shell_environment_policy.set === "object"
|
|
1765
|
+
? (config.shell_environment_policy.set.KSPEC_SESSION_ID ?? null)
|
|
1766
|
+
: null;
|
|
1431
1767
|
// Ensure shell_environment_policy.set exists
|
|
1432
1768
|
if (!config.shell_environment_policy ||
|
|
1433
1769
|
typeof config.shell_environment_policy !== "object") {
|
|
@@ -1438,14 +1774,56 @@ export async function injectCodexEnv(sessionId) {
|
|
|
1438
1774
|
policy.set = {};
|
|
1439
1775
|
}
|
|
1440
1776
|
policy.set.KSPEC_SESSION_ID = sessionId;
|
|
1441
|
-
await fsPromises.writeFile(configPath,
|
|
1777
|
+
await fsPromises.writeFile(configPath, stringifyTOML(config) + "\n", "utf-8");
|
|
1442
1778
|
return {
|
|
1443
1779
|
injected: true,
|
|
1444
1780
|
method: "codex_config",
|
|
1445
1781
|
description: `Added KSPEC_SESSION_ID to Codex config shell_environment_policy.set`,
|
|
1446
1782
|
path: configPath,
|
|
1783
|
+
previousValue,
|
|
1447
1784
|
};
|
|
1448
1785
|
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Remove or restore KSPEC_SESSION_ID in Codex config.
|
|
1788
|
+
*
|
|
1789
|
+
* Reverses the injection performed by injectCodexEnv().
|
|
1790
|
+
* If previousValue is provided, restores it instead of deleting.
|
|
1791
|
+
* Best-effort: silently ignores missing files or missing keys.
|
|
1792
|
+
*
|
|
1793
|
+
* @param previousValue - Value to restore, or null/undefined to delete
|
|
1794
|
+
*/
|
|
1795
|
+
export async function removeCodexEnv(previousValue) {
|
|
1796
|
+
const configDir = path.join(process.env.HOME || process.env.USERPROFILE || "", ".codex");
|
|
1797
|
+
const configPath = path.join(configDir, "config.toml");
|
|
1798
|
+
try {
|
|
1799
|
+
const content = await fsPromises.readFile(configPath, "utf-8");
|
|
1800
|
+
const config = parseTOML(content);
|
|
1801
|
+
const rawPolicy = config.shell_environment_policy;
|
|
1802
|
+
if (rawPolicy && typeof rawPolicy === "object") {
|
|
1803
|
+
const policy = rawPolicy;
|
|
1804
|
+
if (policy.set && typeof policy.set === "object") {
|
|
1805
|
+
if (previousValue) {
|
|
1806
|
+
policy.set.KSPEC_SESSION_ID = previousValue;
|
|
1807
|
+
}
|
|
1808
|
+
else {
|
|
1809
|
+
delete policy.set.KSPEC_SESSION_ID;
|
|
1810
|
+
// Remove set section entirely if empty
|
|
1811
|
+
if (Object.keys(policy.set).length === 0) {
|
|
1812
|
+
delete policy.set;
|
|
1813
|
+
}
|
|
1814
|
+
// Remove shell_environment_policy if empty
|
|
1815
|
+
if (Object.keys(policy).length === 0) {
|
|
1816
|
+
delete config.shell_environment_policy;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
await fsPromises.writeFile(configPath, stringifyTOML(config) + "\n", "utf-8");
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
catch {
|
|
1824
|
+
// Best-effort cleanup — file may not exist or may not be valid TOML
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1449
1827
|
/**
|
|
1450
1828
|
* Inject KSPEC_SESSION_ID into Gemini CLI environment.
|
|
1451
1829
|
*
|
|
@@ -1509,9 +1887,9 @@ export async function injectEnvForAdapter(adapterId, sessionId) {
|
|
|
1509
1887
|
case "claude-agent-acp":
|
|
1510
1888
|
case "claude-code-acp":
|
|
1511
1889
|
return injectClaudeCodeEnv(sessionId);
|
|
1890
|
+
case "codex-acp":
|
|
1891
|
+
return injectCodexEnv(sessionId);
|
|
1512
1892
|
// Future harnesses can be added here:
|
|
1513
|
-
// case "codex-acp":
|
|
1514
|
-
// return injectCodexEnv(sessionId);
|
|
1515
1893
|
// case "gemini-acp":
|
|
1516
1894
|
// return injectGeminiEnv(sessionId);
|
|
1517
1895
|
default:
|
|
@@ -1534,7 +1912,9 @@ export async function removeEnvForAdapter(adapterId, previousValue) {
|
|
|
1534
1912
|
case "claude-code-acp":
|
|
1535
1913
|
await removeClaudeCodeEnv(previousValue);
|
|
1536
1914
|
break;
|
|
1537
|
-
|
|
1915
|
+
case "codex-acp":
|
|
1916
|
+
await removeCodexEnv(previousValue);
|
|
1917
|
+
break;
|
|
1538
1918
|
}
|
|
1539
1919
|
}
|
|
1540
1920
|
// ─── Session Validation ───────────────────────────────────────────────────────
|