sentinelayer-cli 0.11.4 → 0.12.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 +494 -11
- package/src/session/sync.js +343 -0
- package/src/session/transcript.js +3 -0
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fsp from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
|
|
@@ -59,9 +59,12 @@ import {
|
|
|
59
59
|
} from "../session/event-identity.js";
|
|
60
60
|
import { readSessionPreview } from "../session/preview.js";
|
|
61
61
|
import {
|
|
62
|
+
createSessionMessageAction,
|
|
63
|
+
listSessionMessageActions,
|
|
62
64
|
listSessionsFromApi,
|
|
63
65
|
probeSessionAccess,
|
|
64
66
|
pollSessionEventsBefore,
|
|
67
|
+
searchSessionEvents,
|
|
65
68
|
syncSessionEventToApi,
|
|
66
69
|
syncSessionMetadataToApi,
|
|
67
70
|
} from "../session/sync.js";
|
|
@@ -97,6 +100,174 @@ function normalizeString(value) {
|
|
|
97
100
|
return String(value || "").trim();
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
const SESSION_MESSAGE_ACTION_TYPES = new Set([
|
|
104
|
+
"ack",
|
|
105
|
+
"working_on",
|
|
106
|
+
"reply",
|
|
107
|
+
"like",
|
|
108
|
+
"dislike",
|
|
109
|
+
"disregard",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
function normalizeSessionMessageActionType(value) {
|
|
113
|
+
const normalized = normalizeString(value).toLowerCase();
|
|
114
|
+
if (!SESSION_MESSAGE_ACTION_TYPES.has(normalized)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`action type must be one of: ${[...SESSION_MESSAGE_ACTION_TYPES].join(", ")}.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseOptionalPositiveInteger(rawValue, field) {
|
|
123
|
+
if (rawValue === undefined || rawValue === null || String(rawValue).trim() === "") {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return parsePositiveInteger(rawValue, field, null);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function shortSha256(value) {
|
|
130
|
+
return createHash("sha256").update(String(value || "")).digest("hex").slice(0, 32);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function actionEventType(actionType) {
|
|
134
|
+
if (actionType === "reply") return "session_reply";
|
|
135
|
+
if (actionType === "like" || actionType === "dislike") return "session_reaction";
|
|
136
|
+
return "session_action";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function actionTargetSequence(action = {}) {
|
|
140
|
+
const value = Number(action.targetSequenceId ?? action.target_sequence_id ?? 0);
|
|
141
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function actionTargetCursor(action = {}) {
|
|
145
|
+
return normalizeString(action.targetCursor ?? action.target_cursor);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function actionActorId(action = {}) {
|
|
149
|
+
return normalizeString(action.actorId ?? action.actor_id) || "unknown";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function actionCreatedAt(action = {}) {
|
|
153
|
+
return normalizeString(action.createdAt ?? action.created_at) || new Date().toISOString();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function actionDisplayMessage(action = {}) {
|
|
157
|
+
const actionType = normalizeString(action.actionType ?? action.action_type).toLowerCase();
|
|
158
|
+
const targetSequence = actionTargetSequence(action);
|
|
159
|
+
const targetLabel = targetSequence ? `#${targetSequence}` : actionTargetCursor(action) || "target";
|
|
160
|
+
const note = normalizeString(action.note);
|
|
161
|
+
if (note) return `${actionType} ${targetLabel}: ${note}`;
|
|
162
|
+
return `${actionType} ${targetLabel}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildSessionActionEvent(sessionId, action = {}) {
|
|
166
|
+
const actionType = normalizeString(action.actionType ?? action.action_type).toLowerCase();
|
|
167
|
+
if (!SESSION_MESSAGE_ACTION_TYPES.has(actionType)) return null;
|
|
168
|
+
const id =
|
|
169
|
+
normalizeString(action.id) ||
|
|
170
|
+
shortSha256(
|
|
171
|
+
JSON.stringify({
|
|
172
|
+
actionType,
|
|
173
|
+
targetSequenceId: actionTargetSequence(action),
|
|
174
|
+
targetCursor: actionTargetCursor(action),
|
|
175
|
+
actorId: actionActorId(action),
|
|
176
|
+
note: normalizeString(action.note),
|
|
177
|
+
createdAt: actionCreatedAt(action),
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
const actorId = actionActorId(action);
|
|
181
|
+
const event = createAgentEvent({
|
|
182
|
+
event: actionEventType(actionType),
|
|
183
|
+
agent: {
|
|
184
|
+
id: actorId,
|
|
185
|
+
role: normalizeString(action.actorRole ?? action.actor_role) || undefined,
|
|
186
|
+
model: normalizeString(action.actorKind ?? action.actor_kind) || "session-action",
|
|
187
|
+
},
|
|
188
|
+
sessionId,
|
|
189
|
+
ts: actionCreatedAt(action),
|
|
190
|
+
payload: {
|
|
191
|
+
actionId: id,
|
|
192
|
+
actionType,
|
|
193
|
+
targetSequenceId: actionTargetSequence(action),
|
|
194
|
+
targetCursor: actionTargetCursor(action) || null,
|
|
195
|
+
note: normalizeString(action.note) || null,
|
|
196
|
+
message: actionDisplayMessage(action),
|
|
197
|
+
source: "session_action",
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
event.eventId = `session-action-${id}`;
|
|
201
|
+
event.idempotencyToken = normalizeString(action.idempotencyKey ?? action.idempotency_key) || event.eventId;
|
|
202
|
+
event.cursor = `action:${id}`;
|
|
203
|
+
return event;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildSessionActionEvents(sessionId, actions = []) {
|
|
207
|
+
return (Array.isArray(actions) ? actions : [])
|
|
208
|
+
.map((action) => buildSessionActionEvent(sessionId, action))
|
|
209
|
+
.filter(Boolean);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function eventTimestampMs(event = {}) {
|
|
213
|
+
for (const key of ["ts", "timestamp", "createdAt", "at"]) {
|
|
214
|
+
const epoch = Date.parse(normalizeString(event?.[key]));
|
|
215
|
+
if (Number.isFinite(epoch)) return epoch;
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function eventSequenceNumber(event = {}) {
|
|
221
|
+
for (const key of ["sequenceId", "sequence_id", "sequence"]) {
|
|
222
|
+
const value = Number(event?.[key]);
|
|
223
|
+
if (Number.isFinite(value)) return value;
|
|
224
|
+
}
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function mergeSessionActionEvents(events = [], actionEvents = []) {
|
|
229
|
+
return dedupeSessionEvents([...(Array.isArray(events) ? events : []), ...actionEvents])
|
|
230
|
+
.map((event, index) => ({ event, index }))
|
|
231
|
+
.sort((left, right) => {
|
|
232
|
+
const timeDiff = eventTimestampMs(left.event) - eventTimestampMs(right.event);
|
|
233
|
+
if (timeDiff !== 0) return timeDiff;
|
|
234
|
+
const sequenceDiff = eventSequenceNumber(left.event) - eventSequenceNumber(right.event);
|
|
235
|
+
if (sequenceDiff !== 0) return sequenceDiff;
|
|
236
|
+
return left.index - right.index;
|
|
237
|
+
})
|
|
238
|
+
.map((entry) => entry.event);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function appendActionEventIfMissing(sessionId, actionEvent, { targetPath } = {}) {
|
|
242
|
+
if (!actionEvent) return { appended: false, reason: "no_event", event: null };
|
|
243
|
+
const existingEvents = await readStream(sessionId, { targetPath, tail: 0 }).catch(() => []);
|
|
244
|
+
const knownKeys = new Set();
|
|
245
|
+
for (const event of existingEvents) {
|
|
246
|
+
addSessionEventIdentityKeys(knownKeys, event);
|
|
247
|
+
}
|
|
248
|
+
if (sessionEventHasKnownIdentity(actionEvent, knownKeys)) {
|
|
249
|
+
return { appended: false, reason: "already_present", event: actionEvent };
|
|
250
|
+
}
|
|
251
|
+
const appended = await appendToStream(sessionId, actionEvent, {
|
|
252
|
+
targetPath,
|
|
253
|
+
syncRemote: false,
|
|
254
|
+
});
|
|
255
|
+
return { appended: true, reason: "", event: appended };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function defaultActionIdempotencyKey({
|
|
259
|
+
actionType,
|
|
260
|
+
targetSequenceId,
|
|
261
|
+
targetCursor,
|
|
262
|
+
note,
|
|
263
|
+
agentId,
|
|
264
|
+
} = {}) {
|
|
265
|
+
const target = targetSequenceId ? `seq:${targetSequenceId}` : `cursor:${normalizeString(targetCursor)}`;
|
|
266
|
+
const noteHash = note ? shortSha256(note) : "none";
|
|
267
|
+
const actor = normalizeString(agentId) || "user";
|
|
268
|
+
return `cli:${normalizeString(actionType).toLowerCase()}:${target}:${actor}:${noteHash}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
100
271
|
function compareIsoDesc(left = "", right = "") {
|
|
101
272
|
return normalizeString(right).localeCompare(normalizeString(left));
|
|
102
273
|
}
|
|
@@ -653,6 +824,16 @@ function formatEventLine(event = {}) {
|
|
|
653
824
|
const type = normalizeString(event.event || event.type) || "event";
|
|
654
825
|
const agentId = normalizeString(event.agent?.id || event.agentId || "unknown");
|
|
655
826
|
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
827
|
+
if (type === "session_action" || type === "session_reply" || type === "session_reaction") {
|
|
828
|
+
const actionType = normalizeString(payload.actionType) || type.replace(/^session_/, "");
|
|
829
|
+
const targetSequence = Number(payload.targetSequenceId || 0);
|
|
830
|
+
const target = Number.isFinite(targetSequence) && targetSequence > 0
|
|
831
|
+
? `#${Math.floor(targetSequence)}`
|
|
832
|
+
: normalizeString(payload.targetCursor) || "target";
|
|
833
|
+
const note = normalizeString(payload.note || payload.message || "");
|
|
834
|
+
const suffix = note && note !== `${actionType} ${target}` ? `: ${note}` : "";
|
|
835
|
+
return `${ts} ${agentId} ${type} ${actionType} ${target}${suffix}`;
|
|
836
|
+
}
|
|
656
837
|
const message = normalizeString(payload.message || payload.response || payload.alert || payload.reason || "");
|
|
657
838
|
if (message) {
|
|
658
839
|
return `${ts} ${agentId} ${type}: ${message}`;
|
|
@@ -1413,6 +1594,8 @@ export function registerSessionCommand(program) {
|
|
|
1413
1594
|
.description("Send a message to the session")
|
|
1414
1595
|
.option("--agent <id>", "Agent id to emit from", "cli-user")
|
|
1415
1596
|
.option("--to <agent>", "Direct the message to a specific agent id")
|
|
1597
|
+
.option("--reply-to <sequence>", "Mark this message as a reply to a target sequence id")
|
|
1598
|
+
.option("--reply-cursor <cursor>", "Mark this message as a reply to a target event cursor")
|
|
1416
1599
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
1417
1600
|
.option("--json", "Emit machine-readable output")
|
|
1418
1601
|
.action(async (sessionId, message, options, command) => {
|
|
@@ -1430,6 +1613,8 @@ export function registerSessionCommand(program) {
|
|
|
1430
1613
|
targetPath,
|
|
1431
1614
|
});
|
|
1432
1615
|
const to = normalizeString(options.to);
|
|
1616
|
+
const replyToSequenceId = parseOptionalPositiveInteger(options.replyTo, "reply-to");
|
|
1617
|
+
const replyToCursor = normalizeString(options.replyCursor);
|
|
1433
1618
|
const eventPayload = {
|
|
1434
1619
|
message: normalizedMessage,
|
|
1435
1620
|
channel: "session",
|
|
@@ -1437,6 +1622,12 @@ export function registerSessionCommand(program) {
|
|
|
1437
1622
|
if (to) {
|
|
1438
1623
|
eventPayload.to = to;
|
|
1439
1624
|
}
|
|
1625
|
+
if (replyToSequenceId) {
|
|
1626
|
+
eventPayload.replyToSequenceId = replyToSequenceId;
|
|
1627
|
+
}
|
|
1628
|
+
if (replyToCursor) {
|
|
1629
|
+
eventPayload.replyToCursor = replyToCursor;
|
|
1630
|
+
}
|
|
1440
1631
|
const clientMessageId = `cli-${randomUUID()}`;
|
|
1441
1632
|
const event = createAgentEvent({
|
|
1442
1633
|
event: "session_message",
|
|
@@ -1569,6 +1760,140 @@ export function registerSessionCommand(program) {
|
|
|
1569
1760
|
console.log(formatEventLine(persisted));
|
|
1570
1761
|
});
|
|
1571
1762
|
|
|
1763
|
+
async function runMessageActionCommand({
|
|
1764
|
+
sessionId,
|
|
1765
|
+
actionType,
|
|
1766
|
+
options,
|
|
1767
|
+
command,
|
|
1768
|
+
commandName = "session action",
|
|
1769
|
+
targetSequenceId: targetSequenceIdOverride = null,
|
|
1770
|
+
targetCursor: targetCursorOverride = "",
|
|
1771
|
+
note: noteOverride = "",
|
|
1772
|
+
} = {}) {
|
|
1773
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1774
|
+
if (!normalizedSessionId) {
|
|
1775
|
+
throw new Error("session id is required.");
|
|
1776
|
+
}
|
|
1777
|
+
const normalizedActionType = normalizeSessionMessageActionType(actionType);
|
|
1778
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
1779
|
+
const targetSequenceId =
|
|
1780
|
+
targetSequenceIdOverride ||
|
|
1781
|
+
parseOptionalPositiveInteger(options.targetSequence, "target-sequence");
|
|
1782
|
+
const targetCursor = normalizeString(targetCursorOverride) || normalizeString(options.targetCursor);
|
|
1783
|
+
if (!targetSequenceId && !targetCursor) {
|
|
1784
|
+
throw new Error("Provide --target-sequence or --target-cursor.");
|
|
1785
|
+
}
|
|
1786
|
+
await ensureLocalSessionForRemoteCommand(normalizedSessionId, { targetPath });
|
|
1787
|
+
const note = normalizeString(noteOverride) || normalizeString(options.note);
|
|
1788
|
+
const agentId = await defaultAgentId(options.agent, targetPath);
|
|
1789
|
+
const idempotencyKey =
|
|
1790
|
+
normalizeString(options.idempotencyKey) ||
|
|
1791
|
+
defaultActionIdempotencyKey({
|
|
1792
|
+
actionType: normalizedActionType,
|
|
1793
|
+
targetSequenceId,
|
|
1794
|
+
targetCursor,
|
|
1795
|
+
note,
|
|
1796
|
+
agentId,
|
|
1797
|
+
});
|
|
1798
|
+
|
|
1799
|
+
const result = await createSessionMessageAction(normalizedSessionId, {
|
|
1800
|
+
actionType: normalizedActionType,
|
|
1801
|
+
targetPath,
|
|
1802
|
+
targetSequenceId,
|
|
1803
|
+
targetCursor,
|
|
1804
|
+
note,
|
|
1805
|
+
metadata: {
|
|
1806
|
+
source: "cli",
|
|
1807
|
+
agentId,
|
|
1808
|
+
},
|
|
1809
|
+
idempotencyKey,
|
|
1810
|
+
timeoutMs: 15_000,
|
|
1811
|
+
});
|
|
1812
|
+
if (!result.ok || !result.action) {
|
|
1813
|
+
throw new Error(`Session action failed (${result.reason || "unknown"}).`);
|
|
1814
|
+
}
|
|
1815
|
+
const actionEvent = buildSessionActionEvent(normalizedSessionId, result.action);
|
|
1816
|
+
const localAppend = await appendActionEventIfMissing(normalizedSessionId, actionEvent, {
|
|
1817
|
+
targetPath,
|
|
1818
|
+
});
|
|
1819
|
+
const payload = {
|
|
1820
|
+
command: commandName,
|
|
1821
|
+
targetPath,
|
|
1822
|
+
sessionId: normalizedSessionId,
|
|
1823
|
+
actionType: normalizedActionType,
|
|
1824
|
+
duplicate: Boolean(result.duplicate),
|
|
1825
|
+
action: result.action,
|
|
1826
|
+
event: localAppend.event,
|
|
1827
|
+
localAppend: {
|
|
1828
|
+
appended: Boolean(localAppend.appended),
|
|
1829
|
+
reason: localAppend.reason || "",
|
|
1830
|
+
},
|
|
1831
|
+
};
|
|
1832
|
+
if (shouldEmitJson(options, command)) {
|
|
1833
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1834
|
+
return payload;
|
|
1835
|
+
}
|
|
1836
|
+
console.log(formatEventLine(localAppend.event || actionEvent));
|
|
1837
|
+
return payload;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
session
|
|
1841
|
+
.command("action <sessionId> <actionType>")
|
|
1842
|
+
.description("Create a message action for a target session event")
|
|
1843
|
+
.option("--target-sequence <n>", "Target event sequence id")
|
|
1844
|
+
.option("--target-cursor <cursor>", "Target event cursor")
|
|
1845
|
+
.option("--note <text>", "Optional action note or reply body")
|
|
1846
|
+
.option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
|
|
1847
|
+
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
1848
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
1849
|
+
.option("--json", "Emit machine-readable output")
|
|
1850
|
+
.action(async (sessionId, actionType, options, command) => {
|
|
1851
|
+
await runMessageActionCommand({ sessionId, actionType, options, command });
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
session
|
|
1855
|
+
.command("react <sessionId> <reaction>")
|
|
1856
|
+
.description("React to a target session event with like or dislike")
|
|
1857
|
+
.option("--target-sequence <n>", "Target event sequence id")
|
|
1858
|
+
.option("--target-cursor <cursor>", "Target event cursor")
|
|
1859
|
+
.option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
|
|
1860
|
+
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
1861
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
1862
|
+
.option("--json", "Emit machine-readable output")
|
|
1863
|
+
.action(async (sessionId, reaction, options, command) => {
|
|
1864
|
+
const normalizedReaction = normalizeSessionMessageActionType(reaction);
|
|
1865
|
+
if (normalizedReaction !== "like" && normalizedReaction !== "dislike") {
|
|
1866
|
+
throw new Error("reaction must be one of: like, dislike.");
|
|
1867
|
+
}
|
|
1868
|
+
await runMessageActionCommand({
|
|
1869
|
+
sessionId,
|
|
1870
|
+
actionType: normalizedReaction,
|
|
1871
|
+
options,
|
|
1872
|
+
command,
|
|
1873
|
+
commandName: "session react",
|
|
1874
|
+
});
|
|
1875
|
+
});
|
|
1876
|
+
|
|
1877
|
+
session
|
|
1878
|
+
.command("reply <sessionId> <targetSequenceId> <message...>")
|
|
1879
|
+
.description("Reply to a target session event using the message-action channel")
|
|
1880
|
+
.option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
|
|
1881
|
+
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
1882
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
1883
|
+
.option("--json", "Emit machine-readable output")
|
|
1884
|
+
.action(async (sessionId, targetSequenceId, messageParts, options, command) => {
|
|
1885
|
+
const message = Array.isArray(messageParts) ? messageParts.join(" ") : messageParts;
|
|
1886
|
+
await runMessageActionCommand({
|
|
1887
|
+
sessionId,
|
|
1888
|
+
actionType: "reply",
|
|
1889
|
+
options,
|
|
1890
|
+
command,
|
|
1891
|
+
commandName: "session reply",
|
|
1892
|
+
targetSequenceId: parsePositiveInteger(targetSequenceId, "targetSequenceId", 0),
|
|
1893
|
+
note: message,
|
|
1894
|
+
});
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1572
1897
|
session
|
|
1573
1898
|
.command("listen")
|
|
1574
1899
|
.description("Background-poll a session for events addressed to this agent or broadcast")
|
|
@@ -1773,6 +2098,8 @@ export function registerSessionCommand(program) {
|
|
|
1773
2098
|
"--remote",
|
|
1774
2099
|
"Hydrate from the SentinelLayer API before reading (pulls web-posted messages into the local NDJSON)",
|
|
1775
2100
|
)
|
|
2101
|
+
.option("--before-sequence <n>", "Remote page ending before this sequence id")
|
|
2102
|
+
.option("--no-actions", "Do not include remote message actions/replies/reactions")
|
|
1776
2103
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
1777
2104
|
.option("--json", "Emit machine-readable output")
|
|
1778
2105
|
.action(async (sessionId, options, command) => {
|
|
@@ -1782,10 +2109,12 @@ export function registerSessionCommand(program) {
|
|
|
1782
2109
|
}
|
|
1783
2110
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
1784
2111
|
const tail = parsePositiveInteger(options.tail, "tail", 20);
|
|
2112
|
+
const beforeSequence = parseOptionalPositiveInteger(options.beforeSequence, "before-sequence");
|
|
1785
2113
|
const emitJson = shouldEmitJson(options, command);
|
|
1786
2114
|
|
|
1787
2115
|
let hydration = null;
|
|
1788
2116
|
let remoteTail = null;
|
|
2117
|
+
let remoteActions = null;
|
|
1789
2118
|
if (options.remote) {
|
|
1790
2119
|
const authSession = await resolveActiveAuthSession({
|
|
1791
2120
|
cwd: targetPath,
|
|
@@ -1801,9 +2130,17 @@ export function registerSessionCommand(program) {
|
|
|
1801
2130
|
});
|
|
1802
2131
|
remoteTail = await pollSessionEventsBefore(normalizedSessionId, {
|
|
1803
2132
|
targetPath,
|
|
2133
|
+
beforeSequence,
|
|
1804
2134
|
limit: tail,
|
|
1805
2135
|
timeoutMs: 15_000,
|
|
1806
2136
|
});
|
|
2137
|
+
if (options.actions !== false) {
|
|
2138
|
+
remoteActions = await listSessionMessageActions(normalizedSessionId, {
|
|
2139
|
+
targetPath,
|
|
2140
|
+
limit: 500,
|
|
2141
|
+
timeoutMs: 15_000,
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
1807
2144
|
if (!emitJson) {
|
|
1808
2145
|
if (hydration.ok) {
|
|
1809
2146
|
console.log(
|
|
@@ -1825,6 +2162,13 @@ export function registerSessionCommand(program) {
|
|
|
1825
2162
|
),
|
|
1826
2163
|
);
|
|
1827
2164
|
}
|
|
2165
|
+
if (remoteActions && !remoteActions.ok) {
|
|
2166
|
+
console.log(
|
|
2167
|
+
pc.yellow(
|
|
2168
|
+
`Remote message actions skipped (${remoteActions.reason}); showing events only.`,
|
|
2169
|
+
),
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
1828
2172
|
}
|
|
1829
2173
|
}
|
|
1830
2174
|
|
|
@@ -1860,7 +2204,10 @@ export function registerSessionCommand(program) {
|
|
|
1860
2204
|
}
|
|
1861
2205
|
}
|
|
1862
2206
|
}
|
|
1863
|
-
const
|
|
2207
|
+
const actionEvents = remoteActions?.ok
|
|
2208
|
+
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
2209
|
+
: [];
|
|
2210
|
+
const events = mergeSessionActionEvents(displayEvents, actionEvents).slice(-tail);
|
|
1864
2211
|
const remoteVerified = Boolean(
|
|
1865
2212
|
options.remote &&
|
|
1866
2213
|
((hydration && hydration.ok) || (remoteTail && remoteTail.ok))
|
|
@@ -1870,6 +2217,7 @@ export function registerSessionCommand(program) {
|
|
|
1870
2217
|
targetPath,
|
|
1871
2218
|
sessionId: normalizedSessionId,
|
|
1872
2219
|
tail,
|
|
2220
|
+
beforeSequence,
|
|
1873
2221
|
count: events.length,
|
|
1874
2222
|
events,
|
|
1875
2223
|
displaySource: !options.remote
|
|
@@ -1890,11 +2238,24 @@ export function registerSessionCommand(program) {
|
|
|
1890
2238
|
reason: remoteTail.reason || "",
|
|
1891
2239
|
count: Array.isArray(remoteTail.events) ? remoteTail.events.length : 0,
|
|
1892
2240
|
cursor: remoteTail.cursor || null,
|
|
2241
|
+
beforeSequence: remoteTail.beforeSequence || null,
|
|
1893
2242
|
verified: Boolean(remoteTail.ok),
|
|
1894
2243
|
appended: remoteTailAppended,
|
|
1895
2244
|
displayedOnly: remoteTailDisplayedOnly,
|
|
1896
2245
|
}
|
|
1897
2246
|
: null,
|
|
2247
|
+
actions: remoteActions
|
|
2248
|
+
? {
|
|
2249
|
+
ok: Boolean(remoteActions.ok),
|
|
2250
|
+
reason: remoteActions.reason || "",
|
|
2251
|
+
count: Array.isArray(remoteActions.actions)
|
|
2252
|
+
? remoteActions.actions.length
|
|
2253
|
+
: 0,
|
|
2254
|
+
actions: remoteActions.actions || [],
|
|
2255
|
+
syntheticEventCount: actionEvents.length,
|
|
2256
|
+
projection: remoteActions.projection || null,
|
|
2257
|
+
}
|
|
2258
|
+
: null,
|
|
1898
2259
|
}
|
|
1899
2260
|
: hydration,
|
|
1900
2261
|
};
|
|
@@ -1971,6 +2332,57 @@ export function registerSessionCommand(program) {
|
|
|
1971
2332
|
}
|
|
1972
2333
|
});
|
|
1973
2334
|
|
|
2335
|
+
session
|
|
2336
|
+
.command("search <sessionId> <query>")
|
|
2337
|
+
.description("Search durable API session events by text, event type, or agent")
|
|
2338
|
+
.option("--before-sequence <n>", "Return matches older than this sequence id")
|
|
2339
|
+
.option("--limit <n>", "Maximum search results (default 20, max 50)", "20")
|
|
2340
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
2341
|
+
.option("--json", "Emit machine-readable output")
|
|
2342
|
+
.action(async (sessionId, query, options, command) => {
|
|
2343
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
2344
|
+
if (!normalizedSessionId) {
|
|
2345
|
+
throw new Error("session id is required.");
|
|
2346
|
+
}
|
|
2347
|
+
const normalizedQuery = normalizeString(query);
|
|
2348
|
+
if (normalizedQuery.length < 2) {
|
|
2349
|
+
throw new Error("query must be at least 2 characters.");
|
|
2350
|
+
}
|
|
2351
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
2352
|
+
const beforeSequence = parseOptionalPositiveInteger(options.beforeSequence, "before-sequence");
|
|
2353
|
+
const limit = parsePositiveInteger(options.limit, "limit", 20);
|
|
2354
|
+
const result = await searchSessionEvents(normalizedSessionId, {
|
|
2355
|
+
query: normalizedQuery,
|
|
2356
|
+
targetPath,
|
|
2357
|
+
beforeSequence,
|
|
2358
|
+
limit,
|
|
2359
|
+
timeoutMs: 15_000,
|
|
2360
|
+
});
|
|
2361
|
+
const payload = {
|
|
2362
|
+
command: "session search",
|
|
2363
|
+
targetPath,
|
|
2364
|
+
sessionId: normalizedSessionId,
|
|
2365
|
+
...result,
|
|
2366
|
+
nextBeforeSequence: result.nextBeforeSequence || null,
|
|
2367
|
+
};
|
|
2368
|
+
if (shouldEmitJson(options, command)) {
|
|
2369
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (!result.ok) {
|
|
2373
|
+
throw new Error(`Session search failed (${result.reason || "unknown"}).`);
|
|
2374
|
+
}
|
|
2375
|
+
for (const item of result.results || []) {
|
|
2376
|
+
const event = item.event || {};
|
|
2377
|
+
const sequence = item.sequenceId ? `#${item.sequenceId}` : "";
|
|
2378
|
+
const snippet = normalizeString(item.snippet);
|
|
2379
|
+
console.log(`${sequence} ${formatEventLine(event)}${snippet ? ` | ${snippet}` : ""}`);
|
|
2380
|
+
}
|
|
2381
|
+
if (result.hasMore && result.nextBeforeSequence) {
|
|
2382
|
+
console.log(pc.gray(`More results before sequence ${result.nextBeforeSequence}.`));
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
|
|
1974
2386
|
session
|
|
1975
2387
|
.command("sync <sessionId>")
|
|
1976
2388
|
.description(
|
|
@@ -2303,6 +2715,11 @@ export function registerSessionCommand(program) {
|
|
|
2303
2715
|
"json",
|
|
2304
2716
|
)
|
|
2305
2717
|
.option("--out <file>", "Write to file instead of stdout")
|
|
2718
|
+
.option(
|
|
2719
|
+
"--remote",
|
|
2720
|
+
"Hydrate from the SentinelLayer API before exporting and include remote message actions",
|
|
2721
|
+
)
|
|
2722
|
+
.option("--no-actions", "Do not include remote message actions/replies/reactions")
|
|
2306
2723
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
2307
2724
|
.action(async (sessionId, options) => {
|
|
2308
2725
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -2314,6 +2731,21 @@ export function registerSessionCommand(program) {
|
|
|
2314
2731
|
if (format !== "json" && format !== "ndjson") {
|
|
2315
2732
|
throw new Error(`--format must be 'json' or 'ndjson' (received '${format}').`);
|
|
2316
2733
|
}
|
|
2734
|
+
let hydration = null;
|
|
2735
|
+
let remoteActions = null;
|
|
2736
|
+
if (options.remote) {
|
|
2737
|
+
hydration = await hydrateSessionFromRemote({
|
|
2738
|
+
sessionId: normalizedSessionId,
|
|
2739
|
+
targetPath,
|
|
2740
|
+
}).catch((error) => ({ ok: false, reason: error?.message || "hydrate_failed" }));
|
|
2741
|
+
if (options.actions !== false) {
|
|
2742
|
+
remoteActions = await listSessionMessageActions(normalizedSessionId, {
|
|
2743
|
+
targetPath,
|
|
2744
|
+
limit: 500,
|
|
2745
|
+
timeoutMs: 15_000,
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2317
2749
|
|
|
2318
2750
|
const sessionPayload = await getSession(normalizedSessionId, { targetPath });
|
|
2319
2751
|
if (!sessionPayload) {
|
|
@@ -2334,9 +2766,13 @@ export function registerSessionCommand(program) {
|
|
|
2334
2766
|
limit: 5_000,
|
|
2335
2767
|
}),
|
|
2336
2768
|
]);
|
|
2769
|
+
const actionEvents = remoteActions?.ok
|
|
2770
|
+
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
2771
|
+
: [];
|
|
2772
|
+
const exportEvents = mergeSessionActionEvents(events, actionEvents);
|
|
2337
2773
|
const stats = computeTranscriptStats({
|
|
2338
2774
|
sessionMeta: sessionPayload,
|
|
2339
|
-
events,
|
|
2775
|
+
events: exportEvents,
|
|
2340
2776
|
});
|
|
2341
2777
|
const participants = buildSessionParticipants({
|
|
2342
2778
|
statsAgents: stats.agents,
|
|
@@ -2351,7 +2787,10 @@ export function registerSessionCommand(program) {
|
|
|
2351
2787
|
for (const participant of participants) {
|
|
2352
2788
|
lines.push(JSON.stringify({ kind: "participant", value: participant }));
|
|
2353
2789
|
}
|
|
2354
|
-
for (const
|
|
2790
|
+
for (const action of remoteActions?.actions || []) {
|
|
2791
|
+
lines.push(JSON.stringify({ kind: "action", value: action }));
|
|
2792
|
+
}
|
|
2793
|
+
for (const event of exportEvents) lines.push(JSON.stringify({ kind: "event", value: event }));
|
|
2355
2794
|
for (const task of tasks.tasks || []) lines.push(JSON.stringify({ kind: "task", value: task }));
|
|
2356
2795
|
output = `${lines.join("\n")}\n`;
|
|
2357
2796
|
} else {
|
|
@@ -2362,14 +2801,31 @@ export function registerSessionCommand(program) {
|
|
|
2362
2801
|
session: sessionPayload,
|
|
2363
2802
|
agents,
|
|
2364
2803
|
participants,
|
|
2365
|
-
|
|
2804
|
+
actions: remoteActions?.actions || [],
|
|
2805
|
+
actionProjection: remoteActions?.projection || null,
|
|
2806
|
+
actionEvents,
|
|
2807
|
+
events: exportEvents,
|
|
2366
2808
|
tasks: tasks.tasks || [],
|
|
2809
|
+
remote: {
|
|
2810
|
+
hydration,
|
|
2811
|
+
actions: remoteActions
|
|
2812
|
+
? {
|
|
2813
|
+
ok: Boolean(remoteActions.ok),
|
|
2814
|
+
reason: remoteActions.reason || "",
|
|
2815
|
+
count: Array.isArray(remoteActions.actions) ? remoteActions.actions.length : 0,
|
|
2816
|
+
syntheticEventCount: actionEvents.length,
|
|
2817
|
+
}
|
|
2818
|
+
: null,
|
|
2819
|
+
},
|
|
2367
2820
|
counts: {
|
|
2368
2821
|
agents: participants.length,
|
|
2369
2822
|
participants: participants.length,
|
|
2370
2823
|
derivedAgents: stats.agents.length,
|
|
2371
2824
|
registeredAgents: agents.length,
|
|
2372
|
-
events:
|
|
2825
|
+
events: exportEvents.length,
|
|
2826
|
+
rawEvents: events.length,
|
|
2827
|
+
actions: Array.isArray(remoteActions?.actions) ? remoteActions.actions.length : 0,
|
|
2828
|
+
actionEvents: actionEvents.length,
|
|
2373
2829
|
tasks: (tasks.tasks || []).length,
|
|
2374
2830
|
},
|
|
2375
2831
|
totals: stats.totals,
|
|
@@ -2386,7 +2842,7 @@ export function registerSessionCommand(program) {
|
|
|
2386
2842
|
await fsp.writeFile(outPath, output, "utf-8");
|
|
2387
2843
|
console.log(
|
|
2388
2844
|
pc.gray(
|
|
2389
|
-
`Exported ${
|
|
2845
|
+
`Exported ${exportEvents.length} events / ${participants.length} participants (${agents.length} registered agents) / ${
|
|
2390
2846
|
(tasks.tasks || []).length
|
|
2391
2847
|
} tasks → ${outPath}`,
|
|
2392
2848
|
),
|
|
@@ -2410,6 +2866,7 @@ export function registerSessionCommand(program) {
|
|
|
2410
2866
|
"--remote",
|
|
2411
2867
|
"Hydrate from the SentinelLayer API before rendering (pulls web-posted messages into the local NDJSON)",
|
|
2412
2868
|
)
|
|
2869
|
+
.option("--no-actions", "Do not include remote message actions/replies/reactions")
|
|
2413
2870
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
2414
2871
|
.option("--json", "Emit machine-readable output")
|
|
2415
2872
|
.action(async (sessionId, options, command) => {
|
|
@@ -2421,11 +2878,19 @@ export function registerSessionCommand(program) {
|
|
|
2421
2878
|
const emitJson = shouldEmitJson(options, command);
|
|
2422
2879
|
|
|
2423
2880
|
let hydration = null;
|
|
2881
|
+
let remoteActions = null;
|
|
2424
2882
|
if (options.remote) {
|
|
2425
2883
|
hydration = await hydrateSessionFromRemote({
|
|
2426
2884
|
sessionId: normalizedSessionId,
|
|
2427
2885
|
targetPath,
|
|
2428
2886
|
}).catch((error) => ({ ok: false, reason: error?.message || "hydrate_failed" }));
|
|
2887
|
+
if (options.actions !== false) {
|
|
2888
|
+
remoteActions = await listSessionMessageActions(normalizedSessionId, {
|
|
2889
|
+
targetPath,
|
|
2890
|
+
limit: 500,
|
|
2891
|
+
timeoutMs: 15_000,
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2429
2894
|
}
|
|
2430
2895
|
|
|
2431
2896
|
const sessionPayload = await getSession(normalizedSessionId, { targetPath });
|
|
@@ -2437,6 +2902,10 @@ export function registerSessionCommand(program) {
|
|
|
2437
2902
|
listAgents(normalizedSessionId, { targetPath, includeInactive: true }),
|
|
2438
2903
|
readStream(normalizedSessionId, { targetPath, tail: 0 }),
|
|
2439
2904
|
]);
|
|
2905
|
+
const actionEvents = remoteActions?.ok
|
|
2906
|
+
? buildSessionActionEvents(normalizedSessionId, remoteActions.actions)
|
|
2907
|
+
: [];
|
|
2908
|
+
const transcriptEvents = mergeSessionActionEvents(events, actionEvents);
|
|
2440
2909
|
|
|
2441
2910
|
// Pull GitHub/Google avatar + display name from the active auth
|
|
2442
2911
|
// session so any human-id seen in the stream renders with the
|
|
@@ -2472,7 +2941,7 @@ export function registerSessionCommand(program) {
|
|
|
2472
2941
|
createdAt: sessionPayload.createdAt,
|
|
2473
2942
|
status: sessionPayload.status,
|
|
2474
2943
|
},
|
|
2475
|
-
events,
|
|
2944
|
+
events: transcriptEvents,
|
|
2476
2945
|
agents,
|
|
2477
2946
|
speakerProfiles,
|
|
2478
2947
|
options: {
|
|
@@ -2497,7 +2966,10 @@ export function registerSessionCommand(program) {
|
|
|
2497
2966
|
sessionId: normalizedSessionId,
|
|
2498
2967
|
outPath,
|
|
2499
2968
|
bytes: Buffer.byteLength(markdown, "utf-8"),
|
|
2500
|
-
eventCount:
|
|
2969
|
+
eventCount: transcriptEvents.length,
|
|
2970
|
+
rawEventCount: events.length,
|
|
2971
|
+
actionCount: Array.isArray(remoteActions?.actions) ? remoteActions.actions.length : 0,
|
|
2972
|
+
actionEventCount: actionEvents.length,
|
|
2501
2973
|
agentCount: participants.length,
|
|
2502
2974
|
participantCount: participants.length,
|
|
2503
2975
|
derivedAgentCount: stats.agents.length,
|
|
@@ -2506,7 +2978,18 @@ export function registerSessionCommand(program) {
|
|
|
2506
2978
|
sessionLiveSeconds: stats.sessionLiveSeconds,
|
|
2507
2979
|
sentiActions: stats.sentiActions,
|
|
2508
2980
|
totals: stats.totals,
|
|
2509
|
-
remote:
|
|
2981
|
+
remote: {
|
|
2982
|
+
hydration,
|
|
2983
|
+
actions: remoteActions
|
|
2984
|
+
? {
|
|
2985
|
+
ok: Boolean(remoteActions.ok),
|
|
2986
|
+
reason: remoteActions.reason || "",
|
|
2987
|
+
count: Array.isArray(remoteActions.actions) ? remoteActions.actions.length : 0,
|
|
2988
|
+
syntheticEventCount: actionEvents.length,
|
|
2989
|
+
projection: remoteActions.projection || null,
|
|
2990
|
+
}
|
|
2991
|
+
: null,
|
|
2992
|
+
},
|
|
2510
2993
|
};
|
|
2511
2994
|
if (emitJson) {
|
|
2512
2995
|
console.log(JSON.stringify(payload, null, 2));
|
|
@@ -2515,7 +2998,7 @@ export function registerSessionCommand(program) {
|
|
|
2515
2998
|
console.log(pc.bold(`Downloaded session ${normalizedSessionId} → ${outPath}`));
|
|
2516
2999
|
console.log(
|
|
2517
3000
|
pc.gray(
|
|
2518
|
-
`${
|
|
3001
|
+
`${transcriptEvents.length} events · ${participants.length} participants (${agents.length} registered agents) · actions=${payload.actionCount} · live ${stats.sessionLiveSeconds}s · senti=${stats.sentiActions} · tokens=${stats.totals.tokenTotal} · cost=$${stats.totals.costTotalUsd.toFixed(4)}`,
|
|
2519
3002
|
),
|
|
2520
3003
|
);
|
|
2521
3004
|
});
|
package/src/session/sync.js
CHANGED
|
@@ -15,6 +15,8 @@ const HUMAN_MESSAGE_LIMIT_PER_MINUTE = 10;
|
|
|
15
15
|
const HUMAN_MESSAGE_MAX_LENGTH = 2_000;
|
|
16
16
|
const HUMAN_MESSAGE_FETCH_LIMIT = 50;
|
|
17
17
|
const SESSION_EVENT_FETCH_LIMIT = 200;
|
|
18
|
+
const SESSION_ACTION_FETCH_LIMIT = 500;
|
|
19
|
+
const SESSION_SEARCH_FETCH_LIMIT = 50;
|
|
18
20
|
|
|
19
21
|
// Audit §2.9: crash-recovery contract for in-memory circuit state.
|
|
20
22
|
// Persist outbound/inbound circuit state to disk so a process restart
|
|
@@ -1238,6 +1240,347 @@ export async function pollSessionEventsBefore(
|
|
|
1238
1240
|
}
|
|
1239
1241
|
|
|
1240
1242
|
|
|
1243
|
+
export async function listSessionMessageActions(
|
|
1244
|
+
sessionId,
|
|
1245
|
+
{
|
|
1246
|
+
targetPath = process.cwd(),
|
|
1247
|
+
targetSequenceId = null,
|
|
1248
|
+
limit = SESSION_ACTION_FETCH_LIMIT,
|
|
1249
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1250
|
+
forceCircuitProbe = false,
|
|
1251
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1252
|
+
fetchImpl = fetchWithTimeout,
|
|
1253
|
+
nowMs = Date.now,
|
|
1254
|
+
} = {}
|
|
1255
|
+
) {
|
|
1256
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1257
|
+
if (!normalizedSessionId) {
|
|
1258
|
+
return {
|
|
1259
|
+
ok: false,
|
|
1260
|
+
reason: "invalid_session_id",
|
|
1261
|
+
actions: [],
|
|
1262
|
+
count: 0,
|
|
1263
|
+
projection: null,
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
1266
|
+
const normalizedNowMs = Number(nowMs()) || Date.now();
|
|
1267
|
+
if (!forceCircuitProbe && isCircuitOpen(inboundCircuit, normalizedNowMs)) {
|
|
1268
|
+
return {
|
|
1269
|
+
ok: false,
|
|
1270
|
+
reason: "circuit_breaker_open",
|
|
1271
|
+
actions: [],
|
|
1272
|
+
count: 0,
|
|
1273
|
+
projection: null,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
let session = null;
|
|
1278
|
+
try {
|
|
1279
|
+
session = await resolveAuthSession({
|
|
1280
|
+
cwd: targetPath,
|
|
1281
|
+
env: process.env,
|
|
1282
|
+
autoRotate: false,
|
|
1283
|
+
});
|
|
1284
|
+
} catch {
|
|
1285
|
+
return { ok: false, reason: "no_session", actions: [], count: 0, projection: null };
|
|
1286
|
+
}
|
|
1287
|
+
if (!session || !session.token) {
|
|
1288
|
+
return { ok: false, reason: "not_authenticated", actions: [], count: 0, projection: null };
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const apiBaseUrl = resolveApiBaseUrl(session);
|
|
1292
|
+
const query = new URLSearchParams();
|
|
1293
|
+
const normalizedTargetSequence = Number(targetSequenceId);
|
|
1294
|
+
if (Number.isFinite(normalizedTargetSequence) && normalizedTargetSequence > 0) {
|
|
1295
|
+
query.set("targetSequenceId", String(Math.floor(normalizedTargetSequence)));
|
|
1296
|
+
}
|
|
1297
|
+
query.set(
|
|
1298
|
+
"limit",
|
|
1299
|
+
String(Math.max(1, Math.min(SESSION_ACTION_FETCH_LIMIT, normalizePositiveInteger(limit, 200))))
|
|
1300
|
+
);
|
|
1301
|
+
const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/actions?${query.toString()}`;
|
|
1302
|
+
|
|
1303
|
+
try {
|
|
1304
|
+
const response = await fetchImpl(
|
|
1305
|
+
endpoint,
|
|
1306
|
+
{
|
|
1307
|
+
method: "GET",
|
|
1308
|
+
headers: { Authorization: `Bearer ${session.token}` },
|
|
1309
|
+
},
|
|
1310
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
|
|
1311
|
+
);
|
|
1312
|
+
if (!response || !response.ok) {
|
|
1313
|
+
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1314
|
+
return {
|
|
1315
|
+
ok: false,
|
|
1316
|
+
reason: `api_${response ? response.status : "no_response"}`,
|
|
1317
|
+
actions: [],
|
|
1318
|
+
count: 0,
|
|
1319
|
+
projection: null,
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
const payload = await response.json().catch(() => ({}));
|
|
1323
|
+
recordCircuitSuccess(inboundCircuit);
|
|
1324
|
+
const actions = Array.isArray(payload?.actions) ? payload.actions : [];
|
|
1325
|
+
return {
|
|
1326
|
+
ok: true,
|
|
1327
|
+
reason: "",
|
|
1328
|
+
sessionId: normalizeString(payload?.sessionId) || normalizedSessionId,
|
|
1329
|
+
actions,
|
|
1330
|
+
count: Number(payload?.count ?? actions.length) || actions.length,
|
|
1331
|
+
projection: payload?.projection && typeof payload.projection === "object"
|
|
1332
|
+
? payload.projection
|
|
1333
|
+
: null,
|
|
1334
|
+
};
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1337
|
+
return {
|
|
1338
|
+
ok: false,
|
|
1339
|
+
reason: normalizeString(error?.message) || "actions_read_failed",
|
|
1340
|
+
actions: [],
|
|
1341
|
+
count: 0,
|
|
1342
|
+
projection: null,
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export async function createSessionMessageAction(
|
|
1348
|
+
sessionId,
|
|
1349
|
+
{
|
|
1350
|
+
actionType,
|
|
1351
|
+
targetPath = process.cwd(),
|
|
1352
|
+
targetSequenceId = null,
|
|
1353
|
+
targetCursor = "",
|
|
1354
|
+
note = "",
|
|
1355
|
+
metadata = {},
|
|
1356
|
+
idempotencyKey = "",
|
|
1357
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1358
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1359
|
+
fetchImpl = fetchWithTimeout,
|
|
1360
|
+
nowMs = Date.now,
|
|
1361
|
+
} = {}
|
|
1362
|
+
) {
|
|
1363
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1364
|
+
const normalizedActionType = normalizeString(actionType).toLowerCase();
|
|
1365
|
+
if (!normalizedSessionId || !normalizedActionType) {
|
|
1366
|
+
return { ok: false, reason: "invalid_input", action: null };
|
|
1367
|
+
}
|
|
1368
|
+
if (String(process.env.SENTINELAYER_SKIP_REMOTE_SYNC || "").trim() === "1") {
|
|
1369
|
+
return { ok: false, reason: "remote_sync_disabled_env", action: null };
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const normalizedNowMs = Number(nowMs()) || Date.now();
|
|
1373
|
+
if (isCircuitOpen(outboundCircuit, normalizedNowMs)) {
|
|
1374
|
+
return { ok: false, reason: "circuit_breaker_open", action: null };
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
let session = null;
|
|
1378
|
+
try {
|
|
1379
|
+
session = await resolveAuthSession({
|
|
1380
|
+
cwd: targetPath,
|
|
1381
|
+
env: process.env,
|
|
1382
|
+
autoRotate: false,
|
|
1383
|
+
});
|
|
1384
|
+
} catch {
|
|
1385
|
+
return { ok: false, reason: "no_session", action: null };
|
|
1386
|
+
}
|
|
1387
|
+
if (!session || !session.token) {
|
|
1388
|
+
return { ok: false, reason: "not_authenticated", action: null };
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const apiBaseUrl = resolveApiBaseUrl(session);
|
|
1392
|
+
const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/actions`;
|
|
1393
|
+
const body = {
|
|
1394
|
+
actionType: normalizedActionType,
|
|
1395
|
+
metadata: metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : {},
|
|
1396
|
+
};
|
|
1397
|
+
const normalizedTargetSequence = Number(targetSequenceId);
|
|
1398
|
+
if (Number.isFinite(normalizedTargetSequence) && normalizedTargetSequence > 0) {
|
|
1399
|
+
body.targetSequenceId = Math.floor(normalizedTargetSequence);
|
|
1400
|
+
}
|
|
1401
|
+
const normalizedTargetCursor = normalizeString(targetCursor);
|
|
1402
|
+
if (normalizedTargetCursor) {
|
|
1403
|
+
body.targetCursor = normalizedTargetCursor;
|
|
1404
|
+
}
|
|
1405
|
+
const normalizedNote = normalizeString(note);
|
|
1406
|
+
if (normalizedNote) {
|
|
1407
|
+
body.note = normalizedNote;
|
|
1408
|
+
}
|
|
1409
|
+
const normalizedIdempotencyKey = normalizeString(idempotencyKey);
|
|
1410
|
+
if (normalizedIdempotencyKey) {
|
|
1411
|
+
body.idempotencyKey = normalizedIdempotencyKey;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
try {
|
|
1415
|
+
const response = await fetchImpl(
|
|
1416
|
+
endpoint,
|
|
1417
|
+
{
|
|
1418
|
+
method: "POST",
|
|
1419
|
+
headers: {
|
|
1420
|
+
"Content-Type": "application/json",
|
|
1421
|
+
Authorization: `Bearer ${session.token}`,
|
|
1422
|
+
},
|
|
1423
|
+
body: JSON.stringify(body),
|
|
1424
|
+
},
|
|
1425
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
|
|
1426
|
+
);
|
|
1427
|
+
if (!response || !response.ok) {
|
|
1428
|
+
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1429
|
+
return {
|
|
1430
|
+
ok: false,
|
|
1431
|
+
reason: `api_${response ? response.status : "no_response"}`,
|
|
1432
|
+
action: null,
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
const payload = await response.json().catch(() => ({}));
|
|
1436
|
+
recordCircuitSuccess(outboundCircuit);
|
|
1437
|
+
return {
|
|
1438
|
+
ok: Boolean(payload?.ok ?? true),
|
|
1439
|
+
reason: "",
|
|
1440
|
+
duplicate: Boolean(payload?.duplicate),
|
|
1441
|
+
action: payload?.action && typeof payload.action === "object" ? payload.action : null,
|
|
1442
|
+
};
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1445
|
+
return {
|
|
1446
|
+
ok: false,
|
|
1447
|
+
reason: normalizeString(error?.message) || "action_write_failed",
|
|
1448
|
+
action: null,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
export async function searchSessionEvents(
|
|
1454
|
+
sessionId,
|
|
1455
|
+
{
|
|
1456
|
+
query,
|
|
1457
|
+
targetPath = process.cwd(),
|
|
1458
|
+
beforeSequence = null,
|
|
1459
|
+
limit = 20,
|
|
1460
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1461
|
+
forceCircuitProbe = false,
|
|
1462
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1463
|
+
fetchImpl = fetchWithTimeout,
|
|
1464
|
+
nowMs = Date.now,
|
|
1465
|
+
} = {}
|
|
1466
|
+
) {
|
|
1467
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1468
|
+
const normalizedQuery = normalizeString(query);
|
|
1469
|
+
if (!normalizedSessionId || normalizedQuery.length < 2) {
|
|
1470
|
+
return {
|
|
1471
|
+
ok: false,
|
|
1472
|
+
reason: "invalid_input",
|
|
1473
|
+
query: normalizedQuery,
|
|
1474
|
+
results: [],
|
|
1475
|
+
count: 0,
|
|
1476
|
+
hasMore: false,
|
|
1477
|
+
nextBeforeSequence: null,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
const normalizedNowMs = Number(nowMs()) || Date.now();
|
|
1481
|
+
if (!forceCircuitProbe && isCircuitOpen(inboundCircuit, normalizedNowMs)) {
|
|
1482
|
+
return {
|
|
1483
|
+
ok: false,
|
|
1484
|
+
reason: "circuit_breaker_open",
|
|
1485
|
+
query: normalizedQuery,
|
|
1486
|
+
results: [],
|
|
1487
|
+
count: 0,
|
|
1488
|
+
hasMore: false,
|
|
1489
|
+
nextBeforeSequence: null,
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
let session = null;
|
|
1494
|
+
try {
|
|
1495
|
+
session = await resolveAuthSession({
|
|
1496
|
+
cwd: targetPath,
|
|
1497
|
+
env: process.env,
|
|
1498
|
+
autoRotate: false,
|
|
1499
|
+
});
|
|
1500
|
+
} catch {
|
|
1501
|
+
return {
|
|
1502
|
+
ok: false,
|
|
1503
|
+
reason: "no_session",
|
|
1504
|
+
query: normalizedQuery,
|
|
1505
|
+
results: [],
|
|
1506
|
+
count: 0,
|
|
1507
|
+
hasMore: false,
|
|
1508
|
+
nextBeforeSequence: null,
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
if (!session || !session.token) {
|
|
1512
|
+
return {
|
|
1513
|
+
ok: false,
|
|
1514
|
+
reason: "not_authenticated",
|
|
1515
|
+
query: normalizedQuery,
|
|
1516
|
+
results: [],
|
|
1517
|
+
count: 0,
|
|
1518
|
+
hasMore: false,
|
|
1519
|
+
nextBeforeSequence: null,
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
const apiBaseUrl = resolveApiBaseUrl(session);
|
|
1524
|
+
const queryParams = new URLSearchParams();
|
|
1525
|
+
queryParams.set("q", normalizedQuery);
|
|
1526
|
+
const normalizedBeforeSequence = Number(beforeSequence);
|
|
1527
|
+
if (Number.isFinite(normalizedBeforeSequence) && normalizedBeforeSequence > 0) {
|
|
1528
|
+
queryParams.set("beforeSequence", String(Math.floor(normalizedBeforeSequence)));
|
|
1529
|
+
}
|
|
1530
|
+
queryParams.set(
|
|
1531
|
+
"limit",
|
|
1532
|
+
String(Math.max(1, Math.min(SESSION_SEARCH_FETCH_LIMIT, normalizePositiveInteger(limit, 20))))
|
|
1533
|
+
);
|
|
1534
|
+
const endpoint = `${apiBaseUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/events/search?${queryParams.toString()}`;
|
|
1535
|
+
|
|
1536
|
+
try {
|
|
1537
|
+
const response = await fetchImpl(
|
|
1538
|
+
endpoint,
|
|
1539
|
+
{
|
|
1540
|
+
method: "GET",
|
|
1541
|
+
headers: { Authorization: `Bearer ${session.token}` },
|
|
1542
|
+
},
|
|
1543
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)
|
|
1544
|
+
);
|
|
1545
|
+
if (!response || !response.ok) {
|
|
1546
|
+
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1547
|
+
return {
|
|
1548
|
+
ok: false,
|
|
1549
|
+
reason: `api_${response ? response.status : "no_response"}`,
|
|
1550
|
+
query: normalizedQuery,
|
|
1551
|
+
results: [],
|
|
1552
|
+
count: 0,
|
|
1553
|
+
hasMore: false,
|
|
1554
|
+
nextBeforeSequence: null,
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
const payload = await response.json().catch(() => ({}));
|
|
1558
|
+
recordCircuitSuccess(inboundCircuit);
|
|
1559
|
+
const results = Array.isArray(payload?.results) ? payload.results : [];
|
|
1560
|
+
return {
|
|
1561
|
+
ok: true,
|
|
1562
|
+
reason: "",
|
|
1563
|
+
query: normalizeString(payload?.query) || normalizedQuery,
|
|
1564
|
+
results,
|
|
1565
|
+
count: Number(payload?.count ?? results.length) || results.length,
|
|
1566
|
+
hasMore: Boolean(payload?.has_more ?? payload?.hasMore),
|
|
1567
|
+
nextBeforeSequence: Number(payload?.next_before_sequence ?? payload?.nextBeforeSequence) || null,
|
|
1568
|
+
};
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1571
|
+
return {
|
|
1572
|
+
ok: false,
|
|
1573
|
+
reason: normalizeString(error?.message) || "search_failed",
|
|
1574
|
+
query: normalizedQuery,
|
|
1575
|
+
results: [],
|
|
1576
|
+
count: 0,
|
|
1577
|
+
hasMore: false,
|
|
1578
|
+
nextBeforeSequence: null,
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
|
|
1241
1584
|
/**
|
|
1242
1585
|
* List sessions owned by the active user via `GET /api/v1/sessions`.
|
|
1243
1586
|
*
|