@basou/core 0.41.0 → 0.42.1
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 +203 -19
- package/dist/index.js +496 -337
- 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 = {}) {
|
|
@@ -2089,6 +2150,7 @@ var EN = {
|
|
|
2089
2150
|
headingForward: "## Where you are heading",
|
|
2090
2151
|
headingCurrency: "## Is this current",
|
|
2091
2152
|
inFlightTasksHeading: (n) => `### In-flight tasks (${n})`,
|
|
2153
|
+
noTasksRecorded: "(no tasks recorded)",
|
|
2092
2154
|
pendingApprovalsHeading: (n) => `### Pending approvals (${n})`,
|
|
2093
2155
|
suspectSessionsHeading: (n) => `### Suspect sessions (${n})`,
|
|
2094
2156
|
openTracksHeading: (n) => `### Open tracks (shown until closed) (${n})`,
|
|
@@ -2150,6 +2212,8 @@ var EN = {
|
|
|
2150
2212
|
headingNextWork: "## Work to do next",
|
|
2151
2213
|
headingSessions: "## Sessions",
|
|
2152
2214
|
lastTaskLabel: "Last task",
|
|
2215
|
+
noPendingTasks: "(no pending tasks)",
|
|
2216
|
+
noTasksRecorded: "(no tasks recorded)",
|
|
2153
2217
|
decisionStaleNote: "Note: the latest activity postdates this decision. It may already be resolved in conversation \u2014 confirm the continuation point before resuming (conversational decisions are not captured automatically; record them with `basou decision capture`).",
|
|
2154
2218
|
trackCloseInstruction: "When finished, close it with `basou decision void <decision_id>`."
|
|
2155
2219
|
},
|
|
@@ -2185,6 +2249,7 @@ var JA = {
|
|
|
2185
2249
|
headingForward: "## \u3069\u3053\u3078\u5411\u304B\u3046",
|
|
2186
2250
|
headingCurrency: "## \u3053\u308C\u306F\u6700\u65B0\u304B",
|
|
2187
2251
|
inFlightTasksHeading: (n) => `### \u9032\u884C\u4E2D task (${n})`,
|
|
2252
|
+
noTasksRecorded: "(task \u304C 1 \u4EF6\u3082\u8A18\u9332\u3055\u308C\u3066\u3044\u307E\u305B\u3093)",
|
|
2188
2253
|
pendingApprovalsHeading: (n) => `### \u627F\u8A8D\u5F85\u3061 (${n})`,
|
|
2189
2254
|
suspectSessionsHeading: (n) => `### \u8981\u6CE8\u610F session (${n})`,
|
|
2190
2255
|
openTracksHeading: (n) => `### \u672A\u5B8C\u30C8\u30E9\u30C3\u30AF (close \u307E\u3067\u7D99\u7D9A\u8868\u793A) (${n})`,
|
|
@@ -2246,6 +2311,8 @@ var JA = {
|
|
|
2246
2311
|
headingNextWork: "## \u6B21\u306B\u5B9F\u884C\u3059\u3079\u304D\u4F5C\u696D",
|
|
2247
2312
|
headingSessions: "## \u30BB\u30C3\u30B7\u30E7\u30F3\u4E00\u89A7",
|
|
2248
2313
|
lastTaskLabel: "\u6700\u7D42 task",
|
|
2314
|
+
noPendingTasks: "(\u672A\u5B8C\u4E86\u306E task \u306F\u3042\u308A\u307E\u305B\u3093)",
|
|
2315
|
+
noTasksRecorded: "(task \u304C 1 \u4EF6\u3082\u8A18\u9332\u3055\u308C\u3066\u3044\u307E\u305B\u3093)",
|
|
2249
2316
|
decisionStaleNote: "\u6CE8: \u6700\u7D42\u6D3B\u52D5\u306F\u3053\u306E\u5224\u65AD\u3088\u308A\u5F8C\u3067\u3059\u3002\u4F1A\u8A71\u3067\u65E2\u306B\u89E3\u6C7A\u6E08\u307F\u306E\u53EF\u80FD\u6027\u304C\u3042\u308B\u305F\u3081\u3001\u518D\u958B\u524D\u306B\u7D99\u7D9A\u70B9\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044(\u4F1A\u8A71\u3067\u306E\u610F\u601D\u6C7A\u5B9A\u306F\u81EA\u52D5\u8A18\u9332\u3055\u308C\u307E\u305B\u3093\u3002`basou decision capture` \u3067\u8A18\u9332\u3067\u304D\u307E\u3059)\u3002",
|
|
2250
2317
|
trackCloseInstruction: "\u5B8C\u4E86\u3057\u305F\u3089 `basou decision void <decision_id>` \u3067\u9589\u3058\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
2251
2318
|
},
|
|
@@ -2455,11 +2522,11 @@ var PRESET_JA = {
|
|
|
2455
2522
|
|
|
2456
2523
|
// src/storage/sessions.ts
|
|
2457
2524
|
import { readdir as readdir2 } from "fs/promises";
|
|
2458
|
-
import { join as
|
|
2525
|
+
import { join as join6 } from "path";
|
|
2459
2526
|
|
|
2460
2527
|
// src/events/chained-append.ts
|
|
2461
|
-
import { appendFile, readFile as readFile3 } from "fs/promises";
|
|
2462
|
-
import { join as
|
|
2528
|
+
import { appendFile as appendFile2, readFile as readFile3 } from "fs/promises";
|
|
2529
|
+
import { join as join5 } from "path";
|
|
2463
2530
|
|
|
2464
2531
|
// src/storage/lockfile.ts
|
|
2465
2532
|
import { mkdir, readFile as readFile2, unlink as unlink2 } from "fs/promises";
|
|
@@ -2581,6 +2648,61 @@ function chainRawJsonLines(rawLines, sessionId) {
|
|
|
2581
2648
|
return { lines, headHash: prev, count: lines.length };
|
|
2582
2649
|
}
|
|
2583
2650
|
|
|
2651
|
+
// src/events/event-writer.ts
|
|
2652
|
+
import { appendFile } from "fs/promises";
|
|
2653
|
+
import { basename, join as join4 } from "path";
|
|
2654
|
+
async function appendEvent(sessionDir, event) {
|
|
2655
|
+
let validated;
|
|
2656
|
+
try {
|
|
2657
|
+
validated = EventSchema.parse(event);
|
|
2658
|
+
} catch (error) {
|
|
2659
|
+
throw new Error("Invalid Basou event payload", { cause: error });
|
|
2660
|
+
}
|
|
2661
|
+
assertWritableEvent(validated);
|
|
2662
|
+
const line = `${serializeEventLine(validated)}
|
|
2663
|
+
`;
|
|
2664
|
+
try {
|
|
2665
|
+
await appendFile(join4(sessionDir, "events.jsonl"), line, "utf8");
|
|
2666
|
+
} catch (error) {
|
|
2667
|
+
throw new Error("Failed to append event to events.jsonl", { cause: error });
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
async function writeEventsBulk(sessionDir, events, options = {}) {
|
|
2671
|
+
const validated = [];
|
|
2672
|
+
try {
|
|
2673
|
+
for (const event of events) {
|
|
2674
|
+
validated.push(EventSchema.parse(event));
|
|
2675
|
+
}
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
throw new Error("Invalid Basou event payload", { cause: error });
|
|
2678
|
+
}
|
|
2679
|
+
for (const event of validated) assertWritableEvent(event);
|
|
2680
|
+
const filePath = join4(sessionDir, "events.jsonl");
|
|
2681
|
+
let body;
|
|
2682
|
+
let result = null;
|
|
2683
|
+
if (options.chain === true) {
|
|
2684
|
+
const { lines, headHash, count } = chainEvents(validated, basename(sessionDir));
|
|
2685
|
+
body = lines.length > 0 ? `${lines.join("\n")}
|
|
2686
|
+
` : "";
|
|
2687
|
+
result = count > 0 ? { headHash, count } : null;
|
|
2688
|
+
} else {
|
|
2689
|
+
body = validated.length > 0 ? `${validated.map(serializeEventLine).join("\n")}
|
|
2690
|
+
` : "";
|
|
2691
|
+
}
|
|
2692
|
+
try {
|
|
2693
|
+
await atomicReplace(filePath, body);
|
|
2694
|
+
} catch (error) {
|
|
2695
|
+
throw new Error("Failed to write events.jsonl", { cause: error });
|
|
2696
|
+
}
|
|
2697
|
+
return result;
|
|
2698
|
+
}
|
|
2699
|
+
function assertWritableEvent(event) {
|
|
2700
|
+
if (!hasRetiredZeroDuration(event)) return;
|
|
2701
|
+
throw new Error(
|
|
2702
|
+
`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`
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2584
2706
|
// src/events/chained-append.ts
|
|
2585
2707
|
function splitLinesBytes(buf) {
|
|
2586
2708
|
const out = [];
|
|
@@ -2603,7 +2725,7 @@ function carriesPrevHash(line) {
|
|
|
2603
2725
|
}
|
|
2604
2726
|
}
|
|
2605
2727
|
async function inspectChainTail(paths, sessionId) {
|
|
2606
|
-
const filePath =
|
|
2728
|
+
const filePath = join5(paths.sessions, sessionId, "events.jsonl");
|
|
2607
2729
|
let raw;
|
|
2608
2730
|
try {
|
|
2609
2731
|
raw = await readFile3(filePath);
|
|
@@ -2639,10 +2761,11 @@ async function appendChainedEventLocked(paths, sessionId, event) {
|
|
|
2639
2761
|
} catch (error) {
|
|
2640
2762
|
throw new Error("Invalid Basou event payload", { cause: error });
|
|
2641
2763
|
}
|
|
2764
|
+
assertWritableEvent(validated);
|
|
2642
2765
|
const tail = await inspectChainTail(paths, sessionId);
|
|
2643
2766
|
const line = tail.chained ? serializeEventLine({ ...validated, prev_hash: tail.head }) : serializeEventLine(validated);
|
|
2644
2767
|
try {
|
|
2645
|
-
await
|
|
2768
|
+
await appendFile2(join5(paths.sessions, sessionId, "events.jsonl"), `${line}
|
|
2646
2769
|
`, "utf8");
|
|
2647
2770
|
} catch (error) {
|
|
2648
2771
|
throw new Error("Failed to append event to events.jsonl", { cause: error });
|
|
@@ -2754,7 +2877,7 @@ async function enumerateSessionDirs(paths) {
|
|
|
2754
2877
|
}
|
|
2755
2878
|
}
|
|
2756
2879
|
async function readSessionYaml(paths, sessionId) {
|
|
2757
|
-
const filePath =
|
|
2880
|
+
const filePath = join6(paths.sessions, sessionId, "session.yaml");
|
|
2758
2881
|
let raw;
|
|
2759
2882
|
try {
|
|
2760
2883
|
raw = await readYamlFile(filePath);
|
|
@@ -2778,7 +2901,7 @@ async function finalizeSessionYaml(paths, sessionId, mutate) {
|
|
|
2778
2901
|
session.session.integrity = { head_hash: tail.head, event_count: tail.count };
|
|
2779
2902
|
}
|
|
2780
2903
|
const validated = SessionSchema.parse(session);
|
|
2781
|
-
await overwriteYamlFile(
|
|
2904
|
+
await overwriteYamlFile(join6(paths.sessions, sessionId, "session.yaml"), validated);
|
|
2782
2905
|
} finally {
|
|
2783
2906
|
await lock.release();
|
|
2784
2907
|
}
|
|
@@ -2787,7 +2910,7 @@ async function classifySuspect(paths, sessionId, session, now, onWarning) {
|
|
|
2787
2910
|
if (session.session.status !== "running") {
|
|
2788
2911
|
return { suspect: false, suspectReason: null };
|
|
2789
2912
|
}
|
|
2790
|
-
const sessionDir =
|
|
2913
|
+
const sessionDir = join6(paths.sessions, sessionId);
|
|
2791
2914
|
let endedFound = false;
|
|
2792
2915
|
let lastEventOccurredAt = null;
|
|
2793
2916
|
const replayOpts = onWarning !== void 0 ? { onWarning } : {};
|
|
@@ -2897,7 +3020,7 @@ async function renderDecisions(input) {
|
|
|
2897
3020
|
const voids = /* @__PURE__ */ new Map();
|
|
2898
3021
|
const knownEventIds = /* @__PURE__ */ new Set();
|
|
2899
3022
|
for (const entry of entries) {
|
|
2900
|
-
const sessionDir =
|
|
3023
|
+
const sessionDir = join7(input.paths.sessions, entry.sessionId);
|
|
2901
3024
|
try {
|
|
2902
3025
|
for await (const ev of replayEvents(sessionDir, {
|
|
2903
3026
|
onWarning: (w) => input.onWarning?.(w, entry.sessionId)
|
|
@@ -3024,53 +3147,6 @@ function shortDecisionSessionId(sessionId) {
|
|
|
3024
3147
|
return sessionId.slice(0, 10);
|
|
3025
3148
|
}
|
|
3026
3149
|
|
|
3027
|
-
// src/events/event-writer.ts
|
|
3028
|
-
import { appendFile as appendFile2 } from "fs/promises";
|
|
3029
|
-
import { basename, join as join7 } from "path";
|
|
3030
|
-
async function appendEvent(sessionDir, event) {
|
|
3031
|
-
let validated;
|
|
3032
|
-
try {
|
|
3033
|
-
validated = EventSchema.parse(event);
|
|
3034
|
-
} catch (error) {
|
|
3035
|
-
throw new Error("Invalid Basou event payload", { cause: error });
|
|
3036
|
-
}
|
|
3037
|
-
const line = `${serializeEventLine(validated)}
|
|
3038
|
-
`;
|
|
3039
|
-
try {
|
|
3040
|
-
await appendFile2(join7(sessionDir, "events.jsonl"), line, "utf8");
|
|
3041
|
-
} catch (error) {
|
|
3042
|
-
throw new Error("Failed to append event to events.jsonl", { cause: error });
|
|
3043
|
-
}
|
|
3044
|
-
}
|
|
3045
|
-
async function writeEventsBulk(sessionDir, events, options = {}) {
|
|
3046
|
-
const validated = [];
|
|
3047
|
-
try {
|
|
3048
|
-
for (const event of events) {
|
|
3049
|
-
validated.push(EventSchema.parse(event));
|
|
3050
|
-
}
|
|
3051
|
-
} catch (error) {
|
|
3052
|
-
throw new Error("Invalid Basou event payload", { cause: error });
|
|
3053
|
-
}
|
|
3054
|
-
const filePath = join7(sessionDir, "events.jsonl");
|
|
3055
|
-
let body;
|
|
3056
|
-
let result = null;
|
|
3057
|
-
if (options.chain === true) {
|
|
3058
|
-
const { lines, headHash, count } = chainEvents(validated, basename(sessionDir));
|
|
3059
|
-
body = lines.length > 0 ? `${lines.join("\n")}
|
|
3060
|
-
` : "";
|
|
3061
|
-
result = count > 0 ? { headHash, count } : null;
|
|
3062
|
-
} else {
|
|
3063
|
-
body = validated.length > 0 ? `${validated.map(serializeEventLine).join("\n")}
|
|
3064
|
-
` : "";
|
|
3065
|
-
}
|
|
3066
|
-
try {
|
|
3067
|
-
await atomicReplace(filePath, body);
|
|
3068
|
-
} catch (error) {
|
|
3069
|
-
throw new Error("Failed to write events.jsonl", { cause: error });
|
|
3070
|
-
}
|
|
3071
|
-
return result;
|
|
3072
|
-
}
|
|
3073
|
-
|
|
3074
3150
|
// src/events/verify.ts
|
|
3075
3151
|
import { readFile as readFile4 } from "fs/promises";
|
|
3076
3152
|
import { join as join8 } from "path";
|
|
@@ -3777,7 +3853,7 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
3777
3853
|
});
|
|
3778
3854
|
const events = [
|
|
3779
3855
|
{
|
|
3780
|
-
schema_version:
|
|
3856
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
3781
3857
|
id: startedEventId,
|
|
3782
3858
|
session_id: sessionId,
|
|
3783
3859
|
occurred_at: input.occurredAt,
|
|
@@ -3785,7 +3861,7 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
3785
3861
|
type: "session_started"
|
|
3786
3862
|
},
|
|
3787
3863
|
{
|
|
3788
|
-
schema_version:
|
|
3864
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
3789
3865
|
id: statusToRunningEventId,
|
|
3790
3866
|
session_id: sessionId,
|
|
3791
3867
|
occurred_at: input.occurredAt,
|
|
@@ -3796,7 +3872,7 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
3796
3872
|
},
|
|
3797
3873
|
...targetEvents,
|
|
3798
3874
|
{
|
|
3799
|
-
schema_version:
|
|
3875
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
3800
3876
|
id: statusToCompletedEventId,
|
|
3801
3877
|
session_id: sessionId,
|
|
3802
3878
|
occurred_at: input.occurredAt,
|
|
@@ -3806,7 +3882,7 @@ async function createAdHocSessionWithEvent(input) {
|
|
|
3806
3882
|
to: "completed"
|
|
3807
3883
|
},
|
|
3808
3884
|
{
|
|
3809
|
-
schema_version:
|
|
3885
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
3810
3886
|
id: endedEventId,
|
|
3811
3887
|
session_id: sessionId,
|
|
3812
3888
|
occurred_at: input.occurredAt,
|
|
@@ -4268,7 +4344,7 @@ var TaskWriteAfterEventError = class extends Error {
|
|
|
4268
4344
|
};
|
|
4269
4345
|
function buildTaskCreatedEvent(input) {
|
|
4270
4346
|
return {
|
|
4271
|
-
schema_version:
|
|
4347
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4272
4348
|
id: input.eventId,
|
|
4273
4349
|
session_id: input.sessionId,
|
|
4274
4350
|
occurred_at: input.occurredAt,
|
|
@@ -4280,7 +4356,7 @@ function buildTaskCreatedEvent(input) {
|
|
|
4280
4356
|
}
|
|
4281
4357
|
function buildTaskStatusChangedEvent(input) {
|
|
4282
4358
|
return {
|
|
4283
|
-
schema_version:
|
|
4359
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4284
4360
|
id: input.eventId,
|
|
4285
4361
|
session_id: input.sessionId,
|
|
4286
4362
|
occurred_at: input.occurredAt,
|
|
@@ -4313,7 +4389,7 @@ function buildAdHocArchiveLabel(title) {
|
|
|
4313
4389
|
}
|
|
4314
4390
|
function buildTaskReconciledEvent(input) {
|
|
4315
4391
|
return {
|
|
4316
|
-
schema_version:
|
|
4392
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4317
4393
|
id: input.eventId,
|
|
4318
4394
|
session_id: input.sessionId,
|
|
4319
4395
|
occurred_at: input.occurredAt,
|
|
@@ -4327,7 +4403,7 @@ function buildTaskReconciledEvent(input) {
|
|
|
4327
4403
|
}
|
|
4328
4404
|
function buildTaskDeletedEvent(input) {
|
|
4329
4405
|
return {
|
|
4330
|
-
schema_version:
|
|
4406
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4331
4407
|
id: input.eventId,
|
|
4332
4408
|
session_id: input.sessionId,
|
|
4333
4409
|
occurred_at: input.occurredAt,
|
|
@@ -4339,7 +4415,7 @@ function buildTaskDeletedEvent(input) {
|
|
|
4339
4415
|
}
|
|
4340
4416
|
function buildTaskArchivedEvent(input) {
|
|
4341
4417
|
return {
|
|
4342
|
-
schema_version:
|
|
4418
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4343
4419
|
id: input.eventId,
|
|
4344
4420
|
session_id: input.sessionId,
|
|
4345
4421
|
occurred_at: input.occurredAt,
|
|
@@ -4351,7 +4427,7 @@ function buildTaskArchivedEvent(input) {
|
|
|
4351
4427
|
}
|
|
4352
4428
|
function buildTaskLinkageRefreshedEvent(input) {
|
|
4353
4429
|
return {
|
|
4354
|
-
schema_version:
|
|
4430
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
4355
4431
|
id: input.eventId,
|
|
4356
4432
|
session_id: input.sessionId,
|
|
4357
4433
|
occurred_at: input.occurredAt,
|
|
@@ -5325,6 +5401,14 @@ async function archiveTaskLocked(input) {
|
|
|
5325
5401
|
eventId
|
|
5326
5402
|
};
|
|
5327
5403
|
}
|
|
5404
|
+
async function anyTaskEverRecorded(input) {
|
|
5405
|
+
if (input.liveCount > 0 || input.taskCreatedSeen || input.skippedCount > 0) return true;
|
|
5406
|
+
try {
|
|
5407
|
+
return (await enumerateArchivedTaskIds(input.paths)).length > 0;
|
|
5408
|
+
} catch {
|
|
5409
|
+
return true;
|
|
5410
|
+
}
|
|
5411
|
+
}
|
|
5328
5412
|
|
|
5329
5413
|
// src/handoff/handoff-renderer.ts
|
|
5330
5414
|
async function renderHandoff(input) {
|
|
@@ -5421,8 +5505,13 @@ async function renderHandoff(input) {
|
|
|
5421
5505
|
const c = Date.parse(a.occurredAt) - Date.parse(b.occurredAt);
|
|
5422
5506
|
return c !== 0 ? c : a.taskId.localeCompare(b.taskId);
|
|
5423
5507
|
});
|
|
5424
|
-
|
|
5425
|
-
|
|
5508
|
+
let skippedTaskCount = 0;
|
|
5509
|
+
const taskLoadOpts = {
|
|
5510
|
+
onSkip: (taskId, reason) => {
|
|
5511
|
+
skippedTaskCount += 1;
|
|
5512
|
+
input.onTaskSkip?.(taskId, reason);
|
|
5513
|
+
}
|
|
5514
|
+
};
|
|
5426
5515
|
const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
|
|
5427
5516
|
const taskById = /* @__PURE__ */ new Map();
|
|
5428
5517
|
for (const t of taskEntries) taskById.set(t.task.task.id, t);
|
|
@@ -5470,7 +5559,13 @@ async function renderHandoff(input) {
|
|
|
5470
5559
|
latestActivityRecord,
|
|
5471
5560
|
latestTaskDoc,
|
|
5472
5561
|
pendingTasks,
|
|
5473
|
-
totalTaskCount: taskEntries.length
|
|
5562
|
+
totalTaskCount: taskEntries.length,
|
|
5563
|
+
anyTaskEverRecorded: await anyTaskEverRecorded({
|
|
5564
|
+
paths: input.paths,
|
|
5565
|
+
liveCount: taskEntries.length,
|
|
5566
|
+
taskCreatedSeen: tasksCreated.length > 0,
|
|
5567
|
+
skippedCount: skippedTaskCount
|
|
5568
|
+
})
|
|
5474
5569
|
});
|
|
5475
5570
|
return {
|
|
5476
5571
|
body,
|
|
@@ -5581,7 +5676,7 @@ function formatHandoffBody(args) {
|
|
|
5581
5676
|
lines.push(t.handoff.headingNextWork);
|
|
5582
5677
|
lines.push("");
|
|
5583
5678
|
if (args.pendingTasks.length === 0) {
|
|
5584
|
-
lines.push(
|
|
5679
|
+
lines.push(args.anyTaskEverRecorded ? t.handoff.noPendingTasks : t.handoff.noTasksRecorded);
|
|
5585
5680
|
} else {
|
|
5586
5681
|
for (const t2 of args.pendingTasks) {
|
|
5587
5682
|
lines.push(
|
|
@@ -5870,6 +5965,7 @@ async function summarizeOrientation(input) {
|
|
|
5870
5965
|
else if (kind === "note" && typeof value === "string") bucket.notes.push(value);
|
|
5871
5966
|
};
|
|
5872
5967
|
let latestActivityAt = null;
|
|
5968
|
+
let taskCreatedSeen = false;
|
|
5873
5969
|
let latestNote = null;
|
|
5874
5970
|
const noteActivity = (iso) => {
|
|
5875
5971
|
if (latestActivityAt === null || Date.parse(iso) > Date.parse(latestActivityAt)) {
|
|
@@ -5908,6 +6004,8 @@ async function summarizeOrientation(input) {
|
|
|
5908
6004
|
host: entry.host
|
|
5909
6005
|
});
|
|
5910
6006
|
}
|
|
6007
|
+
} else if (ev.type === "task_created") {
|
|
6008
|
+
taskCreatedSeen = true;
|
|
5911
6009
|
} else if (ev.type === "decision_voided") {
|
|
5912
6010
|
voidedDecisionIds.add(ev.decision_id);
|
|
5913
6011
|
}
|
|
@@ -5973,8 +6071,13 @@ async function summarizeOrientation(input) {
|
|
|
5973
6071
|
files
|
|
5974
6072
|
};
|
|
5975
6073
|
});
|
|
5976
|
-
|
|
5977
|
-
|
|
6074
|
+
let skippedTaskCount = 0;
|
|
6075
|
+
const taskLoadOpts = {
|
|
6076
|
+
onSkip: (taskId, reason) => {
|
|
6077
|
+
skippedTaskCount += 1;
|
|
6078
|
+
input.onTaskSkip?.(taskId, reason);
|
|
6079
|
+
}
|
|
6080
|
+
};
|
|
5978
6081
|
const taskEntries = await loadTaskEntries(input.paths, taskLoadOpts);
|
|
5979
6082
|
const inFlightTasks = taskEntries.filter((t) => t.task.task.status === "in_progress" || t.task.task.status === "planned").map((t) => ({
|
|
5980
6083
|
id: t.task.task.id,
|
|
@@ -6068,6 +6171,12 @@ async function summarizeOrientation(input) {
|
|
|
6068
6171
|
recentDirection,
|
|
6069
6172
|
relatedFiles: { displayed, overflow, outOfRoot, omitted: omittedFiles },
|
|
6070
6173
|
inFlightTasks,
|
|
6174
|
+
anyTaskEverRecorded: await anyTaskEverRecorded({
|
|
6175
|
+
paths: input.paths,
|
|
6176
|
+
liveCount: taskEntries.length,
|
|
6177
|
+
taskCreatedSeen,
|
|
6178
|
+
skippedCount: skippedTaskCount
|
|
6179
|
+
}),
|
|
6071
6180
|
plannedTasks,
|
|
6072
6181
|
pendingApprovals,
|
|
6073
6182
|
suspects,
|
|
@@ -6204,7 +6313,7 @@ function formatOrientationBody(summary, opts) {
|
|
|
6204
6313
|
lines.push("");
|
|
6205
6314
|
lines.push(t.orientation.inFlightTasksHeading(summary.inFlightTasks.length));
|
|
6206
6315
|
if (summary.inFlightTasks.length === 0) {
|
|
6207
|
-
lines.push("- (none)");
|
|
6316
|
+
lines.push(summary.anyTaskEverRecorded ? "- (none)" : `- ${t.orientation.noTasksRecorded}`);
|
|
6208
6317
|
} else {
|
|
6209
6318
|
for (const t2 of summary.inFlightTasks) {
|
|
6210
6319
|
const linkedSuffix = t2.linkedSessions > 1 ? ` \u2014 linked_sessions: ${t2.linkedSessions}` : "";
|
|
@@ -7169,9 +7278,13 @@ async function computeWorkStats(input) {
|
|
|
7169
7278
|
for (const entry of entries) {
|
|
7170
7279
|
const events = [];
|
|
7171
7280
|
let eventsUnreadable = false;
|
|
7281
|
+
let eventsLostLines = 0;
|
|
7172
7282
|
try {
|
|
7173
7283
|
for await (const ev of replayEvents(join16(input.paths.sessions, entry.sessionId), {
|
|
7174
|
-
onWarning: (w) =>
|
|
7284
|
+
onWarning: (w) => {
|
|
7285
|
+
if (w.kind === "malformed_json" || w.kind === "schema_violation") eventsLostLines++;
|
|
7286
|
+
input.onWarning?.(w, entry.sessionId);
|
|
7287
|
+
}
|
|
7175
7288
|
})) {
|
|
7176
7289
|
events.push(ev);
|
|
7177
7290
|
}
|
|
@@ -7187,7 +7300,8 @@ async function computeWorkStats(input) {
|
|
|
7187
7300
|
entry.session.session,
|
|
7188
7301
|
events,
|
|
7189
7302
|
now,
|
|
7190
|
-
eventsUnreadable
|
|
7303
|
+
eventsUnreadable,
|
|
7304
|
+
eventsLostLines
|
|
7191
7305
|
)
|
|
7192
7306
|
);
|
|
7193
7307
|
}
|
|
@@ -7205,8 +7319,9 @@ async function computeWorkStats(input) {
|
|
|
7205
7319
|
byDay: computeByDay(sessions, union.merged, timeZone)
|
|
7206
7320
|
};
|
|
7207
7321
|
}
|
|
7208
|
-
function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreadable = false) {
|
|
7322
|
+
function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreadable = false, eventsLostLines = 0) {
|
|
7209
7323
|
let commandCount = 0;
|
|
7324
|
+
let timedCommandCount = 0;
|
|
7210
7325
|
let fileChangedCount = 0;
|
|
7211
7326
|
let decisionCount = 0;
|
|
7212
7327
|
let commandTimeMs = 0;
|
|
@@ -7216,7 +7331,11 @@ function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreada
|
|
|
7216
7331
|
if (Number.isFinite(t)) timestamps.push(t);
|
|
7217
7332
|
if (ev.type === "command_executed") {
|
|
7218
7333
|
commandCount++;
|
|
7219
|
-
|
|
7334
|
+
const observed = readObservedDuration(ev);
|
|
7335
|
+
if (observed !== null) {
|
|
7336
|
+
timedCommandCount++;
|
|
7337
|
+
commandTimeMs += observed;
|
|
7338
|
+
}
|
|
7220
7339
|
} else if (ev.type === "file_changed") {
|
|
7221
7340
|
fileChangedCount++;
|
|
7222
7341
|
} else if (ev.type === "decision_recorded") {
|
|
@@ -7249,7 +7368,26 @@ function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreada
|
|
|
7249
7368
|
tokens,
|
|
7250
7369
|
availability: {
|
|
7251
7370
|
span: true,
|
|
7252
|
-
|
|
7371
|
+
// Derived from what this session actually recorded, like its three
|
|
7372
|
+
// siblings below. A source kind cannot answer this: the share of codex
|
|
7373
|
+
// commands carrying an observed duration went from 1.5% (2026-05) to
|
|
7374
|
+
// 56.0% (2026-08) as the vendor's log format changed, so the same kind
|
|
7375
|
+
// is sometimes timed and sometimes not.
|
|
7376
|
+
//
|
|
7377
|
+
// Not `commandTimeMs > 0`: a sum cannot tell "no command was timed" from
|
|
7378
|
+
// "no command ran", and the second case is the common one — 466 of one
|
|
7379
|
+
// store's 862 sessions run no command at all (`basou note`,
|
|
7380
|
+
// `decision capture`), and calling those unmeasured would poison the
|
|
7381
|
+
// AND-aggregated workspace total forever.
|
|
7382
|
+
//
|
|
7383
|
+
// The two disjuncts need different backing. An observed duration is a
|
|
7384
|
+
// fact about a command basou did see, and a line lost elsewhere in the
|
|
7385
|
+
// stream does not take it away. "Ran no commands", by contrast, is a
|
|
7386
|
+
// claim about the WHOLE stream, so it holds only if the whole stream was
|
|
7387
|
+
// read: an unreadable events.jsonl, or one whose lines were dropped as
|
|
7388
|
+
// malformed / schema-invalid, saw no commands because the log was lost,
|
|
7389
|
+
// and 0ms then measures nothing.
|
|
7390
|
+
commandTime: timedCommandCount > 0 || commandCount === 0 && !eventsUnreadable && eventsLostLines === 0,
|
|
7253
7391
|
activeTime: active.intervals.length > 0,
|
|
7254
7392
|
tokens: hasTokens(tokens),
|
|
7255
7393
|
machineActive: machineActiveTimeMs > 0
|
|
@@ -8651,7 +8789,7 @@ function requireNonEmptyString(value, field) {
|
|
|
8651
8789
|
function buildReviewRecordedEvent(input) {
|
|
8652
8790
|
const { review } = input;
|
|
8653
8791
|
return {
|
|
8654
|
-
schema_version:
|
|
8792
|
+
schema_version: EVENT_SCHEMA_VERSION,
|
|
8655
8793
|
id: input.eventId,
|
|
8656
8794
|
session_id: input.sessionId,
|
|
8657
8795
|
occurred_at: input.occurredAt,
|
|
@@ -8677,6 +8815,7 @@ function truncate(value) {
|
|
|
8677
8815
|
|
|
8678
8816
|
// src/runtime/child-process-runner.ts
|
|
8679
8817
|
import { spawn as spawn2 } from "child_process";
|
|
8818
|
+
import { performance } from "perf_hooks";
|
|
8680
8819
|
var DEFAULT_KILL_GRACE_MS = 5e3;
|
|
8681
8820
|
var ChildProcessRunner = class {
|
|
8682
8821
|
async run(command, args, options) {
|
|
@@ -8691,6 +8830,7 @@ var ChildProcessRunner = class {
|
|
|
8691
8830
|
const snapshotCwd = options.cwd;
|
|
8692
8831
|
const captureMode = options.capture ?? "buffer";
|
|
8693
8832
|
const started_at = /* @__PURE__ */ new Date();
|
|
8833
|
+
const startedHrMs = performance.now();
|
|
8694
8834
|
let child;
|
|
8695
8835
|
try {
|
|
8696
8836
|
child = spawn2(snapshotCommand, [...snapshotArgs], {
|
|
@@ -8777,7 +8917,7 @@ var ChildProcessRunner = class {
|
|
|
8777
8917
|
stderr,
|
|
8778
8918
|
started_at: started_at.toISOString(),
|
|
8779
8919
|
ended_at: ended_at.toISOString(),
|
|
8780
|
-
duration_ms:
|
|
8920
|
+
duration_ms: Math.round(performance.now() - startedHrMs),
|
|
8781
8921
|
pid: child.pid ?? null
|
|
8782
8922
|
});
|
|
8783
8923
|
});
|
|
@@ -8841,15 +8981,28 @@ var SessionInnerImportSchema = z10.object({
|
|
|
8841
8981
|
// imported. Mirrors the accept-and-discard of `prev_hash` on events.
|
|
8842
8982
|
integrity: SessionIntegritySchema.optional()
|
|
8843
8983
|
}).strict();
|
|
8984
|
+
var SESSION_IMPORT_SCHEMA_VERSION = "0.1.0";
|
|
8844
8985
|
var SessionImportPayloadSchema = z10.object({
|
|
8845
|
-
schema_version: z10.string()
|
|
8986
|
+
schema_version: z10.string().meta({
|
|
8987
|
+
const: SESSION_IMPORT_SCHEMA_VERSION,
|
|
8988
|
+
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."
|
|
8989
|
+
}),
|
|
8846
8990
|
session: SessionInnerImportSchema,
|
|
8847
8991
|
events: z10.array(EventSchema)
|
|
8848
8992
|
}).strict();
|
|
8849
8993
|
|
|
8850
8994
|
// src/schemas/json-schema.ts
|
|
8851
|
-
var
|
|
8852
|
-
|
|
8995
|
+
var JSON_SCHEMA_VERSIONS = {
|
|
8996
|
+
manifest: "0.1.0",
|
|
8997
|
+
session: "0.1.0",
|
|
8998
|
+
event: EVENT_SCHEMA_VERSION,
|
|
8999
|
+
task: "0.1.0",
|
|
9000
|
+
approval: "0.1.0",
|
|
9001
|
+
status: "0.1.0",
|
|
9002
|
+
"task-index": "0.1.0",
|
|
9003
|
+
"session-import": SESSION_IMPORT_SCHEMA_VERSION
|
|
9004
|
+
};
|
|
9005
|
+
var ID_BASE = "https://basou.dev/schemas";
|
|
8853
9006
|
var JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
8854
9007
|
var DOCUMENTS = [
|
|
8855
9008
|
{
|
|
@@ -8907,7 +9060,7 @@ function buildJsonSchemas() {
|
|
|
8907
9060
|
const { $schema, ...rest } = generated;
|
|
8908
9061
|
const schema = {
|
|
8909
9062
|
$schema: typeof $schema === "string" ? $schema : JSON_SCHEMA_DIALECT,
|
|
8910
|
-
$id: `${ID_BASE}/${doc.name}.schema.json`,
|
|
9063
|
+
$id: `${ID_BASE}/${JSON_SCHEMA_VERSIONS[doc.name]}/${doc.name}.schema.json`,
|
|
8911
9064
|
title: doc.title,
|
|
8912
9065
|
description: doc.description,
|
|
8913
9066
|
...rest
|
|
@@ -9600,6 +9753,7 @@ export {
|
|
|
9600
9753
|
ChildProcessRunner,
|
|
9601
9754
|
DEFAULT_STOP_HOOK_MIN_EDITS,
|
|
9602
9755
|
DecisionIdSchema,
|
|
9756
|
+
EVENT_SCHEMA_VERSION,
|
|
9603
9757
|
EventIdSchema,
|
|
9604
9758
|
EventSchema,
|
|
9605
9759
|
EventSourceSchema,
|
|
@@ -9608,7 +9762,7 @@ export {
|
|
|
9608
9762
|
GENERATED_START,
|
|
9609
9763
|
ID_PREFIXES,
|
|
9610
9764
|
IsoTimestampSchema,
|
|
9611
|
-
|
|
9765
|
+
JSON_SCHEMA_VERSIONS,
|
|
9612
9766
|
ManifestSchema,
|
|
9613
9767
|
ORIENTATION_END,
|
|
9614
9768
|
ORIENTATION_START,
|
|
@@ -9616,6 +9770,7 @@ export {
|
|
|
9616
9770
|
PROTOCOL_START,
|
|
9617
9771
|
REVIEW_RECORD_NO_INPUT_HINT,
|
|
9618
9772
|
RiskLevelSchema,
|
|
9773
|
+
SESSION_IMPORT_SCHEMA_VERSION,
|
|
9619
9774
|
SESSION_START_HOOK_CONTEXT_LIMIT,
|
|
9620
9775
|
SESSION_START_HOOK_MATCHER,
|
|
9621
9776
|
SESSION_START_HOOK_STATUS_MESSAGE,
|
|
@@ -9637,6 +9792,7 @@ export {
|
|
|
9637
9792
|
TaskStatusSchema,
|
|
9638
9793
|
TaskWriteAfterEventError,
|
|
9639
9794
|
WorkspaceIdSchema,
|
|
9795
|
+
ZERO_DURATION_RETIRED_SINCE,
|
|
9640
9796
|
acquireLock,
|
|
9641
9797
|
appendBasouGitignore,
|
|
9642
9798
|
appendChainedEvent,
|
|
@@ -9683,6 +9839,7 @@ export {
|
|
|
9683
9839
|
genesisHash,
|
|
9684
9840
|
getDiff,
|
|
9685
9841
|
getSnapshot,
|
|
9842
|
+
hasRetiredZeroDuration,
|
|
9686
9843
|
importSessionFromJson,
|
|
9687
9844
|
inspectChainTail,
|
|
9688
9845
|
instructionMode,
|
|
@@ -9716,6 +9873,7 @@ export {
|
|
|
9716
9873
|
readAllEvents,
|
|
9717
9874
|
readManifest,
|
|
9718
9875
|
readMarkdownFile,
|
|
9876
|
+
readObservedDuration,
|
|
9719
9877
|
readSessionYaml,
|
|
9720
9878
|
readStatus,
|
|
9721
9879
|
readTaskFile,
|
|
@@ -9776,6 +9934,7 @@ export {
|
|
|
9776
9934
|
writeEventsBulk,
|
|
9777
9935
|
writeManifest,
|
|
9778
9936
|
writeMarkdownFile,
|
|
9937
|
+
writeObservedDuration,
|
|
9779
9938
|
writeStatus,
|
|
9780
9939
|
writeTaskFile,
|
|
9781
9940
|
writeYamlFile
|