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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.11.4",
3
+ "version": "0.12.0",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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 events = dedupeSessionEvents(displayEvents).slice(-tail);
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 event of events) lines.push(JSON.stringify({ kind: "event", value: event }));
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
- events,
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: events.length,
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 ${events.length} events / ${participants.length} participants (${agents.length} registered agents) / ${
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: events.length,
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: hydration,
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
- `${events.length} events · ${participants.length} participants (${agents.length} registered agents) · live ${stats.sessionLiveSeconds}s · senti=${stats.sentiActions} · tokens=${stats.totals.tokenTotal} · cost=$${stats.totals.costTotalUsd.toFixed(4)}`,
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
  });
@@ -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
  *
@@ -41,6 +41,9 @@ const TRANSCRIPT_EVENT_KINDS = new Set([
41
41
  "session_message",
42
42
  "session_say",
43
43
  "agent_response",
44
+ "session_action",
45
+ "session_reply",
46
+ "session_reaction",
44
47
  "session_usage",
45
48
  "human_relay",
46
49
  "agent_join",