@okostream/mcp 0.12.12 → 0.12.13
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/dist/server.js +164 -57
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -116291,7 +116291,7 @@ function sanitizeScopes(scopes) {
|
|
|
116291
116291
|
}
|
|
116292
116292
|
|
|
116293
116293
|
// build/version.js
|
|
116294
|
-
var OKO_MCP_VERSION = "0.12.
|
|
116294
|
+
var OKO_MCP_VERSION = "0.12.13";
|
|
116295
116295
|
|
|
116296
116296
|
// build/auth.js
|
|
116297
116297
|
var state = null;
|
|
@@ -116702,7 +116702,16 @@ var MAX_CARD_BUTTON_LABEL_LENGTH = 80;
|
|
|
116702
116702
|
var MAX_CARD_BUTTON_URL_LENGTH = 1e3;
|
|
116703
116703
|
var MAX_SOURCE_IMAGE_BYTES = 15 * 1024 * 1024;
|
|
116704
116704
|
var REMOTE_IMAGE_TIMEOUT_MS = 15e3;
|
|
116705
|
+
var REMOTE_IMAGE_TOTAL_TIMEOUT_MS = 4e4;
|
|
116705
116706
|
var REMOTE_IMAGE_MAX_REDIRECTS = 3;
|
|
116707
|
+
function remoteImageHopBudget(deadlineAt, now, perHopMs) {
|
|
116708
|
+
const left = deadlineAt - now;
|
|
116709
|
+
if (left <= 0)
|
|
116710
|
+
return { kind: "exhausted" };
|
|
116711
|
+
if (left < perHopMs)
|
|
116712
|
+
return { kind: "budget", ms: left, capped: true };
|
|
116713
|
+
return { kind: "budget", ms: perHopMs, capped: false };
|
|
116714
|
+
}
|
|
116706
116715
|
var ALLOWED_REMOTE_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/jpg", "image/png", "image/webp", "image/gif", "image/avif"]);
|
|
116707
116716
|
function storagePathFromDownloadUrl(downloadUrl) {
|
|
116708
116717
|
if (!downloadUrl)
|
|
@@ -116816,7 +116825,7 @@ function shareLinkHint(url) {
|
|
|
116816
116825
|
}
|
|
116817
116826
|
return " Use a direct link to the image file (right-click the image \u2192 copy image address), or download the file and pass source.path.";
|
|
116818
116827
|
}
|
|
116819
|
-
async function readRemoteImage(rawUrl, redirects = 0, notes = []) {
|
|
116828
|
+
async function readRemoteImage(rawUrl, redirects = 0, notes = [], deadlineAt = Date.now() + REMOTE_IMAGE_TOTAL_TIMEOUT_MS) {
|
|
116820
116829
|
if (redirects > REMOTE_IMAGE_MAX_REDIRECTS) {
|
|
116821
116830
|
throw new Error("Image URL has too many redirects.");
|
|
116822
116831
|
}
|
|
@@ -116827,21 +116836,28 @@ async function readRemoteImage(rawUrl, redirects = 0, notes = []) {
|
|
|
116827
116836
|
}
|
|
116828
116837
|
let lastConnectError = null;
|
|
116829
116838
|
for (const candidate of pinned.addresses) {
|
|
116830
|
-
const attempt = await readRemoteImageFrom(url, rawUrl, candidate, redirects);
|
|
116839
|
+
const attempt = await readRemoteImageFrom(url, rawUrl, candidate, redirects, deadlineAt);
|
|
116831
116840
|
if ("buffer" in attempt)
|
|
116832
116841
|
return attempt.buffer;
|
|
116833
116842
|
if ("redirectTo" in attempt)
|
|
116834
|
-
return readRemoteImage(attempt.redirectTo, redirects + 1, notes);
|
|
116843
|
+
return readRemoteImage(attempt.redirectTo, redirects + 1, notes, deadlineAt);
|
|
116835
116844
|
lastConnectError = attempt.connectError;
|
|
116836
116845
|
}
|
|
116837
116846
|
throw new Error(`Image URL host ${url.hostname} could not be reached at any of its addresses (${pinned.addresses.map((item) => item.address).join(", ")}): ${lastConnectError?.message ?? "connection failed"}.`);
|
|
116838
116847
|
}
|
|
116839
|
-
|
|
116848
|
+
function totalBudgetMessage() {
|
|
116849
|
+
return `Image URL did not finish loading within ${REMOTE_IMAGE_TOTAL_TIMEOUT_MS / 1e3} seconds, including redirects.`;
|
|
116850
|
+
}
|
|
116851
|
+
async function readRemoteImageFrom(url, rawUrl, candidate, redirects, deadlineAt) {
|
|
116852
|
+
const budget = remoteImageHopBudget(deadlineAt, Date.now(), REMOTE_IMAGE_TIMEOUT_MS);
|
|
116853
|
+
if (budget.kind === "exhausted") {
|
|
116854
|
+
throw new Error(totalBudgetMessage());
|
|
116855
|
+
}
|
|
116840
116856
|
const dispatcher = new import_undici.Agent({
|
|
116841
116857
|
connect: { lookup: pinnedLookup(candidate.address, candidate.family) }
|
|
116842
116858
|
});
|
|
116843
116859
|
const controller = new AbortController();
|
|
116844
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
116860
|
+
const timeout = setTimeout(() => controller.abort(), budget.ms);
|
|
116845
116861
|
try {
|
|
116846
116862
|
let response;
|
|
116847
116863
|
try {
|
|
@@ -116853,7 +116869,7 @@ async function readRemoteImageFrom(url, rawUrl, candidate, redirects) {
|
|
|
116853
116869
|
});
|
|
116854
116870
|
} catch (err) {
|
|
116855
116871
|
if (err instanceof Error && err.name === "AbortError") {
|
|
116856
|
-
throw new Error(
|
|
116872
|
+
throw new Error(budget.capped ? totalBudgetMessage() : `Image URL did not respond within ${REMOTE_IMAGE_TIMEOUT_MS / 1e3} seconds.`);
|
|
116857
116873
|
}
|
|
116858
116874
|
return { connectError: err instanceof Error ? err : new Error(String(err)) };
|
|
116859
116875
|
}
|
|
@@ -118047,6 +118063,27 @@ function markdownMetric(label, value, suffix = "") {
|
|
|
118047
118063
|
const metric = value === null || value === void 0 || value === "" ? "n/a" : String(value);
|
|
118048
118064
|
return `- ${label}: ${metric}${suffix}`;
|
|
118049
118065
|
}
|
|
118066
|
+
function reportQualityCaveats(dataQuality) {
|
|
118067
|
+
if (!dataQuality || typeof dataQuality !== "object" || Array.isArray(dataQuality))
|
|
118068
|
+
return [];
|
|
118069
|
+
const marks = dataQuality;
|
|
118070
|
+
const caveats = [];
|
|
118071
|
+
if (marks.baselineSource === "none")
|
|
118072
|
+
caveats.push("baselineMissing");
|
|
118073
|
+
if (marks.startedAtSource === "end_time")
|
|
118074
|
+
caveats.push("startTimeUnknown");
|
|
118075
|
+
if (marks.watchTime === "unavailable")
|
|
118076
|
+
caveats.push("watchTimeUnmeasured");
|
|
118077
|
+
if (marks.watchTime === "unknown_duration")
|
|
118078
|
+
caveats.push("watchTimeUncapped");
|
|
118079
|
+
return caveats;
|
|
118080
|
+
}
|
|
118081
|
+
var REPORT_QUALITY_CAVEAT_TEXT = {
|
|
118082
|
+
baselineMissing: "The session baseline was not saved: every figure below is counted from zero and includes everything the event accumulated before this broadcast.",
|
|
118083
|
+
startTimeUnknown: "The broadcast start time was not saved: the report is stamped with the moment it ended and its duration reads zero.",
|
|
118084
|
+
watchTimeUnmeasured: "Watch time was not measured: the engagement snapshot could not be read.",
|
|
118085
|
+
watchTimeUncapped: "Watch time is not capped by the broadcast length \u2014 it is unknown, so nothing vouches for the total, average and median times."
|
|
118086
|
+
};
|
|
118050
118087
|
function sessionReportMarkdown({ eventId, event, sessionId, session }) {
|
|
118051
118088
|
const lines = [
|
|
118052
118089
|
`# OKO session report`,
|
|
@@ -118076,6 +118113,12 @@ function sessionReportMarkdown({ eventId, event, sessionId, session }) {
|
|
|
118076
118113
|
markdownMetric("CTA clicks", session.ctaClicks),
|
|
118077
118114
|
markdownMetric("Cards clicks", session.cardsClicks)
|
|
118078
118115
|
];
|
|
118116
|
+
const caveats = reportQualityCaveats(session.dataQuality);
|
|
118117
|
+
if (caveats.length > 0) {
|
|
118118
|
+
lines.push("", "## What this report did not measure");
|
|
118119
|
+
for (const caveat of caveats)
|
|
118120
|
+
lines.push(`- ${REPORT_QUALITY_CAVEAT_TEXT[caveat]}`);
|
|
118121
|
+
}
|
|
118079
118122
|
return lines.join("\n");
|
|
118080
118123
|
}
|
|
118081
118124
|
var TELEGRAM_COPYABLE_KEYS = ["enabled", "channelIds", "autoReminder"];
|
|
@@ -118434,10 +118477,32 @@ async function addEventWithRtdbMirror(eventData) {
|
|
|
118434
118477
|
try {
|
|
118435
118478
|
const ensureRuntime = httpsCallable(functions, "ensureEventRuntime");
|
|
118436
118479
|
await ensureRuntime({ eventId: eventRef.id });
|
|
118480
|
+
return { ref: eventRef, runtime: { status: "ready" } };
|
|
118437
118481
|
} catch (error3) {
|
|
118438
|
-
|
|
118482
|
+
const rawCode = error3?.code;
|
|
118483
|
+
const code = typeof rawCode === "string" ? rawCode : "unknown";
|
|
118484
|
+
console.error(`[OKO MCP] ensureEventRuntime failed for event ${eventRef.id}:`, error3);
|
|
118485
|
+
return {
|
|
118486
|
+
ref: eventRef,
|
|
118487
|
+
runtime: {
|
|
118488
|
+
status: "failed",
|
|
118489
|
+
code,
|
|
118490
|
+
message: error3 instanceof Error ? error3.message : String(error3)
|
|
118491
|
+
}
|
|
118492
|
+
};
|
|
118439
118493
|
}
|
|
118440
|
-
|
|
118494
|
+
}
|
|
118495
|
+
function eventRuntimeHint(runtime) {
|
|
118496
|
+
if (runtime.status === "ready")
|
|
118497
|
+
return [];
|
|
118498
|
+
if (runtime.code === "functions/not-found") {
|
|
118499
|
+
return [
|
|
118500
|
+
"The event document was created, but the server reports the event as already gone and could not confirm its runtime. Do not hand the links out: re-read it with oko_get_event and tell the organizer the creation did not complete."
|
|
118501
|
+
];
|
|
118502
|
+
}
|
|
118503
|
+
return [
|
|
118504
|
+
`The event was created, but the server could not confirm its runtime mirrors (${runtime.code}). Chat and access rules may lag behind: run oko_validate_event_setup before relying on the links, and say so to the organizer.`
|
|
118505
|
+
];
|
|
118441
118506
|
}
|
|
118442
118507
|
|
|
118443
118508
|
// build/tools/setupValidation.js
|
|
@@ -118787,7 +118852,7 @@ function registerEventTools(server2) {
|
|
|
118787
118852
|
const workspaceId = args.workspaceId ?? uid;
|
|
118788
118853
|
await assertCanCreateInWorkspace(uid, workspaceId);
|
|
118789
118854
|
const video = normalizeVideo(args.videoUrl);
|
|
118790
|
-
const
|
|
118855
|
+
const created = await addEventWithRtdbMirror(buildCreateWebinarDocument({
|
|
118791
118856
|
uid,
|
|
118792
118857
|
workspaceId,
|
|
118793
118858
|
title: args.title,
|
|
@@ -118798,11 +118863,14 @@ function registerEventTools(server2) {
|
|
|
118798
118863
|
chatEnabled: args.chatEnabled,
|
|
118799
118864
|
createdAt: serverTimestamp2()
|
|
118800
118865
|
}));
|
|
118866
|
+
const docRef = created.ref;
|
|
118801
118867
|
return textResult({
|
|
118802
118868
|
id: docRef.id,
|
|
118803
118869
|
...eventLinks(docRef.id),
|
|
118804
118870
|
workspaceId,
|
|
118871
|
+
runtime: created.runtime,
|
|
118805
118872
|
hints: [
|
|
118873
|
+
...eventRuntimeHint(created.runtime),
|
|
118806
118874
|
"The page is live at publicUrl immediately; OKO has no draft state.",
|
|
118807
118875
|
"Next: oko_configure_blocks for chat/CTA/placeholder, oko_configure_cards for cards, image uploads, then oko_validate_event_setup.",
|
|
118808
118876
|
'The video placeholder (countdown/cover) is only shown to viewers after oko_set_block_visibility with block "placeholder" and visible true; enabling it in oko_configure_blocks alone keeps it hidden.'
|
|
@@ -119317,7 +119385,6 @@ var MCP_AUDIT_TARGET_KEYS = /* @__PURE__ */ new Set([
|
|
|
119317
119385
|
"postId",
|
|
119318
119386
|
"messageId",
|
|
119319
119387
|
"leadId",
|
|
119320
|
-
"participantUid",
|
|
119321
119388
|
"uid",
|
|
119322
119389
|
"channelId",
|
|
119323
119390
|
"cardId",
|
|
@@ -119733,7 +119800,7 @@ function registerLifecycleTools(server2) {
|
|
|
119733
119800
|
await assertCanCreateInWorkspace(uid, workspaceId);
|
|
119734
119801
|
const sourceHasTelegram = !!source.telegram && typeof source.telegram === "object";
|
|
119735
119802
|
const mayCopyTelegram = sourceHasTelegram && currentScopes().includes("telegram:write") && await eventGrantsMode(eventId, uid, "notify");
|
|
119736
|
-
const
|
|
119803
|
+
const duplicated = await addEventWithRtdbMirror({
|
|
119737
119804
|
ownerId: uid,
|
|
119738
119805
|
workspaceId,
|
|
119739
119806
|
createdAt: serverTimestamp2(),
|
|
@@ -119750,14 +119817,17 @@ function registerLifecycleTools(server2) {
|
|
|
119750
119817
|
// (review 02.09, R3b-5).
|
|
119751
119818
|
...mayCopyTelegram ? { telegram: copyableTelegram(source.telegram) } : {}
|
|
119752
119819
|
});
|
|
119820
|
+
const newRef = duplicated.ref;
|
|
119753
119821
|
return textResult({
|
|
119754
119822
|
id: newRef.id,
|
|
119755
119823
|
sourceEventId: eventId,
|
|
119756
119824
|
workspaceId,
|
|
119825
|
+
runtime: duplicated.runtime,
|
|
119757
119826
|
...eventLinks(newRef.id),
|
|
119758
119827
|
liveStateNotCopied: ["chatMode", "placeholderVisible", "liveBadge", "block visibility overrides"],
|
|
119759
119828
|
telegramCopied: mayCopyTelegram,
|
|
119760
119829
|
hints: [
|
|
119830
|
+
...eventRuntimeHint(duplicated.runtime),
|
|
119761
119831
|
...sourceHasTelegram && !mayCopyTelegram ? ["Telegram config was NOT copied: this key needs telegram:write and the notification permission on the source event. The copy starts with Telegram off; ask the event owner to grant the permission, then configure channels on the copy."] : [],
|
|
119762
119832
|
"Live state was not copied: chat mode is public and the placeholder is hidden on the copy. If the source had chat off or the placeholder shown, set them again with oko_set_chat_mode and oko_set_block_visibility on the new event.",
|
|
119763
119833
|
"The copy is live at publicUrl immediately; edit title, eventDate, and video with oko_update_event, then run oko_validate_event_setup."
|
|
@@ -120132,6 +120202,63 @@ function summarizeSessionRepair(input) {
|
|
|
120132
120202
|
}
|
|
120133
120203
|
|
|
120134
120204
|
// build/tools/sessions.js
|
|
120205
|
+
function sessionBaselineFromRead(read) {
|
|
120206
|
+
if (!read.ok)
|
|
120207
|
+
return "unread";
|
|
120208
|
+
return read.exists ? "present" : "absent";
|
|
120209
|
+
}
|
|
120210
|
+
function sessionRepairAction(input) {
|
|
120211
|
+
if (input.liveSessionBaseline === "unread")
|
|
120212
|
+
return "unknown";
|
|
120213
|
+
return input.liveSessionBaseline === "present" ? "end" : "release";
|
|
120214
|
+
}
|
|
120215
|
+
function sessionRepairRefusal(action) {
|
|
120216
|
+
if (action === "end") {
|
|
120217
|
+
return "This session still has working state in RTDB, so clearing its pointers would leave a running broadcast without a report. End it instead: oko_end_session saves the report and clears the pointers itself.";
|
|
120218
|
+
}
|
|
120219
|
+
if (action === "unknown") {
|
|
120220
|
+
return "Could not read the session working state (events/{eventId}/sessions/{sessionId}/baseline), so it is unknown whether the session is still running. Nothing was changed; retry, or end the session with oko_end_session.";
|
|
120221
|
+
}
|
|
120222
|
+
return null;
|
|
120223
|
+
}
|
|
120224
|
+
async function assertSessionHasNothingToEnd(eventId, sessionId) {
|
|
120225
|
+
if (!sessionId)
|
|
120226
|
+
return;
|
|
120227
|
+
let read;
|
|
120228
|
+
try {
|
|
120229
|
+
const snap = await get(ref(rtdb, `events/${eventId}/sessions/${sessionId}/baseline`));
|
|
120230
|
+
read = { ok: true, exists: snap.exists() };
|
|
120231
|
+
} catch {
|
|
120232
|
+
read = { ok: false };
|
|
120233
|
+
}
|
|
120234
|
+
const refusal = sessionRepairRefusal(sessionRepairAction({ liveSessionBaseline: sessionBaselineFromRead(read) }));
|
|
120235
|
+
if (refusal)
|
|
120236
|
+
throw new Error(refusal);
|
|
120237
|
+
}
|
|
120238
|
+
async function attemptCleanup(path, run) {
|
|
120239
|
+
try {
|
|
120240
|
+
await run();
|
|
120241
|
+
return { path, status: "fulfilled" };
|
|
120242
|
+
} catch (error3) {
|
|
120243
|
+
return { path, status: "rejected", error: error3 instanceof Error ? error3.message : String(error3) };
|
|
120244
|
+
}
|
|
120245
|
+
}
|
|
120246
|
+
async function releaseSessionPointers(eventId, sessionId) {
|
|
120247
|
+
const rtdbUpdates = {
|
|
120248
|
+
[`events/${eventId}/ctrl/activeSessionId`]: null
|
|
120249
|
+
};
|
|
120250
|
+
if (sessionId)
|
|
120251
|
+
rtdbUpdates[`activeSessions/${sessionId}`] = null;
|
|
120252
|
+
const rtdbPaths = sessionId ? ["ctrl/activeSessionId", `activeSessions/${sessionId}`] : ["ctrl/activeSessionId"];
|
|
120253
|
+
const rtdbOutcome = await attemptCleanup("rtdb", () => update(ref(rtdb), rtdbUpdates));
|
|
120254
|
+
const attempts = rtdbPaths.map((path) => ({ ...rtdbOutcome, path }));
|
|
120255
|
+
if (rtdbOutcome.status === "rejected") {
|
|
120256
|
+
attempts.push({ path: "events.activeAnalyticsSessionId", status: "rejected", error: "skipped: RTDB pointers were not released" });
|
|
120257
|
+
return attempts;
|
|
120258
|
+
}
|
|
120259
|
+
attempts.push(await attemptCleanup("events.activeAnalyticsSessionId", () => updateDoc(doc(firestore, "events", eventId), { activeAnalyticsSessionId: deleteField() })));
|
|
120260
|
+
return attempts;
|
|
120261
|
+
}
|
|
120135
120262
|
function registerSessionTools(server2) {
|
|
120136
120263
|
server2.registerTool("oko_start_session", {
|
|
120137
120264
|
title: "Start analytics session",
|
|
@@ -120193,7 +120320,11 @@ function registerSessionTools(server2) {
|
|
|
120193
120320
|
],
|
|
120194
120321
|
returns: "eventId, cleared sessionId if found, and cleaned paths.",
|
|
120195
120322
|
examples: ["Use when oko_end_session says the baseline is missing or dashboard shows a stuck/orphan session."],
|
|
120196
|
-
notes: [
|
|
120323
|
+
notes: [
|
|
120324
|
+
"This does not stop the video stream; OKO Start/End controls analytics sessions only.",
|
|
120325
|
+
"Refuses when the session still has working state in RTDB: clearing the pointers would leave a running broadcast without a report. Use oko_end_session for that case.",
|
|
120326
|
+
"Both RTDB pointers are cleared in one atomic update, and the Firestore pointer only after it succeeds: the repair leaves either all pointers cleared or none."
|
|
120327
|
+
]
|
|
120197
120328
|
}),
|
|
120198
120329
|
inputSchema: {
|
|
120199
120330
|
eventId: eventIdField,
|
|
@@ -120208,41 +120339,11 @@ function registerSessionTools(server2) {
|
|
|
120208
120339
|
const ctrlSnap = await get(ref(rtdb, `events/${eventId}/ctrl/activeSessionId`));
|
|
120209
120340
|
const sessionId = safeString(event.activeAnalyticsSessionId) || safeString(ctrlSnap.val());
|
|
120210
120341
|
requireConfirm(confirm, "oko_fix_orphan_session", { eventId, sessionId: sessionId || null, deleteRtdbSessionData });
|
|
120211
|
-
|
|
120212
|
-
|
|
120213
|
-
path: "events.activeAnalyticsSessionId",
|
|
120214
|
-
promise: updateDoc(doc(firestore, "events", eventId), { activeAnalyticsSessionId: deleteField() })
|
|
120215
|
-
},
|
|
120216
|
-
{
|
|
120217
|
-
path: "ctrl/activeSessionId",
|
|
120218
|
-
promise: remove(ref(rtdb, `events/${eventId}/ctrl/activeSessionId`))
|
|
120219
|
-
}
|
|
120220
|
-
];
|
|
120221
|
-
if (sessionId) {
|
|
120222
|
-
cleanup.push({
|
|
120223
|
-
path: `activeSessions/${sessionId}`,
|
|
120224
|
-
promise: remove(ref(rtdb, `activeSessions/${sessionId}`))
|
|
120225
|
-
});
|
|
120226
|
-
}
|
|
120342
|
+
await assertSessionHasNothingToEnd(eventId, sessionId);
|
|
120343
|
+
const attempts = await releaseSessionPointers(eventId, sessionId);
|
|
120227
120344
|
if (sessionId && deleteRtdbSessionData) {
|
|
120228
|
-
|
|
120229
|
-
path: `events/${eventId}/sessions/${sessionId}`,
|
|
120230
|
-
promise: remove(ref(rtdb, `events/${eventId}/sessions/${sessionId}`))
|
|
120231
|
-
});
|
|
120345
|
+
attempts.push(await attemptCleanup(`events/${eventId}/sessions/${sessionId}`, () => remove(ref(rtdb, `events/${eventId}/sessions/${sessionId}`))));
|
|
120232
120346
|
}
|
|
120233
|
-
const settled = await Promise.allSettled(cleanup.map((operation) => operation.promise));
|
|
120234
|
-
const attempts = settled.map((result, index) => {
|
|
120235
|
-
const operation = cleanup[index];
|
|
120236
|
-
if (!operation)
|
|
120237
|
-
throw new Error("Session repair result has no matching cleanup operation.");
|
|
120238
|
-
return {
|
|
120239
|
-
path: operation.path,
|
|
120240
|
-
status: result.status,
|
|
120241
|
-
...result.status === "rejected" ? {
|
|
120242
|
-
error: result.reason instanceof Error ? result.reason.message : String(result.reason)
|
|
120243
|
-
} : {}
|
|
120244
|
-
};
|
|
120245
|
-
});
|
|
120246
120347
|
let firestorePointer = null;
|
|
120247
120348
|
let rtdbPointer = null;
|
|
120248
120349
|
let verificationError;
|
|
@@ -120278,7 +120379,11 @@ function registerSessionTools(server2) {
|
|
|
120278
120379
|
],
|
|
120279
120380
|
returns: "eventId, sessionId, deleted=true, and cleaned paths.",
|
|
120280
120381
|
examples: ["Use only after confirming the exact session id with the organizer."],
|
|
120281
|
-
notes: [
|
|
120382
|
+
notes: [
|
|
120383
|
+
"Destructive. If the session is active, all three pointers are released first \u2014 atomically in RTDB, then in Firestore \u2014 and only then is the document deleted; the platform refuses to delete the report of a session it still considers active.",
|
|
120384
|
+
"Refuses an active session that still has working state in RTDB: end it with oko_end_session, which saves the report.",
|
|
120385
|
+
"RTDB session data cleanup is best-effort because rules may deny deleting historical session snapshots."
|
|
120386
|
+
]
|
|
120282
120387
|
}),
|
|
120283
120388
|
inputSchema: {
|
|
120284
120389
|
eventId: eventIdField,
|
|
@@ -120293,17 +120398,19 @@ function registerSessionTools(server2) {
|
|
|
120293
120398
|
const event = await readEvent(eventId, uid, "session");
|
|
120294
120399
|
requireConfirm(confirm, "oko_delete_session", { eventId, sessionId, deleteRtdbData });
|
|
120295
120400
|
const isActive = safeString(event.activeAnalyticsSessionId) === sessionId;
|
|
120296
|
-
const
|
|
120401
|
+
const warnings = [];
|
|
120297
120402
|
if (isActive) {
|
|
120298
|
-
|
|
120299
|
-
|
|
120300
|
-
|
|
120301
|
-
|
|
120302
|
-
|
|
120303
|
-
|
|
120304
|
-
|
|
120305
|
-
|
|
120306
|
-
|
|
120403
|
+
await assertSessionHasNothingToEnd(eventId, sessionId);
|
|
120404
|
+
const attempts = await releaseSessionPointers(eventId, sessionId);
|
|
120405
|
+
warnings.push(...attempts.filter((attempt) => attempt.status === "rejected").map((attempt) => `${attempt.path}: ${attempt.error || "cleanup failed"}`));
|
|
120406
|
+
if (warnings.length > 0)
|
|
120407
|
+
throw new Error(`Session delete partially failed: ${warnings.join("; ")}`);
|
|
120408
|
+
}
|
|
120409
|
+
try {
|
|
120410
|
+
await deleteDoc(doc(firestore, "events", eventId, "analyticsSessions", sessionId));
|
|
120411
|
+
} catch (error3) {
|
|
120412
|
+
throw new Error(`Session delete partially failed: ${error3 instanceof Error ? error3.message : String(error3)}`);
|
|
120413
|
+
}
|
|
120307
120414
|
const rtdbCleanupWarnings = [];
|
|
120308
120415
|
if (deleteRtdbData) {
|
|
120309
120416
|
try {
|