sentinelayer-cli 0.39.2 → 0.41.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/README.md +4 -4
- package/package.json +2 -3
- package/src/agents/ai-governance/tools/base.js +19 -166
- package/src/agents/backend/tools/base.js +33 -186
- package/src/agents/code-quality/tools/base.js +27 -154
- package/src/agents/data-layer/tools/base.js +35 -180
- package/src/agents/documentation/tools/base.js +18 -146
- package/src/agents/infrastructure/tools/base.js +17 -164
- package/src/agents/observability/tools/base.js +35 -180
- package/src/agents/performance/tools/base.js +35 -205
- package/src/agents/release/tools/base.js +35 -180
- package/src/agents/reliability/tools/base.js +35 -180
- package/src/agents/security/tools/base.js +33 -187
- package/src/agents/shared-tools/domain-base.js +246 -0
- package/src/agents/supply-chain/tools/base.js +15 -144
- package/src/agents/testing/tools/base.js +33 -184
- package/src/cli.js +5 -0
- package/src/commands/mcp.js +1 -1
- package/src/commands/ring-owner.js +58 -0
- package/src/commands/session.js +703 -263
- package/src/engram/governance.js +67 -0
- package/src/engram/index.js +64 -0
- package/src/engram/namespace.js +89 -0
- package/src/engram/observations.js +34 -0
- package/src/engram/sla.js +104 -0
- package/src/engram/store.js +115 -0
- package/src/engram/tools.js +146 -0
- package/src/engram/trust.js +105 -0
- package/src/legacy-cli.js +6 -3
- package/src/mcp/registry.js +27 -10
- package/src/mcp/session-stdio-server.js +209 -18
- package/src/mcp/smoke.js +9 -8
- package/src/pocket/ring-owner.js +97 -0
- package/src/review/local-review.js +9 -5
- package/src/review/omargate-orchestrator.js +111 -14
- package/src/review/spec-binding.js +16 -1
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +22 -63
- package/src/session/checkpoints.js +194 -0
- package/src/session/coordination-guidance.js +6 -6
- package/src/session/daemon.js +19 -124
- package/src/session/file-lease-integrations.js +1300 -0
- package/src/session/file-locks.js +967 -464
- package/src/session/listener.js +243 -20
- package/src/session/listeners.js +71 -80
- package/src/session/paths.js +2 -0
- package/src/session/recall/embedder.js +181 -0
- package/src/session/recall/entities.js +0 -0
- package/src/session/recall/index-build.js +38 -0
- package/src/session/recall/index-core.js +108 -0
- package/src/session/recall/index.js +131 -0
- package/src/session/recall/needle-eval.js +279 -0
- package/src/session/recall/observation-core.js +183 -0
- package/src/session/recall/observations.js +21 -0
- package/src/session/recall/provenance.js +153 -0
- package/src/session/recall/retrieve.js +374 -0
- package/src/session/recall/text.js +0 -0
- package/src/session/recall/token-stats.js +81 -0
- package/src/session/recap.js +29 -23
- package/src/session/remote-hydrate.js +5 -1
- package/src/session/sync.js +661 -11
- package/src/session/wake/pump.js +8 -3
|
@@ -3,6 +3,7 @@ import process from "node:process";
|
|
|
3
3
|
|
|
4
4
|
import { SentinelayerApiError, requestJson, requestJsonMutation } from "../auth/http.js";
|
|
5
5
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
6
|
+
import { pollSessionEventsBefore } from "./sync.js";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_API_BASE_URL = "https://api.sentinelayer.com";
|
|
8
9
|
const DEFAULT_CHECKPOINT_LIMIT = 100;
|
|
@@ -11,6 +12,10 @@ const DEFAULT_MIN_EVENTS = 20;
|
|
|
11
12
|
const DEFAULT_MAX_EVENTS = 80;
|
|
12
13
|
const DEFAULT_BATCH_MAX_CHECKPOINTS = 5;
|
|
13
14
|
const MAX_BATCH_MAX_CHECKPOINTS = 50;
|
|
15
|
+
const DEFAULT_RESTORE_CONTEXT_EVENTS = 3;
|
|
16
|
+
const DEFAULT_RESTORE_MAX_EVENTS = 120;
|
|
17
|
+
const MAX_RESTORE_CONTEXT_EVENTS = 50;
|
|
18
|
+
const MAX_RESTORE_EVENTS = 200;
|
|
14
19
|
const DEFAULT_CREATED_BY_AGENT_ID = "senti";
|
|
15
20
|
|
|
16
21
|
function normalizeString(value) {
|
|
@@ -33,6 +38,17 @@ function parsePositiveInteger(value, field, fallbackValue = null) {
|
|
|
33
38
|
return Math.floor(parsed);
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
function parseNonNegativeInteger(value, field, fallbackValue = 0) {
|
|
42
|
+
if (value === undefined || value === null || normalizeString(value) === "") {
|
|
43
|
+
return Math.max(0, Math.floor(Number(fallbackValue) || 0));
|
|
44
|
+
}
|
|
45
|
+
const parsed = Number(value);
|
|
46
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
47
|
+
throw new Error(`${field} must be a non-negative integer.`);
|
|
48
|
+
}
|
|
49
|
+
return Math.floor(parsed);
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
function normalizeLimit(value) {
|
|
37
53
|
const parsed = parsePositiveInteger(value, "limit", DEFAULT_CHECKPOINT_LIMIT);
|
|
38
54
|
return Math.max(1, Math.min(MAX_CHECKPOINT_LIMIT, parsed));
|
|
@@ -47,6 +63,16 @@ function normalizeBatchMaxCheckpoints(value) {
|
|
|
47
63
|
return Math.max(1, Math.min(MAX_BATCH_MAX_CHECKPOINTS, parsed));
|
|
48
64
|
}
|
|
49
65
|
|
|
66
|
+
function normalizeRestoreMaxEvents(value) {
|
|
67
|
+
const parsed = parsePositiveInteger(value, "max-events", DEFAULT_RESTORE_MAX_EVENTS);
|
|
68
|
+
return Math.max(1, Math.min(MAX_RESTORE_EVENTS, parsed));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeRestoreContextEvents(value) {
|
|
72
|
+
const parsed = parseNonNegativeInteger(value, "context-events", DEFAULT_RESTORE_CONTEXT_EVENTS);
|
|
73
|
+
return Math.max(0, Math.min(MAX_RESTORE_CONTEXT_EVENTS, parsed));
|
|
74
|
+
}
|
|
75
|
+
|
|
50
76
|
function stableHash(value) {
|
|
51
77
|
return crypto
|
|
52
78
|
.createHash("sha256")
|
|
@@ -148,6 +174,20 @@ function normalizeTokenRange({ tokenStart, tokenEnd } = {}) {
|
|
|
148
174
|
return { start, end };
|
|
149
175
|
}
|
|
150
176
|
|
|
177
|
+
function checkpointIdOf(checkpoint = {}) {
|
|
178
|
+
return normalizeString(checkpoint.checkpointId || checkpoint.checkpoint_id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function checkpointSequenceValue(checkpoint = {}, camelKey, snakeKey) {
|
|
182
|
+
const parsed = Number(checkpoint[camelKey] ?? checkpoint[snakeKey]);
|
|
183
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function eventSequenceValue(event = {}) {
|
|
187
|
+
const parsed = Number(event.sequenceId ?? event.sequence_id ?? event.sequence);
|
|
188
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
151
191
|
export function buildManualCheckpointPayload(sessionId, {
|
|
152
192
|
checkpointId = "",
|
|
153
193
|
startSequence,
|
|
@@ -235,6 +275,99 @@ export function buildGenerateCheckpointPayload(sessionId, {
|
|
|
235
275
|
};
|
|
236
276
|
}
|
|
237
277
|
|
|
278
|
+
export function findCheckpointById(checkpoints = [], checkpointId = "") {
|
|
279
|
+
const normalizedCheckpointId = normalizeString(checkpointId);
|
|
280
|
+
if (!normalizedCheckpointId) return null;
|
|
281
|
+
return Array.isArray(checkpoints)
|
|
282
|
+
? checkpoints.find((checkpoint) => checkpointIdOf(checkpoint) === normalizedCheckpointId) || null
|
|
283
|
+
: null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function buildCheckpointRestoreWindow(checkpoint = {}, {
|
|
287
|
+
contextEvents = DEFAULT_RESTORE_CONTEXT_EVENTS,
|
|
288
|
+
maxEvents = DEFAULT_RESTORE_MAX_EVENTS,
|
|
289
|
+
} = {}) {
|
|
290
|
+
const startSequence = checkpointSequenceValue(checkpoint, "startSequence", "start_sequence");
|
|
291
|
+
const endSequence = checkpointSequenceValue(checkpoint, "endSequence", "end_sequence");
|
|
292
|
+
if (startSequence <= 0) {
|
|
293
|
+
throw new Error("checkpoint startSequence is required for restore.");
|
|
294
|
+
}
|
|
295
|
+
if (endSequence <= 0) {
|
|
296
|
+
throw new Error("checkpoint endSequence is required for restore.");
|
|
297
|
+
}
|
|
298
|
+
if (startSequence > endSequence) {
|
|
299
|
+
throw new Error("checkpoint startSequence must be less than or equal to endSequence.");
|
|
300
|
+
}
|
|
301
|
+
const normalizedContext = normalizeRestoreContextEvents(contextEvents);
|
|
302
|
+
const normalizedMaxEvents = normalizeRestoreMaxEvents(maxEvents);
|
|
303
|
+
const sourceEventCountExpected = endSequence - startSequence + 1;
|
|
304
|
+
const requestedEventCount = sourceEventCountExpected + normalizedContext * 2;
|
|
305
|
+
const limit = Math.min(normalizedMaxEvents, requestedEventCount, MAX_RESTORE_EVENTS);
|
|
306
|
+
return {
|
|
307
|
+
checkpointId: checkpointIdOf(checkpoint) || null,
|
|
308
|
+
startSequence,
|
|
309
|
+
endSequence,
|
|
310
|
+
contextEvents: normalizedContext,
|
|
311
|
+
maxEvents: normalizedMaxEvents,
|
|
312
|
+
limit,
|
|
313
|
+
beforeSequence: endSequence + normalizedContext + 1,
|
|
314
|
+
lowerBoundSequence: Math.max(1, startSequence - normalizedContext),
|
|
315
|
+
sourceEventCountExpected,
|
|
316
|
+
truncatedByLimit: limit < requestedEventCount,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function classifyCheckpointRestoreEvents(events = [], window = {}) {
|
|
321
|
+
const normalizedEvents = Array.isArray(events) ? events : [];
|
|
322
|
+
const startSequence = Number(window.startSequence || 0);
|
|
323
|
+
const endSequence = Number(window.endSequence || 0);
|
|
324
|
+
const sourceEventCountExpected = Number(window.sourceEventCountExpected || 0);
|
|
325
|
+
const beforeContextEvents = [];
|
|
326
|
+
const sourceEvents = [];
|
|
327
|
+
const afterContextEvents = [];
|
|
328
|
+
const sourceSequenceIds = new Set();
|
|
329
|
+
|
|
330
|
+
for (const event of normalizedEvents) {
|
|
331
|
+
const sequence = eventSequenceValue(event);
|
|
332
|
+
if (sequence <= 0) continue;
|
|
333
|
+
if (sequence < startSequence) {
|
|
334
|
+
beforeContextEvents.push(event);
|
|
335
|
+
} else if (sequence > endSequence) {
|
|
336
|
+
afterContextEvents.push(event);
|
|
337
|
+
} else {
|
|
338
|
+
sourceEvents.push(event);
|
|
339
|
+
sourceSequenceIds.add(sequence);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const missingStart = startSequence > 0 && !sourceSequenceIds.has(startSequence);
|
|
344
|
+
const missingEnd = endSequence > 0 && !sourceSequenceIds.has(endSequence);
|
|
345
|
+
const missingSourceEvents = Math.max(0, sourceEventCountExpected - sourceSequenceIds.size);
|
|
346
|
+
return {
|
|
347
|
+
events: normalizedEvents,
|
|
348
|
+
beforeContextEvents,
|
|
349
|
+
sourceEvents,
|
|
350
|
+
afterContextEvents,
|
|
351
|
+
eventCount: normalizedEvents.length,
|
|
352
|
+
beforeContextCount: beforeContextEvents.length,
|
|
353
|
+
sourceEventCount: sourceEvents.length,
|
|
354
|
+
afterContextCount: afterContextEvents.length,
|
|
355
|
+
missingStart,
|
|
356
|
+
missingEnd,
|
|
357
|
+
missingSourceEvents,
|
|
358
|
+
completeSourceRange:
|
|
359
|
+
sourceEventCountExpected > 0 &&
|
|
360
|
+
!missingStart &&
|
|
361
|
+
!missingEnd &&
|
|
362
|
+
missingSourceEvents === 0,
|
|
363
|
+
partial:
|
|
364
|
+
Boolean(window.truncatedByLimit) ||
|
|
365
|
+
missingStart ||
|
|
366
|
+
missingEnd ||
|
|
367
|
+
missingSourceEvents > 0,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
238
371
|
export function normalizeCheckpointGenerationResult(payload = {}) {
|
|
239
372
|
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
240
373
|
const checkpoint =
|
|
@@ -308,6 +441,63 @@ export async function listSessionCheckpoints(sessionId, {
|
|
|
308
441
|
};
|
|
309
442
|
}
|
|
310
443
|
|
|
444
|
+
export async function showSessionCheckpoint(sessionId, checkpointId, {
|
|
445
|
+
targetPath = process.cwd(),
|
|
446
|
+
contextEvents = DEFAULT_RESTORE_CONTEXT_EVENTS,
|
|
447
|
+
maxEvents = DEFAULT_RESTORE_MAX_EVENTS,
|
|
448
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
449
|
+
request = requestJson,
|
|
450
|
+
fetchEventsBefore = pollSessionEventsBefore,
|
|
451
|
+
} = {}) {
|
|
452
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
453
|
+
if (!normalizedSessionId) {
|
|
454
|
+
throw new Error("session id is required.");
|
|
455
|
+
}
|
|
456
|
+
const normalizedCheckpointId = normalizeString(checkpointId);
|
|
457
|
+
if (!normalizedCheckpointId) {
|
|
458
|
+
throw new Error("checkpoint id is required.");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const listed = await listSessionCheckpoints(normalizedSessionId, {
|
|
462
|
+
targetPath,
|
|
463
|
+
limit: MAX_CHECKPOINT_LIMIT,
|
|
464
|
+
resolveAuthSession,
|
|
465
|
+
request,
|
|
466
|
+
});
|
|
467
|
+
const checkpoint = findCheckpointById(listed.checkpoints, normalizedCheckpointId);
|
|
468
|
+
if (!checkpoint) {
|
|
469
|
+
throw new Error(`Checkpoint '${normalizedCheckpointId}' was not found in session ${normalizedSessionId}.`);
|
|
470
|
+
}
|
|
471
|
+
const restoreWindow = buildCheckpointRestoreWindow(checkpoint, {
|
|
472
|
+
contextEvents,
|
|
473
|
+
maxEvents,
|
|
474
|
+
});
|
|
475
|
+
const remote = await fetchEventsBefore(normalizedSessionId, {
|
|
476
|
+
targetPath,
|
|
477
|
+
beforeSequence: restoreWindow.beforeSequence,
|
|
478
|
+
limit: restoreWindow.limit,
|
|
479
|
+
forceCircuitProbe: true,
|
|
480
|
+
resolveAuthSession,
|
|
481
|
+
});
|
|
482
|
+
const classified = classifyCheckpointRestoreEvents(remote?.events || [], restoreWindow);
|
|
483
|
+
return {
|
|
484
|
+
ok: Boolean(remote?.ok),
|
|
485
|
+
reason: normalizeString(remote?.reason),
|
|
486
|
+
sessionId: normalizedSessionId,
|
|
487
|
+
checkpointId: normalizedCheckpointId,
|
|
488
|
+
apiUrl: listed.apiUrl,
|
|
489
|
+
checkpoint,
|
|
490
|
+
window: {
|
|
491
|
+
...restoreWindow,
|
|
492
|
+
...classified,
|
|
493
|
+
remoteOk: Boolean(remote?.ok),
|
|
494
|
+
remoteReason: normalizeString(remote?.reason),
|
|
495
|
+
remoteBeforeSequence: remote?.beforeSequence || null,
|
|
496
|
+
remoteCursor: remote?.cursor || null,
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
311
501
|
export async function createSessionCheckpoint(sessionId, options = {}) {
|
|
312
502
|
const normalizedSessionId = normalizeString(sessionId);
|
|
313
503
|
const {
|
|
@@ -446,5 +636,9 @@ export {
|
|
|
446
636
|
DEFAULT_BATCH_MAX_CHECKPOINTS,
|
|
447
637
|
DEFAULT_MAX_EVENTS,
|
|
448
638
|
DEFAULT_MIN_EVENTS,
|
|
639
|
+
DEFAULT_RESTORE_CONTEXT_EVENTS,
|
|
640
|
+
DEFAULT_RESTORE_MAX_EVENTS,
|
|
449
641
|
MAX_BATCH_MAX_CHECKPOINTS,
|
|
642
|
+
MAX_RESTORE_CONTEXT_EVENTS,
|
|
643
|
+
MAX_RESTORE_EVENTS,
|
|
450
644
|
};
|
|
@@ -2,12 +2,12 @@ export const COORDINATION_GUIDANCE_TITLE = "Multi-Agent Coordination Protocol";
|
|
|
2
2
|
|
|
3
3
|
export const COORDINATION_ETIQUETTE_ITEMS = Object.freeze([
|
|
4
4
|
"Find the recent Senti session for this codebase: run `sl session list --path .` and `sl session list --remote --path .`; join the right room with `sl session join <id> --name <your-name> --role coder`.",
|
|
5
|
-
"
|
|
6
|
-
"Before implementation, post a short plan
|
|
7
|
-
"Claim shared files before editing with `lock
|
|
8
|
-
"Run one background/secondary listener for replies with `sl session listen --session <id> --agent <your-name> --interval 60 --active-interval
|
|
9
|
-
"For long-lived rooms, make sure exactly one visible participant owns the Senti daemon: `sl session daemon --session <id> --recap-interval 300 --checkpoint-interval 60`.
|
|
10
|
-
"Use message actions for low-noise coordination before posting a new top-level message: `sl session react <id> ack --target-sequence <n>` only when an explicit ACK is useful, `sl session action <id> working_on --target-sequence <n>` for ownership, and `sl session reply <id> <sequence> \"<message>\"` / `sl session comment <id> <sequence> \"<message>\"` for threaded responses.
|
|
5
|
+
"Keep transient activity outside chat with `sl session action <id> working_on --target-sequence <n>`; post through `sl session post-agent` only for a material decision, finding, blocker, handoff, or completion so the transcript remains readable.",
|
|
6
|
+
"Before implementation, post a short semantic plan only when peers need it; keep routine ownership and progress in message actions.",
|
|
7
|
+
"Claim shared files outside chat before editing with `sl session lock <id> <file> --agent <your-agent-id> --intent \"<intent>\"`; install terminal/editor preflights once with `sl session guard-install <id> --agent <your-agent-id>`, inspect with `sl session locks <id>`, renew long work with `sl session renew`, and release with `sl session unlock`. Lease lifecycle never belongs in chat; guarded terminal/editor denials report the file, holder, intent, and expiry directly to the blocked agent.",
|
|
8
|
+
"Run one background/secondary listener for replies with `sl session listen --session <id> --agent <your-name> --transport poll --interval 60 --active-interval 60 --emit ndjson --no-presence`; polling applies bounded jitter, exponential transient backoff, and `Retry-After` as a hard floor. `session listen` refuses duplicate local listeners for the same session/agent by default; use `--force` only to stop and replace an existing local owner and `--allow-duplicate` only when you intentionally need parallel wake hooks. `session listen` is only a delivery cursor, not a grounding command; join or recap before acting. For your primary interactive listener, omit `--no-presence` so it renews an ephemeral TTL outside the transcript. Unsupported or degraded presence remains unknown and never falls back to durable heartbeat events. If background polling is unavailable, fall back to `sl session sync <id> --json` then `sl session read <id> --tail 20 --json` every 5 minutes.",
|
|
9
|
+
"For long-lived rooms, make sure exactly one visible participant owns the Senti daemon: `sl session daemon --session <id> --recap-interval 300 --checkpoint-interval 60`. Routine room health is a derived status projection, not a transcript event; run `sl session recap now <id> --remote --agent <your-name> --json` for local grounding and use an explicit checkpoint only for a durable semantic handoff.",
|
|
10
|
+
"Use message actions for low-noise coordination before posting a new top-level message: `sl session react <id> ack --target-sequence <n>` only when an explicit ACK is useful, `sl session action <id> working_on --target-sequence <n>` for ownership, and `sl session reply <id> <sequence> \"<message>\"` / `sl session comment <id> <sequence> \"<message>\"` for threaded responses. `sl session read <id> --remote --agent <your-name>` advances one monotonic per-agent read cursor for the displayed window; it never appends per-message view events; reserve `sl session view <id> <sequence>` for repair/backfill of that same cursor. Run `sl session actions` for the full list.",
|
|
11
11
|
"Search before asking peers to restate context: `sl session search <id> \"<topic>\" --limit 10`.",
|
|
12
12
|
"Run `sl review --diff` after each finished file or PR-ready diff and post the result summary back to the session.",
|
|
13
13
|
"Post findings through `sl session say <id> \"finding: [P2] <title> in <file>:<line>\"` with enough context for a peer to act.",
|
package/src/session/daemon.js
CHANGED
|
@@ -22,11 +22,6 @@ import {
|
|
|
22
22
|
registerAgent,
|
|
23
23
|
unregisterAgent,
|
|
24
24
|
} from "./agent-registry.js";
|
|
25
|
-
import {
|
|
26
|
-
DEFAULT_FILE_LOCK_TTL_SECONDS,
|
|
27
|
-
lockFile,
|
|
28
|
-
unlockFile,
|
|
29
|
-
} from "./file-locks.js";
|
|
30
25
|
import {
|
|
31
26
|
DEFAULT_MAX_EVENTS as DEFAULT_CHECKPOINT_MAX_EVENTS,
|
|
32
27
|
DEFAULT_MIN_EVENTS as DEFAULT_CHECKPOINT_MIN_EVENTS,
|
|
@@ -65,8 +60,6 @@ const DEFAULT_CHECKPOINT_INTERVAL_MS = 60_000;
|
|
|
65
60
|
const DEFAULT_CHECKPOINT_EVENT_THRESHOLD = DEFAULT_CHECKPOINT_MIN_EVENTS;
|
|
66
61
|
const DEFAULT_CHECKPOINT_IDLE_MS = DEFAULT_RECAP_INACTIVITY_MS_OVERRIDE;
|
|
67
62
|
const CHECKPOINT_MEANINGFUL_EVENT_NAMES = new Set([
|
|
68
|
-
"file_lock",
|
|
69
|
-
"file_unlock",
|
|
70
63
|
"finding",
|
|
71
64
|
"help_request",
|
|
72
65
|
"help_response",
|
|
@@ -84,6 +77,9 @@ const CHECKPOINT_IGNORED_EVENT_NAMES = new Set([
|
|
|
84
77
|
"agent_status",
|
|
85
78
|
"context_briefing",
|
|
86
79
|
"daemon_alert",
|
|
80
|
+
"file_lock",
|
|
81
|
+
"file_lock_expired",
|
|
82
|
+
"file_unlock",
|
|
87
83
|
"session_checkpoint",
|
|
88
84
|
"session_listen_error",
|
|
89
85
|
"session_recap",
|
|
@@ -138,14 +134,27 @@ async function emitSentiEvent(
|
|
|
138
134
|
payload = {},
|
|
139
135
|
{ targetPath = process.cwd(), nowIso = new Date().toISOString() } = {}
|
|
140
136
|
) {
|
|
137
|
+
const projectionOnly = normalizeString(event) === "daemon_alert";
|
|
141
138
|
const envelope = createAgentEvent({
|
|
142
139
|
event,
|
|
143
140
|
agentId: SENTI_IDENTITY.id,
|
|
144
141
|
agentModel: SENTI_IDENTITY.model,
|
|
145
142
|
sessionId,
|
|
146
143
|
ts: normalizeIsoTimestamp(nowIso, new Date().toISOString()),
|
|
147
|
-
payload
|
|
144
|
+
payload: projectionOnly
|
|
145
|
+
? {
|
|
146
|
+
...payload,
|
|
147
|
+
ephemeral: true,
|
|
148
|
+
projection: "session_health",
|
|
149
|
+
}
|
|
150
|
+
: payload,
|
|
148
151
|
});
|
|
152
|
+
// Daemon health, presence, conflict hints, and lifecycle notices are
|
|
153
|
+
// operational projections. They are returned to the caller/status surface,
|
|
154
|
+
// but never become durable conversation rows.
|
|
155
|
+
if (projectionOnly) {
|
|
156
|
+
return envelope;
|
|
157
|
+
}
|
|
149
158
|
await appendToStream(sessionId, envelope, {
|
|
150
159
|
targetPath,
|
|
151
160
|
});
|
|
@@ -731,126 +740,12 @@ async function runHelpWatcher(daemonState) {
|
|
|
731
740
|
}
|
|
732
741
|
}
|
|
733
742
|
|
|
734
|
-
function
|
|
735
|
-
const normalized = normalizeString(raw);
|
|
736
|
-
if (!normalized) {
|
|
737
|
-
return {
|
|
738
|
-
filePath: "",
|
|
739
|
-
intent: "",
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
const separatorMatch = /\s(?:—|–|-)\s/.exec(normalized);
|
|
743
|
-
if (!separatorMatch) {
|
|
744
|
-
return {
|
|
745
|
-
filePath: normalizeString(normalized),
|
|
746
|
-
intent: "",
|
|
747
|
-
};
|
|
748
|
-
}
|
|
749
|
-
const separatorIndex = Number(separatorMatch.index || 0);
|
|
750
|
-
return {
|
|
751
|
-
filePath: normalizeString(normalized.slice(0, separatorIndex)),
|
|
752
|
-
intent: normalizeString(normalized.slice(separatorIndex + separatorMatch[0].length)),
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
function parseSessionDirective(event = {}) {
|
|
757
|
-
if (normalizeString(event.event) !== "session_message") {
|
|
758
|
-
return null;
|
|
759
|
-
}
|
|
760
|
-
const message = normalizeString(event.payload?.message);
|
|
761
|
-
if (!message) {
|
|
762
|
-
return null;
|
|
763
|
-
}
|
|
764
|
-
const directive = /^(lock|unlock)\s*:\s*(.+)$/i.exec(message);
|
|
765
|
-
if (!directive) {
|
|
766
|
-
return null;
|
|
767
|
-
}
|
|
768
|
-
const action = normalizeString(directive[1]).toLowerCase();
|
|
769
|
-
const body = normalizeString(directive[2]);
|
|
770
|
-
const parsed = splitFileAndIntent(body);
|
|
771
|
-
if (!parsed.filePath) {
|
|
772
|
-
return null;
|
|
773
|
-
}
|
|
774
|
-
return {
|
|
775
|
-
action,
|
|
776
|
-
filePath: parsed.filePath,
|
|
777
|
-
intent: parsed.intent,
|
|
778
|
-
};
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
async function maybeHandleSessionDirective(daemonState, event) {
|
|
743
|
+
async function maybeHandleTaskDirective(daemonState, event) {
|
|
782
744
|
const agentId = normalizeString(event.agent?.id);
|
|
783
745
|
if (!agentId || agentId === SENTI_IDENTITY.id) {
|
|
784
746
|
return null;
|
|
785
747
|
}
|
|
786
748
|
const nowIso = normalizeIsoTimestamp(event.ts, new Date().toISOString());
|
|
787
|
-
const fileDirective = parseSessionDirective(event);
|
|
788
|
-
if (fileDirective) {
|
|
789
|
-
if (fileDirective.action === "lock") {
|
|
790
|
-
const result = await lockFile(
|
|
791
|
-
daemonState.sessionId,
|
|
792
|
-
agentId,
|
|
793
|
-
fileDirective.filePath,
|
|
794
|
-
{
|
|
795
|
-
intent: fileDirective.intent,
|
|
796
|
-
ttlSeconds: DEFAULT_FILE_LOCK_TTL_SECONDS,
|
|
797
|
-
targetPath: daemonState.targetPath,
|
|
798
|
-
nowIso,
|
|
799
|
-
}
|
|
800
|
-
);
|
|
801
|
-
if (!result.locked) {
|
|
802
|
-
await emitSentiEvent(
|
|
803
|
-
daemonState.sessionId,
|
|
804
|
-
"daemon_alert",
|
|
805
|
-
{
|
|
806
|
-
alert: "file_lock_denied",
|
|
807
|
-
file: result.file || fileDirective.filePath,
|
|
808
|
-
requestedBy: agentId,
|
|
809
|
-
heldBy: result.heldBy || null,
|
|
810
|
-
since: result.since || null,
|
|
811
|
-
suggestion: `${fileDirective.filePath} is locked by ${result.heldBy || "another agent"} (${result.since || "recently"}). Coordinate before editing.`,
|
|
812
|
-
},
|
|
813
|
-
{
|
|
814
|
-
targetPath: daemonState.targetPath,
|
|
815
|
-
nowIso,
|
|
816
|
-
}
|
|
817
|
-
);
|
|
818
|
-
}
|
|
819
|
-
return result;
|
|
820
|
-
}
|
|
821
|
-
if (fileDirective.action === "unlock") {
|
|
822
|
-
const result = await unlockFile(
|
|
823
|
-
daemonState.sessionId,
|
|
824
|
-
agentId,
|
|
825
|
-
fileDirective.filePath,
|
|
826
|
-
{
|
|
827
|
-
reason: "session_message_unlock",
|
|
828
|
-
targetPath: daemonState.targetPath,
|
|
829
|
-
nowIso,
|
|
830
|
-
}
|
|
831
|
-
);
|
|
832
|
-
if (!result.unlocked && result.reason === "held_by_other_agent") {
|
|
833
|
-
await emitSentiEvent(
|
|
834
|
-
daemonState.sessionId,
|
|
835
|
-
"daemon_alert",
|
|
836
|
-
{
|
|
837
|
-
alert: "file_unlock_denied",
|
|
838
|
-
file: result.file || fileDirective.filePath,
|
|
839
|
-
requestedBy: agentId,
|
|
840
|
-
heldBy: result.heldBy || null,
|
|
841
|
-
since: result.since || null,
|
|
842
|
-
suggestion: `${fileDirective.filePath} is locked by ${result.heldBy || "another agent"}. Only the lock holder can release it.`,
|
|
843
|
-
},
|
|
844
|
-
{
|
|
845
|
-
targetPath: daemonState.targetPath,
|
|
846
|
-
nowIso,
|
|
847
|
-
}
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
return result;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
749
|
try {
|
|
855
750
|
return await handleTaskDirective(daemonState.sessionId, event, {
|
|
856
751
|
targetPath: daemonState.targetPath,
|
|
@@ -891,7 +786,7 @@ async function runSessionDirectiveWatcher(daemonState) {
|
|
|
891
786
|
if (normalizeString(event.event) !== "session_message") {
|
|
892
787
|
continue;
|
|
893
788
|
}
|
|
894
|
-
await
|
|
789
|
+
await maybeHandleTaskDirective(daemonState, event);
|
|
895
790
|
}
|
|
896
791
|
} catch (error) {
|
|
897
792
|
if (error && typeof error === "object" && error.name === "AbortError") {
|