sentinelayer-cli 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.
Files changed (69) hide show
  1. package/README.md +22 -4
  2. package/package.json +4 -4
  3. package/src/auth/admission-scope.js +34 -0
  4. package/src/auth/gate.js +6 -0
  5. package/src/auth/http.js +4 -0
  6. package/src/auth/service.js +6 -0
  7. package/src/board/done-gate.js +155 -0
  8. package/src/board/evidence-resolvers.js +138 -0
  9. package/src/cli.js +24 -1
  10. package/src/commands/board.js +116 -0
  11. package/src/commands/session.js +1094 -279
  12. package/src/engram/build-state.js +183 -0
  13. package/src/engram/canonical.js +75 -0
  14. package/src/engram/document.js +190 -0
  15. package/src/engram/governance.js +67 -0
  16. package/src/engram/index.js +73 -0
  17. package/src/engram/ingest-document.js +107 -0
  18. package/src/engram/namespace.js +89 -0
  19. package/src/engram/observations.js +34 -0
  20. package/src/engram/sla.js +104 -0
  21. package/src/engram/store.js +120 -0
  22. package/src/engram/tools.js +152 -0
  23. package/src/engram/trust.js +105 -0
  24. package/src/events/schema.js +18 -0
  25. package/src/legacy-cli.js +6 -3
  26. package/src/mcp/registry.js +27 -10
  27. package/src/mcp/session-stdio-server.js +209 -18
  28. package/src/net/test-egress-guard.js +158 -0
  29. package/src/review/investor-dd-orchestrator.js +14 -2
  30. package/src/session/admission-auth.js +312 -0
  31. package/src/session/admission-preimage.js +90 -0
  32. package/src/session/admission.js +620 -0
  33. package/src/session/agent-registry.js +22 -63
  34. package/src/session/checkpoints.js +25 -1
  35. package/src/session/codebase-context.js +54 -0
  36. package/src/session/coordination-guidance.js +6 -6
  37. package/src/session/daemon-spawn.js +20 -2
  38. package/src/session/daemon.js +26 -125
  39. package/src/session/event-identity.js +9 -2
  40. package/src/session/file-lease-integrations.js +1300 -0
  41. package/src/session/file-locks.js +967 -464
  42. package/src/session/first-message.js +11 -3
  43. package/src/session/listener.js +246 -20
  44. package/src/session/listeners.js +71 -80
  45. package/src/session/live-source.js +47 -16
  46. package/src/session/message-edits.js +89 -0
  47. package/src/session/paths.js +2 -0
  48. package/src/session/recall/embedder.js +181 -0
  49. package/src/session/recall/entities.js +0 -0
  50. package/src/session/recall/index-build.js +38 -0
  51. package/src/session/recall/index-core.js +108 -0
  52. package/src/session/recall/index.js +131 -0
  53. package/src/session/recall/needle-eval.js +279 -0
  54. package/src/session/recall/observation-core.js +183 -0
  55. package/src/session/recall/observations.js +21 -0
  56. package/src/session/recall/provenance.js +153 -0
  57. package/src/session/recall/retrieve.js +374 -0
  58. package/src/session/recall/text.js +0 -0
  59. package/src/session/recall/token-stats.js +81 -0
  60. package/src/session/recap.js +54 -26
  61. package/src/session/remote-hydrate.js +12 -20
  62. package/src/session/store.js +13 -14
  63. package/src/session/stream.js +23 -3
  64. package/src/session/sync.js +789 -22
  65. package/src/session/tickets.js +316 -0
  66. package/src/session/title-sync.js +2 -1
  67. package/src/session/usage-report.js +41 -2
  68. package/src/session/wake/pump.js +8 -3
  69. package/src/session/wake/resolve-target.js +10 -5
@@ -32,8 +32,10 @@ import {
32
32
  sessionEventIdentityKeys,
33
33
  sessionEventHasKnownIdentity,
34
34
  sessionEventUpgradesExisting,
35
+ dedupeSessionEvents,
35
36
  } from "./event-identity.js";
36
37
  import { isSessionControlEvent } from "./control-events.js";
38
+ import { sessionMessageEditSnapshot } from "./message-edits.js";
37
39
 
38
40
  const EVENTS_CURSOR_SUFFIX = "events";
39
41
  const DEFAULT_EVENT_PAGE_LIMIT = 200;
@@ -231,6 +233,10 @@ async function pollSessionEventPages({
231
233
  const pageEvents = Array.isArray(result.events) ? result.events : [];
232
234
  const nextCursor =
233
235
  typeof result.cursor === "string" && result.cursor.trim() ? result.cursor.trim() : cursor;
236
+ const scanExhausted =
237
+ typeof (result.scanExhausted ?? result.scan_exhausted) === "boolean"
238
+ ? (result.scanExhausted ?? result.scan_exhausted)
239
+ : null;
234
240
  const progressed = nextCursor && cursorAdvances(nextCursor, cursor);
235
241
  if (nextCursor && cursor && !progressed && pageEvents.length === 0) {
236
242
  return {
@@ -250,7 +256,7 @@ async function pollSessionEventPages({
250
256
  events.push(...pageEvents);
251
257
  cursor = nextCursor || cursor;
252
258
 
253
- if (pageEvents.length < normalizedLimit) {
259
+ if (pageEvents.length < normalizedLimit && scanExhausted !== false) {
254
260
  return {
255
261
  ok: true,
256
262
  reason: "",
@@ -366,25 +372,11 @@ export async function hydrateSessionFromRemote({
366
372
 
367
373
  // Dedup across sources — both endpoints can return the same event
368
374
  // (e.g. a human relay event). Cursor values are unique per event.
369
- const seenCursors = new Set();
370
- const seenKeys = new Set();
371
- const merged = [];
372
- for (const e of humanResult?.events || []) {
373
- const c = (e && typeof e === "object" && typeof e.cursor === "string") ? e.cursor : "";
374
- if (c && seenCursors.has(c)) continue;
375
- if (sessionEventHasKnownIdentity(e, seenKeys)) continue;
376
- if (c) seenCursors.add(c);
377
- addSessionEventIdentityKeys(seenKeys, e);
378
- merged.push(e);
379
- }
380
- for (const e of eventsResult?.events || []) {
381
- const c = (e && typeof e === "object" && typeof e.cursor === "string") ? e.cursor : "";
382
- if (c && seenCursors.has(c)) continue;
383
- if (sessionEventHasKnownIdentity(e, seenKeys)) continue;
384
- if (c) seenCursors.add(c);
385
- addSessionEventIdentityKeys(seenKeys, e);
386
- merged.push(e);
387
- }
375
+ const sourceEvents = [...(humanResult?.events || []), ...(eventsResult?.events || [])];
376
+ const merged = dedupeSessionEvents(sourceEvents.flatMap((event) => {
377
+ const snapshot = sessionMessageEditSnapshot(event);
378
+ return snapshot ? [snapshot, event] : [event];
379
+ }));
388
380
 
389
381
  // If BOTH pollers failed, surface the human-message failure (the
390
382
  // legacy contract) so existing callers see no behavior change. If
@@ -4,10 +4,10 @@ import path from "node:path";
4
4
  import process from "node:process";
5
5
 
6
6
  import { buildArtifactLineageIndex, verifyArtifactChain } from "../daemon/artifact-lineage.js";
7
- import { collectCodebaseIngest } from "../ingest/engine.js";
7
+ import { readSessionCodebaseContext } from "./codebase-context.js";
8
8
  import { computeSessionAnalytics } from "./analytics.js";
9
9
  import { resolveSessionPaths, resolveSessionsRoot } from "./paths.js";
10
- import { appendToStream } from "./stream.js";
10
+ import { appendToStream, withSessionStreamLock } from "./stream.js";
11
11
 
12
12
  const SESSION_SCHEMA_VERSION = "1.0.0";
13
13
  const DEFAULT_TTL_SECONDS = 24 * 60 * 60;
@@ -247,13 +247,7 @@ function normalizeSessionTemplate(raw = null) {
247
247
  }
248
248
 
249
249
  async function collectSessionCodebaseContext(targetPath) {
250
- const cachedIngestPath = path.join(targetPath, ".sentinelayer", "CODEBASE_INGEST.json");
251
- const cachedIngest = await readJsonFile(cachedIngestPath, { allowMissing: true });
252
- if (cachedIngest && typeof cachedIngest === "object") {
253
- return normalizeCodebaseContext(cachedIngest);
254
- }
255
- const ingest = await collectCodebaseIngest({ rootPath: targetPath });
256
- return normalizeCodebaseContext(ingest);
250
+ return normalizeCodebaseContext(await readSessionCodebaseContext(targetPath));
257
251
  }
258
252
 
259
253
  async function buildArchiveSidecars(
@@ -507,11 +501,16 @@ export async function createSession({
507
501
  }
508
502
  );
509
503
 
510
- await fsp.mkdir(paths.agentsDir, { recursive: true });
511
- await saveMetadata(metadata, paths);
512
- await fsp.writeFile(paths.streamPath, "", { encoding: "utf-8", flag: "a" });
513
-
514
- return buildSessionPayload(metadata, paths, nowIso);
504
+ return withSessionStreamLock(sessionId, async () => {
505
+ // A concurrent materializer may have won while optional context was read.
506
+ // Never reset its title/TTL/status or race its first billing append.
507
+ const existing = await loadMetadata(sessionId, { targetPath: resolvedTargetPath });
508
+ if (existing) return buildSessionPayload(existing.metadata, paths, nowIso);
509
+ await fsp.mkdir(paths.agentsDir, { recursive: true });
510
+ await saveMetadata(metadata, paths);
511
+ await fsp.writeFile(paths.streamPath, "", { encoding: "utf-8", flag: "a" });
512
+ return buildSessionPayload(metadata, paths, nowIso);
513
+ }, { targetPath: resolvedTargetPath });
515
514
  }
516
515
 
517
516
  export async function updateSessionTitle(
@@ -8,6 +8,7 @@ import {
8
8
  sessionEventIdentityKeys,
9
9
  } from "./event-identity.js";
10
10
  import { enrichEventWithMentions } from "./mentions.js";
11
+ import { messageRevision, projectSessionMessageEdits } from "./message-edits.js";
11
12
  import { resolveSessionPaths } from "./paths.js";
12
13
  import { redactEventPayload } from "./redact.js";
13
14
  import { fetchSessionFromApi, listSessionsFromApi, syncSessionEventToApi } from "./sync.js";
@@ -287,6 +288,23 @@ async function releaseLock(lockPath) {
287
288
  await fsp.rm(lockPath, { recursive: true, force: true }).catch(() => {});
288
289
  }
289
290
 
291
+ // Creation and append must coordinate on the SAME lock. In particular, two
292
+ // billing writers may both observe a missing session before either creates it.
293
+ // The callback must not append (append acquires this lock independently).
294
+ export async function withSessionStreamLock(sessionId, operation, {
295
+ targetPath = process.cwd(),
296
+ timeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
297
+ } = {}) {
298
+ const paths = resolveSessionPaths(sessionId, { targetPath });
299
+ await fsp.mkdir(paths.sessionDir, { recursive: true });
300
+ await acquireLock(paths.lockPath, { timeoutMs });
301
+ try {
302
+ return await operation();
303
+ } finally {
304
+ await releaseLock(paths.lockPath);
305
+ }
306
+ }
307
+
290
308
  async function readEventsFromFile(filePath) {
291
309
  try {
292
310
  const raw = await fsp.readFile(filePath, "utf-8");
@@ -359,7 +377,8 @@ function eventsShareIdentity(left = {}, right = {}) {
359
377
  }
360
378
 
361
379
  async function appendOrMergeCurrentStreamEvent(paths, canonicalEvent, { mergeExisting = false } = {}) {
362
- if (!mergeExisting) {
380
+ // Keep prior revisions in the raw audit log. Readers project the latest body.
381
+ if (!mergeExisting || messageRevision(canonicalEvent) > 1) {
363
382
  await fsp.appendFile(paths.streamPath, `${JSON.stringify(canonicalEvent)}\n`, "utf-8");
364
383
  return;
365
384
  }
@@ -427,6 +446,7 @@ export async function appendToStream(
427
446
  maxEvents = DEFAULT_MAX_STREAM_EVENTS,
428
447
  syncRemote = true,
429
448
  awaitRemoteSync = false,
449
+ lockTimeoutMs = DEFAULT_LOCK_TIMEOUT_MS,
430
450
  mergeExisting = false,
431
451
  } = {}
432
452
  ) {
@@ -457,7 +477,7 @@ export async function appendToStream(
457
477
  const nowIso = new Date().toISOString();
458
478
  const normalizedMaxEvents = normalizePositiveInteger(maxEvents, DEFAULT_MAX_STREAM_EVENTS);
459
479
 
460
- await acquireLock(paths.lockPath);
480
+ await acquireLock(paths.lockPath, { timeoutMs: lockTimeoutMs });
461
481
  try {
462
482
  await fsp.mkdir(paths.sessionDir, { recursive: true });
463
483
  await appendOrMergeCurrentStreamEvent(paths, canonicalEvent, { mergeExisting });
@@ -490,7 +510,7 @@ export async function readStream(
490
510
  { tail = 20, since = null, targetPath = process.cwd() } = {}
491
511
  ) {
492
512
  const paths = resolveSessionPaths(sessionId, { targetPath });
493
- const events = await readAllEvents(paths);
513
+ const events = projectSessionMessageEdits(await readAllEvents(paths));
494
514
  const filtered = filterBySince(events, since);
495
515
  const normalizedTail = Number(tail);
496
516
  if (!Number.isFinite(normalizedTail) || normalizedTail <= 0) {