@basou/core 0.40.0 → 0.42.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/index.d.ts +190 -19
- package/dist/index.js +516 -355
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schemas/event.schema.json +18 -7
- package/schemas/retired/0.1.0/event.schema.json +1477 -0
- package/schemas/session-import.schema.json +20 -7
package/dist/index.js
CHANGED
|
@@ -354,6 +354,251 @@ function isValidPrefixedId(value) {
|
|
|
354
354
|
return isValidUlid(ulidPart);
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
+
// src/schemas/event.schema.ts
|
|
358
|
+
import { z as z2 } from "zod";
|
|
359
|
+
|
|
360
|
+
// src/schemas/shared.schema.ts
|
|
361
|
+
import { z } from "zod";
|
|
362
|
+
var SchemaVersionSchema = z.string().regex(/^0\.\d+\.\d+$/, {
|
|
363
|
+
message: "unsupported .basou format version: this basou reads format major 0 (0.x.y). If this workspace was written by a newer basou, upgrade basou to open it."
|
|
364
|
+
});
|
|
365
|
+
var CacheVersionSchema = z.literal("0.1.0");
|
|
366
|
+
var IsoTimestampSchema = z.string().datetime({ offset: true });
|
|
367
|
+
var createPrefixedIdSchema = (prefix) => {
|
|
368
|
+
const refiner = (value) => isValidPrefixedId(value) && value.startsWith(`${prefix}_`);
|
|
369
|
+
return z.string().refine(refiner, { message: `Expected ${prefix}_<ULID>` }).meta({
|
|
370
|
+
pattern: `^${prefix}_[0-7][0-9A-HJKMNP-TV-Z]{25}$`,
|
|
371
|
+
description: `Basou ${prefix} id: \`${prefix}_\` followed by a 26-character Crockford Base32 ULID.`
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
var WorkspaceIdSchema = createPrefixedIdSchema("ws");
|
|
375
|
+
var TaskIdSchema = createPrefixedIdSchema("task");
|
|
376
|
+
var SessionIdSchema = createPrefixedIdSchema("ses");
|
|
377
|
+
var EventIdSchema = createPrefixedIdSchema("evt");
|
|
378
|
+
var ApprovalIdSchema = createPrefixedIdSchema("appr");
|
|
379
|
+
var DecisionIdSchema = createPrefixedIdSchema("decision");
|
|
380
|
+
var RiskLevelSchema = z.enum(["low", "medium", "high", "critical"]);
|
|
381
|
+
var EventSourceSchema = z.string().min(1);
|
|
382
|
+
|
|
383
|
+
// src/schemas/event.schema.ts
|
|
384
|
+
var EVENT_SCHEMA_VERSION = "0.2.0";
|
|
385
|
+
var BaseEventSchema = z2.object({
|
|
386
|
+
schema_version: SchemaVersionSchema,
|
|
387
|
+
id: EventIdSchema,
|
|
388
|
+
session_id: SessionIdSchema,
|
|
389
|
+
occurred_at: IsoTimestampSchema,
|
|
390
|
+
source: EventSourceSchema,
|
|
391
|
+
// Tamper-evidence back-pointer (hex sha-256 of the PREVIOUS event line's
|
|
392
|
+
// written bytes; the first line carries the session-bound genesis hash).
|
|
393
|
+
// Present only on sessions written with chaining enabled (import paths);
|
|
394
|
+
// live/ad-hoc sessions omit it. Declared on the base so the `.strict()`
|
|
395
|
+
// variants below treat it as a known key. Additive optional => no
|
|
396
|
+
// schema_version bump.
|
|
397
|
+
prev_hash: z2.string().optional()
|
|
398
|
+
});
|
|
399
|
+
var SessionStartedEventSchema = BaseEventSchema.extend({
|
|
400
|
+
type: z2.literal("session_started")
|
|
401
|
+
});
|
|
402
|
+
var SessionEndedEventSchema = BaseEventSchema.extend({
|
|
403
|
+
type: z2.literal("session_ended"),
|
|
404
|
+
exit_code: z2.number().int().optional()
|
|
405
|
+
});
|
|
406
|
+
var SessionStatusChangedEventSchema = BaseEventSchema.extend({
|
|
407
|
+
type: z2.literal("session_status_changed"),
|
|
408
|
+
from: z2.string(),
|
|
409
|
+
to: z2.string()
|
|
410
|
+
});
|
|
411
|
+
var ApprovalRequestedEventSchema = BaseEventSchema.extend({
|
|
412
|
+
type: z2.literal("approval_requested"),
|
|
413
|
+
approval_id: ApprovalIdSchema,
|
|
414
|
+
expires_at: IsoTimestampSchema.nullable().default(null),
|
|
415
|
+
risk_level: RiskLevelSchema,
|
|
416
|
+
// `action.kind` is required; additional fields are allowed to support
|
|
417
|
+
// future action shapes (shell_command, external_send, ...).
|
|
418
|
+
action: z2.object({ kind: z2.string() }).passthrough(),
|
|
419
|
+
reason: z2.string(),
|
|
420
|
+
status: z2.literal("pending")
|
|
421
|
+
});
|
|
422
|
+
var ApprovalApprovedEventSchema = BaseEventSchema.extend({
|
|
423
|
+
type: z2.literal("approval_approved"),
|
|
424
|
+
approval_id: ApprovalIdSchema,
|
|
425
|
+
resolver: z2.string().optional(),
|
|
426
|
+
note: z2.string().nullable().optional()
|
|
427
|
+
});
|
|
428
|
+
var ApprovalRejectedEventSchema = BaseEventSchema.extend({
|
|
429
|
+
type: z2.literal("approval_rejected"),
|
|
430
|
+
approval_id: ApprovalIdSchema,
|
|
431
|
+
resolver: z2.string().optional(),
|
|
432
|
+
reason: z2.string()
|
|
433
|
+
});
|
|
434
|
+
var ApprovalExpiredEventSchema = BaseEventSchema.extend({
|
|
435
|
+
type: z2.literal("approval_expired"),
|
|
436
|
+
approval_id: ApprovalIdSchema
|
|
437
|
+
});
|
|
438
|
+
var CommandExecutedEventSchema = BaseEventSchema.extend({
|
|
439
|
+
type: z2.literal("command_executed"),
|
|
440
|
+
command: z2.string().nullable().meta({
|
|
441
|
+
description: "Spawned executable name. null means basou did not observe the executor - not a default, and not a benign value."
|
|
442
|
+
}),
|
|
443
|
+
args: z2.array(z2.string()),
|
|
444
|
+
cwd: z2.string().nullable().meta({
|
|
445
|
+
description: "Working directory the command ran in. null means the directory could not be resolved; it is never the session's directory as a fallback."
|
|
446
|
+
}),
|
|
447
|
+
exit_code: z2.number().int().nullable().meta({
|
|
448
|
+
description: "Child exit code. null means the outcome is unknown (signal-terminated, or never recorded by the source). null is not success."
|
|
449
|
+
}),
|
|
450
|
+
signal: z2.string().nullable().optional(),
|
|
451
|
+
received_signal: z2.string().nullable().optional(),
|
|
452
|
+
// Still accepts 0, because 0.1.0 events carrying it are on disk and are never
|
|
453
|
+
// rewritten; every line read from disk is validated against this schema, so
|
|
454
|
+
// narrowing the domain here would silently drop them. Writers at 0.2.0 and
|
|
455
|
+
// above do not produce it.
|
|
456
|
+
duration_ms: z2.number().int().nonnegative().nullable().meta({
|
|
457
|
+
description: "Observed duration in milliseconds, or null when no duration was observed. Read 0 as unobserved too, on any version: a spawned process cannot run in under half a millisecond, and under schema_version 0.1.0 a writer with nothing to report stored 0 as the floor. Writers at 0.2.0 and above record null instead and never write 0."
|
|
458
|
+
})
|
|
459
|
+
});
|
|
460
|
+
var GitSnapshotEventSchema = BaseEventSchema.extend({
|
|
461
|
+
type: z2.literal("git_snapshot"),
|
|
462
|
+
head: z2.string(),
|
|
463
|
+
branch: z2.string(),
|
|
464
|
+
dirty: z2.boolean(),
|
|
465
|
+
staged: z2.array(z2.string()),
|
|
466
|
+
unstaged: z2.array(z2.string()),
|
|
467
|
+
untracked: z2.array(z2.string()),
|
|
468
|
+
ahead: z2.number().int().nonnegative().optional(),
|
|
469
|
+
behind: z2.number().int().nonnegative().optional()
|
|
470
|
+
});
|
|
471
|
+
var FileChangedEventSchema = BaseEventSchema.extend({
|
|
472
|
+
type: z2.literal("file_changed"),
|
|
473
|
+
path: z2.string(),
|
|
474
|
+
change_type: z2.enum(["added", "modified", "deleted", "renamed"]),
|
|
475
|
+
// Renamed entries record the previous path here. Optional + nullable to
|
|
476
|
+
// keep the wire format stable for added / modified / deleted events.
|
|
477
|
+
old_path: z2.string().nullable().optional()
|
|
478
|
+
});
|
|
479
|
+
var DecisionRecordedEventSchema = BaseEventSchema.extend({
|
|
480
|
+
type: z2.literal("decision_recorded"),
|
|
481
|
+
decision_id: DecisionIdSchema,
|
|
482
|
+
title: z2.string(),
|
|
483
|
+
rationale: z2.string().nullable().optional(),
|
|
484
|
+
alternatives: z2.array(z2.string().min(1)).optional(),
|
|
485
|
+
rejected_reason: z2.string().nullable().optional(),
|
|
486
|
+
linked_events: z2.array(EventIdSchema).optional(),
|
|
487
|
+
linked_files: z2.array(z2.string().min(1).max(4096)).optional(),
|
|
488
|
+
// `track` promotes a decision to a strategic, unfinished DIRECTION ("the next
|
|
489
|
+
// essential thing to build, and why") that orientation/handoff resurface every
|
|
490
|
+
// time until it is explicitly closed with `decision void` / supersede — as
|
|
491
|
+
// opposed to a point-in-time `decision`, which is only ever surfaced as the
|
|
492
|
+
// single latest one. This is the intent-continuity layer: a direction agreed
|
|
493
|
+
// in conversation otherwise sinks into the flat decision list and never carries
|
|
494
|
+
// to the next session. Absent (the default) is a plain `decision`, so all
|
|
495
|
+
// pre-existing decision_recorded events round-trip unchanged (additive optional
|
|
496
|
+
// => no schema_version bump; mirrors `note_added.kind`).
|
|
497
|
+
kind: z2.enum(["decision", "track"]).optional()
|
|
498
|
+
});
|
|
499
|
+
var DecisionVoidedEventSchema = BaseEventSchema.extend({
|
|
500
|
+
type: z2.literal("decision_voided"),
|
|
501
|
+
decision_id: DecisionIdSchema,
|
|
502
|
+
reason: z2.string().nullable().optional(),
|
|
503
|
+
superseded_by: DecisionIdSchema.optional()
|
|
504
|
+
});
|
|
505
|
+
var TaskCreatedEventSchema = BaseEventSchema.extend({
|
|
506
|
+
type: z2.literal("task_created"),
|
|
507
|
+
task_id: TaskIdSchema,
|
|
508
|
+
title: z2.string()
|
|
509
|
+
});
|
|
510
|
+
var TaskStatusChangedEventSchema = BaseEventSchema.extend({
|
|
511
|
+
type: z2.literal("task_status_changed"),
|
|
512
|
+
task_id: TaskIdSchema,
|
|
513
|
+
from: z2.string(),
|
|
514
|
+
to: z2.string()
|
|
515
|
+
});
|
|
516
|
+
var TaskReconciledEventSchema = BaseEventSchema.extend({
|
|
517
|
+
type: z2.literal("task_reconciled"),
|
|
518
|
+
task_id: TaskIdSchema,
|
|
519
|
+
removed_created_in_session: SessionIdSchema.nullable().default(null),
|
|
520
|
+
created_in_session_replacement: SessionIdSchema.nullable().default(null),
|
|
521
|
+
removed_linked_sessions: z2.array(SessionIdSchema).default([])
|
|
522
|
+
}).strict();
|
|
523
|
+
var TaskLinkageRefreshedEventSchema = BaseEventSchema.extend({
|
|
524
|
+
type: z2.literal("task_linkage_refreshed"),
|
|
525
|
+
task_id: TaskIdSchema,
|
|
526
|
+
added_linked_sessions: z2.array(SessionIdSchema).default([]),
|
|
527
|
+
removed_linked_sessions: z2.array(SessionIdSchema).default([]),
|
|
528
|
+
final_count: z2.number().int().nonnegative().optional()
|
|
529
|
+
}).strict();
|
|
530
|
+
var TaskDeletedEventSchema = BaseEventSchema.extend({
|
|
531
|
+
type: z2.literal("task_deleted"),
|
|
532
|
+
task_id: TaskIdSchema,
|
|
533
|
+
title: z2.string().min(1)
|
|
534
|
+
}).strict();
|
|
535
|
+
var TaskArchivedEventSchema = BaseEventSchema.extend({
|
|
536
|
+
type: z2.literal("task_archived"),
|
|
537
|
+
task_id: TaskIdSchema,
|
|
538
|
+
title: z2.string().min(1)
|
|
539
|
+
}).strict();
|
|
540
|
+
var NoteAddedEventSchema = BaseEventSchema.extend({
|
|
541
|
+
type: z2.literal("note_added"),
|
|
542
|
+
body: z2.string(),
|
|
543
|
+
// `next_step` marks a note authored by `basou note` as the operator's resume
|
|
544
|
+
// hint, which orientation surfaces as the next starting point. Absent (the
|
|
545
|
+
// `basou session note` default) is a plain annotation orientation does not
|
|
546
|
+
// surface. Optional so pre-existing note_added events remain valid.
|
|
547
|
+
kind: z2.enum(["note", "next_step"]).optional()
|
|
548
|
+
});
|
|
549
|
+
var ReviewFindingSchema = z2.object({
|
|
550
|
+
title: z2.string().min(1),
|
|
551
|
+
severity: z2.enum(["high", "medium", "low"]).optional(),
|
|
552
|
+
location: z2.string().min(1).optional(),
|
|
553
|
+
summary: z2.string().min(1).optional()
|
|
554
|
+
});
|
|
555
|
+
var ReviewBlockedSchema = z2.object({
|
|
556
|
+
title: z2.string().min(1),
|
|
557
|
+
reason: z2.enum(["spec-deviation", "design-reversal"]),
|
|
558
|
+
why: z2.string().min(1).optional()
|
|
559
|
+
});
|
|
560
|
+
var ReviewRecordedEventSchema = BaseEventSchema.extend({
|
|
561
|
+
type: z2.literal("review_recorded"),
|
|
562
|
+
reviewer: z2.string().min(1),
|
|
563
|
+
target: z2.string().min(1),
|
|
564
|
+
repos: z2.array(z2.string().min(1)).optional(),
|
|
565
|
+
repos_resolved: z2.array(z2.string().min(1)).optional(),
|
|
566
|
+
commits: z2.array(z2.string().min(1)).optional(),
|
|
567
|
+
verdict: z2.enum(["pass", "needs-attention", "fail"]).optional(),
|
|
568
|
+
findings: z2.array(ReviewFindingSchema).optional(),
|
|
569
|
+
blocked: z2.array(ReviewBlockedSchema).optional()
|
|
570
|
+
});
|
|
571
|
+
var AdapterOutputEventSchema = BaseEventSchema.extend({
|
|
572
|
+
type: z2.literal("adapter_output"),
|
|
573
|
+
stream: z2.enum(["stdout", "stderr"]),
|
|
574
|
+
summary: z2.string(),
|
|
575
|
+
raw_ref: z2.string(),
|
|
576
|
+
redacted: z2.boolean().optional()
|
|
577
|
+
}).strict();
|
|
578
|
+
var EventSchema = z2.discriminatedUnion("type", [
|
|
579
|
+
SessionStartedEventSchema,
|
|
580
|
+
SessionEndedEventSchema,
|
|
581
|
+
SessionStatusChangedEventSchema,
|
|
582
|
+
ApprovalRequestedEventSchema,
|
|
583
|
+
ApprovalApprovedEventSchema,
|
|
584
|
+
ApprovalRejectedEventSchema,
|
|
585
|
+
ApprovalExpiredEventSchema,
|
|
586
|
+
CommandExecutedEventSchema,
|
|
587
|
+
GitSnapshotEventSchema,
|
|
588
|
+
FileChangedEventSchema,
|
|
589
|
+
DecisionRecordedEventSchema,
|
|
590
|
+
DecisionVoidedEventSchema,
|
|
591
|
+
TaskCreatedEventSchema,
|
|
592
|
+
TaskStatusChangedEventSchema,
|
|
593
|
+
TaskReconciledEventSchema,
|
|
594
|
+
TaskLinkageRefreshedEventSchema,
|
|
595
|
+
TaskDeletedEventSchema,
|
|
596
|
+
TaskArchivedEventSchema,
|
|
597
|
+
NoteAddedEventSchema,
|
|
598
|
+
ReviewRecordedEventSchema,
|
|
599
|
+
AdapterOutputEventSchema
|
|
600
|
+
]);
|
|
601
|
+
|
|
357
602
|
// src/stats/active-time.ts
|
|
358
603
|
var ACTIVE_GAP_CAP_MS = 5 * 60 * 1e3;
|
|
359
604
|
var ENGAGED_TURNS_METHOD = "engaged-turns";
|
|
@@ -564,7 +809,7 @@ function claudeTranscriptToImportPayload(records, options) {
|
|
|
564
809
|
}
|
|
565
810
|
function baseEvent(occurredAt, sessionId) {
|
|
566
811
|
return {
|
|
567
|
-
schema_version:
|
|
812
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
568
813
|
id: prefixedUlid("evt"),
|
|
569
814
|
session_id: sessionId,
|
|
570
815
|
occurred_at: occurredAt,
|
|
@@ -596,7 +841,9 @@ function commandExecutedEvent(occurredAt, sessionId, command, cwd) {
|
|
|
596
841
|
// tool results across twelve transcripts). Reading it is its own change and
|
|
597
842
|
// has not been made, so nothing is claimed here yet.
|
|
598
843
|
exit_code: null,
|
|
599
|
-
|
|
844
|
+
// Not observed: a Bash tool result carries no timing at all, so 0 here would
|
|
845
|
+
// claim a measured duration of zero for every imported command.
|
|
846
|
+
duration_ms: null
|
|
600
847
|
};
|
|
601
848
|
}
|
|
602
849
|
function fileChangedEvent(occurredAt, sessionId, path2, changeType) {
|
|
@@ -789,6 +1036,44 @@ function findBasouSessionStartHook(hooksFile) {
|
|
|
789
1036
|
return null;
|
|
790
1037
|
}
|
|
791
1038
|
|
|
1039
|
+
// src/schemas/observed-duration.ts
|
|
1040
|
+
function readObservedDuration(ev) {
|
|
1041
|
+
const stored = ev.duration_ms;
|
|
1042
|
+
if (stored === null || stored === 0) return null;
|
|
1043
|
+
return stored;
|
|
1044
|
+
}
|
|
1045
|
+
function writeObservedDuration(measuredMs) {
|
|
1046
|
+
if (measuredMs === null || !Number.isFinite(measuredMs)) return null;
|
|
1047
|
+
const rounded = Math.round(measuredMs);
|
|
1048
|
+
if (rounded <= 0) return null;
|
|
1049
|
+
if (!Number.isSafeInteger(rounded)) return null;
|
|
1050
|
+
return rounded;
|
|
1051
|
+
}
|
|
1052
|
+
var ZERO_DURATION_RETIRED_SINCE = "0.2.0";
|
|
1053
|
+
function hasRetiredZeroDuration(ev) {
|
|
1054
|
+
if (ev.type !== "command_executed") return false;
|
|
1055
|
+
if (ev.duration_ms !== 0) return false;
|
|
1056
|
+
return compareSchemaVersion(ev.schema_version, ZERO_DURATION_RETIRED_SINCE) >= 0;
|
|
1057
|
+
}
|
|
1058
|
+
function compareSchemaVersion(a, b) {
|
|
1059
|
+
const pa = parseSchemaVersion(a);
|
|
1060
|
+
const pb = parseSchemaVersion(b);
|
|
1061
|
+
if (pa === null) return pb === null ? 0 : -1;
|
|
1062
|
+
if (pb === null) return 1;
|
|
1063
|
+
for (let i = 0; i < 3; i++) {
|
|
1064
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
1065
|
+
if (d !== 0) return d;
|
|
1066
|
+
}
|
|
1067
|
+
return 0;
|
|
1068
|
+
}
|
|
1069
|
+
function parseSchemaVersion(version) {
|
|
1070
|
+
const parts = version.split(".");
|
|
1071
|
+
if (parts.length !== 3) return null;
|
|
1072
|
+
const nums = parts.map((part) => /^\d+$/.test(part) ? Number.parseInt(part, 10) : Number.NaN);
|
|
1073
|
+
if (nums.some((n) => !Number.isFinite(n))) return null;
|
|
1074
|
+
return [nums[0] ?? 0, nums[1] ?? 0, nums[2] ?? 0];
|
|
1075
|
+
}
|
|
1076
|
+
|
|
792
1077
|
// src/adapters/codex/rollout-importer.ts
|
|
793
1078
|
var CODEX_IMPORT_SOURCE = "codex-import";
|
|
794
1079
|
function codexRolloutToImportPayload(records, options) {
|
|
@@ -846,7 +1131,7 @@ function codexRolloutToImportPayload(records, options) {
|
|
|
846
1131
|
const scan = scanScript(readString4(payload2.input));
|
|
847
1132
|
if (scan.commands.length === 0) continue;
|
|
848
1133
|
const output2 = readCallId(payload2.call_id, outputsByCallId);
|
|
849
|
-
const durationMs = scan.toolCallCount === 1 ? parseWallTimeMs(output2) :
|
|
1134
|
+
const durationMs = scan.toolCallCount === 1 ? parseWallTimeMs(output2) : null;
|
|
850
1135
|
const scriptTsMs = Date.parse(ts);
|
|
851
1136
|
if (Number.isFinite(scriptTsMs)) engagementTsMs.push(scriptTsMs);
|
|
852
1137
|
for (const command2 of scan.commands) {
|
|
@@ -875,10 +1160,17 @@ function codexRolloutToImportPayload(records, options) {
|
|
|
875
1160
|
const output = readCallId(payload2.call_id, outputsByCallId);
|
|
876
1161
|
const execTsMs = Date.parse(ts);
|
|
877
1162
|
if (Number.isFinite(execTsMs)) engagementTsMs.push(execTsMs);
|
|
1163
|
+
const exitCode = parseExitCode(output);
|
|
878
1164
|
derived.push(
|
|
879
1165
|
commandExecutedEvent2(ts, placeholderSessionId, command.cmd, cwd, {
|
|
880
|
-
exitCode
|
|
881
|
-
|
|
1166
|
+
exitCode,
|
|
1167
|
+
// The duration is gated on the SAME token as the exit code. When this
|
|
1168
|
+
// output reports no exit ("Process running with session ID N", or no
|
|
1169
|
+
// outcome line at all), codex handed the turn back while the child was
|
|
1170
|
+
// still running, so its `Wall time` is the interval codex waited and
|
|
1171
|
+
// not how long the command took. A wall-clock duration cannot have been
|
|
1172
|
+
// observed without observing that the process ended.
|
|
1173
|
+
durationMs: exitCode === null ? null : parseWallTimeMs(output)
|
|
882
1174
|
})
|
|
883
1175
|
);
|
|
884
1176
|
}
|
|
@@ -955,7 +1247,7 @@ function codexRolloutToImportPayload(records, options) {
|
|
|
955
1247
|
}
|
|
956
1248
|
function baseEvent2(occurredAt, sessionId) {
|
|
957
1249
|
return {
|
|
958
|
-
schema_version:
|
|
1250
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
959
1251
|
id: prefixedUlid("evt"),
|
|
960
1252
|
session_id: sessionId,
|
|
961
1253
|
occurred_at: occurredAt,
|
|
@@ -1318,11 +1610,11 @@ function parseExitCode(output) {
|
|
|
1318
1610
|
return match?.[1] !== void 0 ? Number.parseInt(match[1], 10) : null;
|
|
1319
1611
|
}
|
|
1320
1612
|
function parseWallTimeMs(output) {
|
|
1321
|
-
if (output === void 0) return
|
|
1613
|
+
if (output === void 0) return null;
|
|
1322
1614
|
const match = output.match(/Wall time:?\s*([\d.]+)\s*seconds/);
|
|
1323
|
-
if (match?.[1] === void 0) return
|
|
1615
|
+
if (match?.[1] === void 0) return null;
|
|
1324
1616
|
const seconds = Number.parseFloat(match[1]);
|
|
1325
|
-
return Number.isFinite(seconds) ?
|
|
1617
|
+
return writeObservedDuration(Number.isFinite(seconds) ? seconds * 1e3 : null);
|
|
1326
1618
|
}
|
|
1327
1619
|
function indexOutputs(records) {
|
|
1328
1620
|
const byId = /* @__PURE__ */ new Map();
|
|
@@ -1361,50 +1653,25 @@ function findErrorCode(error, code, depth = 4) {
|
|
|
1361
1653
|
}
|
|
1362
1654
|
|
|
1363
1655
|
// src/schemas/approval.schema.ts
|
|
1364
|
-
import { z as
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
import { z } from "zod";
|
|
1368
|
-
var SchemaVersionSchema = z.string().regex(/^0\.\d+\.\d+$/, {
|
|
1369
|
-
message: "unsupported .basou format version: this basou reads format major 0 (0.x.y). If this workspace was written by a newer basou, upgrade basou to open it."
|
|
1370
|
-
});
|
|
1371
|
-
var CacheVersionSchema = z.literal("0.1.0");
|
|
1372
|
-
var IsoTimestampSchema = z.string().datetime({ offset: true });
|
|
1373
|
-
var createPrefixedIdSchema = (prefix) => {
|
|
1374
|
-
const refiner = (value) => isValidPrefixedId(value) && value.startsWith(`${prefix}_`);
|
|
1375
|
-
return z.string().refine(refiner, { message: `Expected ${prefix}_<ULID>` }).meta({
|
|
1376
|
-
pattern: `^${prefix}_[0-7][0-9A-HJKMNP-TV-Z]{25}$`,
|
|
1377
|
-
description: `Basou ${prefix} id: \`${prefix}_\` followed by a 26-character Crockford Base32 ULID.`
|
|
1378
|
-
});
|
|
1379
|
-
};
|
|
1380
|
-
var WorkspaceIdSchema = createPrefixedIdSchema("ws");
|
|
1381
|
-
var TaskIdSchema = createPrefixedIdSchema("task");
|
|
1382
|
-
var SessionIdSchema = createPrefixedIdSchema("ses");
|
|
1383
|
-
var EventIdSchema = createPrefixedIdSchema("evt");
|
|
1384
|
-
var ApprovalIdSchema = createPrefixedIdSchema("appr");
|
|
1385
|
-
var DecisionIdSchema = createPrefixedIdSchema("decision");
|
|
1386
|
-
var RiskLevelSchema = z.enum(["low", "medium", "high", "critical"]);
|
|
1387
|
-
var EventSourceSchema = z.string().min(1);
|
|
1388
|
-
|
|
1389
|
-
// src/schemas/approval.schema.ts
|
|
1390
|
-
var ApprovalStatusSchema = z2.enum(["pending", "approved", "rejected", "expired"]);
|
|
1391
|
-
var ApprovalSchema = z2.looseObject({
|
|
1656
|
+
import { z as z3 } from "zod";
|
|
1657
|
+
var ApprovalStatusSchema = z3.enum(["pending", "approved", "rejected", "expired"]);
|
|
1658
|
+
var ApprovalSchema = z3.looseObject({
|
|
1392
1659
|
schema_version: SchemaVersionSchema,
|
|
1393
1660
|
id: ApprovalIdSchema,
|
|
1394
1661
|
session_id: SessionIdSchema,
|
|
1395
1662
|
created_at: IsoTimestampSchema,
|
|
1396
1663
|
status: ApprovalStatusSchema,
|
|
1397
1664
|
risk_level: RiskLevelSchema,
|
|
1398
|
-
action:
|
|
1399
|
-
reason:
|
|
1665
|
+
action: z3.looseObject({ kind: z3.string() }).passthrough(),
|
|
1666
|
+
reason: z3.string(),
|
|
1400
1667
|
expires_at: IsoTimestampSchema.nullable().default(null),
|
|
1401
1668
|
// The four fields below are null while `status === "pending"` and set
|
|
1402
1669
|
// once a resolver records a decision. Defaulting to null keeps the
|
|
1403
1670
|
// pending YAML free of explicit nulls if a producer omits them.
|
|
1404
|
-
resolver:
|
|
1671
|
+
resolver: z3.string().nullable().default(null),
|
|
1405
1672
|
resolved_at: IsoTimestampSchema.nullable().default(null),
|
|
1406
|
-
note:
|
|
1407
|
-
rejection_reason:
|
|
1673
|
+
note: z3.string().nullable().default(null),
|
|
1674
|
+
rejection_reason: z3.string().nullable().default(null)
|
|
1408
1675
|
});
|
|
1409
1676
|
|
|
1410
1677
|
// src/storage/yaml-store.ts
|
|
@@ -1536,221 +1803,12 @@ function isLazyExpired(approval, now) {
|
|
|
1536
1803
|
|
|
1537
1804
|
// src/decisions/decisions-renderer.ts
|
|
1538
1805
|
import { lstat as lstat2 } from "fs/promises";
|
|
1539
|
-
import { dirname as dirname2, join as
|
|
1806
|
+
import { dirname as dirname2, join as join7, resolve } from "path";
|
|
1540
1807
|
|
|
1541
1808
|
// src/events/event-replay.ts
|
|
1542
1809
|
import { createReadStream } from "fs";
|
|
1543
1810
|
import { stat } from "fs/promises";
|
|
1544
1811
|
import { join as join2 } from "path";
|
|
1545
|
-
|
|
1546
|
-
// src/schemas/event.schema.ts
|
|
1547
|
-
import { z as z3 } from "zod";
|
|
1548
|
-
var BaseEventSchema = z3.object({
|
|
1549
|
-
schema_version: SchemaVersionSchema,
|
|
1550
|
-
id: EventIdSchema,
|
|
1551
|
-
session_id: SessionIdSchema,
|
|
1552
|
-
occurred_at: IsoTimestampSchema,
|
|
1553
|
-
source: EventSourceSchema,
|
|
1554
|
-
// Tamper-evidence back-pointer (hex sha-256 of the PREVIOUS event line's
|
|
1555
|
-
// written bytes; the first line carries the session-bound genesis hash).
|
|
1556
|
-
// Present only on sessions written with chaining enabled (import paths);
|
|
1557
|
-
// live/ad-hoc sessions omit it. Declared on the base so the `.strict()`
|
|
1558
|
-
// variants below treat it as a known key. Additive optional => no
|
|
1559
|
-
// schema_version bump.
|
|
1560
|
-
prev_hash: z3.string().optional()
|
|
1561
|
-
});
|
|
1562
|
-
var SessionStartedEventSchema = BaseEventSchema.extend({
|
|
1563
|
-
type: z3.literal("session_started")
|
|
1564
|
-
});
|
|
1565
|
-
var SessionEndedEventSchema = BaseEventSchema.extend({
|
|
1566
|
-
type: z3.literal("session_ended"),
|
|
1567
|
-
exit_code: z3.number().int().optional()
|
|
1568
|
-
});
|
|
1569
|
-
var SessionStatusChangedEventSchema = BaseEventSchema.extend({
|
|
1570
|
-
type: z3.literal("session_status_changed"),
|
|
1571
|
-
from: z3.string(),
|
|
1572
|
-
to: z3.string()
|
|
1573
|
-
});
|
|
1574
|
-
var ApprovalRequestedEventSchema = BaseEventSchema.extend({
|
|
1575
|
-
type: z3.literal("approval_requested"),
|
|
1576
|
-
approval_id: ApprovalIdSchema,
|
|
1577
|
-
expires_at: IsoTimestampSchema.nullable().default(null),
|
|
1578
|
-
risk_level: RiskLevelSchema,
|
|
1579
|
-
// `action.kind` is required; additional fields are allowed to support
|
|
1580
|
-
// future action shapes (shell_command, external_send, ...).
|
|
1581
|
-
action: z3.object({ kind: z3.string() }).passthrough(),
|
|
1582
|
-
reason: z3.string(),
|
|
1583
|
-
status: z3.literal("pending")
|
|
1584
|
-
});
|
|
1585
|
-
var ApprovalApprovedEventSchema = BaseEventSchema.extend({
|
|
1586
|
-
type: z3.literal("approval_approved"),
|
|
1587
|
-
approval_id: ApprovalIdSchema,
|
|
1588
|
-
resolver: z3.string().optional(),
|
|
1589
|
-
note: z3.string().nullable().optional()
|
|
1590
|
-
});
|
|
1591
|
-
var ApprovalRejectedEventSchema = BaseEventSchema.extend({
|
|
1592
|
-
type: z3.literal("approval_rejected"),
|
|
1593
|
-
approval_id: ApprovalIdSchema,
|
|
1594
|
-
resolver: z3.string().optional(),
|
|
1595
|
-
reason: z3.string()
|
|
1596
|
-
});
|
|
1597
|
-
var ApprovalExpiredEventSchema = BaseEventSchema.extend({
|
|
1598
|
-
type: z3.literal("approval_expired"),
|
|
1599
|
-
approval_id: ApprovalIdSchema
|
|
1600
|
-
});
|
|
1601
|
-
var CommandExecutedEventSchema = BaseEventSchema.extend({
|
|
1602
|
-
type: z3.literal("command_executed"),
|
|
1603
|
-
command: z3.string().nullable(),
|
|
1604
|
-
args: z3.array(z3.string()),
|
|
1605
|
-
cwd: z3.string().nullable(),
|
|
1606
|
-
exit_code: z3.number().int().nullable(),
|
|
1607
|
-
signal: z3.string().nullable().optional(),
|
|
1608
|
-
received_signal: z3.string().nullable().optional(),
|
|
1609
|
-
duration_ms: z3.number().int().nonnegative()
|
|
1610
|
-
});
|
|
1611
|
-
var GitSnapshotEventSchema = BaseEventSchema.extend({
|
|
1612
|
-
type: z3.literal("git_snapshot"),
|
|
1613
|
-
head: z3.string(),
|
|
1614
|
-
branch: z3.string(),
|
|
1615
|
-
dirty: z3.boolean(),
|
|
1616
|
-
staged: z3.array(z3.string()),
|
|
1617
|
-
unstaged: z3.array(z3.string()),
|
|
1618
|
-
untracked: z3.array(z3.string()),
|
|
1619
|
-
ahead: z3.number().int().nonnegative().optional(),
|
|
1620
|
-
behind: z3.number().int().nonnegative().optional()
|
|
1621
|
-
});
|
|
1622
|
-
var FileChangedEventSchema = BaseEventSchema.extend({
|
|
1623
|
-
type: z3.literal("file_changed"),
|
|
1624
|
-
path: z3.string(),
|
|
1625
|
-
change_type: z3.enum(["added", "modified", "deleted", "renamed"]),
|
|
1626
|
-
// Renamed entries record the previous path here. Optional + nullable to
|
|
1627
|
-
// keep the wire format stable for added / modified / deleted events.
|
|
1628
|
-
old_path: z3.string().nullable().optional()
|
|
1629
|
-
});
|
|
1630
|
-
var DecisionRecordedEventSchema = BaseEventSchema.extend({
|
|
1631
|
-
type: z3.literal("decision_recorded"),
|
|
1632
|
-
decision_id: DecisionIdSchema,
|
|
1633
|
-
title: z3.string(),
|
|
1634
|
-
rationale: z3.string().nullable().optional(),
|
|
1635
|
-
alternatives: z3.array(z3.string().min(1)).optional(),
|
|
1636
|
-
rejected_reason: z3.string().nullable().optional(),
|
|
1637
|
-
linked_events: z3.array(EventIdSchema).optional(),
|
|
1638
|
-
linked_files: z3.array(z3.string().min(1).max(4096)).optional(),
|
|
1639
|
-
// `track` promotes a decision to a strategic, unfinished DIRECTION ("the next
|
|
1640
|
-
// essential thing to build, and why") that orientation/handoff resurface every
|
|
1641
|
-
// time until it is explicitly closed with `decision void` / supersede — as
|
|
1642
|
-
// opposed to a point-in-time `decision`, which is only ever surfaced as the
|
|
1643
|
-
// single latest one. This is the intent-continuity layer: a direction agreed
|
|
1644
|
-
// in conversation otherwise sinks into the flat decision list and never carries
|
|
1645
|
-
// to the next session. Absent (the default) is a plain `decision`, so all
|
|
1646
|
-
// pre-existing decision_recorded events round-trip unchanged (additive optional
|
|
1647
|
-
// => no schema_version bump; mirrors `note_added.kind`).
|
|
1648
|
-
kind: z3.enum(["decision", "track"]).optional()
|
|
1649
|
-
});
|
|
1650
|
-
var DecisionVoidedEventSchema = BaseEventSchema.extend({
|
|
1651
|
-
type: z3.literal("decision_voided"),
|
|
1652
|
-
decision_id: DecisionIdSchema,
|
|
1653
|
-
reason: z3.string().nullable().optional(),
|
|
1654
|
-
superseded_by: DecisionIdSchema.optional()
|
|
1655
|
-
});
|
|
1656
|
-
var TaskCreatedEventSchema = BaseEventSchema.extend({
|
|
1657
|
-
type: z3.literal("task_created"),
|
|
1658
|
-
task_id: TaskIdSchema,
|
|
1659
|
-
title: z3.string()
|
|
1660
|
-
});
|
|
1661
|
-
var TaskStatusChangedEventSchema = BaseEventSchema.extend({
|
|
1662
|
-
type: z3.literal("task_status_changed"),
|
|
1663
|
-
task_id: TaskIdSchema,
|
|
1664
|
-
from: z3.string(),
|
|
1665
|
-
to: z3.string()
|
|
1666
|
-
});
|
|
1667
|
-
var TaskReconciledEventSchema = BaseEventSchema.extend({
|
|
1668
|
-
type: z3.literal("task_reconciled"),
|
|
1669
|
-
task_id: TaskIdSchema,
|
|
1670
|
-
removed_created_in_session: SessionIdSchema.nullable().default(null),
|
|
1671
|
-
created_in_session_replacement: SessionIdSchema.nullable().default(null),
|
|
1672
|
-
removed_linked_sessions: z3.array(SessionIdSchema).default([])
|
|
1673
|
-
}).strict();
|
|
1674
|
-
var TaskLinkageRefreshedEventSchema = BaseEventSchema.extend({
|
|
1675
|
-
type: z3.literal("task_linkage_refreshed"),
|
|
1676
|
-
task_id: TaskIdSchema,
|
|
1677
|
-
added_linked_sessions: z3.array(SessionIdSchema).default([]),
|
|
1678
|
-
removed_linked_sessions: z3.array(SessionIdSchema).default([]),
|
|
1679
|
-
final_count: z3.number().int().nonnegative().optional()
|
|
1680
|
-
}).strict();
|
|
1681
|
-
var TaskDeletedEventSchema = BaseEventSchema.extend({
|
|
1682
|
-
type: z3.literal("task_deleted"),
|
|
1683
|
-
task_id: TaskIdSchema,
|
|
1684
|
-
title: z3.string().min(1)
|
|
1685
|
-
}).strict();
|
|
1686
|
-
var TaskArchivedEventSchema = BaseEventSchema.extend({
|
|
1687
|
-
type: z3.literal("task_archived"),
|
|
1688
|
-
task_id: TaskIdSchema,
|
|
1689
|
-
title: z3.string().min(1)
|
|
1690
|
-
}).strict();
|
|
1691
|
-
var NoteAddedEventSchema = BaseEventSchema.extend({
|
|
1692
|
-
type: z3.literal("note_added"),
|
|
1693
|
-
body: z3.string(),
|
|
1694
|
-
// `next_step` marks a note authored by `basou note` as the operator's resume
|
|
1695
|
-
// hint, which orientation surfaces as the next starting point. Absent (the
|
|
1696
|
-
// `basou session note` default) is a plain annotation orientation does not
|
|
1697
|
-
// surface. Optional so pre-existing note_added events remain valid.
|
|
1698
|
-
kind: z3.enum(["note", "next_step"]).optional()
|
|
1699
|
-
});
|
|
1700
|
-
var ReviewFindingSchema = z3.object({
|
|
1701
|
-
title: z3.string().min(1),
|
|
1702
|
-
severity: z3.enum(["high", "medium", "low"]).optional(),
|
|
1703
|
-
location: z3.string().min(1).optional(),
|
|
1704
|
-
summary: z3.string().min(1).optional()
|
|
1705
|
-
});
|
|
1706
|
-
var ReviewBlockedSchema = z3.object({
|
|
1707
|
-
title: z3.string().min(1),
|
|
1708
|
-
reason: z3.enum(["spec-deviation", "design-reversal"]),
|
|
1709
|
-
why: z3.string().min(1).optional()
|
|
1710
|
-
});
|
|
1711
|
-
var ReviewRecordedEventSchema = BaseEventSchema.extend({
|
|
1712
|
-
type: z3.literal("review_recorded"),
|
|
1713
|
-
reviewer: z3.string().min(1),
|
|
1714
|
-
target: z3.string().min(1),
|
|
1715
|
-
repos: z3.array(z3.string().min(1)).optional(),
|
|
1716
|
-
repos_resolved: z3.array(z3.string().min(1)).optional(),
|
|
1717
|
-
commits: z3.array(z3.string().min(1)).optional(),
|
|
1718
|
-
verdict: z3.enum(["pass", "needs-attention", "fail"]).optional(),
|
|
1719
|
-
findings: z3.array(ReviewFindingSchema).optional(),
|
|
1720
|
-
blocked: z3.array(ReviewBlockedSchema).optional()
|
|
1721
|
-
});
|
|
1722
|
-
var AdapterOutputEventSchema = BaseEventSchema.extend({
|
|
1723
|
-
type: z3.literal("adapter_output"),
|
|
1724
|
-
stream: z3.enum(["stdout", "stderr"]),
|
|
1725
|
-
summary: z3.string(),
|
|
1726
|
-
raw_ref: z3.string(),
|
|
1727
|
-
redacted: z3.boolean().optional()
|
|
1728
|
-
}).strict();
|
|
1729
|
-
var EventSchema = z3.discriminatedUnion("type", [
|
|
1730
|
-
SessionStartedEventSchema,
|
|
1731
|
-
SessionEndedEventSchema,
|
|
1732
|
-
SessionStatusChangedEventSchema,
|
|
1733
|
-
ApprovalRequestedEventSchema,
|
|
1734
|
-
ApprovalApprovedEventSchema,
|
|
1735
|
-
ApprovalRejectedEventSchema,
|
|
1736
|
-
ApprovalExpiredEventSchema,
|
|
1737
|
-
CommandExecutedEventSchema,
|
|
1738
|
-
GitSnapshotEventSchema,
|
|
1739
|
-
FileChangedEventSchema,
|
|
1740
|
-
DecisionRecordedEventSchema,
|
|
1741
|
-
DecisionVoidedEventSchema,
|
|
1742
|
-
TaskCreatedEventSchema,
|
|
1743
|
-
TaskStatusChangedEventSchema,
|
|
1744
|
-
TaskReconciledEventSchema,
|
|
1745
|
-
TaskLinkageRefreshedEventSchema,
|
|
1746
|
-
TaskDeletedEventSchema,
|
|
1747
|
-
TaskArchivedEventSchema,
|
|
1748
|
-
NoteAddedEventSchema,
|
|
1749
|
-
ReviewRecordedEventSchema,
|
|
1750
|
-
AdapterOutputEventSchema
|
|
1751
|
-
]);
|
|
1752
|
-
|
|
1753
|
-
// src/events/event-replay.ts
|
|
1754
1812
|
async function* replayEvents(sessionDir, options = {}) {
|
|
1755
1813
|
const filePath = join2(sessionDir, "events.jsonl");
|
|
1756
1814
|
try {
|
|
@@ -1815,6 +1873,9 @@ function processLine(rawLine, lineNo, options) {
|
|
|
1815
1873
|
options.onWarning?.({ kind: "schema_violation", line: lineNo, cause: result.error });
|
|
1816
1874
|
return null;
|
|
1817
1875
|
}
|
|
1876
|
+
if (hasRetiredZeroDuration(result.data)) {
|
|
1877
|
+
options.onWarning?.({ kind: "retired_zero_duration", line: lineNo });
|
|
1878
|
+
}
|
|
1818
1879
|
return result.data;
|
|
1819
1880
|
}
|
|
1820
1881
|
async function readAllEvents(sessionDir, options = {}) {
|
|
@@ -2098,6 +2159,7 @@ var EN = {
|
|
|
2098
2159
|
recentDecisionsLabel: "Decisions",
|
|
2099
2160
|
recentNextStepLabel: "Next step",
|
|
2100
2161
|
recentChangedLabel: "Changed",
|
|
2162
|
+
scratchOmitted: (count) => `(+${count} scratch omitted)`,
|
|
2101
2163
|
trackCloseInstruction: "When finished, close it with `basou decision void <decision_id>`. It stays listed here every time until closed.",
|
|
2102
2164
|
nextStepRecordedLabel: (age) => `Next step (recorded, ${age})`,
|
|
2103
2165
|
noteStaleNote: (age) => `Note: work continued after this was recorded (latest activity ${age}), so this starting point may be stale.`,
|
|
@@ -2193,6 +2255,7 @@ var JA = {
|
|
|
2193
2255
|
recentDecisionsLabel: "\u5224\u65AD",
|
|
2194
2256
|
recentNextStepLabel: "\u6B21\u306E\u8D77\u70B9",
|
|
2195
2257
|
recentChangedLabel: "\u5909\u66F4",
|
|
2258
|
+
scratchOmitted: (count) => `(\u4F5C\u696D\u7528\u4E00\u6642\u30D5\u30A1\u30A4\u30EB ${count} \u4EF6\u306F\u9664\u5916)`,
|
|
2196
2259
|
trackCloseInstruction: "\u5B8C\u4E86\u3057\u305F\u3089 `basou decision void <decision_id>` \u3067\u9589\u3058\u3066\u304F\u3060\u3055\u3044\u3002\u9589\u3058\u308B\u307E\u3067\u6BCE\u56DE\u3053\u3053\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002",
|
|
2197
2260
|
nextStepRecordedLabel: (age) => `\u6B21\u306E\u8D77\u70B9 (\u8A18\u9332\u6E08\u307F, ${age})`,
|
|
2198
2261
|
noteStaleNote: (age) => `\u6CE8: \u3053\u306E\u8D77\u70B9\u306E\u8A18\u9332\u5F8C (\u6700\u7D42\u6D3B\u52D5 ${age}) \u3082\u4F5C\u696D\u304C\u7D9A\u3044\u3066\u3044\u307E\u3059\u3002\u518D\u958B\u70B9\u304C\u53E4\u3044\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`,
|
|
@@ -2453,11 +2516,11 @@ var PRESET_JA = {
|
|
|
2453
2516
|
|
|
2454
2517
|
// src/storage/sessions.ts
|
|
2455
2518
|
import { readdir as readdir2 } from "fs/promises";
|
|
2456
|
-
import { join as
|
|
2519
|
+
import { join as join6 } from "path";
|
|
2457
2520
|
|
|
2458
2521
|
// src/events/chained-append.ts
|
|
2459
|
-
import { appendFile, readFile as readFile3 } from "fs/promises";
|
|
2460
|
-
import { join as
|
|
2522
|
+
import { appendFile as appendFile2, readFile as readFile3 } from "fs/promises";
|
|
2523
|
+
import { join as join5 } from "path";
|
|
2461
2524
|
|
|
2462
2525
|
// src/storage/lockfile.ts
|
|
2463
2526
|
import { mkdir, readFile as readFile2, unlink as unlink2 } from "fs/promises";
|
|
@@ -2533,8 +2596,8 @@ async function isStaleLock(lockPath) {
|
|
|
2533
2596
|
}
|
|
2534
2597
|
}
|
|
2535
2598
|
function lockfilePath(paths, scope, resourceId) {
|
|
2536
|
-
const
|
|
2537
|
-
const ulid2 =
|
|
2599
|
+
const sep2 = resourceId.indexOf("_");
|
|
2600
|
+
const ulid2 = sep2 >= 0 ? resourceId.slice(sep2 + 1) : resourceId;
|
|
2538
2601
|
return join3(paths.locks, `${scope}_${ulid2}.lock`);
|
|
2539
2602
|
}
|
|
2540
2603
|
|
|
@@ -2579,6 +2642,61 @@ function chainRawJsonLines(rawLines, sessionId) {
|
|
|
2579
2642
|
return { lines, headHash: prev, count: lines.length };
|
|
2580
2643
|
}
|
|
2581
2644
|
|
|
2645
|
+
// src/events/event-writer.ts
|
|
2646
|
+
import { appendFile } from "fs/promises";
|
|
2647
|
+
import { basename, join as join4 } from "path";
|
|
2648
|
+
async function appendEvent(sessionDir, event) {
|
|
2649
|
+
let validated;
|
|
2650
|
+
try {
|
|
2651
|
+
validated = EventSchema.parse(event);
|
|
2652
|
+
} catch (error) {
|
|
2653
|
+
throw new Error("Invalid Basou event payload", { cause: error });
|
|
2654
|
+
}
|
|
2655
|
+
assertWritableEvent(validated);
|
|
2656
|
+
const line = `${serializeEventLine(validated)}
|
|
2657
|
+
`;
|
|
2658
|
+
try {
|
|
2659
|
+
await appendFile(join4(sessionDir, "events.jsonl"), line, "utf8");
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
throw new Error("Failed to append event to events.jsonl", { cause: error });
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
async function writeEventsBulk(sessionDir, events, options = {}) {
|
|
2665
|
+
const validated = [];
|
|
2666
|
+
try {
|
|
2667
|
+
for (const event of events) {
|
|
2668
|
+
validated.push(EventSchema.parse(event));
|
|
2669
|
+
}
|
|
2670
|
+
} catch (error) {
|
|
2671
|
+
throw new Error("Invalid Basou event payload", { cause: error });
|
|
2672
|
+
}
|
|
2673
|
+
for (const event of validated) assertWritableEvent(event);
|
|
2674
|
+
const filePath = join4(sessionDir, "events.jsonl");
|
|
2675
|
+
let body;
|
|
2676
|
+
let result = null;
|
|
2677
|
+
if (options.chain === true) {
|
|
2678
|
+
const { lines, headHash, count } = chainEvents(validated, basename(sessionDir));
|
|
2679
|
+
body = lines.length > 0 ? `${lines.join("\n")}
|
|
2680
|
+
` : "";
|
|
2681
|
+
result = count > 0 ? { headHash, count } : null;
|
|
2682
|
+
} else {
|
|
2683
|
+
body = validated.length > 0 ? `${validated.map(serializeEventLine).join("\n")}
|
|
2684
|
+
` : "";
|
|
2685
|
+
}
|
|
2686
|
+
try {
|
|
2687
|
+
await atomicReplace(filePath, body);
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
throw new Error("Failed to write events.jsonl", { cause: error });
|
|
2690
|
+
}
|
|
2691
|
+
return result;
|
|
2692
|
+
}
|
|
2693
|
+
function assertWritableEvent(event) {
|
|
2694
|
+
if (!hasRetiredZeroDuration(event)) return;
|
|
2695
|
+
throw new Error(
|
|
2696
|
+
`Refusing to write command_executed with duration_ms: 0 at schema_version ${event.schema_version}: 0 is not a duration a command can have had, and writers at ${ZERO_DURATION_RETIRED_SINCE} and above record null`
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2582
2700
|
// src/events/chained-append.ts
|
|
2583
2701
|
function splitLinesBytes(buf) {
|
|
2584
2702
|
const out = [];
|
|
@@ -2601,7 +2719,7 @@ function carriesPrevHash(line) {
|
|
|
2601
2719
|
}
|
|
2602
2720
|
}
|
|
2603
2721
|
async function inspectChainTail(paths, sessionId) {
|
|
2604
|
-
const filePath =
|
|
2722
|
+
const filePath = join5(paths.sessions, sessionId, "events.jsonl");
|
|
2605
2723
|
let raw;
|
|
2606
2724
|
try {
|
|
2607
2725
|
raw = await readFile3(filePath);
|
|
@@ -2637,10 +2755,11 @@ async function appendChainedEventLocked(paths, sessionId, event) {
|
|
|
2637
2755
|
} catch (error) {
|
|
2638
2756
|
throw new Error("Invalid Basou event payload", { cause: error });
|
|
2639
2757
|
}
|
|
2758
|
+
assertWritableEvent(validated);
|
|
2640
2759
|
const tail = await inspectChainTail(paths, sessionId);
|
|
2641
2760
|
const line = tail.chained ? serializeEventLine({ ...validated, prev_hash: tail.head }) : serializeEventLine(validated);
|
|
2642
2761
|
try {
|
|
2643
|
-
await
|
|
2762
|
+
await appendFile2(join5(paths.sessions, sessionId, "events.jsonl"), `${line}
|
|
2644
2763
|
`, "utf8");
|
|
2645
2764
|
} catch (error) {
|
|
2646
2765
|
throw new Error("Failed to append event to events.jsonl", { cause: error });
|
|
@@ -2752,7 +2871,7 @@ async function enumerateSessionDirs(paths) {
|
|
|
2752
2871
|
}
|
|
2753
2872
|
}
|
|
2754
2873
|
async function readSessionYaml(paths, sessionId) {
|
|
2755
|
-
const filePath =
|
|
2874
|
+
const filePath = join6(paths.sessions, sessionId, "session.yaml");
|
|
2756
2875
|
let raw;
|
|
2757
2876
|
try {
|
|
2758
2877
|
raw = await readYamlFile(filePath);
|
|
@@ -2776,7 +2895,7 @@ async function finalizeSessionYaml(paths, sessionId, mutate) {
|
|
|
2776
2895
|
session.session.integrity = { head_hash: tail.head, event_count: tail.count };
|
|
2777
2896
|
}
|
|
2778
2897
|
const validated = SessionSchema.parse(session);
|
|
2779
|
-
await overwriteYamlFile(
|
|
2898
|
+
await overwriteYamlFile(join6(paths.sessions, sessionId, "session.yaml"), validated);
|
|
2780
2899
|
} finally {
|
|
2781
2900
|
await lock.release();
|
|
2782
2901
|
}
|
|
@@ -2785,7 +2904,7 @@ async function classifySuspect(paths, sessionId, session, now, onWarning) {
|
|
|
2785
2904
|
if (session.session.status !== "running") {
|
|
2786
2905
|
return { suspect: false, suspectReason: null };
|
|
2787
2906
|
}
|
|
2788
|
-
const sessionDir =
|
|
2907
|
+
const sessionDir = join6(paths.sessions, sessionId);
|
|
2789
2908
|
let endedFound = false;
|
|
2790
2909
|
let lastEventOccurredAt = null;
|
|
2791
2910
|
const replayOpts = onWarning !== void 0 ? { onWarning } : {};
|
|
@@ -2895,7 +3014,7 @@ async function renderDecisions(input) {
|
|
|
2895
3014
|
const voids = /* @__PURE__ */ new Map();
|
|
2896
3015
|
const knownEventIds = /* @__PURE__ */ new Set();
|
|
2897
3016
|
for (const entry of entries) {
|
|
2898
|
-
const sessionDir =
|
|
3017
|
+
const sessionDir = join7(input.paths.sessions, entry.sessionId);
|
|
2899
3018
|
try {
|
|
2900
3019
|
for await (const ev of replayEvents(sessionDir, {
|
|
2901
3020
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
@@ -3022,53 +3141,6 @@ function shortDecisionSessionId(sessionId) {
|
|
|
3022
3141
|
return sessionId.slice(0, 10);
|
|
3023
3142
|
}
|
|
3024
3143
|
|
|
3025
|
-
// src/events/event-writer.ts
|
|
3026
|
-
import { appendFile as appendFile2 } from "fs/promises";
|
|
3027
|
-
import { basename, join as join7 } from "path";
|
|
3028
|
-
async function appendEvent(sessionDir, event) {
|
|
3029
|
-
let validated;
|
|
3030
|
-
try {
|
|
3031
|
-
validated = EventSchema.parse(event);
|
|
3032
|
-
} catch (error) {
|
|
3033
|
-
throw new Error("Invalid Basou event payload", { cause: error });
|
|
3034
|
-
}
|
|
3035
|
-
const line = `${serializeEventLine(validated)}
|
|
3036
|
-
`;
|
|
3037
|
-
try {
|
|
3038
|
-
await appendFile2(join7(sessionDir, "events.jsonl"), line, "utf8");
|
|
3039
|
-
} catch (error) {
|
|
3040
|
-
throw new Error("Failed to append event to events.jsonl", { cause: error });
|
|
3041
|
-
}
|
|
3042
|
-
}
|
|
3043
|
-
async function writeEventsBulk(sessionDir, events, options = {}) {
|
|
3044
|
-
const validated = [];
|
|
3045
|
-
try {
|
|
3046
|
-
for (const event of events) {
|
|
3047
|
-
validated.push(EventSchema.parse(event));
|
|
3048
|
-
}
|
|
3049
|
-
} catch (error) {
|
|
3050
|
-
throw new Error("Invalid Basou event payload", { cause: error });
|
|
3051
|
-
}
|
|
3052
|
-
const filePath = join7(sessionDir, "events.jsonl");
|
|
3053
|
-
let body;
|
|
3054
|
-
let result = null;
|
|
3055
|
-
if (options.chain === true) {
|
|
3056
|
-
const { lines, headHash, count } = chainEvents(validated, basename(sessionDir));
|
|
3057
|
-
body = lines.length > 0 ? `${lines.join("\n")}
|
|
3058
|
-
` : "";
|
|
3059
|
-
result = count > 0 ? { headHash, count } : null;
|
|
3060
|
-
} else {
|
|
3061
|
-
body = validated.length > 0 ? `${validated.map(serializeEventLine).join("\n")}
|
|
3062
|
-
` : "";
|
|
3063
|
-
}
|
|
3064
|
-
try {
|
|
3065
|
-
await atomicReplace(filePath, body);
|
|
3066
|
-
} catch (error) {
|
|
3067
|
-
throw new Error("Failed to write events.jsonl", { cause: error });
|
|
3068
|
-
}
|
|
3069
|
-
return result;
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
3144
|
// src/events/verify.ts
|
|
3073
3145
|
import { readFile as readFile4 } from "fs/promises";
|
|
3074
3146
|
import { join as join8 } from "path";
|
|
@@ -3597,6 +3669,29 @@ function pickLatestSubstantiveEntry(entries) {
|
|
|
3597
3669
|
})[0];
|
|
3598
3670
|
}
|
|
3599
3671
|
|
|
3672
|
+
// src/lib/transient-paths.ts
|
|
3673
|
+
import { tmpdir } from "os";
|
|
3674
|
+
import { normalize, sep } from "path";
|
|
3675
|
+
var TEMP_ROOTS = ["/tmp", "/var/tmp"];
|
|
3676
|
+
var MIN_ENCODED_DASHES = 3;
|
|
3677
|
+
function isEncodedWorkingDirectory(segment) {
|
|
3678
|
+
return segment.startsWith("-") && segment.split("-").length - 1 >= MIN_ENCODED_DASHES;
|
|
3679
|
+
}
|
|
3680
|
+
function withPrivateAlias(root) {
|
|
3681
|
+
const normalized = normalize(root);
|
|
3682
|
+
if (normalized.startsWith(`${sep}private${sep}`)) {
|
|
3683
|
+
return [normalized, normalized.slice(`${sep}private`.length)];
|
|
3684
|
+
}
|
|
3685
|
+
return [normalized, `${sep}private${normalized}`];
|
|
3686
|
+
}
|
|
3687
|
+
function isTransientToolPath(filePath, temp = tmpdir()) {
|
|
3688
|
+
const candidate = normalize(filePath);
|
|
3689
|
+
if (!candidate.startsWith(sep)) return false;
|
|
3690
|
+
const root = [...TEMP_ROOTS, temp].flatMap(withPrivateAlias).find((r) => candidate === r || candidate.startsWith(r + sep));
|
|
3691
|
+
if (root === void 0) return false;
|
|
3692
|
+
return candidate.slice(root.length).split(sep).some(isEncodedWorkingDirectory);
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3600
3695
|
// src/storage/tasks.ts
|
|
3601
3696
|
import { createHash as createHash2 } from "crypto";
|
|
3602
3697
|
import { mkdir as mkdir3, readdir as readdir4, readFile as readFile7, rename as rename2, stat as stat3, unlink as unlink3 } from "fs/promises";
|
|
@@ -3752,7 +3847,7 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
3752
3847
|
});
|
|
3753
3848
|
const events = [
|
|
3754
3849
|
{
|
|
3755
|
-
schema_version:
|
|
3850
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
3756
3851
|
id: startedEventId,
|
|
3757
3852
|
session_id: sessionId,
|
|
3758
3853
|
occurred_at: input.occurredAt,
|
|
@@ -3760,7 +3855,7 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
3760
3855
|
type: "session_started"
|
|
3761
3856
|
},
|
|
3762
3857
|
{
|
|
3763
|
-
schema_version:
|
|
3858
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
3764
3859
|
id: statusToRunningEventId,
|
|
3765
3860
|
session_id: sessionId,
|
|
3766
3861
|
occurred_at: input.occurredAt,
|
|
@@ -3771,7 +3866,7 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
3771
3866
|
},
|
|
3772
3867
|
...targetEvents,
|
|
3773
3868
|
{
|
|
3774
|
-
schema_version:
|
|
3869
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
3775
3870
|
id: statusToCompletedEventId,
|
|
3776
3871
|
session_id: sessionId,
|
|
3777
3872
|
occurred_at: input.occurredAt,
|
|
@@ -3781,7 +3876,7 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
3781
3876
|
to: "completed"
|
|
3782
3877
|
},
|
|
3783
3878
|
{
|
|
3784
|
-
schema_version:
|
|
3879
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
3785
3880
|
id: endedEventId,
|
|
3786
3881
|
session_id: sessionId,
|
|
3787
3882
|
occurred_at: input.occurredAt,
|
|
@@ -4243,7 +4338,7 @@ var TaskWriteAfterEventError = class extends Error {
|
|
|
4243
4338
|
};
|
|
4244
4339
|
function buildTaskCreatedEvent(input) {
|
|
4245
4340
|
return {
|
|
4246
|
-
schema_version:
|
|
4341
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4247
4342
|
id: input.eventId,
|
|
4248
4343
|
session_id: input.sessionId,
|
|
4249
4344
|
occurred_at: input.occurredAt,
|
|
@@ -4255,7 +4350,7 @@ function buildTaskCreatedEvent(input) {
|
|
|
4255
4350
|
}
|
|
4256
4351
|
function buildTaskStatusChangedEvent(input) {
|
|
4257
4352
|
return {
|
|
4258
|
-
schema_version:
|
|
4353
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4259
4354
|
id: input.eventId,
|
|
4260
4355
|
session_id: input.sessionId,
|
|
4261
4356
|
occurred_at: input.occurredAt,
|
|
@@ -4288,7 +4383,7 @@ function buildAdHocArchiveLabel(title) {
|
|
|
4288
4383
|
}
|
|
4289
4384
|
function buildTaskReconciledEvent(input) {
|
|
4290
4385
|
return {
|
|
4291
|
-
schema_version:
|
|
4386
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4292
4387
|
id: input.eventId,
|
|
4293
4388
|
session_id: input.sessionId,
|
|
4294
4389
|
occurred_at: input.occurredAt,
|
|
@@ -4302,7 +4397,7 @@ function buildTaskReconciledEvent(input) {
|
|
|
4302
4397
|
}
|
|
4303
4398
|
function buildTaskDeletedEvent(input) {
|
|
4304
4399
|
return {
|
|
4305
|
-
schema_version:
|
|
4400
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4306
4401
|
id: input.eventId,
|
|
4307
4402
|
session_id: input.sessionId,
|
|
4308
4403
|
occurred_at: input.occurredAt,
|
|
@@ -4314,7 +4409,7 @@ function buildTaskDeletedEvent(input) {
|
|
|
4314
4409
|
}
|
|
4315
4410
|
function buildTaskArchivedEvent(input) {
|
|
4316
4411
|
return {
|
|
4317
|
-
schema_version:
|
|
4412
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4318
4413
|
id: input.eventId,
|
|
4319
4414
|
session_id: input.sessionId,
|
|
4320
4415
|
occurred_at: input.occurredAt,
|
|
@@ -4326,7 +4421,7 @@ function buildTaskArchivedEvent(input) {
|
|
|
4326
4421
|
}
|
|
4327
4422
|
function buildTaskLinkageRefreshedEvent(input) {
|
|
4328
4423
|
return {
|
|
4329
|
-
schema_version:
|
|
4424
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4330
4425
|
id: input.eventId,
|
|
4331
4426
|
session_id: input.sessionId,
|
|
4332
4427
|
occurred_at: input.occurredAt,
|
|
@@ -5416,7 +5511,9 @@ async function renderHandoff(input) {
|
|
|
5416
5511
|
(e) => e.session.session.status !== "archived" && e.session.session.source.kind !== "import"
|
|
5417
5512
|
);
|
|
5418
5513
|
const latestSession = pickLatestSubstantiveEntry(liveEntries);
|
|
5419
|
-
const latestFiles = latestSession?.session.session.related_files ?? []
|
|
5514
|
+
const latestFiles = (latestSession?.session.session.related_files ?? []).filter(
|
|
5515
|
+
(file) => !isTransientToolPath(file)
|
|
5516
|
+
);
|
|
5420
5517
|
const sortedFiles = [...new Set(latestFiles)].sort();
|
|
5421
5518
|
const displayedFiles = sortedFiles.slice(0, limit);
|
|
5422
5519
|
const overflow = Math.max(0, sortedFiles.length - limit);
|
|
@@ -5634,9 +5731,9 @@ function shortHandoffId(sessionId) {
|
|
|
5634
5731
|
return sessionId.slice(0, 10);
|
|
5635
5732
|
}
|
|
5636
5733
|
function shortIdWithPrefix(id) {
|
|
5637
|
-
const
|
|
5638
|
-
if (
|
|
5639
|
-
return id.slice(0,
|
|
5734
|
+
const sep2 = id.indexOf("_");
|
|
5735
|
+
if (sep2 === -1) return id.slice(0, 10);
|
|
5736
|
+
return id.slice(0, sep2 + 1) + id.slice(sep2 + 1, sep2 + 1 + 10);
|
|
5640
5737
|
}
|
|
5641
5738
|
|
|
5642
5739
|
// src/lib/duration.ts
|
|
@@ -5743,10 +5840,10 @@ async function resolveIdInternal(paths, input, kind, options = {}) {
|
|
|
5743
5840
|
// src/lib/source-root-scope.ts
|
|
5744
5841
|
import { promises as fs } from "fs";
|
|
5745
5842
|
import { homedir as osHomedir } from "os";
|
|
5746
|
-
import { basename as basename2, dirname as dirname3, isAbsolute, join as join14, normalize, relative, resolve as resolve2 } from "path";
|
|
5843
|
+
import { basename as basename2, dirname as dirname3, isAbsolute, join as join14, normalize as normalize2, relative, resolve as resolve2 } from "path";
|
|
5747
5844
|
var AGENT_INFRA_DIRS = ["~/.claude", "~/.codex", "~/.basou"];
|
|
5748
5845
|
async function realpathBestEffort(absPath) {
|
|
5749
|
-
let current =
|
|
5846
|
+
let current = normalize2(absPath);
|
|
5750
5847
|
const tail = [];
|
|
5751
5848
|
for (let guard = 0; guard < 4096; guard += 1) {
|
|
5752
5849
|
try {
|
|
@@ -5755,15 +5852,15 @@ async function realpathBestEffort(absPath) {
|
|
|
5755
5852
|
} catch (error) {
|
|
5756
5853
|
const code = error?.code;
|
|
5757
5854
|
if (code !== "ENOENT" && code !== "ENOTDIR") {
|
|
5758
|
-
return
|
|
5855
|
+
return normalize2(absPath);
|
|
5759
5856
|
}
|
|
5760
5857
|
const parent = dirname3(current);
|
|
5761
|
-
if (parent === current) return
|
|
5858
|
+
if (parent === current) return normalize2(absPath);
|
|
5762
5859
|
tail.push(basename2(current));
|
|
5763
5860
|
current = parent;
|
|
5764
5861
|
}
|
|
5765
5862
|
}
|
|
5766
|
-
return
|
|
5863
|
+
return normalize2(absPath);
|
|
5767
5864
|
}
|
|
5768
5865
|
function expandTilde(p, homedir5) {
|
|
5769
5866
|
if (p === "~") return homedir5;
|
|
@@ -5772,8 +5869,8 @@ function expandTilde(p, homedir5) {
|
|
|
5772
5869
|
}
|
|
5773
5870
|
function toAbsolute(p, workingDirAbs, homedir5) {
|
|
5774
5871
|
const expanded = expandTilde(p, homedir5);
|
|
5775
|
-
if (isAbsolute(expanded)) return
|
|
5776
|
-
return
|
|
5872
|
+
if (isAbsolute(expanded)) return normalize2(expanded);
|
|
5873
|
+
return normalize2(resolve2(workingDirAbs, expanded));
|
|
5777
5874
|
}
|
|
5778
5875
|
function isUnder(child, parent) {
|
|
5779
5876
|
if (child === parent) return true;
|
|
@@ -5790,12 +5887,12 @@ async function classifyFilesBySourceRoot(input) {
|
|
|
5790
5887
|
const rootsAbs = [];
|
|
5791
5888
|
for (const r of declared) {
|
|
5792
5889
|
const expanded = expandTilde(r, homedir5);
|
|
5793
|
-
const abs = isAbsolute(expanded) ?
|
|
5890
|
+
const abs = isAbsolute(expanded) ? normalize2(expanded) : normalize2(resolve2(input.masterRoot, expanded));
|
|
5794
5891
|
rootsAbs.push(await realpathBestEffort(abs));
|
|
5795
5892
|
}
|
|
5796
5893
|
for (const e of input.extraInRoot ?? []) {
|
|
5797
5894
|
const expanded = expandTilde(e, homedir5);
|
|
5798
|
-
const abs = isAbsolute(expanded) ?
|
|
5895
|
+
const abs = isAbsolute(expanded) ? normalize2(expanded) : normalize2(resolve2(homedir5, expanded));
|
|
5799
5896
|
rootsAbs.push(await realpathBestEffort(abs));
|
|
5800
5897
|
}
|
|
5801
5898
|
if (rootsAbs.length === 0) {
|
|
@@ -5928,7 +6025,13 @@ async function summarizeOrientation(input) {
|
|
|
5928
6025
|
const decisionTitles = (bucket?.decisions ?? []).filter((d) => !voidedDecisionIds.has(d.decisionId)).map((d) => d.title);
|
|
5929
6026
|
const notes = bucket?.notes ?? [];
|
|
5930
6027
|
const hasIntent = decisionTitles.length > 0 || notes.length > 0;
|
|
5931
|
-
const files = hasIntent ? [] : [
|
|
6028
|
+
const files = hasIntent ? [] : [
|
|
6029
|
+
...new Set(
|
|
6030
|
+
(entry.session.session.related_files ?? []).filter(
|
|
6031
|
+
(file) => !isTransientToolPath(file)
|
|
6032
|
+
)
|
|
6033
|
+
)
|
|
6034
|
+
].sort().slice(0, FILES_PER_DIGEST);
|
|
5932
6035
|
return {
|
|
5933
6036
|
sessionId: entry.sessionId,
|
|
5934
6037
|
label: entry.session.session.label ?? null,
|
|
@@ -5999,8 +6102,10 @@ async function summarizeOrientation(input) {
|
|
|
5999
6102
|
} catch {
|
|
6000
6103
|
sourceRoots = null;
|
|
6001
6104
|
}
|
|
6002
|
-
const
|
|
6105
|
+
const recordedFiles = latestEntry?.session.session.related_files ?? [];
|
|
6106
|
+
const latestFiles = recordedFiles.filter((file) => !isTransientToolPath(file));
|
|
6003
6107
|
const uniqueFiles = new Set(latestFiles);
|
|
6108
|
+
const omittedFiles = new Set(recordedFiles).size - uniqueFiles.size;
|
|
6004
6109
|
const sortedFiles = [...uniqueFiles].sort();
|
|
6005
6110
|
const displayed = sortedFiles.slice(0, limit);
|
|
6006
6111
|
const overflow = Math.max(0, uniqueFiles.size - limit);
|
|
@@ -6031,7 +6136,7 @@ async function summarizeOrientation(input) {
|
|
|
6031
6136
|
openTracks,
|
|
6032
6137
|
latestNote,
|
|
6033
6138
|
recentDirection,
|
|
6034
|
-
relatedFiles: { displayed, overflow, outOfRoot },
|
|
6139
|
+
relatedFiles: { displayed, overflow, outOfRoot, omitted: omittedFiles },
|
|
6035
6140
|
inFlightTasks,
|
|
6036
6141
|
plannedTasks,
|
|
6037
6142
|
pendingApprovals,
|
|
@@ -6119,10 +6224,16 @@ function formatOrientationBody(summary, opts) {
|
|
|
6119
6224
|
`- ${t.common.latestDecisionLabel}: (no decisions recorded yet; capture with \`basou decision capture\`)`
|
|
6120
6225
|
);
|
|
6121
6226
|
}
|
|
6122
|
-
if (summary.relatedFiles.displayed.length > 0) {
|
|
6123
|
-
const
|
|
6124
|
-
|
|
6125
|
-
|
|
6227
|
+
if (summary.relatedFiles.displayed.length > 0 || summary.relatedFiles.omitted > 0) {
|
|
6228
|
+
const parts = [];
|
|
6229
|
+
if (summary.relatedFiles.displayed.length > 0) {
|
|
6230
|
+
const more = summary.relatedFiles.overflow > 0 ? ` (... +${summary.relatedFiles.overflow} more)` : "";
|
|
6231
|
+
parts.push(`${summary.relatedFiles.displayed.join(", ")}${more}`);
|
|
6232
|
+
}
|
|
6233
|
+
if (summary.relatedFiles.omitted > 0) {
|
|
6234
|
+
parts.push(t.orientation.scratchOmitted(summary.relatedFiles.omitted));
|
|
6235
|
+
}
|
|
6236
|
+
lines.push(`- ${t.common.recentFilesLabel}: ${parts.join(" ")}`);
|
|
6126
6237
|
if (summary.relatedFiles.outOfRoot.length > 0) {
|
|
6127
6238
|
const OUT_OF_ROOT_DISPLAY = 10;
|
|
6128
6239
|
const out = summary.relatedFiles.outOfRoot;
|
|
@@ -6371,9 +6482,9 @@ function suspectText(reason) {
|
|
|
6371
6482
|
return "suspect";
|
|
6372
6483
|
}
|
|
6373
6484
|
function shortId(id) {
|
|
6374
|
-
const
|
|
6375
|
-
if (
|
|
6376
|
-
return id.slice(0,
|
|
6485
|
+
const sep2 = id.indexOf("_");
|
|
6486
|
+
if (sep2 === -1) return id.slice(0, 10);
|
|
6487
|
+
return id.slice(0, sep2 + 1) + id.slice(sep2 + 1, sep2 + 1 + 10);
|
|
6377
6488
|
}
|
|
6378
6489
|
|
|
6379
6490
|
// src/project/anchor-starter.ts
|
|
@@ -7128,9 +7239,13 @@ async function computeWorkStats(input) {
|
|
|
7128
7239
|
for (const entry of entries) {
|
|
7129
7240
|
const events = [];
|
|
7130
7241
|
let eventsUnreadable = false;
|
|
7242
|
+
let eventsLostLines = 0;
|
|
7131
7243
|
try {
|
|
7132
7244
|
for await (const ev of replayEvents(join16(input.paths.sessions, entry.sessionId), {
|
|
7133
|
-
onWarning: (w) =>
|
|
7245
|
+
onWarning: (w) => {
|
|
7246
|
+
if (w.kind === "malformed_json" || w.kind === "schema_violation") eventsLostLines++;
|
|
7247
|
+
input.onWarning?.(w, entry.sessionId);
|
|
7248
|
+
}
|
|
7134
7249
|
})) {
|
|
7135
7250
|
events.push(ev);
|
|
7136
7251
|
}
|
|
@@ -7146,7 +7261,8 @@ async function computeWorkStats(input) {
|
|
|
7146
7261
|
entry.session.session,
|
|
7147
7262
|
events,
|
|
7148
7263
|
now,
|
|
7149
|
-
eventsUnreadable
|
|
7264
|
+
eventsUnreadable,
|
|
7265
|
+
eventsLostLines
|
|
7150
7266
|
)
|
|
7151
7267
|
);
|
|
7152
7268
|
}
|
|
@@ -7164,8 +7280,9 @@ async function computeWorkStats(input) {
|
|
|
7164
7280
|
byDay: computeByDay(sessions, union.merged, timeZone)
|
|
7165
7281
|
};
|
|
7166
7282
|
}
|
|
7167
|
-
function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreadable = false) {
|
|
7283
|
+
function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreadable = false, eventsLostLines = 0) {
|
|
7168
7284
|
let commandCount = 0;
|
|
7285
|
+
let timedCommandCount = 0;
|
|
7169
7286
|
let fileChangedCount = 0;
|
|
7170
7287
|
let decisionCount = 0;
|
|
7171
7288
|
let commandTimeMs = 0;
|
|
@@ -7175,7 +7292,11 @@ function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreada
|
|
|
7175
7292
|
if (Number.isFinite(t)) timestamps.push(t);
|
|
7176
7293
|
if (ev.type === "command_executed") {
|
|
7177
7294
|
commandCount++;
|
|
7178
|
-
|
|
7295
|
+
const observed = readObservedDuration(ev);
|
|
7296
|
+
if (observed !== null) {
|
|
7297
|
+
timedCommandCount++;
|
|
7298
|
+
commandTimeMs += observed;
|
|
7299
|
+
}
|
|
7179
7300
|
} else if (ev.type === "file_changed") {
|
|
7180
7301
|
fileChangedCount++;
|
|
7181
7302
|
} else if (ev.type === "decision_recorded") {
|
|
@@ -7208,7 +7329,26 @@ function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreada
|
|
|
7208
7329
|
tokens,
|
|
7209
7330
|
availability: {
|
|
7210
7331
|
span: true,
|
|
7211
|
-
|
|
7332
|
+
// Derived from what this session actually recorded, like its three
|
|
7333
|
+
// siblings below. A source kind cannot answer this: the share of codex
|
|
7334
|
+
// commands carrying an observed duration went from 1.5% (2026-05) to
|
|
7335
|
+
// 56.0% (2026-08) as the vendor's log format changed, so the same kind
|
|
7336
|
+
// is sometimes timed and sometimes not.
|
|
7337
|
+
//
|
|
7338
|
+
// Not `commandTimeMs > 0`: a sum cannot tell "no command was timed" from
|
|
7339
|
+
// "no command ran", and the second case is the common one — 466 of one
|
|
7340
|
+
// store's 862 sessions run no command at all (`basou note`,
|
|
7341
|
+
// `decision capture`), and calling those unmeasured would poison the
|
|
7342
|
+
// AND-aggregated workspace total forever.
|
|
7343
|
+
//
|
|
7344
|
+
// The two disjuncts need different backing. An observed duration is a
|
|
7345
|
+
// fact about a command basou did see, and a line lost elsewhere in the
|
|
7346
|
+
// stream does not take it away. "Ran no commands", by contrast, is a
|
|
7347
|
+
// claim about the WHOLE stream, so it holds only if the whole stream was
|
|
7348
|
+
// read: an unreadable events.jsonl, or one whose lines were dropped as
|
|
7349
|
+
// malformed / schema-invalid, saw no commands because the log was lost,
|
|
7350
|
+
// and 0ms then measures nothing.
|
|
7351
|
+
commandTime: timedCommandCount > 0 || commandCount === 0 && !eventsUnreadable && eventsLostLines === 0,
|
|
7212
7352
|
activeTime: active.intervals.length > 0,
|
|
7213
7353
|
tokens: hasTokens(tokens),
|
|
7214
7354
|
machineActive: machineActiveTimeMs > 0
|
|
@@ -8610,7 +8750,7 @@ function requireNonEmptyString(value, field) {
|
|
|
8610
8750
|
function buildReviewRecordedEvent(input) {
|
|
8611
8751
|
const { review } = input;
|
|
8612
8752
|
return {
|
|
8613
|
-
schema_version:
|
|
8753
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
8614
8754
|
id: input.eventId,
|
|
8615
8755
|
session_id: input.sessionId,
|
|
8616
8756
|
occurred_at: input.occurredAt,
|
|
@@ -8636,6 +8776,7 @@ function truncate(value) {
|
|
|
8636
8776
|
|
|
8637
8777
|
// src/runtime/child-process-runner.ts
|
|
8638
8778
|
import { spawn as spawn2 } from "child_process";
|
|
8779
|
+
import { performance } from "perf_hooks";
|
|
8639
8780
|
var DEFAULT_KILL_GRACE_MS = 5e3;
|
|
8640
8781
|
var ChildProcessRunner = class {
|
|
8641
8782
|
async run(command, args, options) {
|
|
@@ -8650,6 +8791,7 @@ var ChildProcessRunner = class {
|
|
|
8650
8791
|
const snapshotCwd = options.cwd;
|
|
8651
8792
|
const captureMode = options.capture ?? "buffer";
|
|
8652
8793
|
const started_at = /* @__PURE__ */ new Date();
|
|
8794
|
+
const startedHrMs = performance.now();
|
|
8653
8795
|
let child;
|
|
8654
8796
|
try {
|
|
8655
8797
|
child = spawn2(snapshotCommand, [...snapshotArgs], {
|
|
@@ -8736,7 +8878,7 @@ var ChildProcessRunner = class {
|
|
|
8736
8878
|
stderr,
|
|
8737
8879
|
started_at: started_at.toISOString(),
|
|
8738
8880
|
ended_at: ended_at.toISOString(),
|
|
8739
|
-
duration_ms:
|
|
8881
|
+
duration_ms: Math.round(performance.now() - startedHrMs),
|
|
8740
8882
|
pid: child.pid ?? null
|
|
8741
8883
|
});
|
|
8742
8884
|
});
|
|
@@ -8800,15 +8942,28 @@ var SessionInnerImportSchema = z10.object({
|
|
|
8800
8942
|
// imported. Mirrors the accept-and-discard of `prev_hash` on events.
|
|
8801
8943
|
integrity: SessionIntegritySchema.optional()
|
|
8802
8944
|
}).strict();
|
|
8945
|
+
var SESSION_IMPORT_SCHEMA_VERSION = "0.1.0";
|
|
8803
8946
|
var SessionImportPayloadSchema = z10.object({
|
|
8804
|
-
schema_version: z10.string()
|
|
8947
|
+
schema_version: z10.string().meta({
|
|
8948
|
+
const: SESSION_IMPORT_SCHEMA_VERSION,
|
|
8949
|
+
description: "Import envelope version. Must be exactly 0.1.0; any other value is rejected by the importer. This is the envelope's own version, not the version of the events it carries."
|
|
8950
|
+
}),
|
|
8805
8951
|
session: SessionInnerImportSchema,
|
|
8806
8952
|
events: z10.array(EventSchema)
|
|
8807
8953
|
}).strict();
|
|
8808
8954
|
|
|
8809
8955
|
// src/schemas/json-schema.ts
|
|
8810
|
-
var
|
|
8811
|
-
|
|
8956
|
+
var JSON_SCHEMA_VERSIONS = {
|
|
8957
|
+
manifest: "0.1.0",
|
|
8958
|
+
session: "0.1.0",
|
|
8959
|
+
event: EVENT_SCHEMA_VERSION,
|
|
8960
|
+
task: "0.1.0",
|
|
8961
|
+
approval: "0.1.0",
|
|
8962
|
+
status: "0.1.0",
|
|
8963
|
+
"task-index": "0.1.0",
|
|
8964
|
+
"session-import": SESSION_IMPORT_SCHEMA_VERSION
|
|
8965
|
+
};
|
|
8966
|
+
var ID_BASE = "https://basou.dev/schemas";
|
|
8812
8967
|
var JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
8813
8968
|
var DOCUMENTS = [
|
|
8814
8969
|
{
|
|
@@ -8866,7 +9021,7 @@ function buildJsonSchemas() {
|
|
|
8866
9021
|
const { $schema, ...rest } = generated;
|
|
8867
9022
|
const schema = {
|
|
8868
9023
|
$schema: typeof $schema === "string" ? $schema : JSON_SCHEMA_DIALECT,
|
|
8869
|
-
$id: `${ID_BASE}/${doc.name}.schema.json`,
|
|
9024
|
+
$id: `${ID_BASE}/${JSON_SCHEMA_VERSIONS[doc.name]}/${doc.name}.schema.json`,
|
|
8870
9025
|
title: doc.title,
|
|
8871
9026
|
description: doc.description,
|
|
8872
9027
|
...rest
|
|
@@ -9559,6 +9714,7 @@ export {
|
|
|
9559
9714
|
ChildProcessRunner,
|
|
9560
9715
|
DEFAULT_STOP_HOOK_MIN_EDITS,
|
|
9561
9716
|
DecisionIdSchema,
|
|
9717
|
+
EVENT_SCHEMA_VERSION,
|
|
9562
9718
|
EventIdSchema,
|
|
9563
9719
|
EventSchema,
|
|
9564
9720
|
EventSourceSchema,
|
|
@@ -9567,7 +9723,7 @@ export {
|
|
|
9567
9723
|
GENERATED_START,
|
|
9568
9724
|
ID_PREFIXES,
|
|
9569
9725
|
IsoTimestampSchema,
|
|
9570
|
-
|
|
9726
|
+
JSON_SCHEMA_VERSIONS,
|
|
9571
9727
|
ManifestSchema,
|
|
9572
9728
|
ORIENTATION_END,
|
|
9573
9729
|
ORIENTATION_START,
|
|
@@ -9575,6 +9731,7 @@ export {
|
|
|
9575
9731
|
PROTOCOL_START,
|
|
9576
9732
|
REVIEW_RECORD_NO_INPUT_HINT,
|
|
9577
9733
|
RiskLevelSchema,
|
|
9734
|
+
SESSION_IMPORT_SCHEMA_VERSION,
|
|
9578
9735
|
SESSION_START_HOOK_CONTEXT_LIMIT,
|
|
9579
9736
|
SESSION_START_HOOK_MATCHER,
|
|
9580
9737
|
SESSION_START_HOOK_STATUS_MESSAGE,
|
|
@@ -9596,6 +9753,7 @@ export {
|
|
|
9596
9753
|
TaskStatusSchema,
|
|
9597
9754
|
TaskWriteAfterEventError,
|
|
9598
9755
|
WorkspaceIdSchema,
|
|
9756
|
+
ZERO_DURATION_RETIRED_SINCE,
|
|
9599
9757
|
acquireLock,
|
|
9600
9758
|
appendBasouGitignore,
|
|
9601
9759
|
appendChainedEvent,
|
|
@@ -9642,6 +9800,7 @@ export {
|
|
|
9642
9800
|
genesisHash,
|
|
9643
9801
|
getDiff,
|
|
9644
9802
|
getSnapshot,
|
|
9803
|
+
hasRetiredZeroDuration,
|
|
9645
9804
|
importSessionFromJson,
|
|
9646
9805
|
inspectChainTail,
|
|
9647
9806
|
instructionMode,
|
|
@@ -9675,6 +9834,7 @@ export {
|
|
|
9675
9834
|
readAllEvents,
|
|
9676
9835
|
readManifest,
|
|
9677
9836
|
readMarkdownFile,
|
|
9837
|
+
readObservedDuration,
|
|
9678
9838
|
readSessionYaml,
|
|
9679
9839
|
readStatus,
|
|
9680
9840
|
readTaskFile,
|
|
@@ -9735,6 +9895,7 @@ export {
|
|
|
9735
9895
|
writeEventsBulk,
|
|
9736
9896
|
writeManifest,
|
|
9737
9897
|
writeMarkdownFile,
|
|
9898
|
+
writeObservedDuration,
|
|
9738
9899
|
writeStatus,
|
|
9739
9900
|
writeTaskFile,
|
|
9740
9901
|
writeYamlFile
|