@open-everywhere/provider-notion 0.1.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.
Files changed (186) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +48 -0
  4. package/agent/channels/notion-acting-identity.ts +88 -0
  5. package/agent/channels/notion-ancestry-trust.ts +458 -0
  6. package/agent/channels/notion-api.ts +1290 -0
  7. package/agent/channels/notion-bot-user.ts +250 -0
  8. package/agent/channels/notion-catch-up.ts +339 -0
  9. package/agent/channels/notion-credentials.ts +417 -0
  10. package/agent/channels/notion-engagement.ts +372 -0
  11. package/agent/channels/notion-event-normalizer.ts +587 -0
  12. package/agent/channels/notion-identity.ts +271 -0
  13. package/agent/channels/notion-ingress-drain.ts +289 -0
  14. package/agent/channels/notion-ingress.ts +182 -0
  15. package/agent/channels/notion-message-ops.ts +140 -0
  16. package/agent/channels/notion-origin-codec.ts +38 -0
  17. package/agent/channels/notion-post-arm.ts +189 -0
  18. package/agent/channels/notion-question-return.ts +223 -0
  19. package/agent/channels/notion-read.ts +283 -0
  20. package/agent/channels/notion-scope.ts +560 -0
  21. package/agent/channels/notion-stop.ts +169 -0
  22. package/agent/channels/notion-supervisor.ts +856 -0
  23. package/agent/channels/notion-webhook-timing.ts +131 -0
  24. package/agent/channels/notion-webhook-verifier.ts +55 -0
  25. package/agent/channels/notion-webhook.ts +963 -0
  26. package/agent/channels/notion-work-search.ts +74 -0
  27. package/agent/channels/notion-work.ts +144 -0
  28. package/agent/channels/notion.ts +1242 -0
  29. package/agent/contributions.ts +39 -0
  30. package/agent/docs-generated.ts +15 -0
  31. package/agent/memory/blocks.ts +295 -0
  32. package/agent/memory/memory.ts +622 -0
  33. package/agent/provider-events.ts +198 -0
  34. package/agent/register.ts +144 -0
  35. package/agent/schedules/notion-ingress.ts +20 -0
  36. package/agent/surface-parity.ts +105 -0
  37. package/agent/tools/notion_search.ts +110 -0
  38. package/agent/tools/read_notion_comments.ts +250 -0
  39. package/agent/tools/read_notion_page.ts +351 -0
  40. package/dist/agent/channels/notion-acting-identity.d.ts +23 -0
  41. package/dist/agent/channels/notion-acting-identity.d.ts.map +1 -0
  42. package/dist/agent/channels/notion-acting-identity.js +85 -0
  43. package/dist/agent/channels/notion-acting-identity.js.map +1 -0
  44. package/dist/agent/channels/notion-ancestry-trust.d.ts +208 -0
  45. package/dist/agent/channels/notion-ancestry-trust.d.ts.map +1 -0
  46. package/dist/agent/channels/notion-ancestry-trust.js +331 -0
  47. package/dist/agent/channels/notion-ancestry-trust.js.map +1 -0
  48. package/dist/agent/channels/notion-api.d.ts +655 -0
  49. package/dist/agent/channels/notion-api.d.ts.map +1 -0
  50. package/dist/agent/channels/notion-api.js +938 -0
  51. package/dist/agent/channels/notion-api.js.map +1 -0
  52. package/dist/agent/channels/notion-bot-user.d.ts +107 -0
  53. package/dist/agent/channels/notion-bot-user.d.ts.map +1 -0
  54. package/dist/agent/channels/notion-bot-user.js +192 -0
  55. package/dist/agent/channels/notion-bot-user.js.map +1 -0
  56. package/dist/agent/channels/notion-catch-up.d.ts +64 -0
  57. package/dist/agent/channels/notion-catch-up.d.ts.map +1 -0
  58. package/dist/agent/channels/notion-catch-up.js +289 -0
  59. package/dist/agent/channels/notion-catch-up.js.map +1 -0
  60. package/dist/agent/channels/notion-credentials.d.ts +210 -0
  61. package/dist/agent/channels/notion-credentials.d.ts.map +1 -0
  62. package/dist/agent/channels/notion-credentials.js +360 -0
  63. package/dist/agent/channels/notion-credentials.js.map +1 -0
  64. package/dist/agent/channels/notion-engagement.d.ts +219 -0
  65. package/dist/agent/channels/notion-engagement.d.ts.map +1 -0
  66. package/dist/agent/channels/notion-engagement.js +283 -0
  67. package/dist/agent/channels/notion-engagement.js.map +1 -0
  68. package/dist/agent/channels/notion-event-normalizer.d.ts +194 -0
  69. package/dist/agent/channels/notion-event-normalizer.d.ts.map +1 -0
  70. package/dist/agent/channels/notion-event-normalizer.js +542 -0
  71. package/dist/agent/channels/notion-event-normalizer.js.map +1 -0
  72. package/dist/agent/channels/notion-identity.d.ts +119 -0
  73. package/dist/agent/channels/notion-identity.d.ts.map +1 -0
  74. package/dist/agent/channels/notion-identity.js +205 -0
  75. package/dist/agent/channels/notion-identity.js.map +1 -0
  76. package/dist/agent/channels/notion-ingress-drain.d.ts +66 -0
  77. package/dist/agent/channels/notion-ingress-drain.d.ts.map +1 -0
  78. package/dist/agent/channels/notion-ingress-drain.js +185 -0
  79. package/dist/agent/channels/notion-ingress-drain.js.map +1 -0
  80. package/dist/agent/channels/notion-ingress.d.ts +64 -0
  81. package/dist/agent/channels/notion-ingress.d.ts.map +1 -0
  82. package/dist/agent/channels/notion-ingress.js +130 -0
  83. package/dist/agent/channels/notion-ingress.js.map +1 -0
  84. package/dist/agent/channels/notion-message-ops.d.ts +3 -0
  85. package/dist/agent/channels/notion-message-ops.d.ts.map +1 -0
  86. package/dist/agent/channels/notion-message-ops.js +126 -0
  87. package/dist/agent/channels/notion-message-ops.js.map +1 -0
  88. package/dist/agent/channels/notion-origin-codec.d.ts +7 -0
  89. package/dist/agent/channels/notion-origin-codec.d.ts.map +1 -0
  90. package/dist/agent/channels/notion-origin-codec.js +32 -0
  91. package/dist/agent/channels/notion-origin-codec.js.map +1 -0
  92. package/dist/agent/channels/notion-post-arm.d.ts +3 -0
  93. package/dist/agent/channels/notion-post-arm.d.ts.map +1 -0
  94. package/dist/agent/channels/notion-post-arm.js +185 -0
  95. package/dist/agent/channels/notion-post-arm.js.map +1 -0
  96. package/dist/agent/channels/notion-question-return.d.ts +77 -0
  97. package/dist/agent/channels/notion-question-return.d.ts.map +1 -0
  98. package/dist/agent/channels/notion-question-return.js +183 -0
  99. package/dist/agent/channels/notion-question-return.js.map +1 -0
  100. package/dist/agent/channels/notion-read.d.ts +62 -0
  101. package/dist/agent/channels/notion-read.d.ts.map +1 -0
  102. package/dist/agent/channels/notion-read.js +257 -0
  103. package/dist/agent/channels/notion-read.js.map +1 -0
  104. package/dist/agent/channels/notion-scope.d.ts +188 -0
  105. package/dist/agent/channels/notion-scope.d.ts.map +1 -0
  106. package/dist/agent/channels/notion-scope.js +433 -0
  107. package/dist/agent/channels/notion-scope.js.map +1 -0
  108. package/dist/agent/channels/notion-stop.d.ts +48 -0
  109. package/dist/agent/channels/notion-stop.d.ts.map +1 -0
  110. package/dist/agent/channels/notion-stop.js +141 -0
  111. package/dist/agent/channels/notion-stop.js.map +1 -0
  112. package/dist/agent/channels/notion-supervisor.d.ts +462 -0
  113. package/dist/agent/channels/notion-supervisor.d.ts.map +1 -0
  114. package/dist/agent/channels/notion-supervisor.js +482 -0
  115. package/dist/agent/channels/notion-supervisor.js.map +1 -0
  116. package/dist/agent/channels/notion-webhook-timing.d.ts +35 -0
  117. package/dist/agent/channels/notion-webhook-timing.d.ts.map +1 -0
  118. package/dist/agent/channels/notion-webhook-timing.js +99 -0
  119. package/dist/agent/channels/notion-webhook-timing.js.map +1 -0
  120. package/dist/agent/channels/notion-webhook-verifier.d.ts +5 -0
  121. package/dist/agent/channels/notion-webhook-verifier.d.ts.map +1 -0
  122. package/dist/agent/channels/notion-webhook-verifier.js +53 -0
  123. package/dist/agent/channels/notion-webhook-verifier.js.map +1 -0
  124. package/dist/agent/channels/notion-webhook.d.ts +242 -0
  125. package/dist/agent/channels/notion-webhook.d.ts.map +1 -0
  126. package/dist/agent/channels/notion-webhook.js +675 -0
  127. package/dist/agent/channels/notion-webhook.js.map +1 -0
  128. package/dist/agent/channels/notion-work-search.d.ts +9 -0
  129. package/dist/agent/channels/notion-work-search.d.ts.map +1 -0
  130. package/dist/agent/channels/notion-work-search.js +69 -0
  131. package/dist/agent/channels/notion-work-search.js.map +1 -0
  132. package/dist/agent/channels/notion-work.d.ts +68 -0
  133. package/dist/agent/channels/notion-work.d.ts.map +1 -0
  134. package/dist/agent/channels/notion-work.js +126 -0
  135. package/dist/agent/channels/notion-work.js.map +1 -0
  136. package/dist/agent/channels/notion.d.ts +95 -0
  137. package/dist/agent/channels/notion.d.ts.map +1 -0
  138. package/dist/agent/channels/notion.js +1081 -0
  139. package/dist/agent/channels/notion.js.map +1 -0
  140. package/dist/agent/contributions.d.ts +32 -0
  141. package/dist/agent/contributions.d.ts.map +1 -0
  142. package/dist/agent/contributions.js +37 -0
  143. package/dist/agent/contributions.js.map +1 -0
  144. package/dist/agent/docs-generated.d.ts +3 -0
  145. package/dist/agent/docs-generated.d.ts.map +1 -0
  146. package/dist/agent/docs-generated.js +13 -0
  147. package/dist/agent/docs-generated.js.map +1 -0
  148. package/dist/agent/memory/blocks.d.ts +58 -0
  149. package/dist/agent/memory/blocks.d.ts.map +1 -0
  150. package/dist/agent/memory/blocks.js +264 -0
  151. package/dist/agent/memory/blocks.js.map +1 -0
  152. package/dist/agent/memory/memory.d.ts +157 -0
  153. package/dist/agent/memory/memory.d.ts.map +1 -0
  154. package/dist/agent/memory/memory.js +471 -0
  155. package/dist/agent/memory/memory.js.map +1 -0
  156. package/dist/agent/provider-events.d.ts +3 -0
  157. package/dist/agent/provider-events.d.ts.map +1 -0
  158. package/dist/agent/provider-events.js +196 -0
  159. package/dist/agent/provider-events.js.map +1 -0
  160. package/dist/agent/register.d.ts +5 -0
  161. package/dist/agent/register.d.ts.map +1 -0
  162. package/dist/agent/register.js +128 -0
  163. package/dist/agent/register.js.map +1 -0
  164. package/dist/agent/schedules/notion-ingress.d.ts +6 -0
  165. package/dist/agent/schedules/notion-ingress.d.ts.map +1 -0
  166. package/dist/agent/schedules/notion-ingress.js +16 -0
  167. package/dist/agent/schedules/notion-ingress.js.map +1 -0
  168. package/dist/agent/surface-parity.d.ts +3 -0
  169. package/dist/agent/surface-parity.d.ts.map +1 -0
  170. package/dist/agent/surface-parity.js +99 -0
  171. package/dist/agent/surface-parity.js.map +1 -0
  172. package/dist/agent/tools/notion_search.d.ts +56 -0
  173. package/dist/agent/tools/notion_search.d.ts.map +1 -0
  174. package/dist/agent/tools/notion_search.js +100 -0
  175. package/dist/agent/tools/notion_search.js.map +1 -0
  176. package/dist/agent/tools/read_notion_comments.d.ts +57 -0
  177. package/dist/agent/tools/read_notion_comments.d.ts.map +1 -0
  178. package/dist/agent/tools/read_notion_comments.js +236 -0
  179. package/dist/agent/tools/read_notion_comments.js.map +1 -0
  180. package/dist/agent/tools/read_notion_page.d.ts +53 -0
  181. package/dist/agent/tools/read_notion_page.d.ts.map +1 -0
  182. package/dist/agent/tools/read_notion_page.js +327 -0
  183. package/dist/agent/tools/read_notion_page.js.map +1 -0
  184. package/package.json +231 -0
  185. package/tsconfig.build.json +22 -0
  186. package/tsconfig.json +15 -0
@@ -0,0 +1,77 @@
1
+ /** The auth shape both halves read — the delivery's attributes, where `buildNotionAuth` stows the
2
+ * discussion, page and workspace ids. Structural so nothing here needs the provider's own type. */
3
+ export interface NotionAnswerAuth {
4
+ readonly attributes?: Record<string, unknown>;
5
+ }
6
+ /** The room a question is showing in, in the vocabulary the row stores. A discussion, not a page:
7
+ * a page's other discussions are other conversations, and none of them saw this question. */
8
+ export declare function notionQuestionContainerKey(discussionId: string): string;
9
+ /**
10
+ * The store key for one Notion session's outstanding question, from either side's spelling of the
11
+ * token. `namespaced` says which side is calling — see the header's keying paragraph, which is the
12
+ * whole reason this function exists instead of a bare token argument at two call sites.
13
+ *
14
+ * Returns `undefined` for an unusable token, which every caller treats as "no leg": the question is
15
+ * posted and simply not answerable, exactly as it was before this module existed.
16
+ */
17
+ export declare function notionQuestionKey(token: unknown, namespaced: boolean): string | undefined;
18
+ /**
19
+ * Record that this session is parked on a question the discussion can see.
20
+ *
21
+ * NEVER THROWS — it runs inside a channel event handler on an ALREADY-PARKED turn, and a store blip
22
+ * must not break the event dispatch on top of it. Returns whether the row was written, so the caller
23
+ * can log the difference between "this discussion can answer" and "it cannot".
24
+ */
25
+ export declare function rememberNotionQuestion(input: {
26
+ readonly org: string;
27
+ /** eve's NAMESPACED continuation token, from `channel.continuation.token`. */
28
+ readonly namespacedToken: unknown;
29
+ readonly requestId: string;
30
+ readonly discussionId: string;
31
+ readonly wakeId?: string;
32
+ }): Promise<boolean>;
33
+ /** What one inbound comment resolves to, when it resolves to an answer at all. */
34
+ export interface NotionInboundAnswer {
35
+ readonly requestId: string;
36
+ readonly floor: string;
37
+ readonly wakeId?: string;
38
+ readonly taskId?: string;
39
+ }
40
+ /**
41
+ * Is this inbound comment the answer to the session's outstanding question, and may THIS discussion
42
+ * give it?
43
+ *
44
+ * FAIL-SOFT TO AN ORDINARY WAKE, in every direction. No row, an unreadable store, a comment on
45
+ * another discussion, a refused answerer, a thrown floor — all answer `undefined`, which is the
46
+ * behaviour every comment had before this leg existed. The comment is delivered, the seat hears it,
47
+ * and the question stays open for someone who may answer it. Nothing here may drop a comment and
48
+ * nothing here may throw.
49
+ *
50
+ * 🔴 THE ADMISSION IS THE SAME ONE EVERY OTHER ARM RUNS (OPE-574). Five call sites run
51
+ * {@link admitInputResponse} — three WIDGET arms and two ROOM-MESSAGE arms, which core
52
+ * approvals/input-response-floor.ts lists by file. This is the second ROOM-MESSAGE arm. A comment
53
+ * that resolves a worker's question is the same act as a click on that question's widget over a
54
+ * different transport, so it must meet the same floor. Letting the transport decide would make the
55
+ * gate cosmetic.
56
+ *
57
+ * 🔴 BOTH LEGS ARE INERT ON THIS ARM, so the admission refuses nothing here. Leg B
58
+ * (`answererIsGuest`) is not passed: Notion carries no per-member membership probe, and `undefined`
59
+ * is always inert. Leg A, the room floor, needs a governance member supplier for the room, and the
60
+ * Slack bundle registers the only production one (its channels/slack-events.ts), so no roster
61
+ * resolves for a Notion discussion. The unified window's own room-message arm is NOT the
62
+ * same case: it takes the ORIGIN platform for the partition, so a Slack-origin room message
63
+ * resolves a `slack/...` key and leg A does have teeth there.
64
+ *
65
+ * What carries THIS path instead is the question-to-room binding inside
66
+ * `resolveInboundAnswerRouting`: only the discussion the question was posted into may answer it,
67
+ * and core channels/question-bridge.ts answers `foreign-room` on any mismatch.
68
+ */
69
+ export declare function resolveNotionInboundAnswer(input: {
70
+ readonly org: string;
71
+ /** The BARE token the dispatch minted — NOT `channel.continuation.token`. */
72
+ readonly bareToken: string;
73
+ readonly auth: NotionAnswerAuth;
74
+ }): Promise<NotionInboundAnswer | undefined>;
75
+ /** Consume the row. Called ONLY after eve has accepted the answer — see the header's rule 3. */
76
+ export declare function forgetNotionQuestion(org: string, bareToken: string): Promise<boolean>;
77
+ //# sourceMappingURL=notion-question-return.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notion-question-return.d.ts","sourceRoot":"","sources":["../../../agent/channels/notion-question-return.ts"],"names":[],"mappings":"AAwEA;oGACoG;AACpG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/C;AAED;8FAC8F;AAC9F,wBAAgB,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAGzF;AAQD;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,KAAK,EAAE;IAClD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,8EAA8E;IAC9E,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,OAAO,CAAC,OAAO,CAAC,CAQnB;AAED,kFAAkF;AAClF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,0BAA0B,CAAC,KAAK,EAAE;IACtD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;CACjC,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC,CA8C3C;AAED,gGAAgG;AAChG,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAI3F"}
@@ -0,0 +1,183 @@
1
+ // THE NATIVE ROUTE'S RETURN LEG (OPE-1305) — how a discussion's next comment reaches a DELEGATED
2
+ // WORKER that asked a question, instead of being eaten by the seat that proxied it.
3
+ //
4
+ // WHAT WAS BROKEN, AND WHY IT WAS NOT FIXED BY THE HANDLER THAT POSTS THE QUESTION. OPE-1275 gave
5
+ // Notion's channel a real `input.requested` handler: an `ask_question` now lands in the discussion
6
+ // thread where a person can read it. It records nothing. For a question the SEAT ITSELF raised that
7
+ // is complete — eve consumes the thread's next inbound message as the answer. For a DELEGATED WORKER
8
+ // it is not, and core states the mechanism measured (core channels/question-bridge.ts, the OPE-565
9
+ // header): eve proxies the child's request up through the PARENT's channel adapter, so the prompt
10
+ // reaches the handler and lands in the thread, but the answer reaches the child ONLY through an
11
+ // `inputResponses` entry whose `requestId` sits in the parent's proxy map. A plain message falls to
12
+ // `forSelf`, the parent eats it, and the worker stays parked.
13
+ //
14
+ // So OPE-1275 traded an INVISIBLE unanswerable question for a VISIBLE one. That is a net improvement
15
+ // — a human now sees the turn is waiting — but the false promise is new: the human answers in the
16
+ // thread and believes they answered. This module is the leg that makes the belief true.
17
+ //
18
+ // 🔴 THE GAP IS THE NATIVE ROUTE'S, NOT NOTION'S. `rememberBridgedQuestion` /
19
+ // `resolveInboundAnswerRouting` are core's and are surface-agnostic; what was unified-only was the
20
+ // WIRING (`channels/unified.ts` `divertToUnifiedWindow` reads the row). Every native surface posts
21
+ // eve's own default prompt and had no return leg either. Notion is the surface that needs it first
22
+ // because it is the only one with an OE-authored handler, so this module is written as the pattern
23
+ // the next native surface copies: nothing below knows anything about Notion except the two shapes at
24
+ // the top, and the decision itself is core's.
25
+ //
26
+ // ── THE THREE THINGS A CALLER OWES, ALL OF THEM ─────────────────────────────────────────────────
27
+ // 1. {@link rememberNotionQuestion} on a question that was actually POSTED. A dropped question must
28
+ // never eat the room's next comment: nobody was shown anything to answer.
29
+ // 2. {@link resolveNotionInboundAnswer} on every inbound comment, BEFORE the ordinary send.
30
+ // 3. {@link forgetNotionQuestion} the moment an answer is delivered. CONSUME-ONCE: without it the
31
+ // comment AFTER the answer is delivered as an answer too, to a request eve has already settled.
32
+ //
33
+ // ── KEYING: THE TOKEN, AND THE TWO SIDES THAT SPELL IT DIFFERENTLY ──────────────────────────────
34
+ // 🔴 The row is keyed by the session token, exactly as the unified window keys its own by the window
35
+ // token. The two call sites hold that token in DIFFERENT spellings, and getting this wrong is a
36
+ // silent no-op rather than an error:
37
+ // • the `input.requested` handler reads `channel.continuation.token`, which eve NAMESPACES with the
38
+ // channel definition name — `notion:notion:<pageId>:oe-sup:g<n>`;
39
+ // • the dispatch holds the BARE token it minted — `notion:<pageId>:oe-sup:g<n>`.
40
+ // `bareTokenFromEveContext` strips ONE leading segment, so it must be applied to the namespaced side
41
+ // ONLY. Applied to the bare side it would strip the real `notion:` head and key a different row —
42
+ // core's own header states that ambiguity and resolves it toward the strip, because that helper is
43
+ // for eve-context tokens. {@link notionQuestionKey} is the one place either side goes through, so
44
+ // they cannot drift apart.
45
+ //
46
+ // ── THE ROOM IS PART OF THE DECISION ────────────────────────────────────────────────────────────
47
+ // 🔴 The row carries the DISCUSSION the question was posted into, and an inbound comment from any
48
+ // other discussion is an ordinary wake. Without it, a comment on a different discussion of the same
49
+ // page would be consumed as the answer: that person is never heard, and the worker resumes on text
50
+ // from a conversation that never saw its question. This is the same containerKey comparison the
51
+ // unified window runs, and it fails soft in the one safe direction — an absent or mismatched key is
52
+ // a WAKE with the row LEFT IN PLACE, so the question stays open for the right room.
53
+ //
54
+ // ── SELF-AUTHORED COMMENTS CANNOT REACH HERE ────────────────────────────────────────────────────
55
+ // `internalDispatch` is passed `false`, and that is a measured fact rather than an assumption:
56
+ // `classifyNotionComment` (notion-supervisor.ts) DROPS a comment whose author is the bot user before
57
+ // any dispatch happens, so the agent's own reply into the discussion never becomes a wake and can
58
+ // never be consumed as an answer to its own question.
59
+ import { admitInputResponse, bareTokenFromEveContext, bindFrom, forgetBridgedQuestion, getOperationalStore, readBridgedQuestion, rememberBridgedQuestion, resolvePartition, resolveInboundAnswerRouting, } from "@open-everywhere/core/agent/adapter-api.js";
60
+ /** The room a question is showing in, in the vocabulary the row stores. A discussion, not a page:
61
+ * a page's other discussions are other conversations, and none of them saw this question. */
62
+ export function notionQuestionContainerKey(discussionId) {
63
+ return `notion:${discussionId}`;
64
+ }
65
+ /**
66
+ * The store key for one Notion session's outstanding question, from either side's spelling of the
67
+ * token. `namespaced` says which side is calling — see the header's keying paragraph, which is the
68
+ * whole reason this function exists instead of a bare token argument at two call sites.
69
+ *
70
+ * Returns `undefined` for an unusable token, which every caller treats as "no leg": the question is
71
+ * posted and simply not answerable, exactly as it was before this module existed.
72
+ */
73
+ export function notionQuestionKey(token, namespaced) {
74
+ if (typeof token !== "string" || token.length === 0)
75
+ return undefined;
76
+ return namespaced ? bareTokenFromEveContext(token) : token;
77
+ }
78
+ /** The discussion id on a delivery's auth attributes, or `undefined`. */
79
+ function notionDiscussionIdOf(auth) {
80
+ const v = auth?.attributes?.["discussion_id"];
81
+ return typeof v === "string" && v.length > 0 ? v : undefined;
82
+ }
83
+ /**
84
+ * Record that this session is parked on a question the discussion can see.
85
+ *
86
+ * NEVER THROWS — it runs inside a channel event handler on an ALREADY-PARKED turn, and a store blip
87
+ * must not break the event dispatch on top of it. Returns whether the row was written, so the caller
88
+ * can log the difference between "this discussion can answer" and "it cannot".
89
+ */
90
+ export async function rememberNotionQuestion(input) {
91
+ const key = notionQuestionKey(input.namespacedToken, true);
92
+ if (key === undefined)
93
+ return false;
94
+ return rememberBridgedQuestion(getOperationalStore(), input.org, key, {
95
+ requestId: input.requestId,
96
+ containerKey: notionQuestionContainerKey(input.discussionId),
97
+ ...(input.wakeId ? { wakeId: input.wakeId } : {}),
98
+ });
99
+ }
100
+ /**
101
+ * Is this inbound comment the answer to the session's outstanding question, and may THIS discussion
102
+ * give it?
103
+ *
104
+ * FAIL-SOFT TO AN ORDINARY WAKE, in every direction. No row, an unreadable store, a comment on
105
+ * another discussion, a refused answerer, a thrown floor — all answer `undefined`, which is the
106
+ * behaviour every comment had before this leg existed. The comment is delivered, the seat hears it,
107
+ * and the question stays open for someone who may answer it. Nothing here may drop a comment and
108
+ * nothing here may throw.
109
+ *
110
+ * 🔴 THE ADMISSION IS THE SAME ONE EVERY OTHER ARM RUNS (OPE-574). Five call sites run
111
+ * {@link admitInputResponse} — three WIDGET arms and two ROOM-MESSAGE arms, which core
112
+ * approvals/input-response-floor.ts lists by file. This is the second ROOM-MESSAGE arm. A comment
113
+ * that resolves a worker's question is the same act as a click on that question's widget over a
114
+ * different transport, so it must meet the same floor. Letting the transport decide would make the
115
+ * gate cosmetic.
116
+ *
117
+ * 🔴 BOTH LEGS ARE INERT ON THIS ARM, so the admission refuses nothing here. Leg B
118
+ * (`answererIsGuest`) is not passed: Notion carries no per-member membership probe, and `undefined`
119
+ * is always inert. Leg A, the room floor, needs a governance member supplier for the room, and the
120
+ * Slack bundle registers the only production one (its channels/slack-events.ts), so no roster
121
+ * resolves for a Notion discussion. The unified window's own room-message arm is NOT the
122
+ * same case: it takes the ORIGIN platform for the partition, so a Slack-origin room message
123
+ * resolves a `slack/...` key and leg A does have teeth there.
124
+ *
125
+ * What carries THIS path instead is the question-to-room binding inside
126
+ * `resolveInboundAnswerRouting`: only the discussion the question was posted into may answer it,
127
+ * and core channels/question-bridge.ts answers `foreign-room` on any mismatch.
128
+ */
129
+ export async function resolveNotionInboundAnswer(input) {
130
+ try {
131
+ const key = notionQuestionKey(input.bareToken, false);
132
+ if (key === undefined)
133
+ return undefined;
134
+ const discussionId = notionDiscussionIdOf(input.auth);
135
+ const routing = await resolveInboundAnswerRouting({
136
+ // See the header: a self-authored comment is dropped before dispatch, so nothing that
137
+ // reaches this function is a host-authored notification.
138
+ internalDispatch: false,
139
+ ...(discussionId ? { containerKey: notionQuestionContainerKey(discussionId) } : {}),
140
+ }, {
141
+ read: () => readBridgedQuestion(getOperationalStore(), input.org, key),
142
+ admit: async () => {
143
+ // The partition the PLATFORM names for this comment, read through Notion's own registered
144
+ // identity mapper — the room the floor must judge is the one the comment was really made
145
+ // in, not a label the caller passes.
146
+ const bindable = {
147
+ channel: { kind: "notion" },
148
+ session: { auth: { current: input.auth } },
149
+ };
150
+ const p = resolvePartition(bindable);
151
+ return admitInputResponse({
152
+ org: p.org,
153
+ platform: p.platform,
154
+ workspace: p.workspace,
155
+ ...(p.surface === undefined ? {} : { surface: p.surface }),
156
+ surfaceKey: p.surfaceKey,
157
+ principalId: bindFrom(bindable).principalId ?? "",
158
+ responses: 1,
159
+ });
160
+ },
161
+ });
162
+ if (routing.kind !== "answer")
163
+ return undefined;
164
+ return {
165
+ requestId: routing.requestId,
166
+ floor: routing.floor,
167
+ ...(routing.wakeId !== undefined ? { wakeId: routing.wakeId } : {}),
168
+ ...(routing.taskId !== undefined ? { taskId: routing.taskId } : {}),
169
+ };
170
+ }
171
+ catch {
172
+ // A decision that threw is a decision we do not have. One ordinary wake, never a lost comment.
173
+ return undefined;
174
+ }
175
+ }
176
+ /** Consume the row. Called ONLY after eve has accepted the answer — see the header's rule 3. */
177
+ export async function forgetNotionQuestion(org, bareToken) {
178
+ const key = notionQuestionKey(bareToken, false);
179
+ if (key === undefined)
180
+ return false;
181
+ return forgetBridgedQuestion(getOperationalStore(), org, key);
182
+ }
183
+ //# sourceMappingURL=notion-question-return.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notion-question-return.js","sourceRoot":"","sources":["../../../agent/channels/notion-question-return.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import { type BindableContext, type RenderedLine, type WakeReadAddress } from "@open-everywhere/core/agent/adapter-api.js";
2
+ import { type NotionCallOpts, type NotionCommentSummary } from "./notion-api.js";
3
+ /** How much of a discussion's anchor block the header quotes. A cap, not a preference: `read_channel`
4
+ * is the WIDER view, so a page of long anchor blocks must not become the page's prose by another
5
+ * route (see the header's page-content rule). */
6
+ export declare const ANCHOR_SNIPPET_MAX = 120;
7
+ /** Test/transport seam — the same DI shape every notion tool takes. `call` supplies the resolved
8
+ * credential (and an injectable fetch); absent ⇒ this session's own Notion credential. */
9
+ export interface NotionReadDeps {
10
+ readonly call?: NotionCallOpts;
11
+ }
12
+ /**
13
+ * The header line that OPENS a discussion — its id and what it is anchored to. PURE and exported for
14
+ * the render test.
15
+ *
16
+ * It is the rendering half of the thread-first ruling: it names the discussion, so a reader can never
17
+ * take the next top-level comment for a reply to the previous one. The anchor falls back to the block
18
+ * id when the sweep read no block text, and to "the page" when the comment is page-level — three
19
+ * honest answers rather than one invented snippet.
20
+ */
21
+ export declare function notionDiscussionHeader(c: NotionCommentSummary, pageId: string): string;
22
+ /**
23
+ * The CHANNEL projection — every discussion on the page, each opened by its header. PURE and exported
24
+ * for the render test.
25
+ *
26
+ * 🔴 THE HEADER ON EVERY DISCUSSION'S FIRST LINE IS THE POINT, not decoration. Without it two sibling
27
+ * discussions render as consecutive `[author] text` lines, which is exactly the channel-style
28
+ * continuation the ruling forbids — the model would read the second commenter as answering the first
29
+ * when the two never addressed each other.
30
+ */
31
+ export declare function notionChannelLines(comments: readonly NotionCommentSummary[], pageId: string): RenderedLine[];
32
+ export declare const notionReadAdapter: {
33
+ platform: "notion";
34
+ /**
35
+ * OPE-929 — Notion's own spelling of "which container and which thread is this turn in".
36
+ *
37
+ * Both ids come off the binding's post-back target, which `bindNotion` fills from the wake's
38
+ * `page_id` and `discussion_id` attributes. The CONTAINER is the PAGE and the THREAD ROOT is the
39
+ * DISCUSSION.
40
+ *
41
+ * 🔴 THE PAGE IS READ FROM `target.pageId`, NOT from the binding's `surface`. They usually agree,
42
+ * but `bindNotion`'s surface falls back through discussion id and then comment id when no page id
43
+ * arrived — so on an under-identified wake `surface` holds a discussion id, and a read against it
44
+ * would list a discussion id's comments as if it were a page. Naming the page id directly means an
45
+ * under-identified turn resolves NO container and the caller refuses loudly.
46
+ *
47
+ * 🔴 THE THREAD SLOT IS ALWAYS POPULATED WHEN THE CONTAINER IS. `bindNotion` builds `target` only
48
+ * when a discussion id exists, so `pageId` present implies `discussionId` present. That is the
49
+ * thread-first ruling made structural rather than conventional — see the header.
50
+ */
51
+ wakeAddress(ctx: BindableContext): WakeReadAddress | undefined;
52
+ /** The page's discussions — the deliberate WIDER view. `channel` is the page id. */
53
+ readChannel(ctx: BindableContext, channel: string, limit?: number, deps?: NotionReadDeps): Promise<readonly RenderedLine[]>;
54
+ /**
55
+ * ONE discussion — the conversation this turn is in. `threadTs` is the `discussion_id`.
56
+ *
57
+ * Two moves, cheapest first (see the header): the page's own comments, which serve every page-level
58
+ * discussion in a single call, then the bounded block sweep for an inline one.
59
+ */
60
+ readThread(ctx: BindableContext, channel: string, threadTs: string, limit?: number, deps?: NotionReadDeps): Promise<readonly RenderedLine[]>;
61
+ };
62
+ //# sourceMappingURL=notion-read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notion-read.d.ts","sourceRoot":"","sources":["../../../agent/channels/notion-read.ts"],"names":[],"mappings":"AAwDA,OAAO,EAGL,KAAK,eAAe,EAEpB,KAAK,YAAY,EACjB,KAAK,eAAe,EACrB,MAAM,4CAA4C,CAAC;AACpD,OAAO,EAKL,KAAK,cAAc,EACnB,KAAK,oBAAoB,EAC1B,MAAM,iBAAiB,CAAC;AAKzB;;kDAEkD;AAClD,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAUtC;2FAC2F;AAC3F,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,cAAc,CAAC;CAChC;AAwBD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAStF;AAuBD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,SAAS,oBAAoB,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CAS5G;AAmCD,eAAO,MAAM,iBAAiB;IAC5B,QAAQ,EAAE,QAAQ;IAClB;;;;;;;;;;;;;;;;OAgBG;IACH,WAAW,MAAM,eAAe,GAAG,eAAe,GAAG,SAAS;IAO9D,oFAAoF;IAC9E,WAAW,MAAM,eAAe,WAAW,MAAM,UAAU,MAAM,SAAS,cAAc,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC;IAcjI;;;;;OAKG;IACG,UAAU,MAAM,eAAe,WAAW,MAAM,YAAY,MAAM,UAAU,MAAM,SAAS,cAAc,GAAG,OAAO,CAAC,SAAS,YAAY,EAAE,CAAC;CAwBnJ,CAAC"}
@@ -0,0 +1,257 @@
1
+ // The NOTION read adapter (OPE-988) — the sixth surface's arm of the uniform chat read, and the one
2
+ // whose container model is NOT Slack's.
3
+ //
4
+ // WHAT IT CLOSES. `read_channel` / `read_thread` mount whenever a window has sources and the generic
5
+ // read flag is armed, so on a UNIFIED window they were OFFERED to a Notion-origin turn and then threw
6
+ // "reading a notion channel is not supported yet" at execute time (core channels/channel-read.ts
7
+ // `resolveReadAdapter`). That is the OPE-927 mount-honesty class. A Notion seat read through
8
+ // `read_notion_page` / `read_notion_comments`, vendor verbs, so a Slack seat and a Notion seat held
9
+ // different verbs for one conceptual act.
10
+ //
11
+ // 🔴 NOTION IS THREAD-FIRST, AND THAT IS TOMMY'S RULING (2026-08-22, on the ticket). It is the
12
+ // INVERSE of Slack, and the difference is in the platform, not in our modelling of it:
13
+ // • A Slack channel message has NO thread id until somebody replies — the root's `ts` becomes the
14
+ // thread id at that moment. Top-level messages in a Slack channel ARE one conversation.
15
+ // • A Notion comment carries `discussion_id` FROM BIRTH. A new top-level comment MINTS a new
16
+ // discussion. There is no bare-channel state on Notion, and sibling discussions are UNRELATED —
17
+ // each one is anchored to its own block and is about that block.
18
+ // The ruling that follows: a Notion wake always arrives THREAD-SCOPED, and a new top-level comment
19
+ // must NEVER render as a channel-style continuation of the previous top-level comment.
20
+ //
21
+ // 🔴 THE RULING IS STRUCTURAL HERE, NOT A CONVENTION. {@link notionReadAdapter.wakeAddress} reads
22
+ // both ids off the binding's post-back target, and `bindNotion` (channels/notion-identity.ts) builds
23
+ // that target ONLY when a discussion id is present. So a Notion turn either names a container AND a
24
+ // thread root, or it names neither. There is no turn shape that resolves a Notion container with an
25
+ // empty thread slot, which is what "always thread-scoped" has to mean if it is to survive an edit.
26
+ //
27
+ // 🔴 THE PROJECTION, THEN, IS TWO HONEST VIEWS OF ONE CONTRACT:
28
+ // • `read_thread` = ONE discussion — the conversation this turn is actually in. The DEFAULT view.
29
+ // • `read_channel` = the page's discussions, each opened by a header naming its id and quoting its
30
+ // anchor block. The deliberate WIDER view: what else is being discussed in this document.
31
+ // Every discussion's FIRST line carries that header, so two sibling discussions can never read as
32
+ // one conversation — the second one visibly starts a new discussion. That is the rendering half of
33
+ // the ruling, and {@link notionChannelLines} is where it lives.
34
+ //
35
+ // 🔴 PAGE CONTENT STAYS OUT OF THE CHAT PLANE. The page is the WORK object; the COMMENT LAYER is
36
+ // Notion's chat surface. So neither verb reads block prose — only the anchor snippet a discussion
37
+ // needs to be intelligible, capped at {@link ANCHOR_SNIPPET_MAX}. A seat that needs the document
38
+ // itself uses the grant-mounted `read_notion_page` or delegates to a worker.
39
+ //
40
+ // THE FENCE is the shipped `assertNotionPageAllowed` (channels/notion-scope.ts): it fails closed with
41
+ // a `ChannelScopeError` and audits its refusals, exactly as every other surface's read fence does.
42
+ // The one deployment-wide integration key bounds the WORKSPACE and never the domain, which is why the
43
+ // domain allowlist is ours to enforce and why it runs BEFORE any fetch.
44
+ //
45
+ // THE THREAD READ'S COST, STATED PLAINLY. Notion exposes no "list a discussion's comments" endpoint —
46
+ // comments are listed per BLOCK id (`GET /v1/comments?block_id=…`). So a discussion is found by
47
+ // listing and filtering: the page's own comments first (one call, which serves every page-level
48
+ // discussion), then the bounded block sweep `readNotionComments` already performs. That is the same
49
+ // cost the shipped `read_notion_comments` pays on every call, so this verb is no more expensive than
50
+ // the vendor verb it replaces.
51
+ //
52
+ // NO `refutedAuthorIds`. The core contract calls its absence a real answer, and channel-read.ts names
53
+ // this surface: Notion reads are classified by their own taint-table rows. Notion also refutes no
54
+ // author — its trust is an operator declaration (a page under a declared trust root, or a per-surface
55
+ // vouch), never a platform verdict — so there is no per-author evidence to contribute.
56
+ import { bindFrom, getConfig, } from "@open-everywhere/core/agent/adapter-api.js";
57
+ import { listBlockComments, readNotionComments, resolveNotionCredential, toNotionCommentSummary, } from "./notion-api.js";
58
+ import { notionCallerCredential } from "./notion-credentials.js";
59
+ import { assertNotionActingIdentity } from "./notion-acting-identity.js";
60
+ import { assertNotionPageAllowed } from "./notion-scope.js";
61
+ /** How much of a discussion's anchor block the header quotes. A cap, not a preference: `read_channel`
62
+ * is the WIDER view, so a page of long anchor blocks must not become the page's prose by another
63
+ * route (see the header's page-content rule). */
64
+ export const ANCHOR_SNIPPET_MAX = 120;
65
+ const NOTION_CHANNEL_DEFAULT_LIMIT = 50;
66
+ const NOTION_THREAD_DEFAULT_LIMIT = 100;
67
+ /** Blocks one sweep will scan — `readNotionComments`' own default, restated so both verbs share it. */
68
+ const NOTION_MAX_BLOCK_SCANS = 80;
69
+ /** Comments one THREAD sweep may collect before the filter runs. Higher than the thread limit on
70
+ * purpose: the cap applies to the WHOLE page, and a cap that bit before reaching the target
71
+ * discussion would render an existing thread as an empty one. */
72
+ const NOTION_THREAD_SWEEP_COMMENTS = 300;
73
+ function safeDomains() {
74
+ try {
75
+ return getConfig().loadDomains();
76
+ }
77
+ catch {
78
+ return [];
79
+ }
80
+ }
81
+ /** One Notion author rendered into the neutral line's fields. A BOT is marked a machine for the same
82
+ * reason every other adapter marks one: a bot sits below peer standing and is never an outside
83
+ * person. An unattributed comment carries neither id nor mark. */
84
+ function authorFields(c) {
85
+ if (!c.authorId)
86
+ return { author: "(unknown)" };
87
+ const author = `${c.authorKind ?? "user"} ${c.authorId}`;
88
+ return c.authorKind === "bot" ? { author, bot: true } : { author, authorId: c.authorId };
89
+ }
90
+ function clip(text, max) {
91
+ const flat = text.replace(/\s+/gu, " ").trim();
92
+ return flat.length <= max ? flat : `${flat.slice(0, max - 1)}…`;
93
+ }
94
+ /**
95
+ * The header line that OPENS a discussion — its id and what it is anchored to. PURE and exported for
96
+ * the render test.
97
+ *
98
+ * It is the rendering half of the thread-first ruling: it names the discussion, so a reader can never
99
+ * take the next top-level comment for a reply to the previous one. The anchor falls back to the block
100
+ * id when the sweep read no block text, and to "the page" when the comment is page-level — three
101
+ * honest answers rather than one invented snippet.
102
+ */
103
+ export function notionDiscussionHeader(c, pageId) {
104
+ const id = c.discussionId ?? "(unknown)";
105
+ const anchored = c.anchorText && c.anchorText.trim().length > 0
106
+ ? `on "${clip(c.anchorText, ANCHOR_SNIPPET_MAX)}"`
107
+ : c.anchorId && c.anchorId !== pageId
108
+ ? `on block ${c.anchorId}`
109
+ : "on the page";
110
+ return `[discussion ${id} — ${anchored}]`;
111
+ }
112
+ /** Group a flat comment page into discussions, PRESERVING the sweep's order (the page's own comments
113
+ * first, then its blocks in document order). Document order is the meaningful one on Notion: a
114
+ * discussion is anchored to a place in the page, not to a moment in a conversation. */
115
+ function byDiscussion(comments) {
116
+ const groups = new Map();
117
+ const order = [];
118
+ for (const c of comments) {
119
+ // A comment with no discussion id is its own group keyed by anchor — it can still be rendered
120
+ // honestly, and it must never be merged into a real discussion it is not part of.
121
+ const key = c.discussionId ?? `anchor:${c.anchorId ?? "unknown"}`;
122
+ let group = groups.get(key);
123
+ if (!group) {
124
+ group = [];
125
+ groups.set(key, group);
126
+ order.push(key);
127
+ }
128
+ group.push(c);
129
+ }
130
+ return order.map((key) => groups.get(key) ?? []);
131
+ }
132
+ /**
133
+ * The CHANNEL projection — every discussion on the page, each opened by its header. PURE and exported
134
+ * for the render test.
135
+ *
136
+ * 🔴 THE HEADER ON EVERY DISCUSSION'S FIRST LINE IS THE POINT, not decoration. Without it two sibling
137
+ * discussions render as consecutive `[author] text` lines, which is exactly the channel-style
138
+ * continuation the ruling forbids — the model would read the second commenter as answering the first
139
+ * when the two never addressed each other.
140
+ */
141
+ export function notionChannelLines(comments, pageId) {
142
+ const lines = [];
143
+ for (const group of byDiscussion(comments)) {
144
+ group.forEach((c, index) => {
145
+ const body = index === 0 ? `${notionDiscussionHeader(c, pageId)}\n${c.text}` : c.text;
146
+ lines.push({ ...authorFields(c), ts: c.createdTime ?? "", text: body });
147
+ });
148
+ }
149
+ return lines;
150
+ }
151
+ /**
152
+ * The THREAD projection — one discussion, opened by the same header so its anchor block is quoted as
153
+ * the topic. PURE — the render tests reach it through this module's read entry points.
154
+ */
155
+ function notionThreadLines(comments, discussionId, pageId) {
156
+ const mine = comments.filter((c) => c.discussionId === discussionId);
157
+ return mine.map((c, index) => ({
158
+ ...authorFields(c),
159
+ ts: c.createdTime ?? "",
160
+ text: index === 0 ? `${notionDiscussionHeader(c, pageId)}\n${c.text}` : c.text,
161
+ }));
162
+ }
163
+ /**
164
+ * Fence the page, then resolve the credential to read it with. The FIRST steps of both reads, in one
165
+ * place so they can never come apart. The fence runs BEFORE the credential resolves, so a refused
166
+ * page never reaches a call that could fetch it.
167
+ */
168
+ async function admit(ctx, page, deps) {
169
+ const domains = safeDomains();
170
+ await assertNotionPageAllowed(ctx, page, domains);
171
+ // 🔴 OPE-1525 — THE OWN-ACCOUNT GATE, and it runs BEFORE the injected-call short circuit below.
172
+ // This function backs the generic `read_channel` / `read_thread` pair, which read_notion_page.ts's
173
+ // own header calls its SUPERSET — so gating that tool and leaving this one would hand a worker the
174
+ // same pages one verb along. Every credential resolved below is `install`-family, so a member who
175
+ // asked to act as themselves has no answer here. See channels/notion-acting-identity.ts.
176
+ await assertNotionActingIdentity(ctx, "notion_read");
177
+ if (deps?.call)
178
+ return deps.call;
179
+ const cred = await resolveNotionCredential(notionCallerCredential(ctx, domains));
180
+ if (!cred)
181
+ throw new Error("read: no Notion credential is stored for this session, so nothing can be read.");
182
+ return cred;
183
+ }
184
+ export const notionReadAdapter = {
185
+ platform: "notion",
186
+ /**
187
+ * OPE-929 — Notion's own spelling of "which container and which thread is this turn in".
188
+ *
189
+ * Both ids come off the binding's post-back target, which `bindNotion` fills from the wake's
190
+ * `page_id` and `discussion_id` attributes. The CONTAINER is the PAGE and the THREAD ROOT is the
191
+ * DISCUSSION.
192
+ *
193
+ * 🔴 THE PAGE IS READ FROM `target.pageId`, NOT from the binding's `surface`. They usually agree,
194
+ * but `bindNotion`'s surface falls back through discussion id and then comment id when no page id
195
+ * arrived — so on an under-identified wake `surface` holds a discussion id, and a read against it
196
+ * would list a discussion id's comments as if it were a page. Naming the page id directly means an
197
+ * under-identified turn resolves NO container and the caller refuses loudly.
198
+ *
199
+ * 🔴 THE THREAD SLOT IS ALWAYS POPULATED WHEN THE CONTAINER IS. `bindNotion` builds `target` only
200
+ * when a discussion id exists, so `pageId` present implies `discussionId` present. That is the
201
+ * thread-first ruling made structural rather than conventional — see the header.
202
+ */
203
+ wakeAddress(ctx) {
204
+ const target = bindFrom(ctx).target;
205
+ const container = typeof target?.pageId === "string" && target.pageId.length > 0 ? target.pageId : undefined;
206
+ if (!container)
207
+ return undefined;
208
+ const threadRoot = typeof target?.discussionId === "string" && target.discussionId.length > 0 ? target.discussionId : undefined;
209
+ return { container, ...(threadRoot ? { threadRoot } : {}) };
210
+ },
211
+ /** The page's discussions — the deliberate WIDER view. `channel` is the page id. */
212
+ async readChannel(ctx, channel, limit, deps) {
213
+ const call = await admit(ctx, channel, deps);
214
+ const effective = Math.max(1, limit ?? NOTION_CHANNEL_DEFAULT_LIMIT);
215
+ // The sweep's comment cap IS the caller's limit, which is what keeps core's coverage disclosure
216
+ // honest: a capped sweep returns exactly `effective` lines, and `possiblyIncomplete` keys on that
217
+ // count. A larger fetch trimmed afterwards would report a partial page as a complete one.
218
+ const res = await readNotionComments(channel, { ...call, maxComments: effective, maxBlockScans: NOTION_MAX_BLOCK_SCANS });
219
+ // OPE-1039 — stamped AFTER the fetch returns (read_notion_comments.ts's own `readAt` rule). NO
220
+ // `live` line: the page's own prose stays out of the chat plane by ruling (see the header), so a
221
+ // generic Notion read has no live field to distinguish from history — every line here is a
222
+ // comment, dated against this moment, and that is the whole of what this surface can offer.
223
+ const readAt = new Date().toISOString();
224
+ return notionChannelLines(res.comments, channel).map((line) => ({ ...line, readAt }));
225
+ },
226
+ /**
227
+ * ONE discussion — the conversation this turn is in. `threadTs` is the `discussion_id`.
228
+ *
229
+ * Two moves, cheapest first (see the header): the page's own comments, which serve every page-level
230
+ * discussion in a single call, then the bounded block sweep for an inline one.
231
+ */
232
+ async readThread(ctx, channel, threadTs, limit, deps) {
233
+ const call = await admit(ctx, channel, deps);
234
+ const pageLevel = (await listBlockComments(channel, call)).filter((c) => c.discussion_id === threadTs);
235
+ if (pageLevel.length > 0) {
236
+ // No anchor text on this path: a page-level comment anchors to the PAGE, which has no block
237
+ // prose, so the header says "on the page" rather than quoting something.
238
+ const summaries = pageLevel.map((c) => toNotionCommentSummary(c, channel));
239
+ // OPE-1039 — see readChannel's note: dated, no live line.
240
+ const readAt = new Date().toISOString();
241
+ return notionThreadLines(summaries, threadTs, channel)
242
+ .map((line) => ({ ...line, readAt }))
243
+ .slice(-(limit ?? NOTION_THREAD_DEFAULT_LIMIT));
244
+ }
245
+ const res = await readNotionComments(channel, {
246
+ ...call,
247
+ maxComments: NOTION_THREAD_SWEEP_COMMENTS,
248
+ maxBlockScans: NOTION_MAX_BLOCK_SCANS,
249
+ });
250
+ // OPE-1039 — see readChannel's note: dated, no live line.
251
+ const readAt = new Date().toISOString();
252
+ return notionThreadLines(res.comments, threadTs, channel)
253
+ .map((line) => ({ ...line, readAt }))
254
+ .slice(-(limit ?? NOTION_THREAD_DEFAULT_LIMIT));
255
+ },
256
+ };
257
+ //# sourceMappingURL=notion-read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notion-read.js","sourceRoot":"","sources":["../../../agent/channels/notion-read.ts"],"names":[],"mappings":"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"}