sentinelayer-cli 0.34.6 → 0.35.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/package.json +1 -1
- package/src/commands/session.js +114 -17
- package/src/mcp/registry.js +56 -1
- package/src/mcp/session-stdio-server.js +205 -1
- package/src/scan/generator.js +19 -11
- package/src/session/coordination-guidance.js +1 -1
- package/src/session/first-message.js +1 -1
- package/src/session/listener-process.js +259 -0
- package/src/session/listener.js +3 -3
- package/src/session/listeners.js +8 -2
- package/src/session/pricing-ledger.js +208 -1
- package/src/session/recap.js +66 -2
- package/src/session/sync.js +28 -2
- package/src/session/transcript.js +27 -3
- package/src/session/usage-report.js +23 -4
- package/src/session/usage.js +2 -2
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -65,6 +65,12 @@ import {
|
|
|
65
65
|
updateSessionTitle,
|
|
66
66
|
} from "../session/store.js";
|
|
67
67
|
import { fetchSessionListeners, formatListenerLine } from "../session/listeners.js";
|
|
68
|
+
import {
|
|
69
|
+
getListenerProcessStatus,
|
|
70
|
+
removeListenerPidRecord,
|
|
71
|
+
requestListenerProcessStop,
|
|
72
|
+
writeListenerPidRecord,
|
|
73
|
+
} from "../session/listener-process.js";
|
|
68
74
|
import {
|
|
69
75
|
filterSessionMaterialEvents,
|
|
70
76
|
isSessionControlEvent,
|
|
@@ -109,6 +115,7 @@ import {
|
|
|
109
115
|
} from "../session/rotating-log.js";
|
|
110
116
|
import { buildSessionRecap } from "../session/recap.js";
|
|
111
117
|
import { computeTranscriptStats } from "../session/transcript.js";
|
|
118
|
+
import { buildSessionUsageLedger } from "../session/pricing-ledger.js";
|
|
112
119
|
import {
|
|
113
120
|
buildSessionUsageReport,
|
|
114
121
|
buildSessionUsageReportFromLedgerPayload,
|
|
@@ -150,6 +157,22 @@ function normalizeString(value) {
|
|
|
150
157
|
return String(value || "").trim();
|
|
151
158
|
}
|
|
152
159
|
|
|
160
|
+
// A wake hook hands remote-influenced event fields to a local host command
|
|
161
|
+
// (as SL_WAKE_* env vars). Bound + strip each value so a crafted event cannot
|
|
162
|
+
// inject control chars / newlines into the child environment or blow it up.
|
|
163
|
+
const WAKE_VALUE_MAX_LENGTH = 512;
|
|
164
|
+
function sanitizeWakeValue(value) {
|
|
165
|
+
// Strip control chars (incl. NUL / newline) by codepoint so a crafted remote
|
|
166
|
+
// event cannot inject into the child env; a codepoint check avoids putting a
|
|
167
|
+
// control-char regex literal in source.
|
|
168
|
+
let out = "";
|
|
169
|
+
for (const ch of normalizeString(value)) {
|
|
170
|
+
const code = ch.codePointAt(0);
|
|
171
|
+
out += code < 0x20 || code === 0x7f ? " " : ch;
|
|
172
|
+
}
|
|
173
|
+
return out.slice(0, WAKE_VALUE_MAX_LENGTH);
|
|
174
|
+
}
|
|
175
|
+
|
|
153
176
|
function messagePartsHaveContent(messageParts) {
|
|
154
177
|
const parts = Array.isArray(messageParts) ? messageParts : [messageParts];
|
|
155
178
|
return parts.some((part) => normalizeString(part));
|
|
@@ -2160,14 +2183,28 @@ export function createSessionWakeRunner({
|
|
|
2160
2183
|
return;
|
|
2161
2184
|
}
|
|
2162
2185
|
busy = true;
|
|
2186
|
+
const rawSequence = event?.sequenceId ?? event?.sequence_id ?? "";
|
|
2187
|
+
const sanitizedSequence = sanitizeWakeValue(rawSequence);
|
|
2163
2188
|
const env = {
|
|
2164
2189
|
...process.env,
|
|
2165
|
-
SL_WAKE_SESSION_ID:
|
|
2166
|
-
SL_WAKE_AGENT_ID:
|
|
2167
|
-
SL_WAKE_EVENT_TYPE:
|
|
2168
|
-
SL_WAKE_EVENT_CURSOR:
|
|
2169
|
-
SL_WAKE_EVENT_SEQUENCE:
|
|
2170
|
-
SL_WAKE_ACTOR_ID:
|
|
2190
|
+
SL_WAKE_SESSION_ID: sanitizeWakeValue(sessionId),
|
|
2191
|
+
SL_WAKE_AGENT_ID: sanitizeWakeValue(agentId),
|
|
2192
|
+
SL_WAKE_EVENT_TYPE: sanitizeWakeValue(event?.event),
|
|
2193
|
+
SL_WAKE_EVENT_CURSOR: sanitizeWakeValue(event?.cursor),
|
|
2194
|
+
SL_WAKE_EVENT_SEQUENCE: sanitizedSequence,
|
|
2195
|
+
SL_WAKE_ACTOR_ID: sanitizeWakeValue(event?.agent?.id || event?.agentId),
|
|
2196
|
+
};
|
|
2197
|
+
// Metadata-only wake envelope for the host command's stdin. Deliberately
|
|
2198
|
+
// EXCLUDES the event body/content so session message text / PII never
|
|
2199
|
+
// reaches an external process — the woken agent re-reads the room over its
|
|
2200
|
+
// own authenticated channel using this cursor/sequence.
|
|
2201
|
+
const wakePayload = {
|
|
2202
|
+
event: env.SL_WAKE_EVENT_TYPE,
|
|
2203
|
+
sessionId: env.SL_WAKE_SESSION_ID,
|
|
2204
|
+
agentId: env.SL_WAKE_AGENT_ID,
|
|
2205
|
+
cursor: env.SL_WAKE_EVENT_CURSOR,
|
|
2206
|
+
sequenceId: typeof rawSequence === "number" && Number.isFinite(rawSequence) ? rawSequence : sanitizedSequence || null,
|
|
2207
|
+
actorId: env.SL_WAKE_ACTOR_ID,
|
|
2171
2208
|
};
|
|
2172
2209
|
let child;
|
|
2173
2210
|
try {
|
|
@@ -2185,7 +2222,7 @@ export function createSessionWakeRunner({
|
|
|
2185
2222
|
});
|
|
2186
2223
|
try {
|
|
2187
2224
|
if (child && child.stdin) {
|
|
2188
|
-
child.stdin.write(JSON.stringify(
|
|
2225
|
+
child.stdin.write(JSON.stringify(wakePayload));
|
|
2189
2226
|
child.stdin.end();
|
|
2190
2227
|
}
|
|
2191
2228
|
} catch {
|
|
@@ -4507,9 +4544,17 @@ export function registerSessionCommand(program) {
|
|
|
4507
4544
|
.option("--from-now", "Advance the listen cursor to the latest durable event before polling")
|
|
4508
4545
|
.option("--replay", "Emit matching historical events on the first poll")
|
|
4509
4546
|
.option("--max-polls <n>", "Stop after N poll cycles (useful for tests and smoke checks)")
|
|
4547
|
+
.option(
|
|
4548
|
+
"--force",
|
|
4549
|
+
"Stop and replace an existing local listener process for this session/agent",
|
|
4550
|
+
)
|
|
4551
|
+
.option(
|
|
4552
|
+
"--allow-duplicate",
|
|
4553
|
+
"Allow multiple local listeners for the same session/agent (advanced; can duplicate wake hooks)",
|
|
4554
|
+
)
|
|
4510
4555
|
.option(
|
|
4511
4556
|
"--wake <command>",
|
|
4512
|
-
"Wake hook: run this shell command on each matched event (notify->resume bridge).
|
|
4557
|
+
"Wake hook: run this shell command on each matched event (notify->resume bridge). Metadata JSON is piped to stdin; sanitized SL_WAKE_* env vars are set.",
|
|
4513
4558
|
)
|
|
4514
4559
|
.option(
|
|
4515
4560
|
"--wake-host <name>",
|
|
@@ -4635,18 +4680,59 @@ export function registerSessionCommand(program) {
|
|
|
4635
4680
|
if (options.fromNow && options.since !== undefined) {
|
|
4636
4681
|
throw new Error("Use either --from-now or --since, not both.");
|
|
4637
4682
|
}
|
|
4638
|
-
const
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4683
|
+
const listenerId = `listener-${agentId}-${randomUUID()}`;
|
|
4684
|
+
const singletonEnabled = maxPolls === null && !options.allowDuplicate;
|
|
4685
|
+
let listenerPidRecordActive = false;
|
|
4686
|
+
if (singletonEnabled) {
|
|
4687
|
+
const existingListener = await getListenerProcessStatus(normalizedSessionId, agentId, {
|
|
4688
|
+
targetPath,
|
|
4689
|
+
});
|
|
4690
|
+
if (existingListener.running && !options.force) {
|
|
4691
|
+
throw new Error(
|
|
4692
|
+
`A local session listener is already running for session ${normalizedSessionId} as ${agentId} (pid ${existingListener.pid}). Use --force to take over or --allow-duplicate if you intentionally need more than one.`,
|
|
4693
|
+
);
|
|
4694
|
+
}
|
|
4695
|
+
if (existingListener.running && options.force) {
|
|
4696
|
+
const stopResult = await requestListenerProcessStop(existingListener.pid);
|
|
4697
|
+
if (!stopResult.stopped) {
|
|
4698
|
+
throw new Error(
|
|
4699
|
+
`Unable to stop existing listener pid ${existingListener.pid} for session ${normalizedSessionId} as ${agentId} (${stopResult.reason}). Stop it manually or use --allow-duplicate if parallel listeners are intentional.`,
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
await writeListenerPidRecord(normalizedSessionId, agentId, {
|
|
4704
|
+
targetPath,
|
|
4705
|
+
listenerId,
|
|
4706
|
+
transport: listenTransport,
|
|
4707
|
+
intervalSeconds,
|
|
4708
|
+
activeIntervalSeconds,
|
|
4709
|
+
presenceIntervalSeconds,
|
|
4710
|
+
logFile: listenLogPath,
|
|
4711
|
+
});
|
|
4712
|
+
listenerPidRecordActive = true;
|
|
4713
|
+
}
|
|
4714
|
+
let restoreConsoleLog = null;
|
|
4715
|
+
try {
|
|
4716
|
+
restoreConsoleLog = listenLogPath
|
|
4717
|
+
? installRotatingConsoleLog({
|
|
4718
|
+
logPath: listenLogPath,
|
|
4719
|
+
maxBytes: parsePositiveInteger(options.logMaxBytes, "log-max-bytes", DEFAULT_ROTATING_LOG_MAX_BYTES),
|
|
4720
|
+
maxFiles: parsePositiveInteger(options.logMaxFiles, "log-max-files", DEFAULT_ROTATING_LOG_MAX_FILES),
|
|
4721
|
+
tee: true,
|
|
4722
|
+
})
|
|
4723
|
+
: null;
|
|
4724
|
+
} catch (error) {
|
|
4725
|
+
if (listenerPidRecordActive) {
|
|
4726
|
+
await removeListenerPidRecord(normalizedSessionId, agentId, {
|
|
4727
|
+
targetPath,
|
|
4728
|
+
onlyForPid: process.pid,
|
|
4729
|
+
}).catch(() => {});
|
|
4730
|
+
}
|
|
4731
|
+
throw error;
|
|
4732
|
+
}
|
|
4646
4733
|
const ac = new AbortController();
|
|
4647
4734
|
const onSigint = () => ac.abort();
|
|
4648
4735
|
process.on("SIGINT", onSigint);
|
|
4649
|
-
const listenerId = `listener-${agentId}-${randomUUID()}`;
|
|
4650
4736
|
const durablePresenceEnabled = options.presence !== false;
|
|
4651
4737
|
const publishPresence = durablePresenceEnabled && canPublishListenerPresence(agentId);
|
|
4652
4738
|
const presenceIntervalMs = Math.max(1, presenceIntervalSeconds) * 1000;
|
|
@@ -4878,6 +4964,12 @@ export function registerSessionCommand(program) {
|
|
|
4878
4964
|
restoreConsoleLog();
|
|
4879
4965
|
}
|
|
4880
4966
|
process.removeListener("SIGINT", onSigint);
|
|
4967
|
+
if (listenerPidRecordActive) {
|
|
4968
|
+
await removeListenerPidRecord(normalizedSessionId, agentId, {
|
|
4969
|
+
targetPath,
|
|
4970
|
+
onlyForPid: process.pid,
|
|
4971
|
+
}).catch(() => {});
|
|
4972
|
+
}
|
|
4881
4973
|
}
|
|
4882
4974
|
});
|
|
4883
4975
|
|
|
@@ -6267,9 +6359,14 @@ export function registerSessionCommand(program) {
|
|
|
6267
6359
|
const hiddenControlEventCount =
|
|
6268
6360
|
dedupedEvents.length - transcriptBaseEvents.length + rawActionEvents.length - actionEvents.length;
|
|
6269
6361
|
const exportEvents = mergeSessionActionEvents(transcriptBaseEvents, actionEvents);
|
|
6362
|
+
const usageLedger = buildSessionUsageLedger(exportEvents, {
|
|
6363
|
+
sessionId: normalizedSessionId,
|
|
6364
|
+
includeEstimatedMessages: true,
|
|
6365
|
+
});
|
|
6270
6366
|
const stats = computeTranscriptStats({
|
|
6271
6367
|
sessionMeta: sessionPayload,
|
|
6272
6368
|
events: exportEvents,
|
|
6369
|
+
usageLedger,
|
|
6273
6370
|
});
|
|
6274
6371
|
const participants = buildSessionParticipants({
|
|
6275
6372
|
statsAgents: stats.agents,
|
package/src/mcp/registry.js
CHANGED
|
@@ -563,6 +563,47 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
563
563
|
server: "sentinelayer-session-mcp",
|
|
564
564
|
},
|
|
565
565
|
},
|
|
566
|
+
{
|
|
567
|
+
name: "read_history",
|
|
568
|
+
title: "Read Senti History",
|
|
569
|
+
description:
|
|
570
|
+
"Hydrate a bounded recent, older, or after-cursor SentinelLayer session transcript window without recipient filtering.",
|
|
571
|
+
input_schema: {
|
|
572
|
+
type: "object",
|
|
573
|
+
additionalProperties: false,
|
|
574
|
+
required: ["sessionId"],
|
|
575
|
+
properties: {
|
|
576
|
+
sessionId: { type: "string" },
|
|
577
|
+
cursor: { type: "string" },
|
|
578
|
+
beforeSequence: { type: "integer", minimum: 1 },
|
|
579
|
+
limit: { type: "integer", minimum: 1, maximum: 200 },
|
|
580
|
+
actionLimit: { type: "integer", minimum: 1, maximum: 200 },
|
|
581
|
+
includeActions: { type: "boolean" },
|
|
582
|
+
includeControlEvents: { type: "boolean" },
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
transport: {
|
|
586
|
+
type: "internal",
|
|
587
|
+
method: "POST",
|
|
588
|
+
url: "sentinelayer://session/read_history",
|
|
589
|
+
timeout_ms: 15000,
|
|
590
|
+
auth: { mode: "none" },
|
|
591
|
+
},
|
|
592
|
+
budgets: {
|
|
593
|
+
max_calls_per_run: 60,
|
|
594
|
+
max_runtime_ms: 15000,
|
|
595
|
+
},
|
|
596
|
+
security: {
|
|
597
|
+
requires_human_approval: false,
|
|
598
|
+
kill_switch: "enabled",
|
|
599
|
+
scopes: ["session:read"],
|
|
600
|
+
},
|
|
601
|
+
metadata: {
|
|
602
|
+
provider: "sentinelayer",
|
|
603
|
+
adapter: "sentinelayer-cli",
|
|
604
|
+
server: "sentinelayer-session-mcp",
|
|
605
|
+
},
|
|
606
|
+
},
|
|
566
607
|
{
|
|
567
608
|
name: "send_message",
|
|
568
609
|
title: "Send Senti Message",
|
|
@@ -985,6 +1026,13 @@ export function buildHostedSentiSessionConnectorContract({
|
|
|
985
1026
|
long_lived_cli_token_passthrough_allowed: false,
|
|
986
1027
|
},
|
|
987
1028
|
tools: [
|
|
1029
|
+
hostedSessionTool({
|
|
1030
|
+
toolName: "join_and_hydrate",
|
|
1031
|
+
operation: "join_and_hydrate",
|
|
1032
|
+
allowedUserArgs: ["limit", "actionLimit", "includeActions"],
|
|
1033
|
+
requiredScopes: ["session:read"],
|
|
1034
|
+
wakePayload: { policy: "bounded_redacted_window", may_include_message_content: true, max_events: 50 },
|
|
1035
|
+
}),
|
|
988
1036
|
hostedSessionTool({
|
|
989
1037
|
toolName: "poll_inbox",
|
|
990
1038
|
operation: "read_history",
|
|
@@ -992,6 +1040,13 @@ export function buildHostedSentiSessionConnectorContract({
|
|
|
992
1040
|
requiredScopes: ["session:read"],
|
|
993
1041
|
wakePayload: { policy: "bounded_redacted_window", may_include_message_content: true, max_events: 50 },
|
|
994
1042
|
}),
|
|
1043
|
+
hostedSessionTool({
|
|
1044
|
+
toolName: "read_history",
|
|
1045
|
+
operation: "read_history",
|
|
1046
|
+
allowedUserArgs: ["cursor", "beforeSequence", "limit", "actionLimit", "includeActions"],
|
|
1047
|
+
requiredScopes: ["session:read"],
|
|
1048
|
+
wakePayload: { policy: "bounded_redacted_window", may_include_message_content: true, max_events: 50 },
|
|
1049
|
+
}),
|
|
995
1050
|
hostedSessionTool({
|
|
996
1051
|
toolName: "send_message",
|
|
997
1052
|
operation: "send_message",
|
|
@@ -1279,7 +1334,7 @@ export function validateHostedSentiSessionConnectorContract(payload, { registryP
|
|
|
1279
1334
|
const registry = validateMcpToolRegistry(registryPayload);
|
|
1280
1335
|
const localToolNames = new Set(registry.tools.map((tool) => tool.name));
|
|
1281
1336
|
const missingLocalTools = parsed.tools
|
|
1282
|
-
.filter((tool) => tool.operation !== "subscribe_wake" && tool.operation !== "presence_renew" && tool.operation !== "runner_teardown")
|
|
1337
|
+
.filter((tool) => tool.operation !== "join_and_hydrate" && tool.operation !== "subscribe_wake" && tool.operation !== "presence_renew" && tool.operation !== "runner_teardown")
|
|
1283
1338
|
.map((tool) => tool.local_tool_name)
|
|
1284
1339
|
.filter((toolName) => !localToolNames.has(toolName));
|
|
1285
1340
|
if (missingLocalTools.length > 0) {
|
|
@@ -31,6 +31,8 @@ const SESSION_MCP_CONFIRM_PAGE_LIMIT = 200;
|
|
|
31
31
|
const SESSION_MCP_CONFIRM_MAX_PAGES = 10;
|
|
32
32
|
const SESSION_MCP_CONFIRM_TOTAL_TIMEOUT_MS = 10_000;
|
|
33
33
|
const SESSION_MCP_CONFIRM_REQUEST_TIMEOUT_MS = 2_000;
|
|
34
|
+
const SESSION_MCP_HISTORY_MAX_PAGES = 10;
|
|
35
|
+
const SESSION_MCP_HISTORY_MIN_RAW_PAGE_LIMIT = DEFAULT_TOOL_LIMIT;
|
|
34
36
|
const JSON_RPC_VERSION = "2.0";
|
|
35
37
|
const CONTENT_LENGTH_PREFIX = "content-length:";
|
|
36
38
|
const SESSION_MESSAGE_ACTION_TYPES = new Set([
|
|
@@ -444,6 +446,110 @@ function recentHumanActivityFromActions(remoteActions = null, { limit = DEFAULT_
|
|
|
444
446
|
: [];
|
|
445
447
|
}
|
|
446
448
|
|
|
449
|
+
function normalizeHistoryEvents(events = [], { includeControlEvents = false } = {}) {
|
|
450
|
+
return (Array.isArray(events) ? events : []).filter((event) => {
|
|
451
|
+
if (!includeControlEvents && isSessionControlEvent(event)) return false;
|
|
452
|
+
return true;
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function readSessionHistoryWindow({
|
|
457
|
+
sessionId,
|
|
458
|
+
targetPath,
|
|
459
|
+
cursor = null,
|
|
460
|
+
beforeSequence = null,
|
|
461
|
+
limit = DEFAULT_TOOL_LIMIT,
|
|
462
|
+
includeControlEvents = false,
|
|
463
|
+
forceCircuitProbe = false,
|
|
464
|
+
pollSessionEventsFn = pollSessionEvents,
|
|
465
|
+
pollSessionEventsBeforeFn = pollSessionEventsBefore,
|
|
466
|
+
} = {}) {
|
|
467
|
+
const source = cursor ? "after_cursor" : beforeSequence ? "before_sequence" : "latest_tail";
|
|
468
|
+
let pageCursor = cursor;
|
|
469
|
+
let pageBeforeSequence = beforeSequence;
|
|
470
|
+
let responseCursor = cursor;
|
|
471
|
+
let responseBeforeSequence = beforeSequence || null;
|
|
472
|
+
let rawEventCount = 0;
|
|
473
|
+
let pages = 0;
|
|
474
|
+
let events = [];
|
|
475
|
+
|
|
476
|
+
while (events.length < limit && pages < SESSION_MCP_HISTORY_MAX_PAGES) {
|
|
477
|
+
const remaining = Math.max(1, limit - events.length);
|
|
478
|
+
const pageLimit = Math.max(
|
|
479
|
+
remaining,
|
|
480
|
+
Math.min(MAX_TOOL_LIMIT, SESSION_MCP_HISTORY_MIN_RAW_PAGE_LIMIT),
|
|
481
|
+
);
|
|
482
|
+
const result = cursor
|
|
483
|
+
? await pollSessionEventsFn(sessionId, {
|
|
484
|
+
targetPath,
|
|
485
|
+
since: pageCursor,
|
|
486
|
+
limit: pageLimit,
|
|
487
|
+
forceCircuitProbe,
|
|
488
|
+
})
|
|
489
|
+
: await pollSessionEventsBeforeFn(sessionId, {
|
|
490
|
+
targetPath,
|
|
491
|
+
beforeSequence: pageBeforeSequence,
|
|
492
|
+
limit: pageLimit,
|
|
493
|
+
forceCircuitProbe,
|
|
494
|
+
});
|
|
495
|
+
if (!result?.ok) {
|
|
496
|
+
if (pages > 0) break;
|
|
497
|
+
return {
|
|
498
|
+
ok: false,
|
|
499
|
+
reason: result?.reason || "read_history_failed",
|
|
500
|
+
source,
|
|
501
|
+
cursor: responseCursor,
|
|
502
|
+
beforeSequence: responseBeforeSequence,
|
|
503
|
+
events: [],
|
|
504
|
+
eventCount: 0,
|
|
505
|
+
pages,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
pages += 1;
|
|
510
|
+
const rawEvents = Array.isArray(result.events) ? result.events : [];
|
|
511
|
+
rawEventCount += rawEvents.length;
|
|
512
|
+
const materialEvents = normalizeHistoryEvents(rawEvents, { includeControlEvents });
|
|
513
|
+
if (cursor) {
|
|
514
|
+
events.push(...materialEvents);
|
|
515
|
+
const nextCursor = normalizeString(result.cursor) || pageCursor;
|
|
516
|
+
responseCursor = nextCursor || responseCursor;
|
|
517
|
+
if (!rawEvents.length || !nextCursor || nextCursor === pageCursor) break;
|
|
518
|
+
pageCursor = nextCursor;
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
events = [...materialEvents, ...events];
|
|
523
|
+
if (pages === 1) {
|
|
524
|
+
responseCursor = normalizeString(result.cursor) || responseCursor;
|
|
525
|
+
}
|
|
526
|
+
const nextBeforeSequence = normalizePositiveInteger(result.beforeSequence || result.before_sequence, null);
|
|
527
|
+
responseBeforeSequence = nextBeforeSequence;
|
|
528
|
+
if (!rawEvents.length || !nextBeforeSequence || nextBeforeSequence === pageBeforeSequence) break;
|
|
529
|
+
pageBeforeSequence = nextBeforeSequence;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const windowEvents = cursor ? events.slice(0, limit) : events.slice(-limit);
|
|
533
|
+
const firstWindowEvent = windowEvents[0] || null;
|
|
534
|
+
const lastWindowEvent = windowEvents[windowEvents.length - 1] || null;
|
|
535
|
+
const windowCursor = normalizeString(lastWindowEvent?.cursor);
|
|
536
|
+
const windowBeforeSequence = normalizePositiveInteger(
|
|
537
|
+
firstWindowEvent?.sequenceId ?? firstWindowEvent?.sequence_id,
|
|
538
|
+
null,
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
ok: true,
|
|
543
|
+
reason: "",
|
|
544
|
+
source,
|
|
545
|
+
cursor: windowCursor || responseCursor,
|
|
546
|
+
beforeSequence: cursor ? null : windowBeforeSequence || responseBeforeSequence,
|
|
547
|
+
events: windowEvents,
|
|
548
|
+
eventCount: rawEventCount,
|
|
549
|
+
pages,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
447
553
|
function requireSessionId(input = {}) {
|
|
448
554
|
const sessionId = normalizeString(input.sessionId || input.session_id || input.session);
|
|
449
555
|
if (!sessionId) {
|
|
@@ -807,6 +913,83 @@ export function createSessionMcpToolHandlers({
|
|
|
807
913
|
};
|
|
808
914
|
},
|
|
809
915
|
|
|
916
|
+
async read_history(input = {}) {
|
|
917
|
+
const sessionId = requireSessionId(input);
|
|
918
|
+
const cursor = normalizeString(input.cursor || input.after || input.since) || null;
|
|
919
|
+
const beforeSequence = normalizeOptionalPositiveInteger(
|
|
920
|
+
input.beforeSequence || input.before_sequence,
|
|
921
|
+
"beforeSequence",
|
|
922
|
+
);
|
|
923
|
+
if (cursor && beforeSequence) {
|
|
924
|
+
throw new Error("Use either cursor/after/since or beforeSequence, not both.");
|
|
925
|
+
}
|
|
926
|
+
const limit = normalizeLimit(input.limit || input.tail || input.maxEvents || input.max_events);
|
|
927
|
+
const actionLimit = normalizeLimit(input.actionLimit || input.action_limit || limit);
|
|
928
|
+
const includeActions = input.includeActions !== false && input.include_actions !== false;
|
|
929
|
+
const includeControlEvents = Boolean(input.includeControlEvents || input.include_control_events);
|
|
930
|
+
const includeRaw = Boolean(input.includeRaw || input.include_raw);
|
|
931
|
+
const forceCircuitProbe = input.forceCircuitProbe !== false && input.force_circuit_probe !== false;
|
|
932
|
+
|
|
933
|
+
const result = await readSessionHistoryWindow({
|
|
934
|
+
sessionId,
|
|
935
|
+
targetPath,
|
|
936
|
+
cursor,
|
|
937
|
+
beforeSequence,
|
|
938
|
+
limit,
|
|
939
|
+
includeControlEvents,
|
|
940
|
+
forceCircuitProbe,
|
|
941
|
+
pollSessionEventsFn,
|
|
942
|
+
pollSessionEventsBeforeFn,
|
|
943
|
+
});
|
|
944
|
+
if (!result?.ok) {
|
|
945
|
+
return {
|
|
946
|
+
ok: false,
|
|
947
|
+
reason: result?.reason || "read_history_failed",
|
|
948
|
+
sessionId,
|
|
949
|
+
source: result?.source || (cursor ? "after_cursor" : beforeSequence ? "before_sequence" : "latest_tail"),
|
|
950
|
+
cursor,
|
|
951
|
+
beforeSequence: beforeSequence || null,
|
|
952
|
+
events: [],
|
|
953
|
+
eventCount: 0,
|
|
954
|
+
materialEventCount: 0,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const events = Array.isArray(result.events) ? result.events : [];
|
|
959
|
+
const actionResult = includeActions
|
|
960
|
+
? await listSessionMessageActionsFn(sessionId, {
|
|
961
|
+
targetPath,
|
|
962
|
+
limit: actionLimit,
|
|
963
|
+
forceCircuitProbe,
|
|
964
|
+
})
|
|
965
|
+
: null;
|
|
966
|
+
const recentHumanActivity = actionResult?.ok
|
|
967
|
+
? recentHumanActivityFromActions(actionResult, { limit: actionLimit })
|
|
968
|
+
: [];
|
|
969
|
+
|
|
970
|
+
return {
|
|
971
|
+
ok: true,
|
|
972
|
+
reason: "",
|
|
973
|
+
sessionId,
|
|
974
|
+
source: result.source,
|
|
975
|
+
cursor: normalizeString(result.cursor) || cursor,
|
|
976
|
+
beforeSequence: normalizePositiveInteger(result.beforeSequence || result.before_sequence, null),
|
|
977
|
+
eventCount: normalizePositiveInteger(result.eventCount, 0),
|
|
978
|
+
materialEventCount: events.length,
|
|
979
|
+
pageCount: normalizePositiveInteger(result.pages, 0),
|
|
980
|
+
recentHumanActivityCount: recentHumanActivity.length,
|
|
981
|
+
recentHumanActivity,
|
|
982
|
+
actionProjection: actionResult
|
|
983
|
+
? {
|
|
984
|
+
ok: Boolean(actionResult.ok),
|
|
985
|
+
reason: actionResult.reason || "",
|
|
986
|
+
count: Array.isArray(actionResult.actions) ? actionResult.actions.length : 0,
|
|
987
|
+
}
|
|
988
|
+
: null,
|
|
989
|
+
events: includeRaw ? events : events.map((event) => summarizeSessionEvent(event)),
|
|
990
|
+
};
|
|
991
|
+
},
|
|
992
|
+
|
|
810
993
|
async send_message(input = {}) {
|
|
811
994
|
const sessionId = requireSessionId(input);
|
|
812
995
|
const agentId = requireAgentId(input);
|
|
@@ -1013,6 +1196,27 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1013
1196
|
},
|
|
1014
1197
|
},
|
|
1015
1198
|
},
|
|
1199
|
+
{
|
|
1200
|
+
name: "read_history",
|
|
1201
|
+
title: "Read Senti History",
|
|
1202
|
+
description:
|
|
1203
|
+
"Hydrate a bounded recent, older, or after-cursor session transcript window without recipient filtering.",
|
|
1204
|
+
inputSchema: {
|
|
1205
|
+
type: "object",
|
|
1206
|
+
additionalProperties: false,
|
|
1207
|
+
required: ["sessionId"],
|
|
1208
|
+
properties: {
|
|
1209
|
+
sessionId: { type: "string", minLength: 1 },
|
|
1210
|
+
cursor: { type: "string" },
|
|
1211
|
+
beforeSequence: { type: "integer", minimum: 1 },
|
|
1212
|
+
limit: { type: "integer", minimum: 1, maximum: MAX_TOOL_LIMIT, default: DEFAULT_TOOL_LIMIT },
|
|
1213
|
+
actionLimit: { type: "integer", minimum: 1, maximum: MAX_TOOL_LIMIT, default: DEFAULT_TOOL_LIMIT },
|
|
1214
|
+
includeActions: { type: "boolean", default: true },
|
|
1215
|
+
includeControlEvents: { type: "boolean", default: false },
|
|
1216
|
+
includeRaw: { type: "boolean", default: false },
|
|
1217
|
+
},
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1016
1220
|
{
|
|
1017
1221
|
name: "send_message",
|
|
1018
1222
|
title: "Send Senti Message",
|
|
@@ -1285,7 +1489,7 @@ export async function handleMcpJsonRpcMessage(
|
|
|
1285
1489
|
version: "0.20.0",
|
|
1286
1490
|
},
|
|
1287
1491
|
instructions:
|
|
1288
|
-
"Use native Senti tools for coordination actions and generated sl.* bridge tools for the rest of SentinelLayer CLI.
|
|
1492
|
+
"Use native Senti tools for coordination actions and generated sl.* bridge tools for the rest of SentinelLayer CLI. Use poll_inbox for addressed wake-style delivery, read_history for recent/older transcript grounding, session_action/session_react/session_reply/session_lock/session_unlock/session_locks for low-noise session work, send_message for durable posts, and generated command tools such as sl.auth.status or sl.review.scan for ordinary CLI commands.",
|
|
1289
1493
|
});
|
|
1290
1494
|
}
|
|
1291
1495
|
|
package/src/scan/generator.js
CHANGED
|
@@ -227,16 +227,20 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
227
227
|
with: {
|
|
228
228
|
github_token: "${{ github.token }}",
|
|
229
229
|
openai_api_key: "${{ secrets.OPENAI_API_KEY }}",
|
|
230
|
-
google_api_key:
|
|
231
|
-
|
|
230
|
+
google_api_key:
|
|
231
|
+
"${{ secrets.GOOGLE_GEMINI_API_KEY != '' && secrets.GOOGLE_GEMINI_API_KEY || secrets.GOOGLE_API_KEY }}",
|
|
232
|
+
llm_provider:
|
|
233
|
+
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'google' || 'openai' }}",
|
|
232
234
|
sentinelayer_token: `\${{ secrets.${normalizedSecret} }}`,
|
|
233
|
-
sentinelayer_managed_llm: `\${{ secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
|
|
235
|
+
sentinelayer_managed_llm: `\${{ secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
|
|
234
236
|
scan_mode: profile.scanMode || "deep",
|
|
235
237
|
severity_gate: profile.severityGate || "P1",
|
|
236
|
-
model:
|
|
238
|
+
model:
|
|
239
|
+
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-5.3-codex' }}",
|
|
237
240
|
codex_model: "gpt-5.3-codex",
|
|
238
|
-
model_fallback:
|
|
239
|
-
|
|
241
|
+
model_fallback:
|
|
242
|
+
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-4.1-mini' }}",
|
|
243
|
+
use_codex: "${{ secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '' }}",
|
|
240
244
|
codex_only: "false",
|
|
241
245
|
llm_failure_policy: "block",
|
|
242
246
|
max_daily_scans: "${{ vars.OMAR_MAX_DAILY_SCANS || '200' }}",
|
|
@@ -295,11 +299,15 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
|
|
|
295
299
|
OMAR_SEVERITY_GATE: "${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}",
|
|
296
300
|
OMAR_P2_MAX_ALLOWED: "${{ github.event_name == 'workflow_dispatch' && inputs.p2_max_allowed || '5' }}",
|
|
297
301
|
OMAR_OPENAI_KEY_PRESENT: "${{ secrets.OPENAI_API_KEY != '' }}",
|
|
298
|
-
OMAR_GOOGLE_KEY_PRESENT:
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
302
|
+
OMAR_GOOGLE_KEY_PRESENT:
|
|
303
|
+
"${{ secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '' }}",
|
|
304
|
+
OMAR_MANAGED_LLM: `\${{ secrets.GOOGLE_GEMINI_API_KEY == '' && secrets.GOOGLE_API_KEY == '' && secrets.OPENAI_API_KEY == '' && secrets.${normalizedSecret} != '' }}`,
|
|
305
|
+
OMAR_LLM_PROVIDER:
|
|
306
|
+
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'google' || 'openai' }}",
|
|
307
|
+
OMAR_MODEL:
|
|
308
|
+
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-5.3-codex' }}",
|
|
309
|
+
OMAR_MODEL_FALLBACK:
|
|
310
|
+
"${{ (secrets.GOOGLE_GEMINI_API_KEY != '' || secrets.GOOGLE_API_KEY != '') && 'gemini-3.1-flash-lite' || 'gpt-4.1-mini' }}",
|
|
303
311
|
},
|
|
304
312
|
run: [
|
|
305
313
|
"set -euo pipefail",
|
|
@@ -5,7 +5,7 @@ export const COORDINATION_ETIQUETTE_ITEMS = Object.freeze([
|
|
|
5
5
|
"When you have an agent grant, post agent updates with `sl session post-agent <id> \"status: <update>\" --agent <your-agent-id>` so they render as the agent, not the human relay.",
|
|
6
6
|
"Before implementation, post a short plan and file claims with `sl session say <id> \"plan: <scope>; files: <paths>\"`.",
|
|
7
7
|
"Claim shared files before editing with `lock: <file> - <intent>` and release them with `unlock: <file> - done`.",
|
|
8
|
-
"Run
|
|
8
|
+
"Run one background/secondary listener for replies with `sl session listen --session <id> --agent <your-name> --interval 60 --active-interval 5 --emit ndjson --no-presence`; this idles at 60s, switches to 5s after human activity, and avoids durable listener heartbeat noise. `session listen` now 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 other participants can see you online. 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
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`. If no durable recap/checkpoint is appearing, run `sl session recap now <id> --remote --agent <your-name> --json` before posting a long plan.",
|
|
10
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. Read receipts are automatic when you use `sl session read <id> --remote --agent <your-name>`; reserve `sl session view <id> <sequence>` for repair/backfill. Run `sl session actions` for the full list.",
|
|
11
11
|
"Search before asking peers to restate context: `sl session search <id> \"<topic>\" --limit 10`.",
|
|
@@ -38,7 +38,7 @@ export function buildFirstSentiMessage({ sessionId } = {}) {
|
|
|
38
38
|
`7) sl session reply ${sid} <THIS_SEQ> "ACK <AGENT_ID> role=<role> model=<model> locks=none first_action=<one concrete action>" --agent <AGENT_ID>`,
|
|
39
39
|
"",
|
|
40
40
|
"STAY REACHABLE — two honest paths, use YOURS (always pass --agent <AGENT_ID> so the room knows who's listening):",
|
|
41
|
-
`- Persistent process (Codex/GPT): keep ONE listener alive
|
|
41
|
+
`- Persistent process (Codex/GPT): keep ONE listener alive. Re-running listen refuses a duplicate local pid by default; use --force only to stop and replace an existing local owner and --allow-duplicate only for intentional parallel wake hooks. Pair it with a 60s watchdog that restarts it if the pid dies (cursor catch-up replays anything missed):`,
|
|
42
42
|
` sl session listen --session ${sid} --agent <AGENT_ID> --active-interval 30 --active-window 300 --interval 60 --presence-interval 60 --model <MODEL> --display-name <AGENT_ID>`,
|
|
43
43
|
`- No socket (Claude Code): you are NOT continuously connected — you re-arm a wake each turn. 30-270s while actively waiting, 20-30min idle when quiet. Treat time-critical things as POSTED messages you catch next tick, never assumed instant.`,
|
|
44
44
|
`CADENCE both converge to: ~30s active; after 5min quiet → 60s; after another 5 → 90s; +30s every 5min to a floor. Wake IMMEDIATELY on: new human/ORCH msg, a direct reply/@mention, a lock conflict, a deploy/gate notice.`,
|