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.
- package/README.md +22 -4
- package/package.json +4 -4
- package/src/auth/admission-scope.js +34 -0
- package/src/auth/gate.js +6 -0
- package/src/auth/http.js +4 -0
- package/src/auth/service.js +6 -0
- package/src/board/done-gate.js +155 -0
- package/src/board/evidence-resolvers.js +138 -0
- package/src/cli.js +24 -1
- package/src/commands/board.js +116 -0
- package/src/commands/session.js +1094 -279
- package/src/engram/build-state.js +183 -0
- package/src/engram/canonical.js +75 -0
- package/src/engram/document.js +190 -0
- package/src/engram/governance.js +67 -0
- package/src/engram/index.js +73 -0
- package/src/engram/ingest-document.js +107 -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 +120 -0
- package/src/engram/tools.js +152 -0
- package/src/engram/trust.js +105 -0
- package/src/events/schema.js +18 -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/net/test-egress-guard.js +158 -0
- package/src/review/investor-dd-orchestrator.js +14 -2
- package/src/session/admission-auth.js +312 -0
- package/src/session/admission-preimage.js +90 -0
- package/src/session/admission.js +620 -0
- package/src/session/agent-registry.js +22 -63
- package/src/session/checkpoints.js +25 -1
- package/src/session/codebase-context.js +54 -0
- package/src/session/coordination-guidance.js +6 -6
- package/src/session/daemon-spawn.js +20 -2
- package/src/session/daemon.js +26 -125
- package/src/session/event-identity.js +9 -2
- package/src/session/file-lease-integrations.js +1300 -0
- package/src/session/file-locks.js +967 -464
- package/src/session/first-message.js +11 -3
- package/src/session/listener.js +246 -20
- package/src/session/listeners.js +71 -80
- package/src/session/live-source.js +47 -16
- package/src/session/message-edits.js +89 -0
- 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 +54 -26
- package/src/session/remote-hydrate.js +12 -20
- package/src/session/store.js +13 -14
- package/src/session/stream.js +23 -3
- package/src/session/sync.js +789 -22
- package/src/session/tickets.js +316 -0
- package/src/session/title-sync.js +2 -1
- package/src/session/usage-report.js +41 -2
- package/src/session/wake/pump.js +8 -3
- package/src/session/wake/resolve-target.js +10 -5
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ENGRAM §2 — the trust seal on write.
|
|
3
|
+
*
|
|
4
|
+
* Vocabulary is DELIBERATELY aligned with the server-side `ServiceTrustLevel`
|
|
5
|
+
* subsystem (sentinelayer-api `src/models/service_trust.py`: L0_connected ->
|
|
6
|
+
* L5_remediation_ready) so §2.1 converges onto that existing certification
|
|
7
|
+
* system instead of forking a divergent trust model. P0 stays CLI-local and
|
|
8
|
+
* does NOT touch the server subsystem — it only borrows the vocabulary.
|
|
9
|
+
*
|
|
10
|
+
* Rule (from the convergence + guest-token model): only VERIFIED writes are
|
|
11
|
+
* AUTHORITATIVE; untrusted/guest/unverified writes are retrievable-but-marked,
|
|
12
|
+
* never authoritative, never a governed-action trigger. `sealed`/`revoked`
|
|
13
|
+
* memories are never surfaced to an unauthorized scope (the §10 analog).
|
|
14
|
+
*
|
|
15
|
+
* Real verification (AIdenID provider-signature / delegation-proof) is
|
|
16
|
+
* external (§2.1); P0 classifies from local identity signals.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const TRUST_LEVELS = Object.freeze([
|
|
20
|
+
"L0_connected",
|
|
21
|
+
"L1_audited",
|
|
22
|
+
"L2_memory_certified",
|
|
23
|
+
"L3_watch_enabled",
|
|
24
|
+
"L4_governance_grade",
|
|
25
|
+
"L5_remediation_ready",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const RANK = new Map(TRUST_LEVELS.map((level, index) => [level, index]));
|
|
29
|
+
|
|
30
|
+
/** Writes at or above this level are AUTHORITATIVE. */
|
|
31
|
+
export const AUTHORITATIVE_FLOOR = "L2_memory_certified";
|
|
32
|
+
|
|
33
|
+
export function trustRank(level) {
|
|
34
|
+
return RANK.has(level) ? RANK.get(level) : 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function isAuthoritative(level) {
|
|
38
|
+
return trustRank(level) >= trustRank(AUTHORITATIVE_FLOOR);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Classify a write's trust level from the caller. AIdenID verification is
|
|
43
|
+
* external (§2.1); until then a caller is authoritative only if it presents a
|
|
44
|
+
* verified identity.
|
|
45
|
+
* @param {object} [caller] { id, kind?, verified?, trustLevel? }
|
|
46
|
+
* @returns {string} a TRUST_LEVELS value
|
|
47
|
+
*/
|
|
48
|
+
export function classifyWrite(caller = {}) {
|
|
49
|
+
if (!caller || !caller.id || caller.kind === "guest") return "L0_connected";
|
|
50
|
+
if (caller.verified === true) {
|
|
51
|
+
return caller.trustLevel && RANK.has(caller.trustLevel) ? caller.trustLevel : "L2_memory_certified";
|
|
52
|
+
}
|
|
53
|
+
return "L1_audited"; // known but unverified -> retrievable, NOT authoritative
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Classify a source-adapter observation (e.g. a session event) from its
|
|
58
|
+
* author when the item carried no explicit trust.
|
|
59
|
+
* @param {object} obs
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
export function classifyObservation(obs = {}) {
|
|
63
|
+
const agentId = String(obs.agentId || "").toLowerCase();
|
|
64
|
+
if (!agentId || agentId === "unknown" || agentId === "cli-user" || agentId.startsWith("guest") || agentId.startsWith("agent-")) {
|
|
65
|
+
return "L0_connected";
|
|
66
|
+
}
|
|
67
|
+
return "L1_audited";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Attach the trust seal to an item at write time.
|
|
72
|
+
* @param {object} item
|
|
73
|
+
* @param {object} ctx { caller }
|
|
74
|
+
* @returns {object} item with { trust, sealed, revoked }
|
|
75
|
+
*/
|
|
76
|
+
export function sealItem(item, { caller } = {}) {
|
|
77
|
+
// Trust is DERIVED from the caller's identity, never self-asserted. A caller
|
|
78
|
+
// MAY downgrade their own write (mark it less trusted); they may NEVER upgrade
|
|
79
|
+
// above their identity-derived ceiling — otherwise an unverified/guest caller
|
|
80
|
+
// could forge an AUTHORITATIVE memory by supplying `trust` in the payload,
|
|
81
|
+
// defeating the "unverified -> never authoritative" invariant above.
|
|
82
|
+
const derived = classifyWrite(caller);
|
|
83
|
+
const asserted = item?.trust && RANK.has(item.trust) ? item.trust : derived;
|
|
84
|
+
const trust = trustRank(asserted) <= trustRank(derived) ? asserted : derived;
|
|
85
|
+
return {
|
|
86
|
+
...item,
|
|
87
|
+
trust,
|
|
88
|
+
sealed: item?.sealed === true,
|
|
89
|
+
revoked: item?.revoked === true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Recall visibility gate: revoked memory NEVER surfaces; sealed memory
|
|
95
|
+
* surfaces only to an authorized scope. Untrusted memory DOES surface (marked
|
|
96
|
+
* non-authoritative by the caller).
|
|
97
|
+
* @param {object} obs
|
|
98
|
+
* @param {object} [ctx] { authorized }
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
export function visibleUnderScope(obs, { authorized = false } = {}) {
|
|
102
|
+
if (obs?.revoked === true) return false;
|
|
103
|
+
if (obs?.sealed === true && !authorized) return false;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
package/src/events/schema.js
CHANGED
|
@@ -125,6 +125,12 @@ export function createAgentEvent({
|
|
|
125
125
|
cursor,
|
|
126
126
|
sequenceId,
|
|
127
127
|
sequence_id,
|
|
128
|
+
id,
|
|
129
|
+
messageRevision,
|
|
130
|
+
editedAt,
|
|
131
|
+
editedBy,
|
|
132
|
+
canEdit,
|
|
133
|
+
revisionEvidence,
|
|
128
134
|
} = {}) {
|
|
129
135
|
const normalizedEvent = normalizeNonEmptyString(event);
|
|
130
136
|
const normalizedAgent = normalizeAgentShape({
|
|
@@ -149,6 +155,12 @@ export function createAgentEvent({
|
|
|
149
155
|
workItemId: normalizeOptionalString(workItemId),
|
|
150
156
|
requestId: normalizeOptionalString(requestId),
|
|
151
157
|
eventId: normalizeOptionalString(eventId),
|
|
158
|
+
id: normalizeOptionalString(id),
|
|
159
|
+
messageRevision: normalizePositiveInteger(messageRevision),
|
|
160
|
+
editedAt: editedAt === null ? null : normalizeOptionalString(editedAt),
|
|
161
|
+
editedBy: editedBy === null ? null : isPlainObject(editedBy) ? { ...editedBy } : undefined,
|
|
162
|
+
canEdit: typeof canEdit === "boolean" ? canEdit : undefined,
|
|
163
|
+
revisionEvidence: isPlainObject(revisionEvidence) ? { ...revisionEvidence } : undefined,
|
|
152
164
|
idempotencyToken: normalizeOptionalString(idempotencyToken),
|
|
153
165
|
cursor: normalizeOptionalString(cursor),
|
|
154
166
|
sequenceId: normalizePositiveInteger(sequenceId ?? sequence_id),
|
|
@@ -220,6 +232,12 @@ export function normalizeAgentEvent(evt, { allowLegacy = true } = {}) {
|
|
|
220
232
|
workItemId: evt.workItemId,
|
|
221
233
|
requestId: evt.requestId,
|
|
222
234
|
eventId: evt.eventId,
|
|
235
|
+
id: evt.id,
|
|
236
|
+
messageRevision: evt.messageRevision,
|
|
237
|
+
editedAt: evt.editedAt,
|
|
238
|
+
editedBy: evt.editedBy,
|
|
239
|
+
canEdit: evt.canEdit,
|
|
240
|
+
revisionEvidence: evt.revisionEvidence,
|
|
223
241
|
idempotencyToken: evt.idempotencyToken,
|
|
224
242
|
cursor: evt.cursor,
|
|
225
243
|
sequenceId: evt.sequenceId ?? evt.sequence_id,
|
package/src/legacy-cli.js
CHANGED
|
@@ -216,7 +216,10 @@ function printUsage() {
|
|
|
216
216
|
console.log(" sl session templates --json List available session quick-start templates");
|
|
217
217
|
console.log(" sl session join <id> --name <n> Join a session as an agent");
|
|
218
218
|
console.log(" sl session say <id> \"msg\" --json Append a message event to session stream");
|
|
219
|
-
console.log(" sl session
|
|
219
|
+
console.log(" sl session lock <id> <file> --agent <agent> --intent <text> Claim an API-backed file lease outside chat");
|
|
220
|
+
console.log(" sl session guard-install <id> --agent <agent> Install terminal/editor lease preflights");
|
|
221
|
+
console.log(" sl session guard-uninstall <id> --agent <agent> Remove managed preflights before CLI rollback");
|
|
222
|
+
console.log(" sl session unlock <id> <file> --agent <agent> Release an API-backed file lease");
|
|
220
223
|
console.log(" sl session say <id> \"assign: @agent <task>\" Create task assignment + lease");
|
|
221
224
|
console.log(" sl session say <id> \"assign: @*:reviewer <task>\" Wildcard route to least-busy role");
|
|
222
225
|
console.log(" sl session say <id> \"accepted: task <task-id>\" / \"done: task <task-id>\" Task transitions");
|
|
@@ -225,8 +228,8 @@ function printUsage() {
|
|
|
225
228
|
console.log(" sl session action <id> working_on --target-sequence <n> Claim work on a message");
|
|
226
229
|
console.log(" sl session reply <id> <seq> \"msg\" Thread a response under a message");
|
|
227
230
|
console.log(" sl session comment <id> <seq> \"msg\" Alias for threaded reply");
|
|
228
|
-
console.log(" sl session read <id> --remote --agent <id> Read
|
|
229
|
-
console.log(" sl session
|
|
231
|
+
console.log(" sl session read <id> --remote --agent <id> Read events and advance delivery cursor");
|
|
232
|
+
console.log(" sl session edit <id> <seq|reply-uuid> \"msg\" Edit your message without losing history");
|
|
230
233
|
console.log(" sl session pins <id> --json List pinned messages with content (readable by agents)");
|
|
231
234
|
console.log(" sl session lock <id> <files...> --intent <why> Claim file locks (fail-closed, TTL auto-release)");
|
|
232
235
|
console.log(" sl session unlock <id> <files...> Release file locks you hold");
|
package/src/mcp/registry.js
CHANGED
|
@@ -525,7 +525,7 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
525
525
|
name: "poll_inbox",
|
|
526
526
|
title: "Poll Senti Inbox",
|
|
527
527
|
description:
|
|
528
|
-
"Poll durable SentinelLayer session events after an optional cursor
|
|
528
|
+
"Poll durable SentinelLayer session events after an optional cursor, return events visible to one agent, and advance one monotonic read cursor.",
|
|
529
529
|
input_schema: {
|
|
530
530
|
type: "object",
|
|
531
531
|
additionalProperties: false,
|
|
@@ -567,13 +567,14 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
567
567
|
name: "read_history",
|
|
568
568
|
title: "Read Senti History",
|
|
569
569
|
description:
|
|
570
|
-
"Hydrate a bounded recent, older, or after-cursor SentinelLayer session transcript window without
|
|
570
|
+
"Hydrate a bounded recent, older, or after-cursor SentinelLayer session transcript window and advance one monotonic read cursor without appending view events.",
|
|
571
571
|
input_schema: {
|
|
572
572
|
type: "object",
|
|
573
573
|
additionalProperties: false,
|
|
574
574
|
required: ["sessionId"],
|
|
575
575
|
properties: {
|
|
576
576
|
sessionId: { type: "string" },
|
|
577
|
+
agentId: { type: "string" },
|
|
577
578
|
cursor: { type: "string" },
|
|
578
579
|
beforeSequence: { type: "integer", minimum: 1 },
|
|
579
580
|
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
@@ -654,7 +655,7 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
654
655
|
name: "session_action",
|
|
655
656
|
title: "Record Senti Session Action",
|
|
656
657
|
description:
|
|
657
|
-
"Record a low-noise message action such as ack, working_on, disregard,
|
|
658
|
+
"Record a low-noise message action such as ack, working_on, disregard, like, dislike, or reply; view advances the monotonic read cursor instead of appending an action.",
|
|
658
659
|
input_schema: {
|
|
659
660
|
type: "object",
|
|
660
661
|
additionalProperties: false,
|
|
@@ -787,7 +788,7 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
787
788
|
name: "session_lock",
|
|
788
789
|
title: "Lock Senti Files",
|
|
789
790
|
description:
|
|
790
|
-
"Claim session-scoped file
|
|
791
|
+
"Claim session-scoped file leases through the authoritative SentinelLayer API before editing. Lease lifecycle operations never create session events.",
|
|
791
792
|
input_schema: {
|
|
792
793
|
type: "object",
|
|
793
794
|
additionalProperties: false,
|
|
@@ -803,8 +804,16 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
803
804
|
},
|
|
804
805
|
intent: { type: "string" },
|
|
805
806
|
ttlSeconds: { type: "integer", minimum: 1 },
|
|
806
|
-
syncRemote: {
|
|
807
|
-
|
|
807
|
+
syncRemote: {
|
|
808
|
+
type: "boolean",
|
|
809
|
+
description:
|
|
810
|
+
"Deprecated compatibility input; ignored because the authenticated API is always authoritative.",
|
|
811
|
+
},
|
|
812
|
+
awaitRemoteSync: {
|
|
813
|
+
type: "boolean",
|
|
814
|
+
description:
|
|
815
|
+
"Deprecated compatibility input; ignored because the authenticated API is always authoritative.",
|
|
816
|
+
},
|
|
808
817
|
},
|
|
809
818
|
},
|
|
810
819
|
transport: {
|
|
@@ -833,7 +842,7 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
833
842
|
name: "session_unlock",
|
|
834
843
|
title: "Unlock Senti Files",
|
|
835
844
|
description:
|
|
836
|
-
"Release session-scoped file
|
|
845
|
+
"Release authoritative session-scoped file leases held by an agent without writing to the session transcript.",
|
|
837
846
|
input_schema: {
|
|
838
847
|
type: "object",
|
|
839
848
|
additionalProperties: false,
|
|
@@ -849,8 +858,16 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
849
858
|
},
|
|
850
859
|
reason: { type: "string" },
|
|
851
860
|
force: { type: "boolean" },
|
|
852
|
-
syncRemote: {
|
|
853
|
-
|
|
861
|
+
syncRemote: {
|
|
862
|
+
type: "boolean",
|
|
863
|
+
description:
|
|
864
|
+
"Deprecated compatibility input; ignored because the authenticated API is always authoritative.",
|
|
865
|
+
},
|
|
866
|
+
awaitRemoteSync: {
|
|
867
|
+
type: "boolean",
|
|
868
|
+
description:
|
|
869
|
+
"Deprecated compatibility input; ignored because the authenticated API is always authoritative.",
|
|
870
|
+
},
|
|
854
871
|
},
|
|
855
872
|
},
|
|
856
873
|
transport: {
|
|
@@ -879,7 +896,7 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
879
896
|
name: "session_locks",
|
|
880
897
|
title: "List Senti File Locks",
|
|
881
898
|
description:
|
|
882
|
-
"List active file
|
|
899
|
+
"List active authoritative file leases for a session.",
|
|
883
900
|
input_schema: {
|
|
884
901
|
type: "object",
|
|
885
902
|
additionalProperties: false,
|
|
@@ -7,16 +7,19 @@ import {
|
|
|
7
7
|
buildCliCommandMcpTools,
|
|
8
8
|
createCliCommandMcpToolHandlers,
|
|
9
9
|
} from "./cli-command-tools.js";
|
|
10
|
-
import { appendToStream } from "../session/stream.js";
|
|
10
|
+
import { appendToStream, readStream } from "../session/stream.js";
|
|
11
11
|
import { listFileLocks, lockFile, unlockFile } from "../session/file-locks.js";
|
|
12
12
|
import { eventMatchesAgent } from "../session/listener.js";
|
|
13
13
|
import { isSessionControlEvent } from "../session/control-events.js";
|
|
14
|
+
import { buildObservations } from "../session/recall/observations.js";
|
|
15
|
+
import { createMemoryService } from "../engram/index.js";
|
|
14
16
|
import {
|
|
15
17
|
createSessionMessageAction,
|
|
16
18
|
listSessionMessageActions,
|
|
17
19
|
pollSessionEvents,
|
|
18
20
|
pollSessionEventsBefore,
|
|
19
21
|
syncSessionEventToApi,
|
|
22
|
+
updateSessionReadCursor,
|
|
20
23
|
} from "../session/sync.js";
|
|
21
24
|
|
|
22
25
|
export const SESSION_MCP_SERVER_NAME = "sentinelayer-session-mcp";
|
|
@@ -185,6 +188,27 @@ function summarizeSessionEvent(event = {}) {
|
|
|
185
188
|
});
|
|
186
189
|
}
|
|
187
190
|
|
|
191
|
+
function latestReadCursorTarget(events = [], fallbackCursor = "") {
|
|
192
|
+
let targetSequenceId = null;
|
|
193
|
+
let targetCursor = normalizeString(fallbackCursor);
|
|
194
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
195
|
+
const sequenceId = normalizePositiveInteger(
|
|
196
|
+
event?.sequenceId ?? event?.sequence_id,
|
|
197
|
+
null,
|
|
198
|
+
);
|
|
199
|
+
if (
|
|
200
|
+
sequenceId &&
|
|
201
|
+
(targetSequenceId === null || sequenceId >= targetSequenceId)
|
|
202
|
+
) {
|
|
203
|
+
targetSequenceId = sequenceId;
|
|
204
|
+
targetCursor = normalizeString(event?.cursor) || targetCursor;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return targetSequenceId || targetCursor
|
|
208
|
+
? { targetSequenceId, targetCursor }
|
|
209
|
+
: null;
|
|
210
|
+
}
|
|
211
|
+
|
|
188
212
|
function sessionEventMatchesClientMessageId(event, clientMessageId) {
|
|
189
213
|
const normalizedClientMessageId = normalizeString(clientMessageId);
|
|
190
214
|
if (!normalizedClientMessageId || !isPlainObject(event)) {
|
|
@@ -368,7 +392,9 @@ async function confirmSessionEventVisible(
|
|
|
368
392
|
}
|
|
369
393
|
lastReason = "not_visible";
|
|
370
394
|
const nextCursor = normalizeString(result.cursor);
|
|
371
|
-
|
|
395
|
+
const scanExhausted = result.scanExhausted ?? result.scan_exhausted;
|
|
396
|
+
const emptyScanCanContinue = !events.length && scanExhausted === false;
|
|
397
|
+
if (!nextCursor || nextCursor === pageCursor || (!events.length && !emptyScanCanContinue)) {
|
|
372
398
|
break;
|
|
373
399
|
}
|
|
374
400
|
pageCursor = nextCursor;
|
|
@@ -514,7 +540,9 @@ async function readSessionHistoryWindow({
|
|
|
514
540
|
events.push(...materialEvents);
|
|
515
541
|
const nextCursor = normalizeString(result.cursor) || pageCursor;
|
|
516
542
|
responseCursor = nextCursor || responseCursor;
|
|
517
|
-
|
|
543
|
+
const scanExhausted = result.scanExhausted ?? result.scan_exhausted;
|
|
544
|
+
const emptyScanCanContinue = !rawEvents.length && scanExhausted === false;
|
|
545
|
+
if (!nextCursor || nextCursor === pageCursor || (!rawEvents.length && !emptyScanCanContinue)) break;
|
|
518
546
|
pageCursor = nextCursor;
|
|
519
547
|
continue;
|
|
520
548
|
}
|
|
@@ -836,6 +864,7 @@ export function createSessionMcpToolHandlers({
|
|
|
836
864
|
pollSessionEventsBeforeFn = pollSessionEventsBefore,
|
|
837
865
|
listSessionMessageActionsFn = listSessionMessageActions,
|
|
838
866
|
createSessionMessageActionFn = createSessionMessageAction,
|
|
867
|
+
updateSessionReadCursorFn = updateSessionReadCursor,
|
|
839
868
|
lockFileFn = lockFile,
|
|
840
869
|
unlockFileFn = unlockFile,
|
|
841
870
|
listFileLocksFn = listFileLocks,
|
|
@@ -845,6 +874,29 @@ export function createSessionMcpToolHandlers({
|
|
|
845
874
|
uuidFn = randomUUID,
|
|
846
875
|
now = () => new Date().toISOString(),
|
|
847
876
|
} = {}) {
|
|
877
|
+
// ENGRAM §2 (Memory-as-a-Service): session is namespace #1. The SL-specific
|
|
878
|
+
// session adapter maps a session's events to the generic observation model,
|
|
879
|
+
// which keeps the detachable engram core (src/engram/*) free of any
|
|
880
|
+
// session-runtime import. Generic namespaces (project:/org:/ns:) prove the
|
|
881
|
+
// engine is sellable on its own.
|
|
882
|
+
const memoryService = createMemoryService({
|
|
883
|
+
storeRoot: targetPath,
|
|
884
|
+
adapters: {
|
|
885
|
+
session: async (sessionId) => {
|
|
886
|
+
const events = await readStream(sessionId, { targetPath, tail: 0 }).catch(() => []);
|
|
887
|
+
return buildObservations(events, { sessionId }).observations;
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
});
|
|
891
|
+
const memoryCaller = (input = {}) => {
|
|
892
|
+
const agentId = normalizeString(input.agentId || input.agent_id) || "cli-user";
|
|
893
|
+
return {
|
|
894
|
+
id: agentId,
|
|
895
|
+
kind: agentId.startsWith("guest") ? "guest" : "agent",
|
|
896
|
+
// AIdenID verification is external (§2.1); P0 writes are non-authoritative.
|
|
897
|
+
verified: false,
|
|
898
|
+
};
|
|
899
|
+
};
|
|
848
900
|
return {
|
|
849
901
|
async poll_inbox(input = {}) {
|
|
850
902
|
const sessionId = requireSessionId(input);
|
|
@@ -891,6 +943,14 @@ export function createSessionMcpToolHandlers({
|
|
|
891
943
|
const recentHumanActivity = actionResult?.ok
|
|
892
944
|
? recentHumanActivityFromActions(actionResult, { limit: actionLimit })
|
|
893
945
|
: [];
|
|
946
|
+
const readTarget = latestReadCursorTarget(result.events, result.cursor);
|
|
947
|
+
const readCursor = readTarget
|
|
948
|
+
? await updateSessionReadCursorFn(sessionId, {
|
|
949
|
+
targetPath,
|
|
950
|
+
...readTarget,
|
|
951
|
+
agentId,
|
|
952
|
+
})
|
|
953
|
+
: null;
|
|
894
954
|
|
|
895
955
|
return {
|
|
896
956
|
ok: true,
|
|
@@ -902,6 +962,7 @@ export function createSessionMcpToolHandlers({
|
|
|
902
962
|
inboxCount: events.length,
|
|
903
963
|
recentHumanActivityCount: recentHumanActivity.length,
|
|
904
964
|
recentHumanActivity,
|
|
965
|
+
readCursor,
|
|
905
966
|
actionProjection: actionResult
|
|
906
967
|
? {
|
|
907
968
|
ok: Boolean(actionResult.ok),
|
|
@@ -966,6 +1027,21 @@ export function createSessionMcpToolHandlers({
|
|
|
966
1027
|
const recentHumanActivity = actionResult?.ok
|
|
967
1028
|
? recentHumanActivityFromActions(actionResult, { limit: actionLimit })
|
|
968
1029
|
: [];
|
|
1030
|
+
const readAgentId = normalizeAgentId(
|
|
1031
|
+
input.agentId ||
|
|
1032
|
+
input.agent_id ||
|
|
1033
|
+
input.agent ||
|
|
1034
|
+
process.env.SENTINELAYER_AGENT_ID,
|
|
1035
|
+
"",
|
|
1036
|
+
);
|
|
1037
|
+
const readTarget = latestReadCursorTarget(events, result.cursor);
|
|
1038
|
+
const readCursor = readTarget
|
|
1039
|
+
? await updateSessionReadCursorFn(sessionId, {
|
|
1040
|
+
targetPath,
|
|
1041
|
+
...readTarget,
|
|
1042
|
+
agentId: readAgentId,
|
|
1043
|
+
})
|
|
1044
|
+
: null;
|
|
969
1045
|
|
|
970
1046
|
return {
|
|
971
1047
|
ok: true,
|
|
@@ -979,6 +1055,7 @@ export function createSessionMcpToolHandlers({
|
|
|
979
1055
|
pageCount: normalizePositiveInteger(result.pages, 0),
|
|
980
1056
|
recentHumanActivityCount: recentHumanActivity.length,
|
|
981
1057
|
recentHumanActivity,
|
|
1058
|
+
readCursor,
|
|
982
1059
|
actionProjection: actionResult
|
|
983
1060
|
? {
|
|
984
1061
|
ok: Boolean(actionResult.ok),
|
|
@@ -1061,8 +1138,6 @@ export function createSessionMcpToolHandlers({
|
|
|
1061
1138
|
intent,
|
|
1062
1139
|
ttlSeconds,
|
|
1063
1140
|
targetPath,
|
|
1064
|
-
syncRemote: input.syncRemote !== false && input.sync_remote !== false,
|
|
1065
|
-
awaitRemoteSync: input.awaitRemoteSync !== false && input.await_remote_sync !== false,
|
|
1066
1141
|
});
|
|
1067
1142
|
results.push({
|
|
1068
1143
|
file: normalizeString(result?.file) || file,
|
|
@@ -1094,10 +1169,7 @@ export function createSessionMcpToolHandlers({
|
|
|
1094
1169
|
for (const file of files) {
|
|
1095
1170
|
const result = await unlockFileFn(sessionId, agentId, file, {
|
|
1096
1171
|
reason,
|
|
1097
|
-
force: false,
|
|
1098
1172
|
targetPath,
|
|
1099
|
-
syncRemote: input.syncRemote !== false && input.sync_remote !== false,
|
|
1100
|
-
awaitRemoteSync: input.awaitRemoteSync !== false && input.await_remote_sync !== false,
|
|
1101
1173
|
});
|
|
1102
1174
|
results.push({
|
|
1103
1175
|
file: normalizeString(result?.file) || file,
|
|
@@ -1170,15 +1242,113 @@ export function createSessionMcpToolHandlers({
|
|
|
1170
1242
|
sleepFn,
|
|
1171
1243
|
});
|
|
1172
1244
|
},
|
|
1245
|
+
async "memory.write"(input = {}) {
|
|
1246
|
+
return memoryService.tools.write({
|
|
1247
|
+
scope: input.scope,
|
|
1248
|
+
items: input.items,
|
|
1249
|
+
caller: memoryCaller(input),
|
|
1250
|
+
});
|
|
1251
|
+
},
|
|
1252
|
+
async "memory.recall"(input = {}) {
|
|
1253
|
+
return memoryService.tools.recall({
|
|
1254
|
+
scope: input.scope,
|
|
1255
|
+
query: input.query,
|
|
1256
|
+
k: input.k,
|
|
1257
|
+
role: input.role,
|
|
1258
|
+
caller: memoryCaller(input),
|
|
1259
|
+
});
|
|
1260
|
+
},
|
|
1261
|
+
async "memory.summarize"(input = {}) {
|
|
1262
|
+
return memoryService.tools.summarize({
|
|
1263
|
+
scope: input.scope,
|
|
1264
|
+
focus: input.focus,
|
|
1265
|
+
k: input.k,
|
|
1266
|
+
caller: memoryCaller(input),
|
|
1267
|
+
});
|
|
1268
|
+
},
|
|
1173
1269
|
};
|
|
1174
1270
|
}
|
|
1175
1271
|
|
|
1176
1272
|
export const SESSION_MCP_TOOLS = Object.freeze([
|
|
1273
|
+
{
|
|
1274
|
+
name: "memory.write",
|
|
1275
|
+
title: "ENGRAM memory.write",
|
|
1276
|
+
description:
|
|
1277
|
+
"Remember facts/decisions/docs/snippets into a scoped namespace. Idempotent (content-hash dedup); every write is trust-sealed (verified -> authoritative; unverified/guest -> retrievable-but-marked, never authoritative).",
|
|
1278
|
+
inputSchema: {
|
|
1279
|
+
type: "object",
|
|
1280
|
+
additionalProperties: false,
|
|
1281
|
+
required: ["scope", "items"],
|
|
1282
|
+
properties: {
|
|
1283
|
+
scope: {
|
|
1284
|
+
type: "string",
|
|
1285
|
+
minLength: 1,
|
|
1286
|
+
description: "Namespace scope: session:<id> | project:<name> | org:<name> | agent:<id> | ns:<name>.",
|
|
1287
|
+
},
|
|
1288
|
+
items: {
|
|
1289
|
+
type: "array",
|
|
1290
|
+
minItems: 1,
|
|
1291
|
+
maxItems: MAX_TOOL_LIMIT,
|
|
1292
|
+
items: {
|
|
1293
|
+
type: "object",
|
|
1294
|
+
required: ["text"],
|
|
1295
|
+
properties: {
|
|
1296
|
+
text: { type: "string", minLength: 1, maxLength: MAX_MESSAGE_CHARS },
|
|
1297
|
+
kind: { type: "string" },
|
|
1298
|
+
topics: { type: "array", items: { type: "string" } },
|
|
1299
|
+
files: { type: "array", items: { type: "string" } },
|
|
1300
|
+
mentions: { type: "array", items: { type: "string" } },
|
|
1301
|
+
sealed: { type: "boolean" },
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
},
|
|
1305
|
+
agentId: { type: "string" },
|
|
1306
|
+
},
|
|
1307
|
+
},
|
|
1308
|
+
security: { scopes: ["memory:write"], requires_human_approval: false, kill_switch: "enabled" },
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
name: "memory.recall",
|
|
1312
|
+
title: "ENGRAM memory.recall",
|
|
1313
|
+
description:
|
|
1314
|
+
"Recall the top-K relevant memories from a scoped namespace (dense int8 exact-scan + BM25 + RRF + bounded graph diffusion + ACT-R), each with a one-line provenance path. Deterministic + auditable.",
|
|
1315
|
+
inputSchema: {
|
|
1316
|
+
type: "object",
|
|
1317
|
+
additionalProperties: false,
|
|
1318
|
+
required: ["scope", "query"],
|
|
1319
|
+
properties: {
|
|
1320
|
+
scope: { type: "string", minLength: 1 },
|
|
1321
|
+
query: { type: "string", minLength: 2 },
|
|
1322
|
+
k: { type: "integer", minimum: 1, maximum: 50, default: 12 },
|
|
1323
|
+
role: { type: "string" },
|
|
1324
|
+
agentId: { type: "string" },
|
|
1325
|
+
},
|
|
1326
|
+
},
|
|
1327
|
+
security: { scopes: ["memory:read"], durable_receipt_required: true, requires_human_approval: false, kill_switch: "enabled" },
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
name: "memory.summarize",
|
|
1331
|
+
title: "ENGRAM memory.summarize",
|
|
1332
|
+
description:
|
|
1333
|
+
"Summarize a namespace focused on a topic: recall DETERMINISTICALLY selects the relevant subset, then a renderer GENERATES prose over ONLY that subset (never a model in selection).",
|
|
1334
|
+
inputSchema: {
|
|
1335
|
+
type: "object",
|
|
1336
|
+
additionalProperties: false,
|
|
1337
|
+
required: ["scope", "focus"],
|
|
1338
|
+
properties: {
|
|
1339
|
+
scope: { type: "string", minLength: 1 },
|
|
1340
|
+
focus: { type: "string", minLength: 2 },
|
|
1341
|
+
k: { type: "integer", minimum: 1, maximum: 50, default: 12 },
|
|
1342
|
+
agentId: { type: "string" },
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
security: { scopes: ["memory:read"], durable_receipt_required: true, requires_human_approval: false, kill_switch: "enabled" },
|
|
1346
|
+
},
|
|
1177
1347
|
{
|
|
1178
1348
|
name: "poll_inbox",
|
|
1179
1349
|
title: "Poll Senti Inbox",
|
|
1180
1350
|
description:
|
|
1181
|
-
"Poll durable SentinelLayer session events after an optional cursor
|
|
1351
|
+
"Poll durable SentinelLayer session events after an optional cursor, return only events addressed or visible to the agent, and advance one monotonic read cursor.",
|
|
1182
1352
|
inputSchema: {
|
|
1183
1353
|
type: "object",
|
|
1184
1354
|
additionalProperties: false,
|
|
@@ -1200,13 +1370,14 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1200
1370
|
name: "read_history",
|
|
1201
1371
|
title: "Read Senti History",
|
|
1202
1372
|
description:
|
|
1203
|
-
"Hydrate a bounded recent, older, or after-cursor session transcript window without
|
|
1373
|
+
"Hydrate a bounded recent, older, or after-cursor session transcript window and advance one monotonic read cursor without appending view events.",
|
|
1204
1374
|
inputSchema: {
|
|
1205
1375
|
type: "object",
|
|
1206
1376
|
additionalProperties: false,
|
|
1207
1377
|
required: ["sessionId"],
|
|
1208
1378
|
properties: {
|
|
1209
1379
|
sessionId: { type: "string", minLength: 1 },
|
|
1380
|
+
agentId: { type: "string", minLength: 1 },
|
|
1210
1381
|
cursor: { type: "string" },
|
|
1211
1382
|
beforeSequence: { type: "integer", minimum: 1 },
|
|
1212
1383
|
limit: { type: "integer", minimum: 1, maximum: MAX_TOOL_LIMIT, default: DEFAULT_TOOL_LIMIT },
|
|
@@ -1248,7 +1419,7 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1248
1419
|
name: "session_action",
|
|
1249
1420
|
title: "Record Senti Session Action",
|
|
1250
1421
|
description:
|
|
1251
|
-
"Record a low-noise message action such as ack, working_on, disregard,
|
|
1422
|
+
"Record a low-noise message action such as ack, working_on, disregard, like, dislike, or reply; view advances the monotonic read cursor instead of appending an action.",
|
|
1252
1423
|
inputSchema: {
|
|
1253
1424
|
type: "object",
|
|
1254
1425
|
additionalProperties: false,
|
|
@@ -1315,7 +1486,7 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1315
1486
|
name: "session_lock",
|
|
1316
1487
|
title: "Lock Senti Files",
|
|
1317
1488
|
description:
|
|
1318
|
-
"Claim session-scoped file
|
|
1489
|
+
"Claim session-scoped file leases through the authoritative SentinelLayer API before editing. Lease lifecycle operations never create session events.",
|
|
1319
1490
|
inputSchema: {
|
|
1320
1491
|
type: "object",
|
|
1321
1492
|
additionalProperties: false,
|
|
@@ -1331,8 +1502,18 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1331
1502
|
},
|
|
1332
1503
|
intent: { type: "string" },
|
|
1333
1504
|
ttlSeconds: { type: "integer", minimum: 1, default: 300 },
|
|
1334
|
-
syncRemote: {
|
|
1335
|
-
|
|
1505
|
+
syncRemote: {
|
|
1506
|
+
type: "boolean",
|
|
1507
|
+
default: true,
|
|
1508
|
+
description:
|
|
1509
|
+
"Deprecated compatibility input; ignored because the authenticated API is always authoritative.",
|
|
1510
|
+
},
|
|
1511
|
+
awaitRemoteSync: {
|
|
1512
|
+
type: "boolean",
|
|
1513
|
+
default: true,
|
|
1514
|
+
description:
|
|
1515
|
+
"Deprecated compatibility input; ignored because the authenticated API is always authoritative.",
|
|
1516
|
+
},
|
|
1336
1517
|
},
|
|
1337
1518
|
},
|
|
1338
1519
|
},
|
|
@@ -1340,7 +1521,7 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1340
1521
|
name: "session_unlock",
|
|
1341
1522
|
title: "Unlock Senti Files",
|
|
1342
1523
|
description:
|
|
1343
|
-
"Release session-scoped file
|
|
1524
|
+
"Release authoritative session-scoped file leases held by an agent without writing to the session transcript.",
|
|
1344
1525
|
inputSchema: {
|
|
1345
1526
|
type: "object",
|
|
1346
1527
|
additionalProperties: false,
|
|
@@ -1355,8 +1536,18 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1355
1536
|
],
|
|
1356
1537
|
},
|
|
1357
1538
|
reason: { type: "string" },
|
|
1358
|
-
syncRemote: {
|
|
1359
|
-
|
|
1539
|
+
syncRemote: {
|
|
1540
|
+
type: "boolean",
|
|
1541
|
+
default: true,
|
|
1542
|
+
description:
|
|
1543
|
+
"Deprecated compatibility input; ignored because the authenticated API is always authoritative.",
|
|
1544
|
+
},
|
|
1545
|
+
awaitRemoteSync: {
|
|
1546
|
+
type: "boolean",
|
|
1547
|
+
default: true,
|
|
1548
|
+
description:
|
|
1549
|
+
"Deprecated compatibility input; ignored because the authenticated API is always authoritative.",
|
|
1550
|
+
},
|
|
1360
1551
|
},
|
|
1361
1552
|
},
|
|
1362
1553
|
},
|
|
@@ -1364,7 +1555,7 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1364
1555
|
name: "session_locks",
|
|
1365
1556
|
title: "List Senti File Locks",
|
|
1366
1557
|
description:
|
|
1367
|
-
"List active file
|
|
1558
|
+
"List active authoritative file leases for a session.",
|
|
1368
1559
|
inputSchema: {
|
|
1369
1560
|
type: "object",
|
|
1370
1561
|
additionalProperties: false,
|