sentinelayer-cli 0.20.0 → 0.21.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 +204 -6
- package/src/legacy-cli.js +2 -0
- package/src/session/sync.js +117 -0
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { spawn as defaultSpawn } from "node:child_process";
|
|
6
7
|
|
|
7
8
|
import pc from "picocolors";
|
|
8
9
|
|
|
@@ -63,6 +64,7 @@ import {
|
|
|
63
64
|
import { readSessionPreview } from "../session/preview.js";
|
|
64
65
|
import {
|
|
65
66
|
createSessionMessageAction,
|
|
67
|
+
fetchSessionPinnedMessages,
|
|
66
68
|
listSessionMessageActions,
|
|
67
69
|
listSessionsFromApi,
|
|
68
70
|
probeSessionAccess,
|
|
@@ -1537,6 +1539,102 @@ export function shouldBlockImplicitCliUserSessionSay(identity = {}) {
|
|
|
1537
1539
|
return identity?.source === "fallback" && normalizeString(identity?.agentId) === "cli-user";
|
|
1538
1540
|
}
|
|
1539
1541
|
|
|
1542
|
+
/**
|
|
1543
|
+
* Wake hook for `session listen --wake "<command>"`. This is the reusable
|
|
1544
|
+
* notify->resume bridge: when the listener emits an event addressed to this
|
|
1545
|
+
* agent (or broadcast — including low-noise actions like ack/like), it runs a
|
|
1546
|
+
* host command so the host can resume/wake its agent. The event JSON is piped
|
|
1547
|
+
* to the command's stdin and key fields are exposed as SL_WAKE_* env vars.
|
|
1548
|
+
*
|
|
1549
|
+
* Bursts are coalesced: if a wake is already running, the latest event is
|
|
1550
|
+
* queued and fired once when the current one finishes, so a flood of activity
|
|
1551
|
+
* triggers one trailing wake instead of a storm of processes.
|
|
1552
|
+
*/
|
|
1553
|
+
export function createSessionWakeRunner({
|
|
1554
|
+
command,
|
|
1555
|
+
sessionId,
|
|
1556
|
+
agentId,
|
|
1557
|
+
emit = () => {},
|
|
1558
|
+
spawnImpl = defaultSpawn,
|
|
1559
|
+
} = {}) {
|
|
1560
|
+
const wakeCommand = normalizeString(command);
|
|
1561
|
+
let busy = false;
|
|
1562
|
+
let pending = null;
|
|
1563
|
+
|
|
1564
|
+
const run = (event) => {
|
|
1565
|
+
if (!wakeCommand) return;
|
|
1566
|
+
if (busy) {
|
|
1567
|
+
pending = event ?? {};
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
busy = true;
|
|
1571
|
+
const env = {
|
|
1572
|
+
...process.env,
|
|
1573
|
+
SL_WAKE_SESSION_ID: normalizeString(sessionId),
|
|
1574
|
+
SL_WAKE_AGENT_ID: normalizeString(agentId),
|
|
1575
|
+
SL_WAKE_EVENT_TYPE: normalizeString(event?.event),
|
|
1576
|
+
SL_WAKE_EVENT_CURSOR: normalizeString(event?.cursor),
|
|
1577
|
+
SL_WAKE_EVENT_SEQUENCE: String(event?.sequenceId ?? event?.sequence_id ?? ""),
|
|
1578
|
+
SL_WAKE_ACTOR_ID: normalizeString(event?.agent?.id || event?.agentId),
|
|
1579
|
+
};
|
|
1580
|
+
let child;
|
|
1581
|
+
try {
|
|
1582
|
+
child = spawnImpl(wakeCommand, { shell: true, env, stdio: ["pipe", "ignore", "ignore"] });
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
busy = false;
|
|
1585
|
+
emit({ status: "error", reason: normalizeString(error?.message) || "spawn_failed" });
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
emit({
|
|
1589
|
+
status: "fired",
|
|
1590
|
+
eventType: env.SL_WAKE_EVENT_TYPE,
|
|
1591
|
+
cursor: env.SL_WAKE_EVENT_CURSOR,
|
|
1592
|
+
actorId: env.SL_WAKE_ACTOR_ID,
|
|
1593
|
+
});
|
|
1594
|
+
try {
|
|
1595
|
+
if (child && child.stdin) {
|
|
1596
|
+
child.stdin.write(JSON.stringify(event ?? {}));
|
|
1597
|
+
child.stdin.end();
|
|
1598
|
+
}
|
|
1599
|
+
} catch {
|
|
1600
|
+
// Broken pipe (command ignored stdin) is non-fatal for a wake hook.
|
|
1601
|
+
}
|
|
1602
|
+
const finish = (reason) => {
|
|
1603
|
+
busy = false;
|
|
1604
|
+
if (reason) emit({ status: "error", reason });
|
|
1605
|
+
const next = pending;
|
|
1606
|
+
pending = null;
|
|
1607
|
+
if (next !== null) run(next);
|
|
1608
|
+
};
|
|
1609
|
+
if (child && typeof child.on === "function") {
|
|
1610
|
+
child.on("error", (error) => finish(normalizeString(error?.message) || "wake_failed"));
|
|
1611
|
+
child.on("exit", (code) => finish(code && code !== 0 ? `exit_${code}` : ""));
|
|
1612
|
+
} else {
|
|
1613
|
+
finish("");
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
return { trigger: run, hasCommand: Boolean(wakeCommand) };
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Message actions (ack/like/dislike/reply/view/working_on) must be authored by
|
|
1621
|
+
// a concrete agent identity. The CLI's bare `cli-user` default is a reserved
|
|
1622
|
+
// label the API rejects (api_422), so treat it as "unset" and resolve the real
|
|
1623
|
+
// agent the same way `session say` does (explicit --agent > SENTINELAYER_AGENT_ID
|
|
1624
|
+
// > the single joined agent). Returns the resolved identity; callers should use
|
|
1625
|
+
// shouldBlockImplicitCliUserSessionSay() to refuse the implicit cli-user
|
|
1626
|
+
// fallback before sending a request that is guaranteed to fail.
|
|
1627
|
+
export async function resolveMessageActionIdentity({
|
|
1628
|
+
sessionId,
|
|
1629
|
+
optionAgent = "",
|
|
1630
|
+
targetPath = process.cwd(),
|
|
1631
|
+
env = process.env,
|
|
1632
|
+
} = {}) {
|
|
1633
|
+
const explicit = normalizeString(optionAgent);
|
|
1634
|
+
const agentSeed = explicit && explicit.toLowerCase() !== "cli-user" ? explicit : "";
|
|
1635
|
+
return resolveSessionSayIdentity({ sessionId, agentId: agentSeed, targetPath, env });
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1540
1638
|
async function ensureSessionSayAgentRegistered(
|
|
1541
1639
|
sessionId,
|
|
1542
1640
|
agent = {},
|
|
@@ -2879,7 +2977,23 @@ export function registerSessionCommand(program) {
|
|
|
2879
2977
|
}
|
|
2880
2978
|
await ensureLocalSessionForRemoteCommand(normalizedSessionId, { targetPath });
|
|
2881
2979
|
const note = normalizeString(noteOverride) || normalizeString(options.note);
|
|
2882
|
-
|
|
2980
|
+
// Resolve the authoring agent. The bare `cli-user` default is rejected by
|
|
2981
|
+
// the API (api_422); resolveMessageActionIdentity treats it as unset and
|
|
2982
|
+
// falls back to the joined agent. If no concrete identity resolves, fail
|
|
2983
|
+
// with actionable guidance instead of firing a request guaranteed to 422.
|
|
2984
|
+
const identity = await resolveMessageActionIdentity({
|
|
2985
|
+
sessionId: normalizedSessionId,
|
|
2986
|
+
optionAgent: options.agent,
|
|
2987
|
+
targetPath,
|
|
2988
|
+
env: process.env,
|
|
2989
|
+
});
|
|
2990
|
+
if (shouldBlockImplicitCliUserSessionSay(identity)) {
|
|
2991
|
+
throw new Error(
|
|
2992
|
+
identity.identityWarning ||
|
|
2993
|
+
`${commandName} requires an agent identity; pass --agent <id>, set SENTINELAYER_AGENT_ID, or run session join --agent <id> first.`,
|
|
2994
|
+
);
|
|
2995
|
+
}
|
|
2996
|
+
const agentId = identity.agentId;
|
|
2883
2997
|
const idempotencyKey =
|
|
2884
2998
|
normalizeString(options.idempotencyKey) ||
|
|
2885
2999
|
defaultActionIdempotencyKey({
|
|
@@ -2970,7 +3084,7 @@ export function registerSessionCommand(program) {
|
|
|
2970
3084
|
.option("--target-cursor <cursor>", "Target event cursor")
|
|
2971
3085
|
.option("--target-action-id <uuid>", "Target a threaded reply/action by action UUID")
|
|
2972
3086
|
.option("--note <text>", "Optional action note or reply body")
|
|
2973
|
-
.option("--agent <id>", "Agent id
|
|
3087
|
+
.option("--agent <id>", "Agent id authoring the action (defaults to the joined session agent)")
|
|
2974
3088
|
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
2975
3089
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
2976
3090
|
.option("--json", "Emit machine-readable output")
|
|
@@ -2984,7 +3098,7 @@ export function registerSessionCommand(program) {
|
|
|
2984
3098
|
.option("--target-sequence <n>", "Target event sequence id")
|
|
2985
3099
|
.option("--target-cursor <cursor>", "Target event cursor")
|
|
2986
3100
|
.option("--target-action-id <uuid>", "Target a threaded reply/action by action UUID")
|
|
2987
|
-
.option("--agent <id>", "Agent id
|
|
3101
|
+
.option("--agent <id>", "Agent id authoring the action (defaults to the joined session agent)")
|
|
2988
3102
|
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
2989
3103
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
2990
3104
|
.option("--json", "Emit machine-readable output")
|
|
@@ -3005,7 +3119,7 @@ export function registerSessionCommand(program) {
|
|
|
3005
3119
|
session
|
|
3006
3120
|
.command("reply <sessionId> <targetSequenceId> <message...>")
|
|
3007
3121
|
.description("Reply to a target session event using the message-action channel")
|
|
3008
|
-
.option("--agent <id>", "Agent id
|
|
3122
|
+
.option("--agent <id>", "Agent id authoring the action (defaults to the joined session agent)")
|
|
3009
3123
|
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
3010
3124
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3011
3125
|
.option("--json", "Emit machine-readable output")
|
|
@@ -3025,7 +3139,7 @@ export function registerSessionCommand(program) {
|
|
|
3025
3139
|
session
|
|
3026
3140
|
.command("comment <sessionId> <targetSequenceId> <message...>")
|
|
3027
3141
|
.description("Alias for `session reply`; add a threaded comment to a target event")
|
|
3028
|
-
.option("--agent <id>", "Agent id
|
|
3142
|
+
.option("--agent <id>", "Agent id authoring the action (defaults to the joined session agent)")
|
|
3029
3143
|
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
3030
3144
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3031
3145
|
.option("--json", "Emit machine-readable output")
|
|
@@ -3045,7 +3159,7 @@ export function registerSessionCommand(program) {
|
|
|
3045
3159
|
session
|
|
3046
3160
|
.command("view <sessionId> <targetSequenceId>")
|
|
3047
3161
|
.description("Manually backfill a read receipt for a target session event")
|
|
3048
|
-
.option("--agent <id>", "Agent id
|
|
3162
|
+
.option("--agent <id>", "Agent id authoring the action (defaults to the joined session agent)")
|
|
3049
3163
|
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
3050
3164
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
3051
3165
|
.option("--json", "Emit machine-readable output")
|
|
@@ -3060,6 +3174,57 @@ export function registerSessionCommand(program) {
|
|
|
3060
3174
|
});
|
|
3061
3175
|
});
|
|
3062
3176
|
|
|
3177
|
+
session
|
|
3178
|
+
.command("pins <sessionId>")
|
|
3179
|
+
.description("List the session's pinned messages with their content so agents can read them")
|
|
3180
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
3181
|
+
.option("--json", "Emit machine-readable output")
|
|
3182
|
+
.action(async (sessionId, options, command) => {
|
|
3183
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
3184
|
+
if (!normalizedSessionId) {
|
|
3185
|
+
throw new Error("session id is required.");
|
|
3186
|
+
}
|
|
3187
|
+
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
3188
|
+
await ensureLocalSessionForRemoteCommand(normalizedSessionId, { targetPath });
|
|
3189
|
+
const result = await fetchSessionPinnedMessages(normalizedSessionId, { targetPath });
|
|
3190
|
+
if (!result.ok) {
|
|
3191
|
+
throw new Error(`Could not load pinned messages (${result.reason || "unknown"}).`);
|
|
3192
|
+
}
|
|
3193
|
+
const pinLimit = result.pinLimit || 10;
|
|
3194
|
+
const payload = {
|
|
3195
|
+
command: "session pins",
|
|
3196
|
+
sessionId: normalizedSessionId,
|
|
3197
|
+
pinLimit,
|
|
3198
|
+
count: result.count,
|
|
3199
|
+
pins: result.pins,
|
|
3200
|
+
};
|
|
3201
|
+
if (shouldEmitJson(options, command)) {
|
|
3202
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
3203
|
+
return payload;
|
|
3204
|
+
}
|
|
3205
|
+
if (!result.count) {
|
|
3206
|
+
console.log(pc.gray("No pinned messages in this session."));
|
|
3207
|
+
return payload;
|
|
3208
|
+
}
|
|
3209
|
+
console.log(pc.bold(`📌 Pinned messages (${result.count}/${pinLimit})`));
|
|
3210
|
+
for (const pin of result.pins) {
|
|
3211
|
+
const seqLabel = pin.targetSequenceId ? `#${pin.targetSequenceId}` : "(unknown sequence)";
|
|
3212
|
+
const author = pin.author || "unknown";
|
|
3213
|
+
const pinnedBy = pin.pinnedBy ? ` · pinned by ${pin.pinnedBy}` : "";
|
|
3214
|
+
const when = pin.pinnedAt ? ` · ${pin.pinnedAt}` : "";
|
|
3215
|
+
console.log("");
|
|
3216
|
+
console.log(pc.cyan(`${seqLabel} ${author}${pinnedBy}${when}`));
|
|
3217
|
+
if (pin.content) {
|
|
3218
|
+
for (const line of String(pin.content).split("\n")) {
|
|
3219
|
+
console.log(` ${line}`);
|
|
3220
|
+
}
|
|
3221
|
+
} else {
|
|
3222
|
+
console.log(pc.gray(" (no readable text content for this pinned event)"));
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
return payload;
|
|
3226
|
+
});
|
|
3227
|
+
|
|
3063
3228
|
session
|
|
3064
3229
|
.command("listen")
|
|
3065
3230
|
.description("Background-poll a session for events addressed to this agent or broadcast")
|
|
@@ -3103,6 +3268,10 @@ export function registerSessionCommand(program) {
|
|
|
3103
3268
|
.option("--from-now", "Advance the listen cursor to the latest durable event before polling")
|
|
3104
3269
|
.option("--replay", "Emit matching historical events on the first poll")
|
|
3105
3270
|
.option("--max-polls <n>", "Stop after N poll cycles (useful for tests and smoke checks)")
|
|
3271
|
+
.option(
|
|
3272
|
+
"--wake <command>",
|
|
3273
|
+
"Wake hook: run this shell command on each matched event (notify->resume bridge). Event JSON is piped to stdin; SL_WAKE_* env vars are set.",
|
|
3274
|
+
)
|
|
3106
3275
|
.action(async (options) => {
|
|
3107
3276
|
const normalizedSessionId = resolveSessionIdOption(options);
|
|
3108
3277
|
const targetPath = path.resolve(process.cwd(), String(options.path || "."));
|
|
@@ -3126,6 +3295,32 @@ export function registerSessionCommand(program) {
|
|
|
3126
3295
|
if (!["ndjson", "text"].includes(emitFormat)) {
|
|
3127
3296
|
throw new Error("--emit must be one of: ndjson, text.");
|
|
3128
3297
|
}
|
|
3298
|
+
// Optional wake hook: run a host command on each matched event so the
|
|
3299
|
+
// host can resume/wake its agent (the notify->resume bridge).
|
|
3300
|
+
const emitWakeNotice = (payload = {}) => {
|
|
3301
|
+
if (emitFormat === "ndjson") {
|
|
3302
|
+
console.log(
|
|
3303
|
+
JSON.stringify(
|
|
3304
|
+
createAgentEvent({
|
|
3305
|
+
event: "session_wake_hook",
|
|
3306
|
+
agentId,
|
|
3307
|
+
sessionId: normalizedSessionId,
|
|
3308
|
+
payload,
|
|
3309
|
+
}),
|
|
3310
|
+
),
|
|
3311
|
+
);
|
|
3312
|
+
} else {
|
|
3313
|
+
const status = normalizeString(payload.status) || "fired";
|
|
3314
|
+
const detail = payload.reason ? ` (${payload.reason})` : payload.eventType ? ` ${payload.eventType}` : "";
|
|
3315
|
+
console.log(pc.cyan(`wake hook ${status}${detail}`));
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3318
|
+
const wakeRunner = createSessionWakeRunner({
|
|
3319
|
+
command: options.wake,
|
|
3320
|
+
sessionId: normalizedSessionId,
|
|
3321
|
+
agentId,
|
|
3322
|
+
emit: emitWakeNotice,
|
|
3323
|
+
});
|
|
3129
3324
|
const requestedTransport = normalizeString(options.transport).toLowerCase() || "auto";
|
|
3130
3325
|
if (!["auto", "stream", "poll"].includes(requestedTransport)) {
|
|
3131
3326
|
throw new Error("--transport must be one of: auto, stream, poll.");
|
|
@@ -3214,6 +3409,9 @@ export function registerSessionCommand(program) {
|
|
|
3214
3409
|
} else {
|
|
3215
3410
|
console.log(formatEventLine(event));
|
|
3216
3411
|
}
|
|
3412
|
+
// Fire the wake hook for any matched event (incl. ack/like) so the
|
|
3413
|
+
// host can resume its agent.
|
|
3414
|
+
wakeRunner.trigger(event);
|
|
3217
3415
|
},
|
|
3218
3416
|
onError: async (result) => {
|
|
3219
3417
|
const reason = normalizeString(result?.reason) || "poll_failed";
|
package/src/legacy-cli.js
CHANGED
|
@@ -226,6 +226,8 @@ function printUsage() {
|
|
|
226
226
|
console.log(" sl session comment <id> <seq> \"msg\" Alias for threaded reply");
|
|
227
227
|
console.log(" sl session read <id> --remote --agent <id> Read stream events and auto-record views");
|
|
228
228
|
console.log(" sl session view <id> <seq> Manually backfill a read receipt");
|
|
229
|
+
console.log(" sl session pins <id> --json List pinned messages with content (readable by agents)");
|
|
230
|
+
console.log(" sl session listen --session <id> --agent <id> Background-poll a session for new events");
|
|
229
231
|
console.log(" sl session recap now <id> --remote --json Summarize current owners, locks, and work");
|
|
230
232
|
console.log(" sl session daemon --session <id> Run Senti recaps/checkpoints for long rooms");
|
|
231
233
|
console.log(" sl session read <id> --tail 20 Read local session stream events");
|
package/src/session/sync.js
CHANGED
|
@@ -1643,6 +1643,123 @@ export async function listSessionMessageActions(
|
|
|
1643
1643
|
}
|
|
1644
1644
|
}
|
|
1645
1645
|
|
|
1646
|
+
function pinnedEventContentText(event = {}) {
|
|
1647
|
+
const payload = event && typeof event === "object" ? event.payload || {} : {};
|
|
1648
|
+
return (
|
|
1649
|
+
normalizeString(payload.note) ||
|
|
1650
|
+
normalizeString(payload.message) ||
|
|
1651
|
+
normalizeString(payload.text) ||
|
|
1652
|
+
normalizeString(payload.content) ||
|
|
1653
|
+
normalizeString(payload.summary) ||
|
|
1654
|
+
""
|
|
1655
|
+
);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function pinnedEventAuthorId(event = {}) {
|
|
1659
|
+
const agent = event && typeof event === "object" ? event.agent || {} : {};
|
|
1660
|
+
return normalizeString(agent.id || agent.agentId) || normalizeString(event.agentId) || "";
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* Resolve the session's active pinned messages, enriched with each pinned
|
|
1665
|
+
* message's author and content so a CLI agent can actually read them (not just
|
|
1666
|
+
* see sequence numbers). Pins come from the action projection
|
|
1667
|
+
* (`projection.pinnedMessages`, capped at `projection.pinLimit`); content is
|
|
1668
|
+
* resolved per pinned sequence via `/events/before`. Bounded by the pin cap
|
|
1669
|
+
* (<= 10), so at most ~10 single-event lookups.
|
|
1670
|
+
*/
|
|
1671
|
+
export async function fetchSessionPinnedMessages(
|
|
1672
|
+
sessionId,
|
|
1673
|
+
{
|
|
1674
|
+
targetPath = process.cwd(),
|
|
1675
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1676
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1677
|
+
fetchImpl = fetchWithTimeout,
|
|
1678
|
+
nowMs = Date.now,
|
|
1679
|
+
listActions = listSessionMessageActions,
|
|
1680
|
+
fetchEventsBefore = pollSessionEventsBefore,
|
|
1681
|
+
} = {}
|
|
1682
|
+
) {
|
|
1683
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1684
|
+
if (!normalizedSessionId) {
|
|
1685
|
+
return { ok: false, reason: "invalid_session_id", pins: [], pinLimit: 0, count: 0 };
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
const actionsResult = await listActions(normalizedSessionId, {
|
|
1689
|
+
targetPath,
|
|
1690
|
+
timeoutMs,
|
|
1691
|
+
resolveAuthSession,
|
|
1692
|
+
fetchImpl,
|
|
1693
|
+
nowMs,
|
|
1694
|
+
});
|
|
1695
|
+
if (!actionsResult || !actionsResult.ok) {
|
|
1696
|
+
return {
|
|
1697
|
+
ok: false,
|
|
1698
|
+
reason: normalizeString(actionsResult?.reason) || "actions_read_failed",
|
|
1699
|
+
pins: [],
|
|
1700
|
+
pinLimit: 0,
|
|
1701
|
+
count: 0,
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
const projection = actionsResult.projection && typeof actionsResult.projection === "object"
|
|
1706
|
+
? actionsResult.projection
|
|
1707
|
+
: {};
|
|
1708
|
+
const pinLimit = Number(projection.pinLimit) || 0;
|
|
1709
|
+
const pinnedActions = Array.isArray(projection.pinnedMessages) ? projection.pinnedMessages : [];
|
|
1710
|
+
|
|
1711
|
+
const pins = await Promise.all(
|
|
1712
|
+
pinnedActions.map(async (action) => {
|
|
1713
|
+
const targetSequenceId = Number(action?.targetSequenceId) || 0;
|
|
1714
|
+
const base = {
|
|
1715
|
+
targetSequenceId,
|
|
1716
|
+
targetCursor: normalizeString(action?.targetCursor) || null,
|
|
1717
|
+
pinnedBy: normalizeString(action?.actorId) || "",
|
|
1718
|
+
pinnedByKind: normalizeString(action?.actorKind) || "",
|
|
1719
|
+
pinnedAt: normalizeString(action?.createdAt) || "",
|
|
1720
|
+
author: "",
|
|
1721
|
+
content: "",
|
|
1722
|
+
resolved: false,
|
|
1723
|
+
};
|
|
1724
|
+
if (targetSequenceId <= 0) {
|
|
1725
|
+
return base;
|
|
1726
|
+
}
|
|
1727
|
+
const eventsResult = await fetchEventsBefore(normalizedSessionId, {
|
|
1728
|
+
targetPath,
|
|
1729
|
+
beforeSequence: targetSequenceId + 1,
|
|
1730
|
+
limit: 1,
|
|
1731
|
+
timeoutMs,
|
|
1732
|
+
resolveAuthSession,
|
|
1733
|
+
fetchImpl,
|
|
1734
|
+
nowMs,
|
|
1735
|
+
});
|
|
1736
|
+
if (eventsResult && eventsResult.ok && Array.isArray(eventsResult.events)) {
|
|
1737
|
+
const match =
|
|
1738
|
+
eventsResult.events.find(
|
|
1739
|
+
(event) => Number(event?.sequenceId) === targetSequenceId,
|
|
1740
|
+
) ||
|
|
1741
|
+
eventsResult.events[eventsResult.events.length - 1] ||
|
|
1742
|
+
null;
|
|
1743
|
+
if (match) {
|
|
1744
|
+
base.author = pinnedEventAuthorId(match);
|
|
1745
|
+
base.content = pinnedEventContentText(match);
|
|
1746
|
+
base.resolved = true;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return base;
|
|
1750
|
+
}),
|
|
1751
|
+
);
|
|
1752
|
+
|
|
1753
|
+
return {
|
|
1754
|
+
ok: true,
|
|
1755
|
+
reason: "",
|
|
1756
|
+
sessionId: normalizedSessionId,
|
|
1757
|
+
pins,
|
|
1758
|
+
pinLimit,
|
|
1759
|
+
count: pins.length,
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1646
1763
|
export async function createSessionMessageAction(
|
|
1647
1764
|
sessionId,
|
|
1648
1765
|
{
|