@llblab/pi-telegram 0.39.1 → 0.39.2

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/AGENTS.md CHANGED
@@ -81,7 +81,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
81
81
  - A forwarding delivery id is stable across registration replacement and derives from envelope kind, source `update_id`, and stable recipient binding. Runtime instance and registration generation remain separate attempt fences. Persisted message ownership carries the stable binding so replay can rebind only to its current authenticated registration.
82
82
  - A queued receipt persists its acquiring runtime instance, OS pid/process-birth identity, session generation, acquisition id, and acquisition time. Only exact authority may settle or discard it. Same-process session replacement may reconstruct the claim and the original process may settle after transport ownership moves; a foreign process may neither replay nor settle it through generic removal or a copied acquisition id.
83
83
  - Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner cleanup groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS birth proof discards all session-owned sources without replay; a matching proof is `alive`, while Windows or inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
84
- - Execution failures persist bounded diagnostics and attempt state as `retry-wait`. Automatic retry continues indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacy `failed` entries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity. Missing snapshots left by the retired broad temp cleanup rebuild only from a complete provably empty segment chain, while revisionless snapshots may recover from a validated later segment predecessor; otherwise the snapshot and segments move atomically under `tmp/telegram/recovery/` before a fresh journal is published and startup continues with informational recovery evidence.
84
+ - Execution failures persist bounded diagnostics and attempt state as `retry-wait`, except that an exact Telegram HTTP 400 stale/deleted-thread API failure with a proven `{chatId, threadId}` terminally settles the currently executing source after best-effort shared binding invalidation. Automatic retry continues indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacy `failed` entries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity. Missing snapshots left by the retired broad temp cleanup rebuild only from a complete provably empty segment chain, while revisionless snapshots may recover from a validated later segment predecessor; otherwise the snapshot and segments move atomically under `tmp/telegram/recovery/` before a fresh journal is published and startup continues with informational recovery evidence.
85
85
  - An unresolved reaction delays only the exact governed queue item identified by chat/message sources, not unrelated queue work. Queue receipt publication follows in-memory append and precedes dispatch request; receipt-bearing turns remain queued until every exact source commits.
86
86
  - The detailed implementation and release gates live in [`docs/architecture.md`](./docs/architecture.md), [`docs/multi-instance-bus.md`](./docs/multi-instance-bus.md), and [`BACKLOG.md`](./BACKLOG.md).
87
87
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.39.2: Deleted Thread Receipt Hotfix
6
+
7
+ - `Deleted Thread Receipt`: Terminally settles the currently executing durable update when Telegram returns exact HTTP 400 stale/deleted-thread evidence for its `{chatId, threadId}`, including leader-first shared-store invalidation followed by follower settlement. Transient, ambiguous, unrelated, and stale-looking HTTP 5xx failures retain indefinite retry authority; persisted follower records remain restart hints rather than speculative live registrations.
8
+
5
9
  ## 0.39.1: Compaction Failure Formatting Hotfix
6
10
 
7
11
  - `Compaction Failure Formatting`: Preserves the HTML parse mode when a confirmed `/compact` callback reports asynchronous failure, preventing bold standalone failure headings from appearing as literal `<b>…</b>` markup in Telegram.
@@ -209,7 +209,7 @@ The profile-scoped journal separates transport progress from semantic progress.
209
209
 
210
210
  Compaction runs under the journal transaction lock when either 256 unapplied segments or 4 MiB of segment bytes is reached. It publishes the complete private (`0600`) snapshot at revision `R` before best-effort deletion of segments `<= R`; failed cleanup leaves redundant segments that readers ignore. Interrupted cleanup therefore leaves either an older snapshot plus newer authoritative segments or a newer snapshot plus harmless redundant older segments. Revision gaps, conflicting duplicates, malformed segments, and identity mismatches fail closed. The logical reconstructed journal and aggregate unapplied segment bytes are independently bounded at 10,000 entries and 32 MiB as applicable; rejected growth publishes neither snapshot nor segment bytes. Compaction may temporarily require exactly one private complete snapshot of at most 32 MiB. Capacity pauses polling and valid authority files are never automatically deleted, reset, or quarantined. Only a missing-snapshot history that cannot be reconstructed safely uses the explicit evidence-preserving quarantine-and-reset compatibility fallback above.
211
211
 
212
- `pending` entries remain immediately executable while raw interception, routing, or grouping is incomplete. Every execution failure becomes `retry-wait` with durable attempt count, next eligible time, failure class, bounded summary, and latest failure time. The `failed` state remains schema-compatible only for legacy candidate journals and is converted to automatic retry during lifecycle startup. `queued` entries carry exact prompt/control receipts plus the acquiring Pi runtime instance, OS pid/birth identity, session generation, acquisition id, and acquisition time. Queueing alone is never completion.
212
+ `pending` entries remain immediately executable while raw interception, routing, or grouping is incomplete. Execution failures become `retry-wait` with durable attempt count, next eligible time, failure class, bounded summary, and latest failure time, except that an exact HTTP 400 stale/deleted Telegram thread error carrying its proven request `{chatId, threadId}` terminally settles the currently executing source after shared binding invalidation; follower settlement remains idempotent when the leader already persisted that stale binding. The `failed` state remains schema-compatible only for legacy candidate journals and is converted to automatic retry during lifecycle startup. `queued` entries carry exact prompt/control receipts plus the acquiring Pi runtime instance, OS pid/birth identity, session generation, acquisition id, and acquisition time. Queueing alone is never completion.
213
213
 
214
214
  Queue receipt ownership is independent from the Telegram transport lock. A same-instance, same-process generation may reconstruct its local receipt across a fenced session replacement and may settle it after transport ownership moves. A different process reports the receipt as foreign, never republishes it into its local queue, and cannot complete it even if it reads the acquisition id. Startup no longer treats process replacement as proof that an owner died: foreign and legacy unowned receipts remain durable.
215
215
 
package/index.ts CHANGED
@@ -844,6 +844,16 @@ export default function (pi: Pi.ExtensionAPI) {
844
844
  });
845
845
  },
846
846
  );
847
+ const staleTopicApiErrorRecoveryDeps = {
848
+ topicTargetStore: threadStore,
849
+ getSyncState: telegramSyncStateRuntime.getState,
850
+ setSyncState: telegramSyncStateRuntime.setState,
851
+ recordEvent: recordRuntimeEvent,
852
+ };
853
+ const recoverStaleTelegramTopicApiError =
854
+ Sync.createTelegramStaleTopicApiErrorRecoveryRuntime(
855
+ staleTopicApiErrorRecoveryDeps,
856
+ );
847
857
  const {
848
858
  owner: updateWorkerOwnerRuntime,
849
859
  leader: updateAdmissionLifecycleRuntime,
@@ -867,6 +877,12 @@ export default function (pi: Pi.ExtensionAPI) {
867
877
  worker: {
868
878
  defaultHandle: inboundRouteRuntime.handleUpdate,
869
879
  onStateChange: runtimeDiagnostics.scheduleSnapshotPersist,
880
+ settleTerminalExecutionFailure(error) {
881
+ return Sync.settleStaleTelegramTopicExecutionFailure(
882
+ error,
883
+ staleTopicApiErrorRecoveryDeps,
884
+ );
885
+ },
870
886
  },
871
887
  leader: {
872
888
  resolveBinding: resolveTelegramUpdateJournalBinding,
@@ -1056,13 +1072,6 @@ export default function (pi: Pi.ExtensionAPI) {
1056
1072
  onPollingStateChange: runtimeDiagnostics.scheduleSnapshotPersist,
1057
1073
  recordRuntimeEvent,
1058
1074
  });
1059
- const recoverStaleTelegramTopicApiError =
1060
- Sync.createTelegramStaleTopicApiErrorRecoveryRuntime({
1061
- topicTargetStore: threadStore,
1062
- getSyncState: telegramSyncStateRuntime.getState,
1063
- setSyncState: telegramSyncStateRuntime.setState,
1064
- recordEvent: recordRuntimeEvent,
1065
- });
1066
1075
  const authorizeFollowerApiCall = Bus.createTelegramFollowerApiCallAuthorizer({
1067
1076
  isMessageOwned: messageOwnershipRuntime.isOwnedByFollower,
1068
1077
  });
@@ -20,6 +20,7 @@ import type { TelegramTarget } from "./target.ts";
20
20
  import {
21
21
  isTelegramApiMethodRetrySafe,
22
22
  TelegramApiCommitUnknownError,
23
+ TelegramApiStaleTargetError,
23
24
  } from "./telegram-api.ts";
24
25
  import {
25
26
  createTelegramBusFollowerDeliveryIdentity,
@@ -898,6 +899,20 @@ export function createTelegramBusFollowerApiCaller(
898
899
  response?.kind === "bus.ack"
899
900
  ? response.message
900
901
  : "Telegram bus API call did not return an acknowledgement.";
902
+ if (
903
+ response?.kind === "bus.ack" &&
904
+ response.error?.code === "stale-target" &&
905
+ response.error.chatId !== undefined &&
906
+ response.error.threadId !== undefined
907
+ ) {
908
+ throw new TelegramApiStaleTargetError(
909
+ message ?? "Telegram thread target is stale.",
910
+ {
911
+ chatId: response.error.chatId,
912
+ threadId: response.error.threadId,
913
+ },
914
+ );
915
+ }
901
916
  if (
902
917
  response?.kind === "bus.ack" &&
903
918
  response.error?.code === "commit-unknown"
package/lib/bus-leader.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import * as Sync from "./sync.ts";
9
9
  import * as ThreadReconciler from "./thread-reconciler.ts";
10
10
  import {
11
+ getTelegramApiErrorRequestTarget,
11
12
  isTelegramApiCommitUnknownError,
12
13
  type TelegramApiCallOptions,
13
14
  } from "./telegram-api.ts";
@@ -1756,6 +1757,10 @@ async function handleFollowerApiCall(
1756
1757
  result,
1757
1758
  };
1758
1759
  } catch (error) {
1760
+ const staleTarget =
1761
+ Threads.isTelegramTopicTargetStaleError(error)
1762
+ ? getTelegramApiErrorRequestTarget(error)
1763
+ : undefined;
1759
1764
  return {
1760
1765
  kind: "bus.ack",
1761
1766
  requestId: envelope.requestId,
@@ -1764,14 +1769,22 @@ async function handleFollowerApiCall(
1764
1769
  error instanceof Error
1765
1770
  ? error.message
1766
1771
  : "Telegram bus API call failed.",
1767
- ...(isTelegramApiCommitUnknownError(error)
1772
+ ...(staleTarget
1768
1773
  ? {
1769
1774
  error: {
1770
- code: "commit-unknown" as const,
1771
- method: error.method,
1775
+ code: "stale-target" as const,
1776
+ chatId: staleTarget.chatId,
1777
+ threadId: staleTarget.threadId,
1772
1778
  },
1773
1779
  }
1774
- : {}),
1780
+ : isTelegramApiCommitUnknownError(error)
1781
+ ? {
1782
+ error: {
1783
+ code: "commit-unknown" as const,
1784
+ method: error.method,
1785
+ },
1786
+ }
1787
+ : {}),
1775
1788
  };
1776
1789
  }
1777
1790
  }
package/lib/bus.ts CHANGED
@@ -816,8 +816,11 @@ export type TelegramBusEnvelope = (
816
816
  | "commit-unknown"
817
817
  | "request-id-collision"
818
818
  | "ledger-overloaded"
819
- | "incompatible-protocol";
819
+ | "incompatible-protocol"
820
+ | "stale-target";
820
821
  method?: string;
822
+ chatId?: number;
823
+ threadId?: number;
821
824
  };
822
825
  }
823
826
  ) & { auth?: string };
@@ -2527,13 +2530,24 @@ function parseAckEnvelope(
2527
2530
  code === "commit-unknown" ||
2528
2531
  code === "request-id-collision" ||
2529
2532
  code === "ledger-overloaded" ||
2530
- code === "incompatible-protocol"
2533
+ code === "incompatible-protocol" ||
2534
+ code === "stale-target"
2531
2535
  ) {
2536
+ const chatId = value.error.chatId;
2537
+ const threadId = value.error.threadId;
2538
+ if (
2539
+ code === "stale-target" &&
2540
+ (!Number.isSafeInteger(chatId) || !Number.isSafeInteger(threadId))
2541
+ ) {
2542
+ return undefined;
2543
+ }
2532
2544
  envelope.error = {
2533
2545
  code,
2534
2546
  ...(typeof value.error.method === "string"
2535
2547
  ? { method: value.error.method }
2536
2548
  : {}),
2549
+ ...(typeof chatId === "number" ? { chatId } : {}),
2550
+ ...(typeof threadId === "number" ? { threadId } : {}),
2537
2551
  };
2538
2552
  }
2539
2553
  }
package/lib/sync.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Owns pure contracts for deciding when local Telegram mirror state should be refreshed without querying Telegram on every action
5
5
  */
6
6
 
7
+ import { getTelegramApiErrorRequestTarget } from "./telegram-api.ts";
7
8
  import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
8
9
  import * as ThreadReconciler from "./thread-reconciler.ts";
9
10
  import {
@@ -423,6 +424,22 @@ export function createTelegramStaleTopicApiErrorRecoveryRuntime<
423
424
  recoverStaleTelegramTopicApiError(apiBody, error, deps);
424
425
  }
425
426
 
427
+ export async function settleStaleTelegramTopicExecutionFailure<
428
+ TSyncState extends TelegramSyncState,
429
+ >(
430
+ error: unknown,
431
+ deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState>,
432
+ ): Promise<boolean> {
433
+ const target = getTelegramApiErrorRequestTarget(error);
434
+ if (!target || !isTelegramTopicTargetStaleError(error)) return false;
435
+ await recoverStaleTelegramTopicApiError(
436
+ { chat_id: target.chatId, message_thread_id: target.threadId },
437
+ error,
438
+ deps,
439
+ );
440
+ return true;
441
+ }
442
+
426
443
  export async function recoverStaleTelegramTopicApiError<
427
444
  TSyncState extends TelegramSyncState,
428
445
  >(
@@ -587,6 +587,7 @@ export function isTelegramApiCommitUnknownError(
587
587
  class TelegramApiHttpError extends Error {
588
588
  readonly status: number | undefined;
589
589
  readonly retryAfterSeconds: number | undefined;
590
+ requestTarget?: { chatId: number; threadId: number };
590
591
  constructor(
591
592
  message: string,
592
593
  status: number | undefined,
@@ -598,6 +599,41 @@ class TelegramApiHttpError extends Error {
598
599
  }
599
600
  }
600
601
 
602
+ function attachTelegramApiRequestTarget(
603
+ error: unknown,
604
+ body: Record<string, unknown> | Record<string, string>,
605
+ ): void {
606
+ if (!(error instanceof TelegramApiHttpError)) return;
607
+ const chatId = Number(body.chat_id);
608
+ const threadId = Number(body.message_thread_id);
609
+ if (!Number.isSafeInteger(chatId) || !Number.isSafeInteger(threadId)) return;
610
+ error.requestTarget = { chatId, threadId };
611
+ }
612
+
613
+ export class TelegramApiStaleTargetError extends Error {
614
+ readonly requestTarget: { chatId: number; threadId: number };
615
+
616
+ constructor(
617
+ message: string,
618
+ requestTarget: { chatId: number; threadId: number },
619
+ ) {
620
+ super(message);
621
+ this.name = "TelegramApiStaleTargetError";
622
+ this.requestTarget = { ...requestTarget };
623
+ }
624
+ }
625
+
626
+ export function getTelegramApiErrorRequestTarget(
627
+ error: unknown,
628
+ ): { chatId: number; threadId: number } | undefined {
629
+ const target =
630
+ error instanceof TelegramApiHttpError ||
631
+ error instanceof TelegramApiStaleTargetError
632
+ ? error.requestTarget
633
+ : undefined;
634
+ return target ? { ...target } : undefined;
635
+ }
636
+
601
637
  export function isTelegramMessageNotModifiedError(error: unknown): boolean {
602
638
  return (
603
639
  error instanceof Error && error.message.includes("message is not modified")
@@ -1156,21 +1192,26 @@ export async function callTelegram<TResponse>(
1156
1192
  options?: TelegramApiCallOptions,
1157
1193
  ): Promise<TResponse> {
1158
1194
  const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
1159
- return callTelegramWithRetry(
1160
- method,
1161
- async (family) =>
1162
- telegramFetch(
1163
- `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1164
- {
1165
- method: "POST",
1166
- headers: { "content-type": "application/json" },
1167
- body: JSON.stringify(body),
1168
- signal: options?.signal,
1169
- },
1170
- family,
1171
- ),
1172
- options,
1173
- );
1195
+ try {
1196
+ return await callTelegramWithRetry(
1197
+ method,
1198
+ async (family) =>
1199
+ telegramFetch(
1200
+ `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1201
+ {
1202
+ method: "POST",
1203
+ headers: { "content-type": "application/json" },
1204
+ body: JSON.stringify(body),
1205
+ signal: options?.signal,
1206
+ },
1207
+ family,
1208
+ ),
1209
+ options,
1210
+ );
1211
+ } catch (error) {
1212
+ attachTelegramApiRequestTarget(error, body);
1213
+ throw error;
1214
+ }
1174
1215
  }
1175
1216
 
1176
1217
  export type TelegramBotIdentityResponse = Pick<
@@ -1206,43 +1247,48 @@ export async function callTelegramMultipart<TResponse>(
1206
1247
  ): Promise<TResponse> {
1207
1248
  const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
1208
1249
  const fileBlob = await openAsBlob(filePath);
1209
- return callTelegramWithRetry(
1210
- method,
1211
- async (family) => {
1212
- if (family) {
1213
- const multipart = await buildTelegramMultipartBody(
1214
- fields,
1215
- fileField,
1216
- fileBlob,
1217
- fileName,
1218
- );
1250
+ try {
1251
+ return await callTelegramWithRetry(
1252
+ method,
1253
+ async (family) => {
1254
+ if (family) {
1255
+ const multipart = await buildTelegramMultipartBody(
1256
+ fields,
1257
+ fileField,
1258
+ fileBlob,
1259
+ fileName,
1260
+ );
1261
+ return telegramFetch(
1262
+ `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1263
+ {
1264
+ method: "POST",
1265
+ headers: { "content-type": multipart.contentType },
1266
+ body: multipart.body as unknown as BodyInit,
1267
+ signal: options?.signal,
1268
+ },
1269
+ family,
1270
+ );
1271
+ }
1272
+ const form = new FormData();
1273
+ for (const [key, value] of Object.entries(fields)) {
1274
+ form.set(key, value);
1275
+ }
1276
+ form.set(fileField, fileBlob, fileName);
1219
1277
  return telegramFetch(
1220
1278
  `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1221
1279
  {
1222
1280
  method: "POST",
1223
- headers: { "content-type": multipart.contentType },
1224
- body: multipart.body as unknown as BodyInit,
1281
+ body: form,
1225
1282
  signal: options?.signal,
1226
1283
  },
1227
- family,
1228
1284
  );
1229
- }
1230
- const form = new FormData();
1231
- for (const [key, value] of Object.entries(fields)) {
1232
- form.set(key, value);
1233
- }
1234
- form.set(fileField, fileBlob, fileName);
1235
- return telegramFetch(
1236
- `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1237
- {
1238
- method: "POST",
1239
- body: form,
1240
- signal: options?.signal,
1241
- },
1242
- );
1243
- },
1244
- options,
1245
- );
1285
+ },
1286
+ options,
1287
+ );
1288
+ } catch (error) {
1289
+ attachTelegramApiRequestTarget(error, fields);
1290
+ throw error;
1291
+ }
1246
1292
  }
1247
1293
 
1248
1294
  export async function downloadTelegramFile(
package/lib/threads.ts CHANGED
@@ -2882,6 +2882,11 @@ export function getTelegramTargetFromApiBody(
2882
2882
 
2883
2883
  export function isTelegramTopicTargetStaleError(error: unknown): boolean {
2884
2884
  if (!(error instanceof Error)) return false;
2885
+ const status =
2886
+ "status" in error && typeof error.status === "number"
2887
+ ? error.status
2888
+ : undefined;
2889
+ if (status !== undefined && status !== 400) return false;
2885
2890
  const message = error.message.toLowerCase();
2886
2891
  return (
2887
2892
  message.includes("topic_id_invalid") ||
package/lib/updates.ts CHANGED
@@ -1782,6 +1782,7 @@ export interface TelegramUpdateWorkerRuntimeDeps<TContext> {
1782
1782
  classifyExecutionFailure?: (
1783
1783
  error: unknown,
1784
1784
  ) => TelegramUpdateExecutionFailureClassification;
1785
+ settleTerminalExecutionFailure?: (error: unknown) => Promise<boolean>;
1785
1786
  scheduleRetry?: (callback: () => void, delayMs: number) => unknown;
1786
1787
  cancelRetry?: (handle: unknown) => void;
1787
1788
  batchSize?: number;
@@ -4833,13 +4834,20 @@ export function createTelegramUpdateAdmissionWorkerRuntime<
4833
4834
  });
4834
4835
  worker = createTelegramUpdateWorkerRuntime({
4835
4836
  ...deps,
4836
- executeUpdate: (update, ctx, signal) => {
4837
+ async executeUpdate(update, ctx, signal) {
4837
4838
  const typedUpdate = update as TUpdate;
4838
- return executeUpdate(
4839
- deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate,
4840
- ctx,
4841
- signal,
4842
- );
4839
+ try {
4840
+ return await executeUpdate(
4841
+ deps.prepareUpdateForExecution?.(typedUpdate) ?? typedUpdate,
4842
+ ctx,
4843
+ signal,
4844
+ );
4845
+ } catch (error) {
4846
+ if (await deps.settleTerminalExecutionFailure?.(error)) {
4847
+ return { kind: "complete" };
4848
+ }
4849
+ throw error;
4850
+ }
4843
4851
  },
4844
4852
  });
4845
4853
  return worker;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.39.1",
3
+ "version": "0.39.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"