sentinelayer-cli 0.46.4 → 0.48.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/README.md +63 -3
- package/package.json +12 -7
- package/skills/omar-gate-setup/SKILL.md +103 -0
- package/skills/senti-agent-identity/SKILL.md +80 -0
- package/skills/senti-judge/SKILL.md +139 -0
- package/skills/senti-room-notifications/SKILL.md +73 -0
- package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
- package/skills/senti-room-notifications/package.json +1 -0
- package/skills/senti-room-notifications/test-classifier.js +109 -0
- package/skills/senti-room-notifications/test-receipt.js +54 -0
- package/skills/sentinelayer-cli/SKILL.md +222 -0
- package/src/agents/devtestbot/runner.js +22 -11
- package/src/agents/devtestbot/tool.js +24 -11
- package/src/agents/jules/fix-cycle.js +7 -1
- package/src/agents/jules/tools/runtime-audit.js +17 -12
- package/src/agents/shared-tools/dispatch-core.js +10 -5
- package/src/ai/aidenid.js +4 -2
- package/src/ai/domain-target-store.js +11 -5
- package/src/ai/identity-store.js +11 -5
- package/src/ai/site-store.js +8 -4
- package/src/audit/orchestrator.js +21 -14
- package/src/audit/package.js +6 -4
- package/src/audit/persona-loop.js +7 -0
- package/src/audit/replay.js +4 -2
- package/src/auth/gate.js +46 -8
- package/src/auth/service.js +34 -15
- package/src/auth/session-store.js +105 -47
- package/src/board/evidence-resolvers.js +3 -4
- package/src/cli.js +23 -3
- package/src/commands/ai/identity-lifecycle.js +30 -26
- package/src/commands/ai/provision-governance.js +32 -24
- package/src/commands/ai/shared.js +6 -4
- package/src/commands/audit.js +14 -12
- package/src/commands/auth.js +24 -4
- package/src/commands/board.js +2 -2
- package/src/commands/chat.js +8 -6
- package/src/commands/judge.js +125 -0
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/mcp.js +45 -52
- package/src/commands/omargate.js +2 -1
- package/src/commands/plugin.js +11 -3
- package/src/commands/review.js +9 -4
- package/src/commands/scan.js +7 -7
- package/src/commands/session.js +607 -57
- package/src/commands/skills.js +45 -0
- package/src/commands/watch.js +9 -7
- package/src/config/output-filesystem.js +156 -0
- package/src/config/schema.js +4 -2
- package/src/config/service.js +40 -4
- package/src/cost/history.js +15 -9
- package/src/daemon/artifact-lineage.js +15 -12
- package/src/daemon/assignment-ledger.js +31 -25
- package/src/daemon/ast-drift.js +7 -3
- package/src/daemon/budget-governor.js +26 -20
- package/src/daemon/error-worker.js +28 -22
- package/src/daemon/fix-cycle.js +5 -4
- package/src/daemon/hybrid-mapper.js +19 -16
- package/src/daemon/ingest-refresh.js +4 -0
- package/src/daemon/jira-lifecycle.js +21 -17
- package/src/daemon/operator-control.js +18 -15
- package/src/daemon/reliability-lane.js +18 -15
- package/src/daemon/scope-engine.js +12 -8
- package/src/daemon/watchdog.js +15 -12
- package/src/engram/build-state.js +24 -3
- package/src/engram/capability-index.js +367 -0
- package/src/engram/compile-graph.js +1063 -0
- package/src/engram/contracts/action-receipt.js +82 -0
- package/src/engram/contracts/capability-index.js +259 -0
- package/src/engram/contracts/capability.js +377 -0
- package/src/engram/contracts/content-hash.js +8 -0
- package/src/engram/contracts/evidence.js +258 -0
- package/src/engram/contracts/graph.js +594 -0
- package/src/engram/contracts/identity.js +8 -0
- package/src/engram/contracts/index.js +149 -0
- package/src/engram/contracts/media-asset.js +85 -0
- package/src/engram/contracts/money.js +249 -0
- package/src/engram/contracts/source-snapshot.js +57 -0
- package/src/engram/contracts/task-context-pack.js +114 -0
- package/src/engram/contracts/temporal-segment.js +72 -0
- package/src/engram/contracts/versions.js +96 -0
- package/src/engram/extract-html.js +692 -0
- package/src/engram/graph-store.js +877 -0
- package/src/engram/index.js +98 -14
- package/src/engram/ingest-site.js +132 -0
- package/src/engram/live-state.js +1180 -0
- package/src/engram/namespace.js +34 -1
- package/src/engram/retrieve-site.js +753 -0
- package/src/engram/safe-reader.js +597 -0
- package/src/engram/source-intake.js +251 -0
- package/src/engram/store.js +227 -9
- package/src/ingest/engine.js +21 -28
- package/src/judge/verdict.js +305 -0
- package/src/legacy-cli.js +96 -19
- package/src/mcp/bridge-inputs.js +28 -4
- package/src/mcp/cli-command-tools.js +70 -0
- package/src/mcp/registry.js +17 -13
- package/src/mcp/session-stdio-server.js +297 -19
- package/src/memory/blackboard.js +5 -4
- package/src/net/gh-api-path.js +35 -9
- package/src/net/test-egress-guard.js +16 -4
- package/src/plugin/manifest.js +12 -8
- package/src/review/ai-review.js +110 -10
- package/src/review/investor-dd-devtestbot.js +17 -6
- package/src/review/investor-dd-orchestrator.js +21 -13
- package/src/review/local-review.js +26 -16
- package/src/review/omargate-cache.js +5 -5
- package/src/review/omargate-orchestrator.js +89 -17
- package/src/review/replay.js +9 -4
- package/src/review/report.js +22 -13
- package/src/review/typesafe-severity.js +201 -0
- package/src/session/admission-auth.js +34 -7
- package/src/session/admission.js +322 -71
- package/src/session/agent-access.js +137 -0
- package/src/session/agent-registry.js +2 -1
- package/src/session/attachment-summary.js +77 -0
- package/src/session/edit-summary.js +19 -0
- package/src/session/invitations.js +65 -1
- package/src/session/mentions.js +17 -9
- package/src/session/message-edits.js +5 -4
- package/src/session/preview.js +10 -2
- package/src/session/recap.js +3 -2
- package/src/session/room-files.js +519 -0
- package/src/session/route-classes.js +22 -8
- package/src/session/setup-guides.js +4 -0
- package/src/session/standup.js +192 -0
- package/src/session/sync.js +229 -10
- package/src/session/tickets.js +41 -0
- package/src/session/transcript.js +2 -1
- package/src/session/wake/resolve-target.js +7 -3
- package/src/skills/listener-runtime.cjs +92 -0
- package/src/skills/service.js +207 -0
- package/src/swarm/factory.js +5 -5
- package/src/swarm/pentest.js +9 -9
- package/src/swarm/report.js +4 -4
- package/src/swarm/runtime.js +15 -12
- package/src/swarm/scenario-dsl.js +7 -5
- package/src/telemetry/ledger.js +16 -7
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { checkedTransport, credentialFor, isAuthenticated } from "../auth/credential-destinations.js";
|
|
3
|
+
import { requestJson } from "../auth/http.js";
|
|
4
|
+
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
5
|
+
import { urlPathSegment } from "../net/url-path.js";
|
|
6
|
+
|
|
7
|
+
const SECTIONS = [
|
|
8
|
+
["accepted", "Accepted"],
|
|
9
|
+
["ownerAttested", "Owner-attested (unverified)"],
|
|
10
|
+
["reportedButUnverified", "Reported but unverified"],
|
|
11
|
+
["inProgress", "In progress"],
|
|
12
|
+
["open", "Open"],
|
|
13
|
+
["blocked", "Blocked"],
|
|
14
|
+
["inReview", "In review"],
|
|
15
|
+
["needsOwnerDecision", "Needs an owner decision"],
|
|
16
|
+
["unknown", "Unknown"],
|
|
17
|
+
];
|
|
18
|
+
const BOARD_LIMIT = 200;
|
|
19
|
+
const EVIDENCE_KINDS = new Set(["pr-merged", "sha-on-branch", "check-conclusive", "deploy-sha"]);
|
|
20
|
+
|
|
21
|
+
const text = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
|
|
22
|
+
const boolean = (value) => typeof value === "boolean" ? value : null;
|
|
23
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
24
|
+
|
|
25
|
+
function timestamp(value) {
|
|
26
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/.test(value)) return null;
|
|
27
|
+
const ms = Date.parse(value), day = Date.parse(`${value.slice(0, 10)}T00:00:00Z`);
|
|
28
|
+
if (!Number.isFinite(ms) || !Number.isFinite(day) || new Date(day).toISOString().slice(0, 10) !== value.slice(0, 10)) return null;
|
|
29
|
+
return new Date(ms).toISOString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sinceTime(since, now) {
|
|
33
|
+
if (since === undefined) return null;
|
|
34
|
+
if (since === "24h") return new Date(now - 24 * 60 * 60 * 1000).toISOString();
|
|
35
|
+
const parsed = timestamp(since);
|
|
36
|
+
if (!parsed) throw new Error("--since must be 24h or an ISO timestamp with a timezone.");
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function evidenceClaim(claim) {
|
|
41
|
+
let reference = null;
|
|
42
|
+
if (claim.kind === "pr-merged") {
|
|
43
|
+
const pr = Number.isSafeInteger(claim.pr) ? String(claim.pr) : text(claim.pr);
|
|
44
|
+
if (pr && /^[1-9]\d*$/.test(pr)) reference = `#${pr}`;
|
|
45
|
+
} else if (claim.kind === "sha-on-branch" || claim.kind === "deploy-sha") {
|
|
46
|
+
reference = text(claim.sha)?.slice(0, 12) || null;
|
|
47
|
+
} else if (claim.kind === "check-conclusive") {
|
|
48
|
+
reference = text(claim.check);
|
|
49
|
+
}
|
|
50
|
+
reference ||= text(claim.url);
|
|
51
|
+
if (reference?.length > 80) reference = `${reference.slice(0, 79)}…`;
|
|
52
|
+
return { kind: claim.kind, reference };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ticketRecord(raw) {
|
|
56
|
+
const t = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
57
|
+
const evidence = Array.isArray(t.doneVerdict?.evidence)
|
|
58
|
+
? t.doneVerdict.evidence.filter((claim) => claim && EVIDENCE_KINDS.has(claim.kind)).map(evidenceClaim) : [];
|
|
59
|
+
const ownerAttested = t.doneVerdict?.kind === "owner-attested" || t.completion?.source === "owner_attested";
|
|
60
|
+
return {
|
|
61
|
+
id: text(t.id), title: text(t.title), status: text(t.status),
|
|
62
|
+
assignee: text(t.assignee), acceptedBy: text(t.acceptedBy),
|
|
63
|
+
claimedAt: timestamp(t.claimedAt), createdAt: timestamp(t.createdAt), updatedAt: timestamp(t.updatedAt),
|
|
64
|
+
hasDoneVerdict: boolean(t.hasDoneVerdict), unaccepted: boolean(t.unaccepted), staleClaim: boolean(t.staleClaim),
|
|
65
|
+
lastVerdict: t.lastVerdict && typeof t.lastVerdict === "object"
|
|
66
|
+
? { verdict: text(t.lastVerdict.verdict), checkedAt: timestamp(t.lastVerdict.checkedAt) } : null,
|
|
67
|
+
completion: t.completion && typeof t.completion === "object"
|
|
68
|
+
? { source: text(t.completion.source), verified: boolean(t.completion.verified) } : null,
|
|
69
|
+
verdict: { kind: evidence[0]?.kind || null, reference: evidence[0]?.reference || null, evidence },
|
|
70
|
+
ownerAttestation: ownerAttested ? {
|
|
71
|
+
statement: text(t.doneVerdict?.statement), attestedBy: text(t.doneVerdict?.attestedBy),
|
|
72
|
+
attestedAt: timestamp(t.doneVerdict?.attestedAt), verified: false,
|
|
73
|
+
} : null,
|
|
74
|
+
unknown: [],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Select a whole record, never graft a prior verdict onto a newer claim. When
|
|
79
|
+
// timestamps cannot resolve different records, retain only their shared identity.
|
|
80
|
+
function coalesce(items) {
|
|
81
|
+
const groups = new Map();
|
|
82
|
+
for (const raw of items) {
|
|
83
|
+
const item = ticketRecord(raw);
|
|
84
|
+
const key = item.id ? `id:${item.id}` : `record:${JSON.stringify(item)}`;
|
|
85
|
+
if (!groups.has(key)) groups.set(key, new Map());
|
|
86
|
+
groups.get(key).set(JSON.stringify(item), item);
|
|
87
|
+
}
|
|
88
|
+
return [...groups.values()].map((group) => {
|
|
89
|
+
const records = [...group.values()].sort((a, b) => compare(b.updatedAt || "", a.updatedAt || ""));
|
|
90
|
+
if (records.length === 1) return records[0];
|
|
91
|
+
if (records.every((t) => t.updatedAt) && records[0].updatedAt !== records[1].updatedAt) return records[0];
|
|
92
|
+
const conflict = ticketRecord({ id: records[0].id,
|
|
93
|
+
updatedAt: records.every((t) => t.updatedAt) ? records[0].updatedAt : null });
|
|
94
|
+
conflict.unknown.push("conflicting duplicate records");
|
|
95
|
+
return conflict;
|
|
96
|
+
}).sort((a, b) => compare(a.id || "", b.id || "") || compare(JSON.stringify(a), JSON.stringify(b)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function buildSessionStandup(items, { sessionId, since, now = Date.now(), truncated = false } = {}) {
|
|
100
|
+
const cutoff = sinceTime(since, now);
|
|
101
|
+
const report = { sessionId, since: cutoff, truncated: truncated === true || items.length >= BOARD_LIMIT,
|
|
102
|
+
...Object.fromEntries(SECTIONS.map(([key]) => [key, []])) };
|
|
103
|
+
for (const item of coalesce(items)) {
|
|
104
|
+
if (cutoff && item.updatedAt && item.updatedAt < cutoff) continue;
|
|
105
|
+
for (const field of ["id", "title", "status", "updatedAt"]) {
|
|
106
|
+
if (item[field] === null) item.unknown.push(field);
|
|
107
|
+
}
|
|
108
|
+
let section = "unknown";
|
|
109
|
+
if (!item.id || item.unknown.includes("conflicting duplicate records") || (cutoff && !item.updatedAt)) {
|
|
110
|
+
// There is not enough evidence to identify or date this work.
|
|
111
|
+
} else if (item.status === "done") {
|
|
112
|
+
if (item.hasDoneVerdict === true && item.ownerAttestation) {
|
|
113
|
+
section = "ownerAttested";
|
|
114
|
+
for (const field of ["statement", "attestedBy", "attestedAt"]) {
|
|
115
|
+
if (item.ownerAttestation[field] === null) item.unknown.push(`ownerAttestation.${field}`);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
section = item.hasDoneVerdict === true ? "accepted" : "reportedButUnverified";
|
|
119
|
+
if (item.hasDoneVerdict === null) item.unknown.push("hasDoneVerdict");
|
|
120
|
+
if (!item.verdict.kind) item.unknown.push("verdict.kind");
|
|
121
|
+
if (!item.verdict.evidence.length || item.verdict.evidence.some((claim) => !claim.reference)) item.unknown.push("verdict.reference");
|
|
122
|
+
}
|
|
123
|
+
} else if (item.unaccepted === true) {
|
|
124
|
+
section = "needsOwnerDecision";
|
|
125
|
+
if (!item.createdAt) item.unknown.push("createdAt");
|
|
126
|
+
} else if (["working", "in_progress"].includes(item.status)) {
|
|
127
|
+
section = "inProgress";
|
|
128
|
+
for (const field of ["assignee", "claimedAt", "staleClaim"]) {
|
|
129
|
+
if (item[field] === null) item.unknown.push(field);
|
|
130
|
+
}
|
|
131
|
+
} else if (["open", "assigned", "accepted"].includes(item.status)) {
|
|
132
|
+
section = "open";
|
|
133
|
+
} else if (item.status === "blocked") {
|
|
134
|
+
section = "blocked";
|
|
135
|
+
} else if (item.status === "in_review") {
|
|
136
|
+
section = "inReview";
|
|
137
|
+
} else {
|
|
138
|
+
item.unknown.push("classification");
|
|
139
|
+
if (item.unaccepted === null) item.unknown.push("unaccepted");
|
|
140
|
+
}
|
|
141
|
+
report[section].push(item);
|
|
142
|
+
}
|
|
143
|
+
report.needsOwnerDecision.sort((a, b) => compare(a.createdAt || "~", b.createdAt || "~") || compare(a.id, b.id));
|
|
144
|
+
return report;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function sessionStandup(sessionId, {
|
|
148
|
+
since, targetPath = process.cwd(), now = Date.now(),
|
|
149
|
+
resolveAuthSession = resolveActiveAuthSession, requestRead = requestJson,
|
|
150
|
+
} = {}) {
|
|
151
|
+
const cutoff = sinceTime(since, now); // Invalid windows refuse before any request.
|
|
152
|
+
const auth = await resolveAuthSession({ cwd: targetPath, env: process.env, autoRotate: false });
|
|
153
|
+
if (!isAuthenticated(auth) || !auth?.apiUrl) throw new Error("Not authenticated for this session.");
|
|
154
|
+
let body;
|
|
155
|
+
try {
|
|
156
|
+
body = await checkedTransport(requestRead)(`${auth.apiUrl.replace(/\/+$/, "")}/api/v1/sessions/${urlPathSegment(sessionId, { label: "sessionId" })}/tickets?limit=${BOARD_LIMIT}`, {
|
|
157
|
+
method: "GET", credential: await credentialFor(auth),
|
|
158
|
+
});
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error?.status === 403 || error?.status === 404) {
|
|
161
|
+
throw new Error(`Standup refused (HTTP ${error.status}): this session's tickets are unavailable or you do not have permission to read them.`, { cause: error });
|
|
162
|
+
}
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
if (!Array.isArray(body?.items)) throw new Error("The ticket response has no items array; refusing to invent a standup.");
|
|
166
|
+
const truncated = body.hasMore === true || Boolean(text(body.nextBeforeId) || text(body.nextCursor) || text(body.nextAfterId));
|
|
167
|
+
return buildSessionStandup(body.items, { sessionId, ...(cutoff ? { since: cutoff } : {}), now, truncated });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function formatSessionStandup(report) {
|
|
171
|
+
// Keep untrusted server text on one terminal line, without directional overrides.
|
|
172
|
+
const display = (value) => String(value ?? "unknown").replace(/[\u0000-\u001f\u007f-\u009f\u2028-\u202e\u2066-\u2069]/g, " ");
|
|
173
|
+
const lines = [`Standup ${display(report.sessionId)} · since ${report.since || "all returned records"}`];
|
|
174
|
+
if (report.truncated) lines.push("Warning: ticket board is truncated; more tickets may exist beyond this page.");
|
|
175
|
+
for (const [key, heading] of SECTIONS) {
|
|
176
|
+
lines.push("", heading);
|
|
177
|
+
if (!report[key].length) lines.push(" (none)");
|
|
178
|
+
for (const item of report[key]) {
|
|
179
|
+
let detail = "";
|
|
180
|
+
if (key === "accepted") detail = item.verdict.evidence.length
|
|
181
|
+
? ` · verdict: ${item.verdict.evidence.map((claim) => `${display(claim.kind)} ${display(claim.reference)}`).join("; ")}`
|
|
182
|
+
: ` · re-checkable verdict on record (details not in this read; checked ${display(item.lastVerdict?.checkedAt)})`;
|
|
183
|
+
if (key === "ownerAttested") detail = ` · owner statement by ${display(item.ownerAttestation.attestedBy)} (unverified): ${display(item.ownerAttestation.statement ?? "statement not in this read")}`;
|
|
184
|
+
if (key === "reportedButUnverified") detail = " · no re-checkable done verdict";
|
|
185
|
+
if (key === "inProgress") detail = ` · assignee: ${display(item.assignee)} · since: ${display(item.claimedAt)}${item.staleClaim === true ? " · STALE claim" : ""}`;
|
|
186
|
+
if (key === "needsOwnerDecision") detail = ` · created: ${display(item.createdAt)}`;
|
|
187
|
+
const missing = item.unknown.length ? ` · unknown: ${item.unknown.join(", ")}` : "";
|
|
188
|
+
lines.push(` ${display(item.id)} ${display(item.title)}${detail}${missing}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return lines.join("\n");
|
|
192
|
+
}
|
package/src/session/sync.js
CHANGED
|
@@ -8,8 +8,10 @@ import { credentialedRequest, credentialFor, isAuthenticated } from "../auth/cre
|
|
|
8
8
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
9
9
|
import { createAgentEvent } from "../events/schema.js";
|
|
10
10
|
import { isSessionControlEvent } from "./control-events.js";
|
|
11
|
-
import { createSessionMutationHeaders } from "./invitations.js";
|
|
11
|
+
import { createSessionMutationHeaders, throwIfPendingSessionInvitation } from "./invitations.js";
|
|
12
12
|
import { messageRevision } from "./message-edits.js";
|
|
13
|
+
import { appendAttachmentSummary } from "./attachment-summary.js";
|
|
14
|
+
import { readAdmissionCredentialState } from "./admission.js";
|
|
13
15
|
import { installTestEgressGuard } from "../net/test-egress-guard.js";
|
|
14
16
|
import { isUrlPathSegment, urlPathSegment } from "../net/url-path.js";
|
|
15
17
|
|
|
@@ -660,9 +662,12 @@ async function readResponseJsonWithTimeout(
|
|
|
660
662
|
}
|
|
661
663
|
}
|
|
662
664
|
|
|
663
|
-
async function readResponseJsonSafely(response, timeoutMs = DEFAULT_SYNC_TIMEOUT_MS) {
|
|
665
|
+
async function readResponseJsonSafely(response, timeoutMs = DEFAULT_SYNC_TIMEOUT_MS, controller = null) {
|
|
664
666
|
try {
|
|
665
|
-
return await readResponseJsonWithTimeout(response, timeoutMs, {
|
|
667
|
+
return await readResponseJsonWithTimeout(response, timeoutMs, {
|
|
668
|
+
fallback: null,
|
|
669
|
+
onTimeout: () => abortRequestController(controller, "response_body_timeout"),
|
|
670
|
+
});
|
|
666
671
|
} catch {
|
|
667
672
|
return null;
|
|
668
673
|
}
|
|
@@ -701,6 +706,7 @@ export async function fetchJsonWithFullTimeout(
|
|
|
701
706
|
},
|
|
702
707
|
);
|
|
703
708
|
|
|
709
|
+
const sessionRequest = new URL(url).pathname.startsWith("/api/v1/sessions/");
|
|
704
710
|
let payload = {};
|
|
705
711
|
if ((response?.ok && readSuccessBody) || readErrorBody || errorBodyStatuses.includes(response?.status)) {
|
|
706
712
|
const elapsedMs = Math.max(0, Date.now() - startedAtMs);
|
|
@@ -709,9 +715,15 @@ export async function fetchJsonWithFullTimeout(
|
|
|
709
715
|
fallback: {},
|
|
710
716
|
onTimeout: () => abortRequestController(controller, "response_body_timeout"),
|
|
711
717
|
});
|
|
718
|
+
} else if (sessionRequest && (response?.status === 403 || response?.status === 404)) {
|
|
719
|
+
// Older callers discard refusal bodies. Inspect only the invitation signal,
|
|
720
|
+
// within the request deadline; unreadable bodies keep the existing refusal.
|
|
721
|
+
const remainingMs = Math.max(1, resolvedTimeoutMs - (Date.now() - startedAtMs));
|
|
722
|
+
payload = await readResponseJsonSafely(response, remainingMs, controller);
|
|
712
723
|
} else {
|
|
713
724
|
cancelResponseBody(response);
|
|
714
725
|
}
|
|
726
|
+
if (sessionRequest) throwIfPendingSessionInvitation(payload, response?.status);
|
|
715
727
|
return { response, payload };
|
|
716
728
|
}
|
|
717
729
|
|
|
@@ -957,6 +969,8 @@ export async function syncSessionEventToApi(
|
|
|
957
969
|
resolveAuthSession = resolveActiveAuthSession,
|
|
958
970
|
fetchImpl = fetchWithTimeout,
|
|
959
971
|
nowMs = Date.now,
|
|
972
|
+
noAutoGrant = false,
|
|
973
|
+
readCredential = readAdmissionCredentialState,
|
|
960
974
|
} = {}
|
|
961
975
|
) {
|
|
962
976
|
const normalizedSessionId = normalizeString(sessionId);
|
|
@@ -1069,6 +1083,31 @@ export async function syncSessionEventToApi(
|
|
|
1069
1083
|
return { synced: false, reason: "api_403" };
|
|
1070
1084
|
}
|
|
1071
1085
|
const agentId = normalizeString(event?.agent?.id);
|
|
1086
|
+
if (noAutoGrant) {
|
|
1087
|
+
return { synced: false, reason: "api_403" };
|
|
1088
|
+
}
|
|
1089
|
+
// Check the stored admission credential: a keeper that joined with
|
|
1090
|
+
// --no-auto-grant persists the preference.
|
|
1091
|
+
if (!noAutoGrant && agentId) {
|
|
1092
|
+
try {
|
|
1093
|
+
const cred = await readCredential(normalizedSessionId, agentId);
|
|
1094
|
+
// A present-but-unreadable or corrupt credential (state "refused")
|
|
1095
|
+
// means the preference cannot be determined; fail closed rather than
|
|
1096
|
+
// silently treating it as "no preference" and auto-granting.
|
|
1097
|
+
if (cred?.state === "refused") {
|
|
1098
|
+
return { synced: false, reason: "api_403" };
|
|
1099
|
+
}
|
|
1100
|
+
if (cred?.credential?.noAutoGrant) {
|
|
1101
|
+
return { synced: false, reason: "api_403" };
|
|
1102
|
+
}
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
// Only a missing file means no preference. Any other read error
|
|
1105
|
+
// (permissions, corruption, transient I/O) must fail closed: a
|
|
1106
|
+
// 403 must NOT auto-grant because the preference is unreadable.
|
|
1107
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
1108
|
+
// credential file absent — no preference to apply
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1072
1111
|
if (!agentId || isReservedAgentIdForGrant(agentId)) {
|
|
1073
1112
|
// Reserved or empty — server enforcement is intentional, no grant
|
|
1074
1113
|
// attempt is meaningful. Treat as a normal 403 failure.
|
|
@@ -1104,6 +1143,7 @@ export async function syncSessionEventToApi(
|
|
|
1104
1143
|
remainingMs(), fetchImpl, { readSuccessBody: false },
|
|
1105
1144
|
));
|
|
1106
1145
|
} catch (grantError) {
|
|
1146
|
+
if (grantError?.code === "INVITATION_PENDING") throw grantError;
|
|
1107
1147
|
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1108
1148
|
return {
|
|
1109
1149
|
synced: false,
|
|
@@ -1130,6 +1170,7 @@ export async function syncSessionEventToApi(
|
|
|
1130
1170
|
endpoint, requestInit, remainingMs(), fetchImpl, { readSuccessBody: false },
|
|
1131
1171
|
));
|
|
1132
1172
|
} catch (retryError) {
|
|
1173
|
+
if (retryError?.code === "INVITATION_PENDING") throw retryError;
|
|
1133
1174
|
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1134
1175
|
return {
|
|
1135
1176
|
synced: false,
|
|
@@ -1158,6 +1199,7 @@ export async function syncSessionEventToApi(
|
|
|
1158
1199
|
reason: `api_${response ? response.status : "no_response"}`,
|
|
1159
1200
|
};
|
|
1160
1201
|
} catch (error) {
|
|
1202
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
1161
1203
|
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1162
1204
|
return {
|
|
1163
1205
|
synced: false,
|
|
@@ -1254,6 +1296,7 @@ async function syncSessionAuxPayload(
|
|
|
1254
1296
|
status: response.status,
|
|
1255
1297
|
};
|
|
1256
1298
|
} catch (error) {
|
|
1299
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
1257
1300
|
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1258
1301
|
return {
|
|
1259
1302
|
synced: false,
|
|
@@ -1284,6 +1327,63 @@ export async function syncSessionErrorToApi(sessionId, error, options = {}) {
|
|
|
1284
1327
|
return syncSessionAuxPayload(sessionId, "/errors", { error }, options);
|
|
1285
1328
|
}
|
|
1286
1329
|
|
|
1330
|
+
/**
|
|
1331
|
+
* POST a validated judge-verdict document to the room's judge-verdicts route. The body is the
|
|
1332
|
+
* document only: the server decides who judged from the credential. Never throws; returns
|
|
1333
|
+
* `{ ok, status, code, body, reason }` so the caller can tell "route not here" (404 without a
|
|
1334
|
+
* room or target code) from a refusal it must report.
|
|
1335
|
+
*/
|
|
1336
|
+
export async function postJudgeVerdictToApi(
|
|
1337
|
+
sessionId,
|
|
1338
|
+
verdict,
|
|
1339
|
+
{
|
|
1340
|
+
targetPath = process.cwd(),
|
|
1341
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1342
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
1343
|
+
fetchImpl = fetchWithTimeout,
|
|
1344
|
+
} = {},
|
|
1345
|
+
) {
|
|
1346
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
1347
|
+
if (!isUrlPathSegment(normalizedSessionId) || !verdict || typeof verdict !== "object" || Array.isArray(verdict)) {
|
|
1348
|
+
return { ok: false, status: 0, code: "", body: null, reason: "invalid_input" };
|
|
1349
|
+
}
|
|
1350
|
+
if (String(process.env.SENTINELAYER_SKIP_REMOTE_SYNC || "").trim() === "1") {
|
|
1351
|
+
return { ok: false, status: 0, code: "", body: null, reason: "remote_sync_disabled_env" };
|
|
1352
|
+
}
|
|
1353
|
+
let session = null;
|
|
1354
|
+
try {
|
|
1355
|
+
session = await resolveAuthSession({ cwd: targetPath, env: process.env, autoRotate: false });
|
|
1356
|
+
} catch {
|
|
1357
|
+
return { ok: false, status: 0, code: "", body: null, reason: "no_session" };
|
|
1358
|
+
}
|
|
1359
|
+
if (!isAuthenticated(session)) {
|
|
1360
|
+
return { ok: false, status: 0, code: "", body: null, reason: "not_authenticated" };
|
|
1361
|
+
}
|
|
1362
|
+
const endpoint = `${resolveApiBaseUrl(session)}/api/v1/sessions/${urlPathSegment(normalizedSessionId, { label: "sessionId" })}/judge-verdicts`;
|
|
1363
|
+
try {
|
|
1364
|
+
const { response, payload } = await fetchJsonWithFullTimeout(
|
|
1365
|
+
endpoint,
|
|
1366
|
+
{
|
|
1367
|
+
method: "POST",
|
|
1368
|
+
headers: { "Content-Type": "application/json" },
|
|
1369
|
+
credential: await credentialFor(session),
|
|
1370
|
+
body: JSON.stringify(verdict),
|
|
1371
|
+
},
|
|
1372
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
|
|
1373
|
+
fetchImpl,
|
|
1374
|
+
{ readErrorBody: true },
|
|
1375
|
+
);
|
|
1376
|
+
const status = response ? response.status : 0;
|
|
1377
|
+
const body = payload && typeof payload === "object" ? payload : null;
|
|
1378
|
+
const code = normalizeString(body?.detail?.error?.code || body?.error?.code || body?.code);
|
|
1379
|
+
if (response?.ok) return { ok: true, status, code: "", body, reason: "" };
|
|
1380
|
+
return { ok: false, status, code, body, reason: `api_${status || "no_response"}` };
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
1383
|
+
return { ok: false, status: 0, code: "", body: null, reason: normalizeString(error?.message) || "request_failed" };
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1287
1387
|
/**
|
|
1288
1388
|
* Poll human messages for a session from sentinelayer-api and map them into
|
|
1289
1389
|
* canonical high-priority session events for local relay.
|
|
@@ -1417,6 +1517,7 @@ export async function pollHumanMessages(
|
|
|
1417
1517
|
dropped,
|
|
1418
1518
|
};
|
|
1419
1519
|
} catch (error) {
|
|
1520
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
1420
1521
|
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1421
1522
|
return {
|
|
1422
1523
|
ok: false,
|
|
@@ -1603,6 +1704,7 @@ export async function pollSessionEvents(
|
|
|
1603
1704
|
scanExhausted: typeof rawScanExhausted === "boolean" ? rawScanExhausted : null,
|
|
1604
1705
|
};
|
|
1605
1706
|
} catch (error) {
|
|
1707
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
1606
1708
|
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1607
1709
|
return {
|
|
1608
1710
|
ok: false,
|
|
@@ -1745,6 +1847,7 @@ export async function fetchSessionPresence(
|
|
|
1745
1847
|
present,
|
|
1746
1848
|
};
|
|
1747
1849
|
} catch (error) {
|
|
1850
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
1748
1851
|
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
1749
1852
|
return {
|
|
1750
1853
|
ok: false,
|
|
@@ -1771,6 +1874,7 @@ export async function renewSessionPresence(
|
|
|
1771
1874
|
displayName = "",
|
|
1772
1875
|
provider = "",
|
|
1773
1876
|
clientKind = "cli",
|
|
1877
|
+
minimalPresence = false,
|
|
1774
1878
|
observedAt = new Date().toISOString(),
|
|
1775
1879
|
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
1776
1880
|
signal = undefined,
|
|
@@ -1854,11 +1958,12 @@ export async function renewSessionPresence(
|
|
|
1854
1958
|
"Content-Type": "application/json",
|
|
1855
1959
|
},
|
|
1856
1960
|
credential: await credentialFor(session),
|
|
1857
|
-
body: JSON.stringify(body),
|
|
1961
|
+
body: JSON.stringify(minimalPresence ? { agentId: body.agentId, model: body.model } : body),
|
|
1858
1962
|
signal,
|
|
1859
1963
|
},
|
|
1860
1964
|
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
|
|
1861
1965
|
fetchImpl,
|
|
1966
|
+
{ errorBodyStatuses: [403] },
|
|
1862
1967
|
);
|
|
1863
1968
|
if (!response?.ok) {
|
|
1864
1969
|
if (response?.status === 404) {
|
|
@@ -1882,6 +1987,7 @@ export async function renewSessionPresence(
|
|
|
1882
1987
|
return {
|
|
1883
1988
|
ok: false,
|
|
1884
1989
|
reason: `api_${response ? response.status : "no_response"}`,
|
|
1990
|
+
code: normalizeString(payload?.error?.code || payload?.code || payload?.detail?.code).toUpperCase(),
|
|
1885
1991
|
status: "degraded",
|
|
1886
1992
|
recorded: false,
|
|
1887
1993
|
retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
|
|
@@ -1897,6 +2003,7 @@ export async function renewSessionPresence(
|
|
|
1897
2003
|
ttlSeconds: normalizePositiveInteger(payload?.ttlSeconds || payload?.ttl_seconds, null),
|
|
1898
2004
|
};
|
|
1899
2005
|
} catch (error) {
|
|
2006
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
1900
2007
|
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
1901
2008
|
return {
|
|
1902
2009
|
ok: false,
|
|
@@ -2017,6 +2124,7 @@ export async function requestSessionListenerStop(
|
|
|
2017
2124
|
control,
|
|
2018
2125
|
};
|
|
2019
2126
|
} catch (error) {
|
|
2127
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
2020
2128
|
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
2021
2129
|
return {
|
|
2022
2130
|
ok: false,
|
|
@@ -2139,6 +2247,7 @@ export async function updateSessionReadCursor(
|
|
|
2139
2247
|
),
|
|
2140
2248
|
};
|
|
2141
2249
|
} catch (error) {
|
|
2250
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
2142
2251
|
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
2143
2252
|
return {
|
|
2144
2253
|
ok: false,
|
|
@@ -2230,6 +2339,7 @@ export async function streamSessionEvents(
|
|
|
2230
2339
|
const controller = new AbortController();
|
|
2231
2340
|
const normalizedTimeoutMs = normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS);
|
|
2232
2341
|
const normalizedIdleTimeoutMs = normalizePositiveInteger(idleTimeoutMs, 0);
|
|
2342
|
+
const deadlineMs = Date.now() + normalizedTimeoutMs;
|
|
2233
2343
|
const timeoutHandle = setTimeout(() => controller.abort(), normalizedTimeoutMs);
|
|
2234
2344
|
if (typeof timeoutHandle.unref === "function") {
|
|
2235
2345
|
timeoutHandle.unref();
|
|
@@ -2321,6 +2431,11 @@ export async function streamSessionEvents(
|
|
|
2321
2431
|
|
|
2322
2432
|
try {
|
|
2323
2433
|
if (!response || !response.ok || !response.body) {
|
|
2434
|
+
if (response?.status === 403 || response?.status === 404) {
|
|
2435
|
+
const remainingMs = Math.max(1, deadlineMs - Date.now());
|
|
2436
|
+
const payload = await readResponseJsonSafely(response, remainingMs, controller);
|
|
2437
|
+
throwIfPendingSessionInvitation(payload, response.status);
|
|
2438
|
+
}
|
|
2324
2439
|
return {
|
|
2325
2440
|
ok: false,
|
|
2326
2441
|
reason: `api_${response ? response.status : "no_response"}`,
|
|
@@ -2347,6 +2462,7 @@ export async function streamSessionEvents(
|
|
|
2347
2462
|
await processSseBlock(buffer, handlers);
|
|
2348
2463
|
}
|
|
2349
2464
|
} catch (error) {
|
|
2465
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
2350
2466
|
return {
|
|
2351
2467
|
ok: false,
|
|
2352
2468
|
reason: idleTimedOut
|
|
@@ -2495,8 +2611,13 @@ export async function pollSessionEventsBefore(
|
|
|
2495
2611
|
events,
|
|
2496
2612
|
cursor: normalizeString(lastEvent?.cursor) || null,
|
|
2497
2613
|
beforeSequence: nextBeforeSequenceFromPayload(payload, payload?.events || []),
|
|
2614
|
+
hasMore: typeof payload?.has_more === "boolean" ? payload.has_more : null,
|
|
2615
|
+
partial: typeof payload?.partial === "boolean" ? payload.partial : null,
|
|
2616
|
+
rawEvents: payload?.events,
|
|
2617
|
+
rawNextBeforeSequence: payload?.next_before_sequence ?? null,
|
|
2498
2618
|
};
|
|
2499
2619
|
} catch (error) {
|
|
2620
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
2500
2621
|
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
2501
2622
|
return {
|
|
2502
2623
|
ok: false,
|
|
@@ -2607,6 +2728,7 @@ export async function listSessionMessageActions(
|
|
|
2607
2728
|
: null,
|
|
2608
2729
|
};
|
|
2609
2730
|
} catch (error) {
|
|
2731
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
2610
2732
|
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
2611
2733
|
return {
|
|
2612
2734
|
ok: false,
|
|
@@ -2698,6 +2820,7 @@ export async function fetchSessionUsageLedger(
|
|
|
2698
2820
|
payload: payload && typeof payload === "object" ? payload : {},
|
|
2699
2821
|
};
|
|
2700
2822
|
} catch (error) {
|
|
2823
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
2701
2824
|
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
2702
2825
|
return {
|
|
2703
2826
|
ok: false,
|
|
@@ -2807,7 +2930,10 @@ export async function fetchSessionPinnedMessages(
|
|
|
2807
2930
|
null;
|
|
2808
2931
|
if (match) {
|
|
2809
2932
|
base.author = pinnedEventAuthorId(match);
|
|
2810
|
-
base.content = pinnedEventContentText(match);
|
|
2933
|
+
base.content = appendAttachmentSummary(pinnedEventContentText(match), { sessionId: normalizedSessionId, ...match });
|
|
2934
|
+
if (Array.isArray(match.payload?.attachments) && match.payload.attachments.length) {
|
|
2935
|
+
base.attachments = match.payload.attachments;
|
|
2936
|
+
}
|
|
2811
2937
|
base.resolved = true;
|
|
2812
2938
|
}
|
|
2813
2939
|
}
|
|
@@ -2825,6 +2951,92 @@ export async function fetchSessionPinnedMessages(
|
|
|
2825
2951
|
};
|
|
2826
2952
|
}
|
|
2827
2953
|
|
|
2954
|
+
// An attachment id as the API accepts it in a path.
|
|
2955
|
+
const SESSION_ATTACHMENT_ID_PATTERN = /^[A-Za-z0-9._:-]{8,120}$/;
|
|
2956
|
+
|
|
2957
|
+
/**
|
|
2958
|
+
* Read one room attachment's metadata and a short-lived signed download link:
|
|
2959
|
+
* `GET /api/v1/sessions/{id}/attachments/{attachmentId}?download=true`, which answers
|
|
2960
|
+
* `{ ok, attachment, download: { url, method, expiresIn, expiresAt } }`. The link points at the
|
|
2961
|
+
* file store and carries its own signature; fetch it without API credentials. Someone other
|
|
2962
|
+
* than the uploader gets 404 for a file no sent message has published yet.
|
|
2963
|
+
*
|
|
2964
|
+
* Never throws; a refusal reports the HTTP status and the API error code.
|
|
2965
|
+
*/
|
|
2966
|
+
export async function fetchSessionAttachment(
|
|
2967
|
+
sessionId,
|
|
2968
|
+
attachmentId,
|
|
2969
|
+
{
|
|
2970
|
+
targetPath = process.cwd(),
|
|
2971
|
+
timeoutMs = DEFAULT_SYNC_TIMEOUT_MS,
|
|
2972
|
+
resolveAuthSession = resolveActiveAuthSession,
|
|
2973
|
+
fetchImpl = fetchWithTimeout,
|
|
2974
|
+
nowMs = Date.now,
|
|
2975
|
+
} = {}
|
|
2976
|
+
) {
|
|
2977
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
2978
|
+
const normalizedAttachmentId = normalizeString(attachmentId);
|
|
2979
|
+
if (!isUrlPathSegment(normalizedSessionId)) {
|
|
2980
|
+
return { ok: false, reason: "invalid_session_id", status: null };
|
|
2981
|
+
}
|
|
2982
|
+
if (!SESSION_ATTACHMENT_ID_PATTERN.test(normalizedAttachmentId) || normalizedAttachmentId.includes("..")) {
|
|
2983
|
+
return { ok: false, reason: "invalid_attachment_id", status: null };
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
let session = null;
|
|
2987
|
+
try {
|
|
2988
|
+
session = await resolveAuthSession({
|
|
2989
|
+
cwd: targetPath,
|
|
2990
|
+
env: process.env,
|
|
2991
|
+
autoRotate: false,
|
|
2992
|
+
});
|
|
2993
|
+
} catch {
|
|
2994
|
+
return { ok: false, reason: "no_session", status: null };
|
|
2995
|
+
}
|
|
2996
|
+
if (!isAuthenticated(session)) {
|
|
2997
|
+
return { ok: false, reason: "not_authenticated", status: null };
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
const apiBaseUrl = resolveApiBaseUrl(session);
|
|
3001
|
+
const endpoint =
|
|
3002
|
+
`${apiBaseUrl}/api/v1/sessions/${urlPathSegment(normalizedSessionId, { label: "sessionId" })}` +
|
|
3003
|
+
`/attachments/${urlPathSegment(normalizedAttachmentId, { label: "attachmentId" })}?download=true`;
|
|
3004
|
+
try {
|
|
3005
|
+
const { response, payload } = await fetchJsonWithFullTimeout(
|
|
3006
|
+
endpoint,
|
|
3007
|
+
{
|
|
3008
|
+
method: "GET",
|
|
3009
|
+
credential: await credentialFor(session),
|
|
3010
|
+
},
|
|
3011
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
|
|
3012
|
+
fetchImpl,
|
|
3013
|
+
{ readErrorBody: true },
|
|
3014
|
+
);
|
|
3015
|
+
if (!response || !response.ok) {
|
|
3016
|
+
const status = response ? response.status : null;
|
|
3017
|
+
return {
|
|
3018
|
+
ok: false,
|
|
3019
|
+
reason: `api_${status ?? "no_response"}`,
|
|
3020
|
+
status,
|
|
3021
|
+
errorCode: normalizeString(payload?.error?.code) || null,
|
|
3022
|
+
retryAfterMs: isRateLimitResponse(response, payload)
|
|
3023
|
+
? retryAfterMsFromResponse(response, payload, Number(nowMs()) || Date.now())
|
|
3024
|
+
: null,
|
|
3025
|
+
};
|
|
3026
|
+
}
|
|
3027
|
+
return {
|
|
3028
|
+
ok: true,
|
|
3029
|
+
reason: "",
|
|
3030
|
+
status: response.status,
|
|
3031
|
+
attachment: payload?.attachment && typeof payload.attachment === "object" ? payload.attachment : null,
|
|
3032
|
+
download: payload?.download && typeof payload.download === "object" ? payload.download : null,
|
|
3033
|
+
};
|
|
3034
|
+
} catch (error) {
|
|
3035
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
3036
|
+
return { ok: false, reason: normalizeString(error?.message) || "attachment_read_failed", status: null };
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
|
|
2828
3040
|
// Native edits never fall back to POST /events or mutate an optimistic local row.
|
|
2829
3041
|
export async function editSessionMessage(sessionId, {
|
|
2830
3042
|
targetSequenceId = null,
|
|
@@ -2912,6 +3124,7 @@ export async function editSessionMessage(sessionId, {
|
|
|
2912
3124
|
recordCircuitSuccess(outboundCircuit);
|
|
2913
3125
|
return { ok: true, changed: Boolean(payload.changed), replayed: Boolean(payload.replayed), event: payload.event || null, reply: payload.reply || null, notification: payload.notification || null };
|
|
2914
3126
|
} catch (error) {
|
|
3127
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
2915
3128
|
recordCircuitFailure(outboundCircuit, timestamp);
|
|
2916
3129
|
return { ok: false, reason: normalizeString(error?.message) || "message_edit_failed" };
|
|
2917
3130
|
}
|
|
@@ -3063,6 +3276,7 @@ export async function createSessionMessageAction(
|
|
|
3063
3276
|
action: payload?.action && typeof payload.action === "object" ? payload.action : null,
|
|
3064
3277
|
};
|
|
3065
3278
|
} catch (error) {
|
|
3279
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
3066
3280
|
recordCircuitFailure(outboundCircuit, normalizedNowMs);
|
|
3067
3281
|
return {
|
|
3068
3282
|
ok: false,
|
|
@@ -3189,6 +3403,7 @@ export async function searchSessionEvents(
|
|
|
3189
3403
|
nextBeforeSequence: Number(payload?.next_before_sequence ?? payload?.nextBeforeSequence) || null,
|
|
3190
3404
|
};
|
|
3191
3405
|
} catch (error) {
|
|
3406
|
+
if (error?.code === "INVITATION_PENDING") throw error;
|
|
3192
3407
|
recordCircuitFailure(inboundCircuit, normalizedNowMs);
|
|
3193
3408
|
return {
|
|
3194
3409
|
ok: false,
|
|
@@ -3294,6 +3509,7 @@ export async function listSessionsFromApi({
|
|
|
3294
3509
|
fetchImpl,
|
|
3295
3510
|
));
|
|
3296
3511
|
} catch (err) {
|
|
3512
|
+
if (err?.code === "INVITATION_PENDING") throw err;
|
|
3297
3513
|
return {
|
|
3298
3514
|
ok: false,
|
|
3299
3515
|
reason: normalizeString(err?.message) || "list_failed",
|
|
@@ -3414,6 +3630,7 @@ export async function fetchSessionFromApi(
|
|
|
3414
3630
|
fetchImpl,
|
|
3415
3631
|
));
|
|
3416
3632
|
} catch (err) {
|
|
3633
|
+
if (err?.code === "INVITATION_PENDING") throw err;
|
|
3417
3634
|
return {
|
|
3418
3635
|
ok: false,
|
|
3419
3636
|
reason: normalizeString(err?.message) || "fetch_failed",
|
|
@@ -3482,13 +3699,15 @@ export async function probeSessionAccess(
|
|
|
3482
3699
|
|
|
3483
3700
|
let response;
|
|
3484
3701
|
try {
|
|
3485
|
-
response = await
|
|
3486
|
-
await credentialFor(session),
|
|
3702
|
+
({ response } = await fetchJsonWithFullTimeout(
|
|
3487
3703
|
endpoint,
|
|
3488
|
-
{ method: "GET" },
|
|
3489
|
-
|
|
3490
|
-
|
|
3704
|
+
{ method: "GET", credential: await credentialFor(session) },
|
|
3705
|
+
normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
|
|
3706
|
+
fetchImpl,
|
|
3707
|
+
{ readSuccessBody: false },
|
|
3708
|
+
));
|
|
3491
3709
|
} catch (err) {
|
|
3710
|
+
if (err?.code === "INVITATION_PENDING") throw err;
|
|
3492
3711
|
return {
|
|
3493
3712
|
accessible: false,
|
|
3494
3713
|
reason: normalizeString(err?.message) || "probe_failed",
|