sentinelayer-cli 0.30.11 → 0.30.12
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 +209 -0
- package/src/session/daemon.js +1 -0
- package/src/session/preview.js +2 -0
- package/src/session/transcript.js +46 -0
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -1712,6 +1712,47 @@ function canPublishListenerPresence(agentId) {
|
|
|
1712
1712
|
);
|
|
1713
1713
|
}
|
|
1714
1714
|
|
|
1715
|
+
const SESSION_OBSERVATION_SEVERITIES = new Set(["info", "p3", "p2", "p1", "p0"]);
|
|
1716
|
+
const SESSION_OBSERVATION_KINDS = new Set([
|
|
1717
|
+
"ux",
|
|
1718
|
+
"process",
|
|
1719
|
+
"reliability",
|
|
1720
|
+
"security",
|
|
1721
|
+
"billing",
|
|
1722
|
+
"coordination",
|
|
1723
|
+
"architecture",
|
|
1724
|
+
"testing",
|
|
1725
|
+
"release",
|
|
1726
|
+
"other",
|
|
1727
|
+
]);
|
|
1728
|
+
const SESSION_OBSERVATION_SUMMARY_MAX_LENGTH = 4_000;
|
|
1729
|
+
const SESSION_OBSERVATION_OPTION_MAX_LENGTH = 512;
|
|
1730
|
+
const SESSION_OBSERVATION_PROPOSAL_MAX_LENGTH = 2_000;
|
|
1731
|
+
|
|
1732
|
+
function requireGrantedNonHumanAgentId(rawAgentId, commandName) {
|
|
1733
|
+
const agentId = normalizeAgentId(rawAgentId, "");
|
|
1734
|
+
if (!agentId || !canPublishListenerPresence(agentId)) {
|
|
1735
|
+
throw new Error(`${commandName} requires a granted non-human agent id.`);
|
|
1736
|
+
}
|
|
1737
|
+
return agentId;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function normalizeSessionObservationChoice(rawValue, allowedValues, fallbackValue, fieldName) {
|
|
1741
|
+
const value = normalizeString(rawValue || fallbackValue).toLowerCase();
|
|
1742
|
+
if (!allowedValues.has(value)) {
|
|
1743
|
+
throw new Error(`${fieldName} must be one of: ${[...allowedValues].join(", ")}.`);
|
|
1744
|
+
}
|
|
1745
|
+
return value;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function normalizeSessionObservationText(rawValue, fieldName, maxLength) {
|
|
1749
|
+
const value = normalizeString(rawValue);
|
|
1750
|
+
if (value.length > maxLength) {
|
|
1751
|
+
throw new Error(`${fieldName} must be ${maxLength} characters or fewer.`);
|
|
1752
|
+
}
|
|
1753
|
+
return value;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1715
1756
|
function listenerLifecycleEventName(type = "") {
|
|
1716
1757
|
const normalized = normalizeString(type).toLowerCase();
|
|
1717
1758
|
if (normalized === "started") return "session_listener_started";
|
|
@@ -3660,6 +3701,174 @@ export function registerSessionCommand(program) {
|
|
|
3660
3701
|
console.log(formatEventLine(persisted));
|
|
3661
3702
|
});
|
|
3662
3703
|
|
|
3704
|
+
session
|
|
3705
|
+
.command("observe <sessionId> [summary...]")
|
|
3706
|
+
.description("Record a durable product/process observation in the session")
|
|
3707
|
+
.requiredOption("--agent <id>", "Granted non-human agent id to record the observation as")
|
|
3708
|
+
.option("--model <model>", "Agent model/provider hint", "cli")
|
|
3709
|
+
.option("--display-name <name>", "Human-readable agent display name")
|
|
3710
|
+
.option("--role <role>", "Agent role metadata", "observer")
|
|
3711
|
+
.option(
|
|
3712
|
+
"--kind <kind>",
|
|
3713
|
+
`Observation kind: ${[...SESSION_OBSERVATION_KINDS].join(", ")}`,
|
|
3714
|
+
"process",
|
|
3715
|
+
)
|
|
3716
|
+
.option(
|
|
3717
|
+
"--severity <level>",
|
|
3718
|
+
`Observation severity: ${[...SESSION_OBSERVATION_SEVERITIES].join(", ")}`,
|
|
3719
|
+
"info",
|
|
3720
|
+
)
|
|
3721
|
+
.option("--owner <owner>", "Suggested owner/team for follow-up")
|
|
3722
|
+
.option("--batch <batch>", "Suggested PR batch or backlog lane")
|
|
3723
|
+
.option("--target-sequence <n>", "Anchor the observation to a session sequence id")
|
|
3724
|
+
.option("--target-cursor <cursor>", "Anchor the observation to a session event cursor")
|
|
3725
|
+
.option("--proposal <text>", "Suggested remediation or next step")
|
|
3726
|
+
.option("--message-file <path>", "Read the observation summary from a UTF-8 file")
|
|
3727
|
+
.option("--stdin", "Read the observation summary from stdin")
|
|
3728
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
3729
|
+
.option("--json", "Emit machine-readable output")
|
|
3730
|
+
.action(async (sessionId, summaryParts, options, command) => {
|
|
3731
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
3732
|
+
if (!normalizedSessionId) {
|
|
3733
|
+
throw new Error("session id is required.");
|
|
3734
|
+
}
|
|
3735
|
+
const summary = normalizeSessionObservationText(
|
|
3736
|
+
await resolveSessionSayMessageInput(summaryParts, options),
|
|
3737
|
+
"summary",
|
|
3738
|
+
SESSION_OBSERVATION_SUMMARY_MAX_LENGTH,
|
|
3739
|
+
);
|
|
3740
|
+
if (!summary) {
|
|
3741
|
+
throw new Error("summary is required.");
|
|
3742
|
+
}
|
|
3743
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
3744
|
+
const agentId = requireGrantedNonHumanAgentId(options.agent, "observe");
|
|
3745
|
+
const kind = normalizeSessionObservationChoice(
|
|
3746
|
+
options.kind,
|
|
3747
|
+
SESSION_OBSERVATION_KINDS,
|
|
3748
|
+
"process",
|
|
3749
|
+
"kind",
|
|
3750
|
+
);
|
|
3751
|
+
const severity = normalizeSessionObservationChoice(
|
|
3752
|
+
options.severity,
|
|
3753
|
+
SESSION_OBSERVATION_SEVERITIES,
|
|
3754
|
+
"info",
|
|
3755
|
+
"severity",
|
|
3756
|
+
);
|
|
3757
|
+
const owner = normalizeSessionObservationText(
|
|
3758
|
+
options.owner,
|
|
3759
|
+
"owner",
|
|
3760
|
+
SESSION_OBSERVATION_OPTION_MAX_LENGTH,
|
|
3761
|
+
);
|
|
3762
|
+
const proposedBatch = normalizeSessionObservationText(
|
|
3763
|
+
options.batch,
|
|
3764
|
+
"batch",
|
|
3765
|
+
SESSION_OBSERVATION_OPTION_MAX_LENGTH,
|
|
3766
|
+
);
|
|
3767
|
+
const proposal = normalizeSessionObservationText(
|
|
3768
|
+
options.proposal,
|
|
3769
|
+
"proposal",
|
|
3770
|
+
SESSION_OBSERVATION_PROPOSAL_MAX_LENGTH,
|
|
3771
|
+
);
|
|
3772
|
+
const targetSequenceId = parseOptionalPositiveInteger(options.targetSequence, "target-sequence");
|
|
3773
|
+
const targetCursor = normalizeSessionObservationText(
|
|
3774
|
+
options.targetCursor,
|
|
3775
|
+
"target-cursor",
|
|
3776
|
+
SESSION_OBSERVATION_OPTION_MAX_LENGTH,
|
|
3777
|
+
);
|
|
3778
|
+
const localSession = await ensureLocalSessionForRemoteCommand(normalizedSessionId, {
|
|
3779
|
+
targetPath,
|
|
3780
|
+
});
|
|
3781
|
+
const clientMessageId = `cli-observation-${randomUUID()}`;
|
|
3782
|
+
const eventPayload = {
|
|
3783
|
+
schema: "session_observation/v1",
|
|
3784
|
+
summary,
|
|
3785
|
+
message: summary,
|
|
3786
|
+
kind,
|
|
3787
|
+
severity,
|
|
3788
|
+
channel: "session",
|
|
3789
|
+
source: "session_observe",
|
|
3790
|
+
clientKind: "cli",
|
|
3791
|
+
clientMessageId,
|
|
3792
|
+
};
|
|
3793
|
+
if (owner) eventPayload.owner = owner;
|
|
3794
|
+
if (proposedBatch) eventPayload.proposedBatch = proposedBatch;
|
|
3795
|
+
if (proposal) eventPayload.proposal = proposal;
|
|
3796
|
+
if (targetSequenceId) eventPayload.targetSequenceId = targetSequenceId;
|
|
3797
|
+
if (targetCursor) eventPayload.targetCursor = targetCursor;
|
|
3798
|
+
|
|
3799
|
+
const agent = await resolveSessionAgentEnvelope(normalizedSessionId, agentId, {
|
|
3800
|
+
targetPath,
|
|
3801
|
+
model: options.model,
|
|
3802
|
+
role: normalizeString(options.role) || "observer",
|
|
3803
|
+
displayName: options.displayName,
|
|
3804
|
+
});
|
|
3805
|
+
agent.role = normalizeString(agent.role) || "observer";
|
|
3806
|
+
agent.model = normalizeString(agent.model) || "cli";
|
|
3807
|
+
agent.clientKind = normalizeString(agent.clientKind) || "cli";
|
|
3808
|
+
|
|
3809
|
+
const event = createAgentEvent({
|
|
3810
|
+
event: "session_observation",
|
|
3811
|
+
agent,
|
|
3812
|
+
sessionId: normalizedSessionId,
|
|
3813
|
+
payload: eventPayload,
|
|
3814
|
+
});
|
|
3815
|
+
event.eventId = clientMessageId;
|
|
3816
|
+
event.idempotencyToken = clientMessageId;
|
|
3817
|
+
|
|
3818
|
+
const remoteConfirmationAnchor = await readSessionConfirmationAnchor(normalizedSessionId, {
|
|
3819
|
+
targetPath,
|
|
3820
|
+
});
|
|
3821
|
+
if (!remoteConfirmationAnchor?.ok) {
|
|
3822
|
+
if (remoteConfirmationAnchor?.reason === "api_403") {
|
|
3823
|
+
throw new Error(
|
|
3824
|
+
`Observation failed (api_403). Ensure this user has an active grant for '${agentId}'.`,
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
3827
|
+
throw new Error(
|
|
3828
|
+
`Observation confirmation anchor failed (${remoteConfirmationAnchor?.reason || "unknown"}); local cache was not updated.`,
|
|
3829
|
+
);
|
|
3830
|
+
}
|
|
3831
|
+
const remoteSync = await syncSessionEventToApi(normalizedSessionId, event, {
|
|
3832
|
+
targetPath,
|
|
3833
|
+
});
|
|
3834
|
+
if (!remoteSync?.synced) {
|
|
3835
|
+
throw new Error(
|
|
3836
|
+
`Observation failed (${remoteSync?.reason || "unknown"}). Ensure this user has an active grant for '${agentId}'.`,
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3839
|
+
const remoteConfirmation = await confirmSessionEventVisible(normalizedSessionId, clientMessageId, {
|
|
3840
|
+
targetPath,
|
|
3841
|
+
anchorCursor: remoteConfirmationAnchor?.cursor,
|
|
3842
|
+
});
|
|
3843
|
+
if (!remoteConfirmation?.confirmed) {
|
|
3844
|
+
throw new Error(
|
|
3845
|
+
`Observation was accepted but not visible in canonical session events (${remoteConfirmation?.reason || "not_visible"}); local cache was not updated.`,
|
|
3846
|
+
);
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
const persisted = await appendToStream(normalizedSessionId, event, {
|
|
3850
|
+
targetPath,
|
|
3851
|
+
syncRemote: false,
|
|
3852
|
+
});
|
|
3853
|
+
const payload = {
|
|
3854
|
+
command: "session observe",
|
|
3855
|
+
targetPath,
|
|
3856
|
+
sessionId: normalizedSessionId,
|
|
3857
|
+
agentId,
|
|
3858
|
+
event: persisted,
|
|
3859
|
+
materializedLocalSession: localSession.materialized,
|
|
3860
|
+
refreshedLocalSession: Boolean(localSession.refreshed),
|
|
3861
|
+
remoteSync,
|
|
3862
|
+
remoteConfirmationAnchor,
|
|
3863
|
+
remoteConfirmation,
|
|
3864
|
+
};
|
|
3865
|
+
if (shouldEmitJson(options, command)) {
|
|
3866
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3867
|
+
return;
|
|
3868
|
+
}
|
|
3869
|
+
console.log(formatEventLine(persisted));
|
|
3870
|
+
});
|
|
3871
|
+
|
|
3663
3872
|
async function runMessageActionCommand({
|
|
3664
3873
|
sessionId,
|
|
3665
3874
|
actionType,
|
package/src/session/daemon.js
CHANGED
package/src/session/preview.js
CHANGED
|
@@ -12,6 +12,7 @@ const PREVIEW_EVENTS = new Set([
|
|
|
12
12
|
"session_message",
|
|
13
13
|
"session_say",
|
|
14
14
|
"agent_response",
|
|
15
|
+
"session_observation",
|
|
15
16
|
"human_relay",
|
|
16
17
|
"daemon_alert",
|
|
17
18
|
"session_admin_kill",
|
|
@@ -45,6 +46,7 @@ export function pickLatestPreview(events = []) {
|
|
|
45
46
|
if (!kind || !PREVIEW_EVENTS.has(kind)) continue;
|
|
46
47
|
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
47
48
|
const text =
|
|
49
|
+
payload.summary ||
|
|
48
50
|
payload.message ||
|
|
49
51
|
payload.response ||
|
|
50
52
|
payload.alert ||
|
|
@@ -47,6 +47,7 @@ const TRANSCRIPT_EVENT_KINDS = new Set([
|
|
|
47
47
|
"session_reply",
|
|
48
48
|
"session_reaction",
|
|
49
49
|
"session_usage",
|
|
50
|
+
"session_observation",
|
|
50
51
|
"human_relay",
|
|
51
52
|
"agent_join",
|
|
52
53
|
"agent_left",
|
|
@@ -209,11 +210,56 @@ function actionBody(event) {
|
|
|
209
210
|
return "";
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
function boundedMarkdownText(value, { maxLength = 2_000 } = {}) {
|
|
214
|
+
const text = normalize(value).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
215
|
+
if (!maxLength || text.length <= maxLength) return text;
|
|
216
|
+
const head = Math.max(120, Math.floor((maxLength - 3) * 0.75));
|
|
217
|
+
const tail = Math.max(40, maxLength - 3 - head);
|
|
218
|
+
return `${text.slice(0, head)}...${text.slice(-tail)}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function observationTargetLabel(payload = {}) {
|
|
222
|
+
const targetSequence = Number(payload.targetSequenceId || payload.target_sequence_id || 0);
|
|
223
|
+
if (Number.isFinite(targetSequence) && targetSequence > 0) {
|
|
224
|
+
return `#${Math.floor(targetSequence)}`;
|
|
225
|
+
}
|
|
226
|
+
const targetCursor = tableText(payload.targetCursor || payload.target_cursor, { maxLength: 96 });
|
|
227
|
+
return targetCursor ? `cursor ${targetCursor}` : "";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function observationBody(event) {
|
|
231
|
+
const payload = event && typeof event.payload === "object" ? event.payload : {};
|
|
232
|
+
const severity = tableText(payload.severity || "info", { maxLength: 16 }).toLowerCase();
|
|
233
|
+
const kind = tableText(payload.kind || "process", { maxLength: 32 }).toLowerCase();
|
|
234
|
+
const summary = boundedMarkdownText(payload.summary || payload.message || payload.text, {
|
|
235
|
+
maxLength: 4_000,
|
|
236
|
+
});
|
|
237
|
+
const proposal = boundedMarkdownText(payload.proposal || payload.recommendation, {
|
|
238
|
+
maxLength: 2_000,
|
|
239
|
+
});
|
|
240
|
+
const owner = tableText(payload.owner || payload.assignee, { maxLength: 96 });
|
|
241
|
+
const proposedBatch = tableText(payload.proposedBatch || payload.batch, { maxLength: 96 });
|
|
242
|
+
const target = observationTargetLabel(payload);
|
|
243
|
+
const metadata = [];
|
|
244
|
+
if (owner) metadata.push(`Owner: \`${owner}\``);
|
|
245
|
+
if (proposedBatch) metadata.push(`Batch: \`${proposedBatch}\``);
|
|
246
|
+
if (target) metadata.push(`Target: \`${target}\``);
|
|
247
|
+
|
|
248
|
+
const lines = [`**Observation:** \`${severity || "info"}\` · \`${kind || "process"}\``];
|
|
249
|
+
if (metadata.length) lines.push(metadata.join(" · "));
|
|
250
|
+
if (summary) lines.push("", summary);
|
|
251
|
+
if (proposal) lines.push("", "**Proposal:**", proposal);
|
|
252
|
+
return lines.join("\n");
|
|
253
|
+
}
|
|
254
|
+
|
|
212
255
|
function eventBody(event) {
|
|
213
256
|
const kind = normalize(event?.event || event?.type);
|
|
214
257
|
if (kind === "session_action" || kind === "session_reply" || kind === "session_reaction") {
|
|
215
258
|
return actionBody(event);
|
|
216
259
|
}
|
|
260
|
+
if (kind === "session_observation") {
|
|
261
|
+
return observationBody(event);
|
|
262
|
+
}
|
|
217
263
|
const payload = event && typeof event.payload === "object" ? event.payload : {};
|
|
218
264
|
// session_usage carries the response inside payload.response.text
|
|
219
265
|
const responseText =
|