@immediately-run/sdk 0.46.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/dist/MDXProvider.cjs +1 -5
  2. package/dist/MDXProvider.cjs.map +1 -1
  3. package/dist/MDXProvider.js +1 -5
  4. package/dist/MDXProvider.js.map +1 -1
  5. package/dist/RoutingSpec.cjs.map +1 -1
  6. package/dist/TinkerableContext.cjs.map +1 -1
  7. package/dist/TinkerableContext.js.map +1 -1
  8. package/dist/auth.cjs.map +1 -1
  9. package/dist/auth.js.map +1 -1
  10. package/dist/boot.cjs +2 -9
  11. package/dist/boot.cjs.map +1 -1
  12. package/dist/boot.d.cts +2 -2
  13. package/dist/boot.d.ts +2 -2
  14. package/dist/boot.js +2 -9
  15. package/dist/boot.js.map +1 -1
  16. package/dist/catalog.cjs.map +1 -1
  17. package/dist/catalog.js.map +1 -1
  18. package/dist/components/Admonition.cjs +4 -13
  19. package/dist/components/Admonition.cjs.map +1 -1
  20. package/dist/components/Admonition.js +4 -13
  21. package/dist/components/Admonition.js.map +1 -1
  22. package/dist/components/FileRouter.cjs +11 -3
  23. package/dist/components/FileRouter.cjs.map +1 -1
  24. package/dist/components/FileRouter.js +11 -3
  25. package/dist/components/FileRouter.js.map +1 -1
  26. package/dist/components/HeadingAnchor.cjs.map +1 -1
  27. package/dist/components/HeadingAnchor.js.map +1 -1
  28. package/dist/components/Link.cjs.map +1 -1
  29. package/dist/components/Link.js.map +1 -1
  30. package/dist/components/MainContent.cjs +11 -1
  31. package/dist/components/MainContent.cjs.map +1 -1
  32. package/dist/components/MainContent.js +11 -1
  33. package/dist/components/MainContent.js.map +1 -1
  34. package/dist/components/MountImage.cjs +1 -9
  35. package/dist/components/MountImage.cjs.map +1 -1
  36. package/dist/components/MountImage.js +1 -9
  37. package/dist/components/MountImage.js.map +1 -1
  38. package/dist/components/Routes.cjs +1 -4
  39. package/dist/components/Routes.cjs.map +1 -1
  40. package/dist/components/Routes.d.cts +1 -1
  41. package/dist/components/Routes.d.ts +1 -1
  42. package/dist/components/Routes.js +1 -4
  43. package/dist/components/Routes.js.map +1 -1
  44. package/dist/components/SafeInclude.cjs +1 -4
  45. package/dist/components/SafeInclude.cjs.map +1 -1
  46. package/dist/components/SafeInclude.js +1 -4
  47. package/dist/components/SafeInclude.js.map +1 -1
  48. package/dist/components/WikiLink.cjs +1 -10
  49. package/dist/components/WikiLink.cjs.map +1 -1
  50. package/dist/components/WikiLink.js +1 -10
  51. package/dist/components/WikiLink.js.map +1 -1
  52. package/dist/components/defaults.cjs.map +1 -1
  53. package/dist/components/defaults.d.cts +1 -1
  54. package/dist/components/defaults.d.ts +1 -1
  55. package/dist/components/defaults.js.map +1 -1
  56. package/dist/components/errors.cjs +3 -1
  57. package/dist/components/errors.cjs.map +1 -1
  58. package/dist/components/errors.js +3 -1
  59. package/dist/components/errors.js.map +1 -1
  60. package/dist/contextUtils.cjs.map +1 -1
  61. package/dist/contextUtils.js.map +1 -1
  62. package/dist/contribute.cjs.map +1 -1
  63. package/dist/contribute.js.map +1 -1
  64. package/dist/debug.cjs +2 -1
  65. package/dist/debug.cjs.map +1 -1
  66. package/dist/debug.js +3 -8
  67. package/dist/debug.js.map +1 -1
  68. package/dist/diagnostics.cjs.map +1 -1
  69. package/dist/diagnostics.js.map +1 -1
  70. package/dist/editor.cjs.map +1 -1
  71. package/dist/editor.js.map +1 -1
  72. package/dist/editorContext.cjs.map +1 -1
  73. package/dist/editorContext.js.map +1 -1
  74. package/dist/fs.cjs.map +1 -1
  75. package/dist/fs.js.map +1 -1
  76. package/dist/hostTransport.cjs.map +1 -1
  77. package/dist/hostTransport.js.map +1 -1
  78. package/dist/index.cjs.map +1 -1
  79. package/dist/index.d.cts +1 -1
  80. package/dist/index.d.ts +1 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/injectedBundler.cjs.map +1 -1
  83. package/dist/injectedBundler.js.map +1 -1
  84. package/dist/ipc.cjs +1 -4
  85. package/dist/ipc.cjs.map +1 -1
  86. package/dist/ipc.js +1 -4
  87. package/dist/ipc.js.map +1 -1
  88. package/dist/irMarkers.cjs.map +1 -1
  89. package/dist/irMarkers.d.cts +11 -11
  90. package/dist/irMarkers.d.ts +11 -11
  91. package/dist/irMarkers.js.map +1 -1
  92. package/dist/launch.cjs.map +1 -1
  93. package/dist/launch.js.map +1 -1
  94. package/dist/llm.cjs +9 -6
  95. package/dist/llm.cjs.map +1 -1
  96. package/dist/llm.d.cts +40 -1
  97. package/dist/llm.d.ts +40 -1
  98. package/dist/llm.js +8 -6
  99. package/dist/llm.js.map +1 -1
  100. package/dist/loading.cjs +18 -14
  101. package/dist/loading.cjs.map +1 -1
  102. package/dist/loading.d.cts +2 -2
  103. package/dist/loading.d.ts +2 -2
  104. package/dist/loading.js +19 -19
  105. package/dist/loading.js.map +1 -1
  106. package/dist/markers.cjs.map +1 -1
  107. package/dist/markers.js.map +1 -1
  108. package/dist/metadataSource.cjs +1 -5
  109. package/dist/metadataSource.cjs.map +1 -1
  110. package/dist/metadataSource.d.cts +1 -1
  111. package/dist/metadataSource.d.ts +1 -1
  112. package/dist/metadataSource.js +1 -5
  113. package/dist/metadataSource.js.map +1 -1
  114. package/dist/moduleCache.cjs +4 -1
  115. package/dist/moduleCache.cjs.map +1 -1
  116. package/dist/moduleCache.d.cts +1 -1
  117. package/dist/moduleCache.d.ts +1 -1
  118. package/dist/moduleCache.js +4 -1
  119. package/dist/moduleCache.js.map +1 -1
  120. package/dist/mountMatch.cjs.map +1 -1
  121. package/dist/mountMatch.js.map +1 -1
  122. package/dist/mounts.cjs +6 -1
  123. package/dist/mounts.cjs.map +1 -1
  124. package/dist/mounts.d.cts +2 -2
  125. package/dist/mounts.d.ts +2 -2
  126. package/dist/mounts.js +6 -1
  127. package/dist/mounts.js.map +1 -1
  128. package/dist/netFetch.cjs +3 -5
  129. package/dist/netFetch.cjs.map +1 -1
  130. package/dist/netFetch.js +3 -5
  131. package/dist/netFetch.js.map +1 -1
  132. package/dist/onFsChange.cjs.map +1 -1
  133. package/dist/onFsChange.js.map +1 -1
  134. package/dist/pathUtils.cjs +9 -12
  135. package/dist/pathUtils.cjs.map +1 -1
  136. package/dist/pathUtils.js +9 -12
  137. package/dist/pathUtils.js.map +1 -1
  138. package/dist/protocolDeadline.cjs.map +1 -1
  139. package/dist/protocolDeadline.js.map +1 -1
  140. package/dist/protocolStream.cjs +2 -13
  141. package/dist/protocolStream.cjs.map +1 -1
  142. package/dist/protocolStream.js +2 -13
  143. package/dist/protocolStream.js.map +1 -1
  144. package/dist/ready.cjs.map +1 -1
  145. package/dist/ready.js.map +1 -1
  146. package/dist/routing.cjs +3 -1
  147. package/dist/routing.cjs.map +1 -1
  148. package/dist/routing.js +3 -1
  149. package/dist/routing.js.map +1 -1
  150. package/dist/safeContent/index.cjs.map +1 -1
  151. package/dist/safeContent/index.js.map +1 -1
  152. package/dist/safeContent/parseSafeMdast.cjs.map +1 -1
  153. package/dist/safeContent/parseSafeMdast.js.map +1 -1
  154. package/dist/safeContent/renderMdast.cjs.map +1 -1
  155. package/dist/safeContent/renderMdast.js.map +1 -1
  156. package/dist/sandboxTypes.cjs.map +1 -1
  157. package/dist/sandboxUtils.cjs +1 -6
  158. package/dist/sandboxUtils.cjs.map +1 -1
  159. package/dist/sandboxUtils.js +1 -6
  160. package/dist/sandboxUtils.js.map +1 -1
  161. package/dist/scrollToId.cjs.map +1 -1
  162. package/dist/scrollToId.js.map +1 -1
  163. package/dist/secrets.cjs.map +1 -1
  164. package/dist/secrets.js.map +1 -1
  165. package/dist/tasks.cjs +12 -2
  166. package/dist/tasks.cjs.map +1 -1
  167. package/dist/tasks.js +12 -2
  168. package/dist/tasks.js.map +1 -1
  169. package/dist/testing.cjs +1 -3
  170. package/dist/testing.cjs.map +1 -1
  171. package/dist/testing.js +1 -3
  172. package/dist/testing.js.map +1 -1
  173. package/dist/theme.cjs.map +1 -1
  174. package/dist/theme.js.map +1 -1
  175. package/dist/urlUtils.cjs +7 -18
  176. package/dist/urlUtils.cjs.map +1 -1
  177. package/dist/urlUtils.js +7 -18
  178. package/dist/urlUtils.js.map +1 -1
  179. package/dist/vcs.cjs +1 -3
  180. package/dist/vcs.cjs.map +1 -1
  181. package/dist/vcs.js +1 -3
  182. package/dist/vcs.js.map +1 -1
  183. package/dist/version.cjs +1 -1
  184. package/dist/version.cjs.map +1 -1
  185. package/dist/version.d.cts +1 -1
  186. package/dist/version.d.ts +1 -1
  187. package/dist/version.js +1 -1
  188. package/dist/version.js.map +1 -1
  189. package/package.json +10 -4
@@ -37,18 +37,15 @@ const joinPaths = (...pathPart) => pathPart.reduce((acc, part) => {
37
37
  const absPath = (rawPath) => {
38
38
  const absCandidate = joinPaths.apply(
39
39
  null,
40
- rawPath.split(PATH_SEPARATOR).reduce(
41
- (partialAbsPath, currentPathPart) => {
42
- if (currentPathPart == ".") {
43
- return partialAbsPath;
44
- }
45
- if (currentPathPart == "..") {
46
- return partialAbsPath.slice(0, -1);
47
- }
48
- return partialAbsPath.concat(currentPathPart);
49
- },
50
- []
51
- )
40
+ rawPath.split(PATH_SEPARATOR).reduce((partialAbsPath, currentPathPart) => {
41
+ if (currentPathPart == ".") {
42
+ return partialAbsPath;
43
+ }
44
+ if (currentPathPart == "..") {
45
+ return partialAbsPath.slice(0, -1);
46
+ }
47
+ return partialAbsPath.concat(currentPathPart);
48
+ }, [])
52
49
  );
53
50
  if (absCandidate === "" && rawPath.startsWith(PATH_SEPARATOR)) {
54
51
  return PATH_SEPARATOR;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pathUtils.ts"],"sourcesContent":["const PATH_SEPARATOR = \"/\";\n\nexport const joinPaths = (...pathPart: string[]) => pathPart.reduce((acc, part) => {\n const left = (acc.endsWith(PATH_SEPARATOR)) ? acc.slice(0, -1) : acc;\n const right = (part.startsWith(PATH_SEPARATOR)) ? part.substring(1) : part;\n if (left || acc === PATH_SEPARATOR) {\n return `${left}${PATH_SEPARATOR}${right}`;\n }\n if (part.startsWith(PATH_SEPARATOR)) {\n return `${PATH_SEPARATOR}${right}`;\n }\n return right;\n}, \"\");\n\nexport const absPath = (rawPath: string): string => {\n const absCandidate = joinPaths.apply(\n null,\n rawPath.split(PATH_SEPARATOR).reduce(\n (partialAbsPath: string[], currentPathPart: string) => {\n if (currentPathPart == '.') {\n return partialAbsPath;\n }\n if (currentPathPart == '..') {\n return partialAbsPath.slice(0, -1)\n }\n return partialAbsPath.concat(currentPathPart);\n },\n []\n ));\n if (absCandidate === '' && rawPath.startsWith(PATH_SEPARATOR)) {\n return PATH_SEPARATOR;\n }\n return absCandidate;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAAM,iBAAiB;AAEhB,MAAM,YAAY,IAAI,aAAuB,SAAS,OAAO,CAAC,KAAK,SAAS;AACjF,QAAM,OAAQ,IAAI,SAAS,cAAc,IAAK,IAAI,MAAM,GAAG,EAAE,IAAI;AACjE,QAAM,QAAS,KAAK,WAAW,cAAc,IAAK,KAAK,UAAU,CAAC,IAAI;AACtE,MAAI,QAAQ,QAAQ,gBAAgB;AAClC,WAAO,GAAG,IAAI,GAAG,cAAc,GAAG,KAAK;AAAA,EACzC;AACA,MAAI,KAAK,WAAW,cAAc,GAAG;AACnC,WAAO,GAAG,cAAc,GAAG,KAAK;AAAA,EAClC;AACA,SAAO;AACT,GAAG,EAAE;AAEE,MAAM,UAAU,CAAC,YAA4B;AAClD,QAAM,eAAe,UAAU;AAAA,IAC7B;AAAA,IACA,QAAQ,MAAM,cAAc,EAAE;AAAA,MAC5B,CAAC,gBAA0B,oBAA4B;AACrD,YAAI,mBAAmB,KAAK;AAC1B,iBAAO;AAAA,QACT;AACA,YAAI,mBAAmB,MAAM;AAC3B,iBAAO,eAAe,MAAM,GAAG,EAAE;AAAA,QACnC;AACA,eAAO,eAAe,OAAO,eAAe;AAAA,MAC9C;AAAA,MACA,CAAC;AAAA,IACH;AAAA,EAAC;AACH,MAAI,iBAAiB,MAAM,QAAQ,WAAW,cAAc,GAAG;AAC7D,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/pathUtils.ts"],"sourcesContent":["const PATH_SEPARATOR = '/';\n\nexport const joinPaths = (...pathPart: string[]) =>\n pathPart.reduce((acc, part) => {\n const left = acc.endsWith(PATH_SEPARATOR) ? acc.slice(0, -1) : acc;\n const right = part.startsWith(PATH_SEPARATOR) ? part.substring(1) : part;\n if (left || acc === PATH_SEPARATOR) {\n return `${left}${PATH_SEPARATOR}${right}`;\n }\n if (part.startsWith(PATH_SEPARATOR)) {\n return `${PATH_SEPARATOR}${right}`;\n }\n return right;\n }, '');\n\nexport const absPath = (rawPath: string): string => {\n const absCandidate = joinPaths.apply(\n null,\n rawPath.split(PATH_SEPARATOR).reduce((partialAbsPath: string[], currentPathPart: string) => {\n if (currentPathPart == '.') {\n return partialAbsPath;\n }\n if (currentPathPart == '..') {\n return partialAbsPath.slice(0, -1);\n }\n return partialAbsPath.concat(currentPathPart);\n }, []),\n );\n if (absCandidate === '' && rawPath.startsWith(PATH_SEPARATOR)) {\n return PATH_SEPARATOR;\n }\n return absCandidate;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAAM,iBAAiB;AAEhB,MAAM,YAAY,IAAI,aAC3B,SAAS,OAAO,CAAC,KAAK,SAAS;AAC7B,QAAM,OAAO,IAAI,SAAS,cAAc,IAAI,IAAI,MAAM,GAAG,EAAE,IAAI;AAC/D,QAAM,QAAQ,KAAK,WAAW,cAAc,IAAI,KAAK,UAAU,CAAC,IAAI;AACpE,MAAI,QAAQ,QAAQ,gBAAgB;AAClC,WAAO,GAAG,IAAI,GAAG,cAAc,GAAG,KAAK;AAAA,EACzC;AACA,MAAI,KAAK,WAAW,cAAc,GAAG;AACnC,WAAO,GAAG,cAAc,GAAG,KAAK;AAAA,EAClC;AACA,SAAO;AACT,GAAG,EAAE;AAEA,MAAM,UAAU,CAAC,YAA4B;AAClD,QAAM,eAAe,UAAU;AAAA,IAC7B;AAAA,IACA,QAAQ,MAAM,cAAc,EAAE,OAAO,CAAC,gBAA0B,oBAA4B;AAC1F,UAAI,mBAAmB,KAAK;AAC1B,eAAO;AAAA,MACT;AACA,UAAI,mBAAmB,MAAM;AAC3B,eAAO,eAAe,MAAM,GAAG,EAAE;AAAA,MACnC;AACA,aAAO,eAAe,OAAO,eAAe;AAAA,IAC9C,GAAG,CAAC,CAAC;AAAA,EACP;AACA,MAAI,iBAAiB,MAAM,QAAQ,WAAW,cAAc,GAAG;AAC7D,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
package/dist/pathUtils.js CHANGED
@@ -14,18 +14,15 @@ const joinPaths = (...pathPart) => pathPart.reduce((acc, part) => {
14
14
  const absPath = (rawPath) => {
15
15
  const absCandidate = joinPaths.apply(
16
16
  null,
17
- rawPath.split(PATH_SEPARATOR).reduce(
18
- (partialAbsPath, currentPathPart) => {
19
- if (currentPathPart == ".") {
20
- return partialAbsPath;
21
- }
22
- if (currentPathPart == "..") {
23
- return partialAbsPath.slice(0, -1);
24
- }
25
- return partialAbsPath.concat(currentPathPart);
26
- },
27
- []
28
- )
17
+ rawPath.split(PATH_SEPARATOR).reduce((partialAbsPath, currentPathPart) => {
18
+ if (currentPathPart == ".") {
19
+ return partialAbsPath;
20
+ }
21
+ if (currentPathPart == "..") {
22
+ return partialAbsPath.slice(0, -1);
23
+ }
24
+ return partialAbsPath.concat(currentPathPart);
25
+ }, [])
29
26
  );
30
27
  if (absCandidate === "" && rawPath.startsWith(PATH_SEPARATOR)) {
31
28
  return PATH_SEPARATOR;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pathUtils.ts"],"sourcesContent":["const PATH_SEPARATOR = \"/\";\n\nexport const joinPaths = (...pathPart: string[]) => pathPart.reduce((acc, part) => {\n const left = (acc.endsWith(PATH_SEPARATOR)) ? acc.slice(0, -1) : acc;\n const right = (part.startsWith(PATH_SEPARATOR)) ? part.substring(1) : part;\n if (left || acc === PATH_SEPARATOR) {\n return `${left}${PATH_SEPARATOR}${right}`;\n }\n if (part.startsWith(PATH_SEPARATOR)) {\n return `${PATH_SEPARATOR}${right}`;\n }\n return right;\n}, \"\");\n\nexport const absPath = (rawPath: string): string => {\n const absCandidate = joinPaths.apply(\n null,\n rawPath.split(PATH_SEPARATOR).reduce(\n (partialAbsPath: string[], currentPathPart: string) => {\n if (currentPathPart == '.') {\n return partialAbsPath;\n }\n if (currentPathPart == '..') {\n return partialAbsPath.slice(0, -1)\n }\n return partialAbsPath.concat(currentPathPart);\n },\n []\n ));\n if (absCandidate === '' && rawPath.startsWith(PATH_SEPARATOR)) {\n return PATH_SEPARATOR;\n }\n return absCandidate;\n}\n"],"mappings":";AAAA,MAAM,iBAAiB;AAEhB,MAAM,YAAY,IAAI,aAAuB,SAAS,OAAO,CAAC,KAAK,SAAS;AACjF,QAAM,OAAQ,IAAI,SAAS,cAAc,IAAK,IAAI,MAAM,GAAG,EAAE,IAAI;AACjE,QAAM,QAAS,KAAK,WAAW,cAAc,IAAK,KAAK,UAAU,CAAC,IAAI;AACtE,MAAI,QAAQ,QAAQ,gBAAgB;AAClC,WAAO,GAAG,IAAI,GAAG,cAAc,GAAG,KAAK;AAAA,EACzC;AACA,MAAI,KAAK,WAAW,cAAc,GAAG;AACnC,WAAO,GAAG,cAAc,GAAG,KAAK;AAAA,EAClC;AACA,SAAO;AACT,GAAG,EAAE;AAEE,MAAM,UAAU,CAAC,YAA4B;AAClD,QAAM,eAAe,UAAU;AAAA,IAC7B;AAAA,IACA,QAAQ,MAAM,cAAc,EAAE;AAAA,MAC5B,CAAC,gBAA0B,oBAA4B;AACrD,YAAI,mBAAmB,KAAK;AAC1B,iBAAO;AAAA,QACT;AACA,YAAI,mBAAmB,MAAM;AAC3B,iBAAO,eAAe,MAAM,GAAG,EAAE;AAAA,QACnC;AACA,eAAO,eAAe,OAAO,eAAe;AAAA,MAC9C;AAAA,MACA,CAAC;AAAA,IACH;AAAA,EAAC;AACH,MAAI,iBAAiB,MAAM,QAAQ,WAAW,cAAc,GAAG;AAC7D,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/pathUtils.ts"],"sourcesContent":["const PATH_SEPARATOR = '/';\n\nexport const joinPaths = (...pathPart: string[]) =>\n pathPart.reduce((acc, part) => {\n const left = acc.endsWith(PATH_SEPARATOR) ? acc.slice(0, -1) : acc;\n const right = part.startsWith(PATH_SEPARATOR) ? part.substring(1) : part;\n if (left || acc === PATH_SEPARATOR) {\n return `${left}${PATH_SEPARATOR}${right}`;\n }\n if (part.startsWith(PATH_SEPARATOR)) {\n return `${PATH_SEPARATOR}${right}`;\n }\n return right;\n }, '');\n\nexport const absPath = (rawPath: string): string => {\n const absCandidate = joinPaths.apply(\n null,\n rawPath.split(PATH_SEPARATOR).reduce((partialAbsPath: string[], currentPathPart: string) => {\n if (currentPathPart == '.') {\n return partialAbsPath;\n }\n if (currentPathPart == '..') {\n return partialAbsPath.slice(0, -1);\n }\n return partialAbsPath.concat(currentPathPart);\n }, []),\n );\n if (absCandidate === '' && rawPath.startsWith(PATH_SEPARATOR)) {\n return PATH_SEPARATOR;\n }\n return absCandidate;\n};\n"],"mappings":";AAAA,MAAM,iBAAiB;AAEhB,MAAM,YAAY,IAAI,aAC3B,SAAS,OAAO,CAAC,KAAK,SAAS;AAC7B,QAAM,OAAO,IAAI,SAAS,cAAc,IAAI,IAAI,MAAM,GAAG,EAAE,IAAI;AAC/D,QAAM,QAAQ,KAAK,WAAW,cAAc,IAAI,KAAK,UAAU,CAAC,IAAI;AACpE,MAAI,QAAQ,QAAQ,gBAAgB;AAClC,WAAO,GAAG,IAAI,GAAG,cAAc,GAAG,KAAK;AAAA,EACzC;AACA,MAAI,KAAK,WAAW,cAAc,GAAG;AACnC,WAAO,GAAG,cAAc,GAAG,KAAK;AAAA,EAClC;AACA,SAAO;AACT,GAAG,EAAE;AAEA,MAAM,UAAU,CAAC,YAA4B;AAClD,QAAM,eAAe,UAAU;AAAA,IAC7B;AAAA,IACA,QAAQ,MAAM,cAAc,EAAE,OAAO,CAAC,gBAA0B,oBAA4B;AAC1F,UAAI,mBAAmB,KAAK;AAC1B,eAAO;AAAA,MACT;AACA,UAAI,mBAAmB,MAAM;AAC3B,eAAO,eAAe,MAAM,GAAG,EAAE;AAAA,MACnC;AACA,aAAO,eAAe,OAAO,eAAe;AAAA,IAC9C,GAAG,CAAC,CAAC;AAAA,EACP;AACA,MAAI,iBAAiB,MAAM,QAAQ,WAAW,cAAc,GAAG;AAC7D,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/protocolDeadline.ts"],"sourcesContent":["// Deadlines for host protocol calls (R3-298) — so no platform request can hang forever.\n//\n// THE FAILURE THIS FIXES. `protocolRequest` and `hostFetch` carried no timeout, so a host\n// operation that never resolved presented as an indefinite \"Running…\" with no error, no\n// cancel, and nothing in the console. The GLM dogfood run hit exactly this: the first\n// `chat()` of a session parks on a WebAuthn unseal that never completes, and the surface\n// simply waits. A first-run user inside the setup wizard (R3-299) would be stranded on\n// \"Testing…\" — at the worst possible moment, on the screen whose whole purpose is to prove\n// setup worked.\n//\n// WHY A SINGLE CONSTANT IS THE WRONG ANSWER, AND WHAT IS DONE INSTEAD. A blanket timeout\n// was deferred once as risky, correctly: some host operations legitimately await a HUMAN —\n// a passkey tap, a consent decision, a file picker — and a flat deadline aborts those. So\n// calls are classified, and the two classes get very different bounds:\n//\n// unattended — a network call or a channel round-trip. Nobody is being asked anything, so\n// a reply that has not arrived in tens of seconds is a fault. Short bound.\n// attended — the host may draw chrome and wait for the user. The bound exists only to\n// stop an ABANDONED prompt from pinning the caller forever, so it is set far\n// beyond human reaction time.\n//\n// NOTHING IS UNBOUNDED. \"A long or absent deadline\" was the licence; absent is declined,\n// because absent is the bug. An attended bound of minutes never aborts a person who is\n// actually deciding, and does release a caller whose user walked away — which is the\n// difference between a slow flow and a wedged one.\n//\n// THE IMPRECISION, AND HOW R3-307 REMOVED IT. Attendedness is classified per\n// (scheme, method), but it is really a property of a MOMENT: `spaces:mount` is unattended\n// when the grant is already held and attended on first use, because the host raises consent\n// inside the request (`spaceHandler` `presentMountConsent`). A per-method table cannot see\n// that, so R3-298 classified any method that MAY prompt as `attended` and gave it the long\n// bound — which meant the common grant-held path also waited ten minutes before reporting a\n// fault.\n//\n// R3-307 added the host-attention channel (`hostAttention.ts`), on which the host says\n// whether a person is being asked something RIGHT NOW. So a call now carries TWO bounds:\n//\n// idle — in force while the host is not prompting. A may-prompt call runs on this,\n// which is a real correctness gain: the grant-held `spaces:mount` faults in\n// seconds, as it always should have.\n// ceiling — absolute, from call start, NEVER suspended. An abandoned prompt still\n// releases the caller. The signal may EXTEND a deadline, never remove it.\n//\n// AND THE SHORTENING IS OPT-IN PER ENTRY, which is the part worth not losing. A scheme\n// drops to the short idle bound only when EVERY prompt it can raise is one the host\n// actually announces on that channel (the powerbox, the add-secret modal, the passkey\n// unlock, and the spaceHandler consent surfaces — the presenters site-main wraps). A task\n// app's interaction and the contribute diff-approval are human-paced but are NOT host\n// prompts, so no signal would ever fire for them and they keep the full attended bound. A\n// signal that cannot fire must never be allowed to shorten a deadline.\n\nimport type { HostAttentionKind } from './hostAttention';\n\n/** Whether a call may block on a human being asked something. */\nexport type Attendance = 'unattended' | 'attended';\n\n/** Milliseconds. Exported so callers can reason about the defaults they are overriding. */\nexport const UNATTENDED_TIMEOUT_MS = 30_000;\n/** Network calls reach arbitrary upstreams; the host bounds the fetch itself, so this is a\n * backstop against the host never replying, not a request budget. */\nexport const NETWORK_TIMEOUT_MS = 120_000;\n/** Far beyond human reaction time — this exists only so an ABANDONED prompt releases the\n * caller. It must never be short enough to abort someone who is deciding. */\nexport const ATTENDED_TIMEOUT_MS = 600_000;\n/** A stream's first frame may be behind an unseal, so it gets an attended-scale bound of\n * its own. See `firstFrameTimeoutFor`. */\nexport const ATTENDED_FIRST_FRAME_MS = 300_000;\n/** After the first frame, silence this long means the stream is wedged: the host is no\n * longer producing and no human is being asked. */\nexport const STREAM_IDLE_TIMEOUT_MS = 120_000;\n/** When a call passes this, `onPending` fires so a caller can render a waiting state\n * instead of an unexplained stall. */\nexport const PENDING_NOTICE_MS = 3_000;\n\n/**\n * Methods that may draw host chrome and wait for the user.\n *\n * Each entry is a `scheme:method` or a bare `scheme` (matching every method of it). The\n * REASON is recorded per entry, because this table is the item's actual content — a future\n * reader must be able to see why a method is on the long bound without re-deriving it from\n * the host source.\n */\ninterface AttendedEntry {\n /** Why this call may block on a human. */\n reason: string;\n /**\n * The bound this call runs on while the host reports NOBODY is being asked (R3-307).\n *\n * Set it ONLY when every prompt this entry can raise is one the host announces on the\n * host-attention channel — i.e. a presenter site-main wraps (`hostAttention.ts` in that\n * repo). Omitted ⇒ the call keeps the full attended bound at all times, because a signal\n * that never fires must not be allowed to shorten a deadline.\n */\n idleMs?: number;\n}\n\nconst ATTENDED: Record<string, AttendedEntry> = {\n // The powerbox and the add-secret modal are host-drawn and wait for the user to type or\n // pick; the first use of any stored secret additionally raises a WebAuthn assertion\n // (SECRETS_SPEC §3 — one unlock per session, from a live gesture). All three are wrapped\n // presenters, so the signal covers this scheme completely.\n secrets: {\n reason: 'host-drawn key entry / picker, and the per-session passkey unlock',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n // Consent is raised INSIDE the request: presentMountConsent, presentGrantPicker,\n // presentCreateConsent, presentShareDisclosure, presentReferenceConsent — every one of\n // them a wrapped presenter. Unattended once the grant is held, attended on first use, and\n // since R3-307 the host says which of those is happening.\n spaces: {\n reason: 'first-use mount/share/create consent is drawn inside the request',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n settings: {\n reason: 'settings verbs reach the same consent and picker surfaces as spaces',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n // The contribute flow shows the full diff for approval before anything is written\n // (TRUST_AND_SAFETY TS-19b: the approval MUST show the real diff, so a human reads it).\n // NOT a wrapped presenter — no `idleMs`.\n contribute: { reason: 'the diff-approval step is a human read of the whole change' },\n // A task is an app bound to a transient slot that the user interacts with; it returns\n // when they finish, which is human-paced by construction. That is an APP's interaction,\n // not a host prompt, so the attention channel never fires for it — no `idleMs`.\n task: { reason: 'a task app runs an interaction and returns when the user finishes' },\n // Launching a target can raise consent for a not-yet-granted app — through the launch\n // flow's own surface, not one of the wrapped presenters. No `idleMs`.\n launch: { reason: 'may raise first-use consent for the launched target' },\n // A drag is a gesture in progress — its duration is the user's hand, and no host prompt\n // is up while it happens. No `idleMs`.\n dnd: { reason: 'a drag is a human gesture in flight' },\n // The chat stream's FIRST frame sits behind the session's first passkey unseal — the\n // exact hang the dogfood run found — and that unseal IS a wrapped presenter. But the idle\n // bound here is the NETWORK one, not the channel one: with no prompt up, this call is\n // waiting on an arbitrary upstream model, and thirty seconds is a normal generation.\n llm: {\n reason: 'the first frame can sit behind the session passkey unseal',\n idleMs: NETWORK_TIMEOUT_MS,\n },\n};\n\n/** Look up a scheme/method in the attended table, preferring the exact method entry. */\nfunction attendedEntry(scheme: string, method: string): AttendedEntry | undefined {\n return ATTENDED[`${scheme}:${method}`] ?? ATTENDED[scheme];\n}\n\n/** Look up a scheme/method in the attended table, preferring the exact method entry. */\nfunction attendedReason(scheme: string, method: string): string | undefined {\n return attendedEntry(scheme, method)?.reason;\n}\n\n/** Whether a call may block on a human. Exported for the classification test + tooling. */\nexport function attendanceOf(scheme: string, method: string): Attendance {\n return attendedReason(scheme, method) ? 'attended' : 'unattended';\n}\n\n/** Why a call is classified attended, or `undefined` when it is not. Exported so the\n * classification is legible from a test failure rather than only from this source. */\nexport function attendanceReason(scheme: string, method: string): string | undefined {\n return attendedReason(scheme, method);\n}\n\n/** The default deadline for a one-shot `protocolRequest`. */\nexport function timeoutFor(scheme: string, method: string): number {\n if (attendanceOf(scheme, method) === 'attended') return ATTENDED_TIMEOUT_MS;\n // `fetch` reaches an arbitrary upstream, so it gets the network bound rather than the\n // channel-round-trip one.\n if (scheme === 'fetch') return NETWORK_TIMEOUT_MS;\n return UNATTENDED_TIMEOUT_MS;\n}\n\n/**\n * The default TIME-TO-FIRST-FRAME bound for a stream.\n *\n * Deliberately not a total-duration bound: a long generation that is streaming normally is\n * healthy, and killing it would be a worse bug than the one being fixed. The hang has a\n * distinct shape — NO frames at all — so that is what is bounded, plus an idle gap between\n * frames once flowing. Together they fire exactly on a wedged stream and never on a slow\n * one.\n */\nexport function firstFrameTimeoutFor(scheme: string, method: string): number {\n return attendanceOf(scheme, method) === 'attended'\n ? ATTENDED_FIRST_FRAME_MS\n : NETWORK_TIMEOUT_MS;\n}\n\n/**\n * The two bounds a call runs under (R3-307).\n *\n * `idleMs` is in force while the host reports nobody is being asked; it is cleared while a\n * host prompt is up and restarted, in full, when the prompt goes away. `ceilingMs` runs from\n * call start and is NEVER suspended — it is what releases a caller whose user walked away.\n */\nexport interface CallBounds {\n /** The bound in force while the host is not waiting on a person. */\n idleMs: number;\n /** The absolute bound from call start. Never suspended. */\n ceilingMs: number;\n}\n\n/** Which of a call's two bounds elapsed. `idle` means the host was NOT prompting — nobody\n * was being asked anything, so this is a fault. `ceiling` means the absolute bound ran out,\n * which for an attended call is an abandoned prompt. */\nexport type DeadlineBound = 'idle' | 'ceiling';\n\n/** The bounds for a one-shot `protocolRequest`. */\nexport function boundsFor(scheme: string, method: string): CallBounds {\n const ceilingMs = timeoutFor(scheme, method);\n const idleMs = attendedEntry(scheme, method)?.idleMs;\n // `Math.min` so an `idleMs` can only ever tighten: an entry that named a bound longer than\n // its own ceiling would otherwise silently disable the idle leg.\n return { idleMs: idleMs === undefined ? ceilingMs : Math.min(idleMs, ceilingMs), ceilingMs };\n}\n\n/** The bounds for a stream's time-to-first-frame. */\nexport function firstFrameBoundsFor(scheme: string, method: string): CallBounds {\n const ceilingMs = firstFrameTimeoutFor(scheme, method);\n const idleMs = attendedEntry(scheme, method)?.idleMs;\n return { idleMs: idleMs === undefined ? ceilingMs : Math.min(idleMs, ceilingMs), ceilingMs };\n}\n\n/** A live deadline that the host-attention signal can suspend. */\nexport interface SuspendableDeadline {\n /** Tell the deadline whether the host is waiting on a person right now. */\n setAwaiting(awaiting: boolean): void;\n /** Clear every timer. Idempotent — safe to call from a `finally`. */\n dispose(): void;\n}\n\n/**\n * A deadline with a suspendable idle leg and an unsuspendable ceiling.\n *\n * Pure and injectable (`setTimer`/`clearTimer`) so the suspension rules are unit-testable\n * against a fake clock rather than by waiting minutes for real ones.\n *\n * The idle leg RESTARTS in full when a prompt clears rather than resuming where it left off.\n * That is deliberate: after the user dismisses a prompt the host begins fresh work, and the\n * seconds that elapsed before the prompt say nothing about how long that work should take.\n * The ceiling is what stops a repeatedly-prompting call from running forever.\n */\nexport function createSuspendableDeadline(opts: {\n bounds: CallBounds;\n /** Called once, with the bound that elapsed and its length in ms. */\n onExpire: (bound: DeadlineBound, boundMs: number) => void;\n setTimer?: (fn: () => void, ms: number) => unknown;\n clearTimer?: (handle: unknown) => void;\n}): SuspendableDeadline {\n const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));\n const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));\n const { idleMs, ceilingMs } = opts.bounds;\n // An idle leg at or above the ceiling can never fire first, so don't arm one — that keeps\n // the common unattended case (idle === ceiling) on exactly one timer, as before R3-307.\n const hasIdleLeg = Number.isFinite(idleMs) && idleMs < ceilingMs;\n\n let done = false;\n let idle: unknown;\n let ceiling: unknown;\n\n const expire = (bound: DeadlineBound, boundMs: number) => {\n if (done) return;\n done = true;\n opts.onExpire(bound, boundMs);\n };\n\n const armIdle = () => {\n if (done || !hasIdleLeg || idle !== undefined) return;\n idle = setTimer(() => {\n idle = undefined;\n expire('idle', idleMs);\n }, idleMs);\n };\n const disarmIdle = () => {\n if (idle !== undefined) {\n clearTimer(idle);\n idle = undefined;\n }\n };\n\n if (Number.isFinite(ceilingMs)) {\n ceiling = setTimer(() => {\n ceiling = undefined;\n expire('ceiling', ceilingMs);\n }, ceilingMs);\n }\n armIdle();\n\n return {\n setAwaiting(awaiting: boolean) {\n if (done) return;\n if (awaiting) disarmIdle();\n else armIdle();\n },\n dispose() {\n done = true;\n disarmIdle();\n if (ceiling !== undefined) {\n clearTimer(ceiling);\n ceiling = undefined;\n }\n },\n };\n}\n\n/** The error a bounded call rejects with. `code` is `'timeout'` — the code R3-303's typed\n * provider-error taxonomy adopts, so apps see one vocabulary. */\nexport class ProtocolTimeoutError extends Error {\n readonly code = 'timeout';\n /** `scheme:method` of the call that timed out. */\n readonly call: string;\n /** The bound that elapsed, in ms. */\n readonly timeoutMs: number;\n /** Whether the call was on the attended or unattended bound — the first thing anyone\n * debugging a timeout needs, and otherwise invisible. */\n readonly attendance: Attendance;\n /** WHICH bound elapsed (R3-307). An attended call that faults on its `idle` bound was not\n * waiting on anyone — the host said so — and that is a genuinely different diagnosis from\n * an abandoned prompt hitting the `ceiling`. */\n readonly bound: DeadlineBound;\n constructor(\n call: string,\n timeoutMs: number,\n attendance: Attendance,\n bound: DeadlineBound = attendance === 'attended' ? 'ceiling' : 'idle',\n ) {\n super(\n attendance === 'attended' && bound === 'ceiling'\n ? `immediately.run: ${call} was abandoned after ${Math.round(timeoutMs / 1000)}s waiting for you`\n : `immediately.run: ${call} did not respond within ${Math.round(timeoutMs / 1000)}s`,\n );\n this.name = 'ProtocolTimeoutError';\n this.call = call;\n this.timeoutMs = timeoutMs;\n this.attendance = attendance;\n this.bound = bound;\n }\n}\n\n/** The error a cancelled call rejects with. */\nexport class ProtocolCancelledError extends Error {\n readonly code = 'cancelled';\n constructor(call: string) {\n super(`immediately.run: ${call} was cancelled`);\n this.name = 'ProtocolCancelledError';\n }\n}\n\n/** What the host is waiting for at this moment, as reported on the host-attention channel\n * (R3-307). Present on a {@link PendingState} only while the host IS prompting. */\nexport interface PendingAttention {\n /** The kind of prompt on screen, or `null` when the host reports a wait it cannot name. */\n kind: HostAttentionKind | null;\n /** `Date.now()` when the wait began, or `null`. */\n since: number | null;\n}\n\n/** What `onPending` is told when a call is taking a while. */\nexport interface PendingState {\n call: string;\n attendance: Attendance;\n elapsedMs: number;\n /** Why this call *may* be waiting on a person — present only when attended. It comes from\n * the classification table, so it is a standing possibility, not a live fact. */\n reason?: string;\n /**\n * What the host is waiting for RIGHT NOW (R3-307) — present only while a host prompt is\n * actually up. Prefer it over {@link reason} when rendering: \"tap your passkey\" is a\n * sentence the user can act on; \"this may need you\" is not.\n */\n awaiting?: PendingAttention;\n}\n\n/** Options accepted by every bounded host call. */\nexport interface BoundedCallOptions {\n /** Override the classified default. `Infinity` disables the bound — an escape hatch for a\n * caller that genuinely owns the wait (it must then provide its own way out).\n *\n * An explicit value is the WHOLE bound: it is never suspended by the host-attention\n * signal, because a caller that named a number owns the wait. */\n timeoutMs?: number;\n /** Abort the wait. The SDK stops waiting and rejects with `code: 'cancelled'`. */\n signal?: AbortSignal;\n /** Fired when the call passes `PENDING_NOTICE_MS`, so a caller can render a waiting state\n * rather than an unexplained pause — and again, after that, whenever the host starts or\n * stops waiting on the user, so the waiting state can name what is on screen now. */\n onPending?: (state: PendingState) => void;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/protocolDeadline.ts"],"sourcesContent":["// Deadlines for host protocol calls (R3-298) — so no platform request can hang forever.\n//\n// THE FAILURE THIS FIXES. `protocolRequest` and `hostFetch` carried no timeout, so a host\n// operation that never resolved presented as an indefinite \"Running…\" with no error, no\n// cancel, and nothing in the console. The GLM dogfood run hit exactly this: the first\n// `chat()` of a session parks on a WebAuthn unseal that never completes, and the surface\n// simply waits. A first-run user inside the setup wizard (R3-299) would be stranded on\n// \"Testing…\" — at the worst possible moment, on the screen whose whole purpose is to prove\n// setup worked.\n//\n// WHY A SINGLE CONSTANT IS THE WRONG ANSWER, AND WHAT IS DONE INSTEAD. A blanket timeout\n// was deferred once as risky, correctly: some host operations legitimately await a HUMAN —\n// a passkey tap, a consent decision, a file picker — and a flat deadline aborts those. So\n// calls are classified, and the two classes get very different bounds:\n//\n// unattended — a network call or a channel round-trip. Nobody is being asked anything, so\n// a reply that has not arrived in tens of seconds is a fault. Short bound.\n// attended — the host may draw chrome and wait for the user. The bound exists only to\n// stop an ABANDONED prompt from pinning the caller forever, so it is set far\n// beyond human reaction time.\n//\n// NOTHING IS UNBOUNDED. \"A long or absent deadline\" was the licence; absent is declined,\n// because absent is the bug. An attended bound of minutes never aborts a person who is\n// actually deciding, and does release a caller whose user walked away — which is the\n// difference between a slow flow and a wedged one.\n//\n// THE IMPRECISION, AND HOW R3-307 REMOVED IT. Attendedness is classified per\n// (scheme, method), but it is really a property of a MOMENT: `spaces:mount` is unattended\n// when the grant is already held and attended on first use, because the host raises consent\n// inside the request (`spaceHandler` `presentMountConsent`). A per-method table cannot see\n// that, so R3-298 classified any method that MAY prompt as `attended` and gave it the long\n// bound — which meant the common grant-held path also waited ten minutes before reporting a\n// fault.\n//\n// R3-307 added the host-attention channel (`hostAttention.ts`), on which the host says\n// whether a person is being asked something RIGHT NOW. So a call now carries TWO bounds:\n//\n// idle — in force while the host is not prompting. A may-prompt call runs on this,\n// which is a real correctness gain: the grant-held `spaces:mount` faults in\n// seconds, as it always should have.\n// ceiling — absolute, from call start, NEVER suspended. An abandoned prompt still\n// releases the caller. The signal may EXTEND a deadline, never remove it.\n//\n// AND THE SHORTENING IS OPT-IN PER ENTRY, which is the part worth not losing. A scheme\n// drops to the short idle bound only when EVERY prompt it can raise is one the host\n// actually announces on that channel (the powerbox, the add-secret modal, the passkey\n// unlock, and the spaceHandler consent surfaces — the presenters site-main wraps). A task\n// app's interaction and the contribute diff-approval are human-paced but are NOT host\n// prompts, so no signal would ever fire for them and they keep the full attended bound. A\n// signal that cannot fire must never be allowed to shorten a deadline.\n\nimport type { HostAttentionKind } from './hostAttention';\n\n/** Whether a call may block on a human being asked something. */\nexport type Attendance = 'unattended' | 'attended';\n\n/** Milliseconds. Exported so callers can reason about the defaults they are overriding. */\nexport const UNATTENDED_TIMEOUT_MS = 30_000;\n/** Network calls reach arbitrary upstreams; the host bounds the fetch itself, so this is a\n * backstop against the host never replying, not a request budget. */\nexport const NETWORK_TIMEOUT_MS = 120_000;\n/** Far beyond human reaction time — this exists only so an ABANDONED prompt releases the\n * caller. It must never be short enough to abort someone who is deciding. */\nexport const ATTENDED_TIMEOUT_MS = 600_000;\n/** A stream's first frame may be behind an unseal, so it gets an attended-scale bound of\n * its own. See `firstFrameTimeoutFor`. */\nexport const ATTENDED_FIRST_FRAME_MS = 300_000;\n/** After the first frame, silence this long means the stream is wedged: the host is no\n * longer producing and no human is being asked. */\nexport const STREAM_IDLE_TIMEOUT_MS = 120_000;\n/** When a call passes this, `onPending` fires so a caller can render a waiting state\n * instead of an unexplained stall. */\nexport const PENDING_NOTICE_MS = 3_000;\n\n/**\n * Methods that may draw host chrome and wait for the user.\n *\n * Each entry is a `scheme:method` or a bare `scheme` (matching every method of it). The\n * REASON is recorded per entry, because this table is the item's actual content — a future\n * reader must be able to see why a method is on the long bound without re-deriving it from\n * the host source.\n */\ninterface AttendedEntry {\n /** Why this call may block on a human. */\n reason: string;\n /**\n * The bound this call runs on while the host reports NOBODY is being asked (R3-307).\n *\n * Set it ONLY when every prompt this entry can raise is one the host announces on the\n * host-attention channel — i.e. a presenter site-main wraps (`hostAttention.ts` in that\n * repo). Omitted ⇒ the call keeps the full attended bound at all times, because a signal\n * that never fires must not be allowed to shorten a deadline.\n */\n idleMs?: number;\n}\n\nconst ATTENDED: Record<string, AttendedEntry> = {\n // The powerbox and the add-secret modal are host-drawn and wait for the user to type or\n // pick; the first use of any stored secret additionally raises a WebAuthn assertion\n // (SECRETS_SPEC §3 — one unlock per session, from a live gesture). All three are wrapped\n // presenters, so the signal covers this scheme completely.\n secrets: {\n reason: 'host-drawn key entry / picker, and the per-session passkey unlock',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n // Consent is raised INSIDE the request: presentMountConsent, presentGrantPicker,\n // presentCreateConsent, presentShareDisclosure, presentReferenceConsent — every one of\n // them a wrapped presenter. Unattended once the grant is held, attended on first use, and\n // since R3-307 the host says which of those is happening.\n spaces: {\n reason: 'first-use mount/share/create consent is drawn inside the request',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n settings: {\n reason: 'settings verbs reach the same consent and picker surfaces as spaces',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n // The contribute flow shows the full diff for approval before anything is written\n // (TRUST_AND_SAFETY TS-19b: the approval MUST show the real diff, so a human reads it).\n // NOT a wrapped presenter — no `idleMs`.\n contribute: { reason: 'the diff-approval step is a human read of the whole change' },\n // A task is an app bound to a transient slot that the user interacts with; it returns\n // when they finish, which is human-paced by construction. That is an APP's interaction,\n // not a host prompt, so the attention channel never fires for it — no `idleMs`.\n task: { reason: 'a task app runs an interaction and returns when the user finishes' },\n // Launching a target can raise consent for a not-yet-granted app — through the launch\n // flow's own surface, not one of the wrapped presenters. No `idleMs`.\n launch: { reason: 'may raise first-use consent for the launched target' },\n // A drag is a gesture in progress — its duration is the user's hand, and no host prompt\n // is up while it happens. No `idleMs`.\n dnd: { reason: 'a drag is a human gesture in flight' },\n // The chat stream's FIRST frame sits behind the session's first passkey unseal — the\n // exact hang the dogfood run found — and that unseal IS a wrapped presenter. But the idle\n // bound here is the NETWORK one, not the channel one: with no prompt up, this call is\n // waiting on an arbitrary upstream model, and thirty seconds is a normal generation.\n llm: {\n reason: 'the first frame can sit behind the session passkey unseal',\n idleMs: NETWORK_TIMEOUT_MS,\n },\n};\n\n/** Look up a scheme/method in the attended table, preferring the exact method entry. */\nfunction attendedEntry(scheme: string, method: string): AttendedEntry | undefined {\n return ATTENDED[`${scheme}:${method}`] ?? ATTENDED[scheme];\n}\n\n/** Look up a scheme/method in the attended table, preferring the exact method entry. */\nfunction attendedReason(scheme: string, method: string): string | undefined {\n return attendedEntry(scheme, method)?.reason;\n}\n\n/** Whether a call may block on a human. Exported for the classification test + tooling. */\nexport function attendanceOf(scheme: string, method: string): Attendance {\n return attendedReason(scheme, method) ? 'attended' : 'unattended';\n}\n\n/** Why a call is classified attended, or `undefined` when it is not. Exported so the\n * classification is legible from a test failure rather than only from this source. */\nexport function attendanceReason(scheme: string, method: string): string | undefined {\n return attendedReason(scheme, method);\n}\n\n/** The default deadline for a one-shot `protocolRequest`. */\nexport function timeoutFor(scheme: string, method: string): number {\n if (attendanceOf(scheme, method) === 'attended') return ATTENDED_TIMEOUT_MS;\n // `fetch` reaches an arbitrary upstream, so it gets the network bound rather than the\n // channel-round-trip one.\n if (scheme === 'fetch') return NETWORK_TIMEOUT_MS;\n return UNATTENDED_TIMEOUT_MS;\n}\n\n/**\n * The default TIME-TO-FIRST-FRAME bound for a stream.\n *\n * Deliberately not a total-duration bound: a long generation that is streaming normally is\n * healthy, and killing it would be a worse bug than the one being fixed. The hang has a\n * distinct shape — NO frames at all — so that is what is bounded, plus an idle gap between\n * frames once flowing. Together they fire exactly on a wedged stream and never on a slow\n * one.\n */\nexport function firstFrameTimeoutFor(scheme: string, method: string): number {\n return attendanceOf(scheme, method) === 'attended' ? ATTENDED_FIRST_FRAME_MS : NETWORK_TIMEOUT_MS;\n}\n\n/**\n * The two bounds a call runs under (R3-307).\n *\n * `idleMs` is in force while the host reports nobody is being asked; it is cleared while a\n * host prompt is up and restarted, in full, when the prompt goes away. `ceilingMs` runs from\n * call start and is NEVER suspended — it is what releases a caller whose user walked away.\n */\nexport interface CallBounds {\n /** The bound in force while the host is not waiting on a person. */\n idleMs: number;\n /** The absolute bound from call start. Never suspended. */\n ceilingMs: number;\n}\n\n/** Which of a call's two bounds elapsed. `idle` means the host was NOT prompting — nobody\n * was being asked anything, so this is a fault. `ceiling` means the absolute bound ran out,\n * which for an attended call is an abandoned prompt. */\nexport type DeadlineBound = 'idle' | 'ceiling';\n\n/** The bounds for a one-shot `protocolRequest`. */\nexport function boundsFor(scheme: string, method: string): CallBounds {\n const ceilingMs = timeoutFor(scheme, method);\n const idleMs = attendedEntry(scheme, method)?.idleMs;\n // `Math.min` so an `idleMs` can only ever tighten: an entry that named a bound longer than\n // its own ceiling would otherwise silently disable the idle leg.\n return { idleMs: idleMs === undefined ? ceilingMs : Math.min(idleMs, ceilingMs), ceilingMs };\n}\n\n/** The bounds for a stream's time-to-first-frame. */\nexport function firstFrameBoundsFor(scheme: string, method: string): CallBounds {\n const ceilingMs = firstFrameTimeoutFor(scheme, method);\n const idleMs = attendedEntry(scheme, method)?.idleMs;\n return { idleMs: idleMs === undefined ? ceilingMs : Math.min(idleMs, ceilingMs), ceilingMs };\n}\n\n/** A live deadline that the host-attention signal can suspend. */\nexport interface SuspendableDeadline {\n /** Tell the deadline whether the host is waiting on a person right now. */\n setAwaiting(awaiting: boolean): void;\n /** Clear every timer. Idempotent — safe to call from a `finally`. */\n dispose(): void;\n}\n\n/**\n * A deadline with a suspendable idle leg and an unsuspendable ceiling.\n *\n * Pure and injectable (`setTimer`/`clearTimer`) so the suspension rules are unit-testable\n * against a fake clock rather than by waiting minutes for real ones.\n *\n * The idle leg RESTARTS in full when a prompt clears rather than resuming where it left off.\n * That is deliberate: after the user dismisses a prompt the host begins fresh work, and the\n * seconds that elapsed before the prompt say nothing about how long that work should take.\n * The ceiling is what stops a repeatedly-prompting call from running forever.\n */\nexport function createSuspendableDeadline(opts: {\n bounds: CallBounds;\n /** Called once, with the bound that elapsed and its length in ms. */\n onExpire: (bound: DeadlineBound, boundMs: number) => void;\n setTimer?: (fn: () => void, ms: number) => unknown;\n clearTimer?: (handle: unknown) => void;\n}): SuspendableDeadline {\n const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));\n const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));\n const { idleMs, ceilingMs } = opts.bounds;\n // An idle leg at or above the ceiling can never fire first, so don't arm one — that keeps\n // the common unattended case (idle === ceiling) on exactly one timer, as before R3-307.\n const hasIdleLeg = Number.isFinite(idleMs) && idleMs < ceilingMs;\n\n let done = false;\n let idle: unknown;\n let ceiling: unknown;\n\n const expire = (bound: DeadlineBound, boundMs: number) => {\n if (done) return;\n done = true;\n opts.onExpire(bound, boundMs);\n };\n\n const armIdle = () => {\n if (done || !hasIdleLeg || idle !== undefined) return;\n idle = setTimer(() => {\n idle = undefined;\n expire('idle', idleMs);\n }, idleMs);\n };\n const disarmIdle = () => {\n if (idle !== undefined) {\n clearTimer(idle);\n idle = undefined;\n }\n };\n\n if (Number.isFinite(ceilingMs)) {\n ceiling = setTimer(() => {\n ceiling = undefined;\n expire('ceiling', ceilingMs);\n }, ceilingMs);\n }\n armIdle();\n\n return {\n setAwaiting(awaiting: boolean) {\n if (done) return;\n if (awaiting) disarmIdle();\n else armIdle();\n },\n dispose() {\n done = true;\n disarmIdle();\n if (ceiling !== undefined) {\n clearTimer(ceiling);\n ceiling = undefined;\n }\n },\n };\n}\n\n/** The error a bounded call rejects with. `code` is `'timeout'` — the code R3-303's typed\n * provider-error taxonomy adopts, so apps see one vocabulary. */\nexport class ProtocolTimeoutError extends Error {\n readonly code = 'timeout';\n /** `scheme:method` of the call that timed out. */\n readonly call: string;\n /** The bound that elapsed, in ms. */\n readonly timeoutMs: number;\n /** Whether the call was on the attended or unattended bound — the first thing anyone\n * debugging a timeout needs, and otherwise invisible. */\n readonly attendance: Attendance;\n /** WHICH bound elapsed (R3-307). An attended call that faults on its `idle` bound was not\n * waiting on anyone — the host said so — and that is a genuinely different diagnosis from\n * an abandoned prompt hitting the `ceiling`. */\n readonly bound: DeadlineBound;\n constructor(\n call: string,\n timeoutMs: number,\n attendance: Attendance,\n bound: DeadlineBound = attendance === 'attended' ? 'ceiling' : 'idle',\n ) {\n super(\n attendance === 'attended' && bound === 'ceiling'\n ? `immediately.run: ${call} was abandoned after ${Math.round(timeoutMs / 1000)}s waiting for you`\n : `immediately.run: ${call} did not respond within ${Math.round(timeoutMs / 1000)}s`,\n );\n this.name = 'ProtocolTimeoutError';\n this.call = call;\n this.timeoutMs = timeoutMs;\n this.attendance = attendance;\n this.bound = bound;\n }\n}\n\n/** The error a cancelled call rejects with. */\nexport class ProtocolCancelledError extends Error {\n readonly code = 'cancelled';\n constructor(call: string) {\n super(`immediately.run: ${call} was cancelled`);\n this.name = 'ProtocolCancelledError';\n }\n}\n\n/** What the host is waiting for at this moment, as reported on the host-attention channel\n * (R3-307). Present on a {@link PendingState} only while the host IS prompting. */\nexport interface PendingAttention {\n /** The kind of prompt on screen, or `null` when the host reports a wait it cannot name. */\n kind: HostAttentionKind | null;\n /** `Date.now()` when the wait began, or `null`. */\n since: number | null;\n}\n\n/** What `onPending` is told when a call is taking a while. */\nexport interface PendingState {\n call: string;\n attendance: Attendance;\n elapsedMs: number;\n /** Why this call *may* be waiting on a person — present only when attended. It comes from\n * the classification table, so it is a standing possibility, not a live fact. */\n reason?: string;\n /**\n * What the host is waiting for RIGHT NOW (R3-307) — present only while a host prompt is\n * actually up. Prefer it over {@link reason} when rendering: \"tap your passkey\" is a\n * sentence the user can act on; \"this may need you\" is not.\n */\n awaiting?: PendingAttention;\n}\n\n/** Options accepted by every bounded host call. */\nexport interface BoundedCallOptions {\n /** Override the classified default. `Infinity` disables the bound — an escape hatch for a\n * caller that genuinely owns the wait (it must then provide its own way out).\n *\n * An explicit value is the WHOLE bound: it is never suspended by the host-attention\n * signal, because a caller that named a number owns the wait. */\n timeoutMs?: number;\n /** Abort the wait. The SDK stops waiting and rejects with `code: 'cancelled'`. */\n signal?: AbortSignal;\n /** Fired when the call passes `PENDING_NOTICE_MS`, so a caller can render a waiting state\n * rather than an unexplained pause — and again, after that, whenever the host starts or\n * stops waiting on the user, so the waiting state can name what is on screen now. */\n onPending?: (state: PendingState) => void;\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/protocolDeadline.ts"],"sourcesContent":["// Deadlines for host protocol calls (R3-298) — so no platform request can hang forever.\n//\n// THE FAILURE THIS FIXES. `protocolRequest` and `hostFetch` carried no timeout, so a host\n// operation that never resolved presented as an indefinite \"Running…\" with no error, no\n// cancel, and nothing in the console. The GLM dogfood run hit exactly this: the first\n// `chat()` of a session parks on a WebAuthn unseal that never completes, and the surface\n// simply waits. A first-run user inside the setup wizard (R3-299) would be stranded on\n// \"Testing…\" — at the worst possible moment, on the screen whose whole purpose is to prove\n// setup worked.\n//\n// WHY A SINGLE CONSTANT IS THE WRONG ANSWER, AND WHAT IS DONE INSTEAD. A blanket timeout\n// was deferred once as risky, correctly: some host operations legitimately await a HUMAN —\n// a passkey tap, a consent decision, a file picker — and a flat deadline aborts those. So\n// calls are classified, and the two classes get very different bounds:\n//\n// unattended — a network call or a channel round-trip. Nobody is being asked anything, so\n// a reply that has not arrived in tens of seconds is a fault. Short bound.\n// attended — the host may draw chrome and wait for the user. The bound exists only to\n// stop an ABANDONED prompt from pinning the caller forever, so it is set far\n// beyond human reaction time.\n//\n// NOTHING IS UNBOUNDED. \"A long or absent deadline\" was the licence; absent is declined,\n// because absent is the bug. An attended bound of minutes never aborts a person who is\n// actually deciding, and does release a caller whose user walked away — which is the\n// difference between a slow flow and a wedged one.\n//\n// THE IMPRECISION, AND HOW R3-307 REMOVED IT. Attendedness is classified per\n// (scheme, method), but it is really a property of a MOMENT: `spaces:mount` is unattended\n// when the grant is already held and attended on first use, because the host raises consent\n// inside the request (`spaceHandler` `presentMountConsent`). A per-method table cannot see\n// that, so R3-298 classified any method that MAY prompt as `attended` and gave it the long\n// bound — which meant the common grant-held path also waited ten minutes before reporting a\n// fault.\n//\n// R3-307 added the host-attention channel (`hostAttention.ts`), on which the host says\n// whether a person is being asked something RIGHT NOW. So a call now carries TWO bounds:\n//\n// idle — in force while the host is not prompting. A may-prompt call runs on this,\n// which is a real correctness gain: the grant-held `spaces:mount` faults in\n// seconds, as it always should have.\n// ceiling — absolute, from call start, NEVER suspended. An abandoned prompt still\n// releases the caller. The signal may EXTEND a deadline, never remove it.\n//\n// AND THE SHORTENING IS OPT-IN PER ENTRY, which is the part worth not losing. A scheme\n// drops to the short idle bound only when EVERY prompt it can raise is one the host\n// actually announces on that channel (the powerbox, the add-secret modal, the passkey\n// unlock, and the spaceHandler consent surfaces — the presenters site-main wraps). A task\n// app's interaction and the contribute diff-approval are human-paced but are NOT host\n// prompts, so no signal would ever fire for them and they keep the full attended bound. A\n// signal that cannot fire must never be allowed to shorten a deadline.\n\nimport type { HostAttentionKind } from './hostAttention';\n\n/** Whether a call may block on a human being asked something. */\nexport type Attendance = 'unattended' | 'attended';\n\n/** Milliseconds. Exported so callers can reason about the defaults they are overriding. */\nexport const UNATTENDED_TIMEOUT_MS = 30_000;\n/** Network calls reach arbitrary upstreams; the host bounds the fetch itself, so this is a\n * backstop against the host never replying, not a request budget. */\nexport const NETWORK_TIMEOUT_MS = 120_000;\n/** Far beyond human reaction time — this exists only so an ABANDONED prompt releases the\n * caller. It must never be short enough to abort someone who is deciding. */\nexport const ATTENDED_TIMEOUT_MS = 600_000;\n/** A stream's first frame may be behind an unseal, so it gets an attended-scale bound of\n * its own. See `firstFrameTimeoutFor`. */\nexport const ATTENDED_FIRST_FRAME_MS = 300_000;\n/** After the first frame, silence this long means the stream is wedged: the host is no\n * longer producing and no human is being asked. */\nexport const STREAM_IDLE_TIMEOUT_MS = 120_000;\n/** When a call passes this, `onPending` fires so a caller can render a waiting state\n * instead of an unexplained stall. */\nexport const PENDING_NOTICE_MS = 3_000;\n\n/**\n * Methods that may draw host chrome and wait for the user.\n *\n * Each entry is a `scheme:method` or a bare `scheme` (matching every method of it). The\n * REASON is recorded per entry, because this table is the item's actual content — a future\n * reader must be able to see why a method is on the long bound without re-deriving it from\n * the host source.\n */\ninterface AttendedEntry {\n /** Why this call may block on a human. */\n reason: string;\n /**\n * The bound this call runs on while the host reports NOBODY is being asked (R3-307).\n *\n * Set it ONLY when every prompt this entry can raise is one the host announces on the\n * host-attention channel — i.e. a presenter site-main wraps (`hostAttention.ts` in that\n * repo). Omitted ⇒ the call keeps the full attended bound at all times, because a signal\n * that never fires must not be allowed to shorten a deadline.\n */\n idleMs?: number;\n}\n\nconst ATTENDED: Record<string, AttendedEntry> = {\n // The powerbox and the add-secret modal are host-drawn and wait for the user to type or\n // pick; the first use of any stored secret additionally raises a WebAuthn assertion\n // (SECRETS_SPEC §3 — one unlock per session, from a live gesture). All three are wrapped\n // presenters, so the signal covers this scheme completely.\n secrets: {\n reason: 'host-drawn key entry / picker, and the per-session passkey unlock',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n // Consent is raised INSIDE the request: presentMountConsent, presentGrantPicker,\n // presentCreateConsent, presentShareDisclosure, presentReferenceConsent — every one of\n // them a wrapped presenter. Unattended once the grant is held, attended on first use, and\n // since R3-307 the host says which of those is happening.\n spaces: {\n reason: 'first-use mount/share/create consent is drawn inside the request',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n settings: {\n reason: 'settings verbs reach the same consent and picker surfaces as spaces',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n // The contribute flow shows the full diff for approval before anything is written\n // (TRUST_AND_SAFETY TS-19b: the approval MUST show the real diff, so a human reads it).\n // NOT a wrapped presenter — no `idleMs`.\n contribute: { reason: 'the diff-approval step is a human read of the whole change' },\n // A task is an app bound to a transient slot that the user interacts with; it returns\n // when they finish, which is human-paced by construction. That is an APP's interaction,\n // not a host prompt, so the attention channel never fires for it — no `idleMs`.\n task: { reason: 'a task app runs an interaction and returns when the user finishes' },\n // Launching a target can raise consent for a not-yet-granted app — through the launch\n // flow's own surface, not one of the wrapped presenters. No `idleMs`.\n launch: { reason: 'may raise first-use consent for the launched target' },\n // A drag is a gesture in progress — its duration is the user's hand, and no host prompt\n // is up while it happens. No `idleMs`.\n dnd: { reason: 'a drag is a human gesture in flight' },\n // The chat stream's FIRST frame sits behind the session's first passkey unseal — the\n // exact hang the dogfood run found — and that unseal IS a wrapped presenter. But the idle\n // bound here is the NETWORK one, not the channel one: with no prompt up, this call is\n // waiting on an arbitrary upstream model, and thirty seconds is a normal generation.\n llm: {\n reason: 'the first frame can sit behind the session passkey unseal',\n idleMs: NETWORK_TIMEOUT_MS,\n },\n};\n\n/** Look up a scheme/method in the attended table, preferring the exact method entry. */\nfunction attendedEntry(scheme: string, method: string): AttendedEntry | undefined {\n return ATTENDED[`${scheme}:${method}`] ?? ATTENDED[scheme];\n}\n\n/** Look up a scheme/method in the attended table, preferring the exact method entry. */\nfunction attendedReason(scheme: string, method: string): string | undefined {\n return attendedEntry(scheme, method)?.reason;\n}\n\n/** Whether a call may block on a human. Exported for the classification test + tooling. */\nexport function attendanceOf(scheme: string, method: string): Attendance {\n return attendedReason(scheme, method) ? 'attended' : 'unattended';\n}\n\n/** Why a call is classified attended, or `undefined` when it is not. Exported so the\n * classification is legible from a test failure rather than only from this source. */\nexport function attendanceReason(scheme: string, method: string): string | undefined {\n return attendedReason(scheme, method);\n}\n\n/** The default deadline for a one-shot `protocolRequest`. */\nexport function timeoutFor(scheme: string, method: string): number {\n if (attendanceOf(scheme, method) === 'attended') return ATTENDED_TIMEOUT_MS;\n // `fetch` reaches an arbitrary upstream, so it gets the network bound rather than the\n // channel-round-trip one.\n if (scheme === 'fetch') return NETWORK_TIMEOUT_MS;\n return UNATTENDED_TIMEOUT_MS;\n}\n\n/**\n * The default TIME-TO-FIRST-FRAME bound for a stream.\n *\n * Deliberately not a total-duration bound: a long generation that is streaming normally is\n * healthy, and killing it would be a worse bug than the one being fixed. The hang has a\n * distinct shape — NO frames at all — so that is what is bounded, plus an idle gap between\n * frames once flowing. Together they fire exactly on a wedged stream and never on a slow\n * one.\n */\nexport function firstFrameTimeoutFor(scheme: string, method: string): number {\n return attendanceOf(scheme, method) === 'attended'\n ? ATTENDED_FIRST_FRAME_MS\n : NETWORK_TIMEOUT_MS;\n}\n\n/**\n * The two bounds a call runs under (R3-307).\n *\n * `idleMs` is in force while the host reports nobody is being asked; it is cleared while a\n * host prompt is up and restarted, in full, when the prompt goes away. `ceilingMs` runs from\n * call start and is NEVER suspended — it is what releases a caller whose user walked away.\n */\nexport interface CallBounds {\n /** The bound in force while the host is not waiting on a person. */\n idleMs: number;\n /** The absolute bound from call start. Never suspended. */\n ceilingMs: number;\n}\n\n/** Which of a call's two bounds elapsed. `idle` means the host was NOT prompting — nobody\n * was being asked anything, so this is a fault. `ceiling` means the absolute bound ran out,\n * which for an attended call is an abandoned prompt. */\nexport type DeadlineBound = 'idle' | 'ceiling';\n\n/** The bounds for a one-shot `protocolRequest`. */\nexport function boundsFor(scheme: string, method: string): CallBounds {\n const ceilingMs = timeoutFor(scheme, method);\n const idleMs = attendedEntry(scheme, method)?.idleMs;\n // `Math.min` so an `idleMs` can only ever tighten: an entry that named a bound longer than\n // its own ceiling would otherwise silently disable the idle leg.\n return { idleMs: idleMs === undefined ? ceilingMs : Math.min(idleMs, ceilingMs), ceilingMs };\n}\n\n/** The bounds for a stream's time-to-first-frame. */\nexport function firstFrameBoundsFor(scheme: string, method: string): CallBounds {\n const ceilingMs = firstFrameTimeoutFor(scheme, method);\n const idleMs = attendedEntry(scheme, method)?.idleMs;\n return { idleMs: idleMs === undefined ? ceilingMs : Math.min(idleMs, ceilingMs), ceilingMs };\n}\n\n/** A live deadline that the host-attention signal can suspend. */\nexport interface SuspendableDeadline {\n /** Tell the deadline whether the host is waiting on a person right now. */\n setAwaiting(awaiting: boolean): void;\n /** Clear every timer. Idempotent — safe to call from a `finally`. */\n dispose(): void;\n}\n\n/**\n * A deadline with a suspendable idle leg and an unsuspendable ceiling.\n *\n * Pure and injectable (`setTimer`/`clearTimer`) so the suspension rules are unit-testable\n * against a fake clock rather than by waiting minutes for real ones.\n *\n * The idle leg RESTARTS in full when a prompt clears rather than resuming where it left off.\n * That is deliberate: after the user dismisses a prompt the host begins fresh work, and the\n * seconds that elapsed before the prompt say nothing about how long that work should take.\n * The ceiling is what stops a repeatedly-prompting call from running forever.\n */\nexport function createSuspendableDeadline(opts: {\n bounds: CallBounds;\n /** Called once, with the bound that elapsed and its length in ms. */\n onExpire: (bound: DeadlineBound, boundMs: number) => void;\n setTimer?: (fn: () => void, ms: number) => unknown;\n clearTimer?: (handle: unknown) => void;\n}): SuspendableDeadline {\n const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));\n const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));\n const { idleMs, ceilingMs } = opts.bounds;\n // An idle leg at or above the ceiling can never fire first, so don't arm one — that keeps\n // the common unattended case (idle === ceiling) on exactly one timer, as before R3-307.\n const hasIdleLeg = Number.isFinite(idleMs) && idleMs < ceilingMs;\n\n let done = false;\n let idle: unknown;\n let ceiling: unknown;\n\n const expire = (bound: DeadlineBound, boundMs: number) => {\n if (done) return;\n done = true;\n opts.onExpire(bound, boundMs);\n };\n\n const armIdle = () => {\n if (done || !hasIdleLeg || idle !== undefined) return;\n idle = setTimer(() => {\n idle = undefined;\n expire('idle', idleMs);\n }, idleMs);\n };\n const disarmIdle = () => {\n if (idle !== undefined) {\n clearTimer(idle);\n idle = undefined;\n }\n };\n\n if (Number.isFinite(ceilingMs)) {\n ceiling = setTimer(() => {\n ceiling = undefined;\n expire('ceiling', ceilingMs);\n }, ceilingMs);\n }\n armIdle();\n\n return {\n setAwaiting(awaiting: boolean) {\n if (done) return;\n if (awaiting) disarmIdle();\n else armIdle();\n },\n dispose() {\n done = true;\n disarmIdle();\n if (ceiling !== undefined) {\n clearTimer(ceiling);\n ceiling = undefined;\n }\n },\n };\n}\n\n/** The error a bounded call rejects with. `code` is `'timeout'` — the code R3-303's typed\n * provider-error taxonomy adopts, so apps see one vocabulary. */\nexport class ProtocolTimeoutError extends Error {\n readonly code = 'timeout';\n /** `scheme:method` of the call that timed out. */\n readonly call: string;\n /** The bound that elapsed, in ms. */\n readonly timeoutMs: number;\n /** Whether the call was on the attended or unattended bound — the first thing anyone\n * debugging a timeout needs, and otherwise invisible. */\n readonly attendance: Attendance;\n /** WHICH bound elapsed (R3-307). An attended call that faults on its `idle` bound was not\n * waiting on anyone — the host said so — and that is a genuinely different diagnosis from\n * an abandoned prompt hitting the `ceiling`. */\n readonly bound: DeadlineBound;\n constructor(\n call: string,\n timeoutMs: number,\n attendance: Attendance,\n bound: DeadlineBound = attendance === 'attended' ? 'ceiling' : 'idle',\n ) {\n super(\n attendance === 'attended' && bound === 'ceiling'\n ? `immediately.run: ${call} was abandoned after ${Math.round(timeoutMs / 1000)}s waiting for you`\n : `immediately.run: ${call} did not respond within ${Math.round(timeoutMs / 1000)}s`,\n );\n this.name = 'ProtocolTimeoutError';\n this.call = call;\n this.timeoutMs = timeoutMs;\n this.attendance = attendance;\n this.bound = bound;\n }\n}\n\n/** The error a cancelled call rejects with. */\nexport class ProtocolCancelledError extends Error {\n readonly code = 'cancelled';\n constructor(call: string) {\n super(`immediately.run: ${call} was cancelled`);\n this.name = 'ProtocolCancelledError';\n }\n}\n\n/** What the host is waiting for at this moment, as reported on the host-attention channel\n * (R3-307). Present on a {@link PendingState} only while the host IS prompting. */\nexport interface PendingAttention {\n /** The kind of prompt on screen, or `null` when the host reports a wait it cannot name. */\n kind: HostAttentionKind | null;\n /** `Date.now()` when the wait began, or `null`. */\n since: number | null;\n}\n\n/** What `onPending` is told when a call is taking a while. */\nexport interface PendingState {\n call: string;\n attendance: Attendance;\n elapsedMs: number;\n /** Why this call *may* be waiting on a person — present only when attended. It comes from\n * the classification table, so it is a standing possibility, not a live fact. */\n reason?: string;\n /**\n * What the host is waiting for RIGHT NOW (R3-307) — present only while a host prompt is\n * actually up. Prefer it over {@link reason} when rendering: \"tap your passkey\" is a\n * sentence the user can act on; \"this may need you\" is not.\n */\n awaiting?: PendingAttention;\n}\n\n/** Options accepted by every bounded host call. */\nexport interface BoundedCallOptions {\n /** Override the classified default. `Infinity` disables the bound — an escape hatch for a\n * caller that genuinely owns the wait (it must then provide its own way out).\n *\n * An explicit value is the WHOLE bound: it is never suspended by the host-attention\n * signal, because a caller that named a number owns the wait. */\n timeoutMs?: number;\n /** Abort the wait. The SDK stops waiting and rejects with `code: 'cancelled'`. */\n signal?: AbortSignal;\n /** Fired when the call passes `PENDING_NOTICE_MS`, so a caller can render a waiting state\n * rather than an unexplained pause — and again, after that, whenever the host starts or\n * stops waiting on the user, so the waiting state can name what is on screen now. */\n onPending?: (state: PendingState) => void;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/protocolDeadline.ts"],"sourcesContent":["// Deadlines for host protocol calls (R3-298) — so no platform request can hang forever.\n//\n// THE FAILURE THIS FIXES. `protocolRequest` and `hostFetch` carried no timeout, so a host\n// operation that never resolved presented as an indefinite \"Running…\" with no error, no\n// cancel, and nothing in the console. The GLM dogfood run hit exactly this: the first\n// `chat()` of a session parks on a WebAuthn unseal that never completes, and the surface\n// simply waits. A first-run user inside the setup wizard (R3-299) would be stranded on\n// \"Testing…\" — at the worst possible moment, on the screen whose whole purpose is to prove\n// setup worked.\n//\n// WHY A SINGLE CONSTANT IS THE WRONG ANSWER, AND WHAT IS DONE INSTEAD. A blanket timeout\n// was deferred once as risky, correctly: some host operations legitimately await a HUMAN —\n// a passkey tap, a consent decision, a file picker — and a flat deadline aborts those. So\n// calls are classified, and the two classes get very different bounds:\n//\n// unattended — a network call or a channel round-trip. Nobody is being asked anything, so\n// a reply that has not arrived in tens of seconds is a fault. Short bound.\n// attended — the host may draw chrome and wait for the user. The bound exists only to\n// stop an ABANDONED prompt from pinning the caller forever, so it is set far\n// beyond human reaction time.\n//\n// NOTHING IS UNBOUNDED. \"A long or absent deadline\" was the licence; absent is declined,\n// because absent is the bug. An attended bound of minutes never aborts a person who is\n// actually deciding, and does release a caller whose user walked away — which is the\n// difference between a slow flow and a wedged one.\n//\n// THE IMPRECISION, AND HOW R3-307 REMOVED IT. Attendedness is classified per\n// (scheme, method), but it is really a property of a MOMENT: `spaces:mount` is unattended\n// when the grant is already held and attended on first use, because the host raises consent\n// inside the request (`spaceHandler` `presentMountConsent`). A per-method table cannot see\n// that, so R3-298 classified any method that MAY prompt as `attended` and gave it the long\n// bound — which meant the common grant-held path also waited ten minutes before reporting a\n// fault.\n//\n// R3-307 added the host-attention channel (`hostAttention.ts`), on which the host says\n// whether a person is being asked something RIGHT NOW. So a call now carries TWO bounds:\n//\n// idle — in force while the host is not prompting. A may-prompt call runs on this,\n// which is a real correctness gain: the grant-held `spaces:mount` faults in\n// seconds, as it always should have.\n// ceiling — absolute, from call start, NEVER suspended. An abandoned prompt still\n// releases the caller. The signal may EXTEND a deadline, never remove it.\n//\n// AND THE SHORTENING IS OPT-IN PER ENTRY, which is the part worth not losing. A scheme\n// drops to the short idle bound only when EVERY prompt it can raise is one the host\n// actually announces on that channel (the powerbox, the add-secret modal, the passkey\n// unlock, and the spaceHandler consent surfaces — the presenters site-main wraps). A task\n// app's interaction and the contribute diff-approval are human-paced but are NOT host\n// prompts, so no signal would ever fire for them and they keep the full attended bound. A\n// signal that cannot fire must never be allowed to shorten a deadline.\n\nimport type { HostAttentionKind } from './hostAttention';\n\n/** Whether a call may block on a human being asked something. */\nexport type Attendance = 'unattended' | 'attended';\n\n/** Milliseconds. Exported so callers can reason about the defaults they are overriding. */\nexport const UNATTENDED_TIMEOUT_MS = 30_000;\n/** Network calls reach arbitrary upstreams; the host bounds the fetch itself, so this is a\n * backstop against the host never replying, not a request budget. */\nexport const NETWORK_TIMEOUT_MS = 120_000;\n/** Far beyond human reaction time — this exists only so an ABANDONED prompt releases the\n * caller. It must never be short enough to abort someone who is deciding. */\nexport const ATTENDED_TIMEOUT_MS = 600_000;\n/** A stream's first frame may be behind an unseal, so it gets an attended-scale bound of\n * its own. See `firstFrameTimeoutFor`. */\nexport const ATTENDED_FIRST_FRAME_MS = 300_000;\n/** After the first frame, silence this long means the stream is wedged: the host is no\n * longer producing and no human is being asked. */\nexport const STREAM_IDLE_TIMEOUT_MS = 120_000;\n/** When a call passes this, `onPending` fires so a caller can render a waiting state\n * instead of an unexplained stall. */\nexport const PENDING_NOTICE_MS = 3_000;\n\n/**\n * Methods that may draw host chrome and wait for the user.\n *\n * Each entry is a `scheme:method` or a bare `scheme` (matching every method of it). The\n * REASON is recorded per entry, because this table is the item's actual content — a future\n * reader must be able to see why a method is on the long bound without re-deriving it from\n * the host source.\n */\ninterface AttendedEntry {\n /** Why this call may block on a human. */\n reason: string;\n /**\n * The bound this call runs on while the host reports NOBODY is being asked (R3-307).\n *\n * Set it ONLY when every prompt this entry can raise is one the host announces on the\n * host-attention channel — i.e. a presenter site-main wraps (`hostAttention.ts` in that\n * repo). Omitted ⇒ the call keeps the full attended bound at all times, because a signal\n * that never fires must not be allowed to shorten a deadline.\n */\n idleMs?: number;\n}\n\nconst ATTENDED: Record<string, AttendedEntry> = {\n // The powerbox and the add-secret modal are host-drawn and wait for the user to type or\n // pick; the first use of any stored secret additionally raises a WebAuthn assertion\n // (SECRETS_SPEC §3 — one unlock per session, from a live gesture). All three are wrapped\n // presenters, so the signal covers this scheme completely.\n secrets: {\n reason: 'host-drawn key entry / picker, and the per-session passkey unlock',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n // Consent is raised INSIDE the request: presentMountConsent, presentGrantPicker,\n // presentCreateConsent, presentShareDisclosure, presentReferenceConsent — every one of\n // them a wrapped presenter. Unattended once the grant is held, attended on first use, and\n // since R3-307 the host says which of those is happening.\n spaces: {\n reason: 'first-use mount/share/create consent is drawn inside the request',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n settings: {\n reason: 'settings verbs reach the same consent and picker surfaces as spaces',\n idleMs: UNATTENDED_TIMEOUT_MS,\n },\n // The contribute flow shows the full diff for approval before anything is written\n // (TRUST_AND_SAFETY TS-19b: the approval MUST show the real diff, so a human reads it).\n // NOT a wrapped presenter — no `idleMs`.\n contribute: { reason: 'the diff-approval step is a human read of the whole change' },\n // A task is an app bound to a transient slot that the user interacts with; it returns\n // when they finish, which is human-paced by construction. That is an APP's interaction,\n // not a host prompt, so the attention channel never fires for it — no `idleMs`.\n task: { reason: 'a task app runs an interaction and returns when the user finishes' },\n // Launching a target can raise consent for a not-yet-granted app — through the launch\n // flow's own surface, not one of the wrapped presenters. No `idleMs`.\n launch: { reason: 'may raise first-use consent for the launched target' },\n // A drag is a gesture in progress — its duration is the user's hand, and no host prompt\n // is up while it happens. No `idleMs`.\n dnd: { reason: 'a drag is a human gesture in flight' },\n // The chat stream's FIRST frame sits behind the session's first passkey unseal — the\n // exact hang the dogfood run found — and that unseal IS a wrapped presenter. But the idle\n // bound here is the NETWORK one, not the channel one: with no prompt up, this call is\n // waiting on an arbitrary upstream model, and thirty seconds is a normal generation.\n llm: {\n reason: 'the first frame can sit behind the session passkey unseal',\n idleMs: NETWORK_TIMEOUT_MS,\n },\n};\n\n/** Look up a scheme/method in the attended table, preferring the exact method entry. */\nfunction attendedEntry(scheme: string, method: string): AttendedEntry | undefined {\n return ATTENDED[`${scheme}:${method}`] ?? ATTENDED[scheme];\n}\n\n/** Look up a scheme/method in the attended table, preferring the exact method entry. */\nfunction attendedReason(scheme: string, method: string): string | undefined {\n return attendedEntry(scheme, method)?.reason;\n}\n\n/** Whether a call may block on a human. Exported for the classification test + tooling. */\nexport function attendanceOf(scheme: string, method: string): Attendance {\n return attendedReason(scheme, method) ? 'attended' : 'unattended';\n}\n\n/** Why a call is classified attended, or `undefined` when it is not. Exported so the\n * classification is legible from a test failure rather than only from this source. */\nexport function attendanceReason(scheme: string, method: string): string | undefined {\n return attendedReason(scheme, method);\n}\n\n/** The default deadline for a one-shot `protocolRequest`. */\nexport function timeoutFor(scheme: string, method: string): number {\n if (attendanceOf(scheme, method) === 'attended') return ATTENDED_TIMEOUT_MS;\n // `fetch` reaches an arbitrary upstream, so it gets the network bound rather than the\n // channel-round-trip one.\n if (scheme === 'fetch') return NETWORK_TIMEOUT_MS;\n return UNATTENDED_TIMEOUT_MS;\n}\n\n/**\n * The default TIME-TO-FIRST-FRAME bound for a stream.\n *\n * Deliberately not a total-duration bound: a long generation that is streaming normally is\n * healthy, and killing it would be a worse bug than the one being fixed. The hang has a\n * distinct shape — NO frames at all — so that is what is bounded, plus an idle gap between\n * frames once flowing. Together they fire exactly on a wedged stream and never on a slow\n * one.\n */\nexport function firstFrameTimeoutFor(scheme: string, method: string): number {\n return attendanceOf(scheme, method) === 'attended' ? ATTENDED_FIRST_FRAME_MS : NETWORK_TIMEOUT_MS;\n}\n\n/**\n * The two bounds a call runs under (R3-307).\n *\n * `idleMs` is in force while the host reports nobody is being asked; it is cleared while a\n * host prompt is up and restarted, in full, when the prompt goes away. `ceilingMs` runs from\n * call start and is NEVER suspended — it is what releases a caller whose user walked away.\n */\nexport interface CallBounds {\n /** The bound in force while the host is not waiting on a person. */\n idleMs: number;\n /** The absolute bound from call start. Never suspended. */\n ceilingMs: number;\n}\n\n/** Which of a call's two bounds elapsed. `idle` means the host was NOT prompting — nobody\n * was being asked anything, so this is a fault. `ceiling` means the absolute bound ran out,\n * which for an attended call is an abandoned prompt. */\nexport type DeadlineBound = 'idle' | 'ceiling';\n\n/** The bounds for a one-shot `protocolRequest`. */\nexport function boundsFor(scheme: string, method: string): CallBounds {\n const ceilingMs = timeoutFor(scheme, method);\n const idleMs = attendedEntry(scheme, method)?.idleMs;\n // `Math.min` so an `idleMs` can only ever tighten: an entry that named a bound longer than\n // its own ceiling would otherwise silently disable the idle leg.\n return { idleMs: idleMs === undefined ? ceilingMs : Math.min(idleMs, ceilingMs), ceilingMs };\n}\n\n/** The bounds for a stream's time-to-first-frame. */\nexport function firstFrameBoundsFor(scheme: string, method: string): CallBounds {\n const ceilingMs = firstFrameTimeoutFor(scheme, method);\n const idleMs = attendedEntry(scheme, method)?.idleMs;\n return { idleMs: idleMs === undefined ? ceilingMs : Math.min(idleMs, ceilingMs), ceilingMs };\n}\n\n/** A live deadline that the host-attention signal can suspend. */\nexport interface SuspendableDeadline {\n /** Tell the deadline whether the host is waiting on a person right now. */\n setAwaiting(awaiting: boolean): void;\n /** Clear every timer. Idempotent — safe to call from a `finally`. */\n dispose(): void;\n}\n\n/**\n * A deadline with a suspendable idle leg and an unsuspendable ceiling.\n *\n * Pure and injectable (`setTimer`/`clearTimer`) so the suspension rules are unit-testable\n * against a fake clock rather than by waiting minutes for real ones.\n *\n * The idle leg RESTARTS in full when a prompt clears rather than resuming where it left off.\n * That is deliberate: after the user dismisses a prompt the host begins fresh work, and the\n * seconds that elapsed before the prompt say nothing about how long that work should take.\n * The ceiling is what stops a repeatedly-prompting call from running forever.\n */\nexport function createSuspendableDeadline(opts: {\n bounds: CallBounds;\n /** Called once, with the bound that elapsed and its length in ms. */\n onExpire: (bound: DeadlineBound, boundMs: number) => void;\n setTimer?: (fn: () => void, ms: number) => unknown;\n clearTimer?: (handle: unknown) => void;\n}): SuspendableDeadline {\n const setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));\n const clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h as ReturnType<typeof setTimeout>));\n const { idleMs, ceilingMs } = opts.bounds;\n // An idle leg at or above the ceiling can never fire first, so don't arm one — that keeps\n // the common unattended case (idle === ceiling) on exactly one timer, as before R3-307.\n const hasIdleLeg = Number.isFinite(idleMs) && idleMs < ceilingMs;\n\n let done = false;\n let idle: unknown;\n let ceiling: unknown;\n\n const expire = (bound: DeadlineBound, boundMs: number) => {\n if (done) return;\n done = true;\n opts.onExpire(bound, boundMs);\n };\n\n const armIdle = () => {\n if (done || !hasIdleLeg || idle !== undefined) return;\n idle = setTimer(() => {\n idle = undefined;\n expire('idle', idleMs);\n }, idleMs);\n };\n const disarmIdle = () => {\n if (idle !== undefined) {\n clearTimer(idle);\n idle = undefined;\n }\n };\n\n if (Number.isFinite(ceilingMs)) {\n ceiling = setTimer(() => {\n ceiling = undefined;\n expire('ceiling', ceilingMs);\n }, ceilingMs);\n }\n armIdle();\n\n return {\n setAwaiting(awaiting: boolean) {\n if (done) return;\n if (awaiting) disarmIdle();\n else armIdle();\n },\n dispose() {\n done = true;\n disarmIdle();\n if (ceiling !== undefined) {\n clearTimer(ceiling);\n ceiling = undefined;\n }\n },\n };\n}\n\n/** The error a bounded call rejects with. `code` is `'timeout'` — the code R3-303's typed\n * provider-error taxonomy adopts, so apps see one vocabulary. */\nexport class ProtocolTimeoutError extends Error {\n readonly code = 'timeout';\n /** `scheme:method` of the call that timed out. */\n readonly call: string;\n /** The bound that elapsed, in ms. */\n readonly timeoutMs: number;\n /** Whether the call was on the attended or unattended bound — the first thing anyone\n * debugging a timeout needs, and otherwise invisible. */\n readonly attendance: Attendance;\n /** WHICH bound elapsed (R3-307). An attended call that faults on its `idle` bound was not\n * waiting on anyone — the host said so — and that is a genuinely different diagnosis from\n * an abandoned prompt hitting the `ceiling`. */\n readonly bound: DeadlineBound;\n constructor(\n call: string,\n timeoutMs: number,\n attendance: Attendance,\n bound: DeadlineBound = attendance === 'attended' ? 'ceiling' : 'idle',\n ) {\n super(\n attendance === 'attended' && bound === 'ceiling'\n ? `immediately.run: ${call} was abandoned after ${Math.round(timeoutMs / 1000)}s waiting for you`\n : `immediately.run: ${call} did not respond within ${Math.round(timeoutMs / 1000)}s`,\n );\n this.name = 'ProtocolTimeoutError';\n this.call = call;\n this.timeoutMs = timeoutMs;\n this.attendance = attendance;\n this.bound = bound;\n }\n}\n\n/** The error a cancelled call rejects with. */\nexport class ProtocolCancelledError extends Error {\n readonly code = 'cancelled';\n constructor(call: string) {\n super(`immediately.run: ${call} was cancelled`);\n this.name = 'ProtocolCancelledError';\n }\n}\n\n/** What the host is waiting for at this moment, as reported on the host-attention channel\n * (R3-307). Present on a {@link PendingState} only while the host IS prompting. */\nexport interface PendingAttention {\n /** The kind of prompt on screen, or `null` when the host reports a wait it cannot name. */\n kind: HostAttentionKind | null;\n /** `Date.now()` when the wait began, or `null`. */\n since: number | null;\n}\n\n/** What `onPending` is told when a call is taking a while. */\nexport interface PendingState {\n call: string;\n attendance: Attendance;\n elapsedMs: number;\n /** Why this call *may* be waiting on a person — present only when attended. It comes from\n * the classification table, so it is a standing possibility, not a live fact. */\n reason?: string;\n /**\n * What the host is waiting for RIGHT NOW (R3-307) — present only while a host prompt is\n * actually up. Prefer it over {@link reason} when rendering: \"tap your passkey\" is a\n * sentence the user can act on; \"this may need you\" is not.\n */\n awaiting?: PendingAttention;\n}\n\n/** Options accepted by every bounded host call. */\nexport interface BoundedCallOptions {\n /** Override the classified default. `Infinity` disables the bound — an escape hatch for a\n * caller that genuinely owns the wait (it must then provide its own way out).\n *\n * An explicit value is the WHOLE bound: it is never suspended by the host-attention\n * signal, because a caller that named a number owns the wait. */\n timeoutMs?: number;\n /** Abort the wait. The SDK stops waiting and rejects with `code: 'cancelled'`. */\n signal?: AbortSignal;\n /** Fired when the call passes `PENDING_NOTICE_MS`, so a caller can render a waiting state\n * rather than an unexplained pause — and again, after that, whenever the host starts or\n * stops waiting on the user, so the waiting state can name what is on screen now. */\n onPending?: (state: PendingState) => void;\n}\n"],"mappings":"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","names":[]}
@@ -79,10 +79,7 @@ async function* consumeStream(transport, type, method, params, msgId = nextMsgId
79
79
  try {
80
80
  await new Promise((resolve, reject) => {
81
81
  wake = resolve;
82
- deadline = setTimeout(
83
- () => reject(new import_protocolDeadline.ProtocolTimeoutError(call, budget, attendance)),
84
- budget
85
- );
82
+ deadline = setTimeout(() => reject(new import_protocolDeadline.ProtocolTimeoutError(call, budget, attendance)), budget);
86
83
  if (opts?.onPending && !noticed) {
87
84
  notice = setTimeout(() => {
88
85
  noticed = true;
@@ -138,15 +135,7 @@ const bundlerTransport = {
138
135
  cancel: (msg) => (0, import_sandboxUtils.sendMessage)(msg.type, msg)
139
136
  };
140
137
  function protocolStream(protocolName, method, params, signal, opts) {
141
- return consumeStream(
142
- bundlerTransport,
143
- protocolName,
144
- method,
145
- params,
146
- void 0,
147
- signal,
148
- opts
149
- );
138
+ return consumeStream(bundlerTransport, protocolName, method, params, void 0, signal, opts);
150
139
  }
151
140
  // Annotate the CommonJS export names for ESM import in node:
152
141
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\nimport {\n attendanceOf,\n attendanceReason,\n firstFrameTimeoutFor,\n PENDING_NOTICE_MS,\n ProtocolTimeoutError,\n STREAM_IDLE_TIMEOUT_MS,\n type BoundedCallOptions,\n} from './protocolDeadline';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (\n type: string,\n handler: (msg: { msgId?: number; stream?: StreamFrame }) => void\n ) => () => void;\n // Tell the host to STOP the stream early — abort the in-flight generation (and,\n // for `llm:chat`, the upstream provider fetch so it stops BILLING). Sent when the\n // consumer bails before a terminal frame: an early `break`/`return` out of the\n // `for await`, or an `AbortSignal` firing. Carries the same `(type, msgId)` the\n // host tagged its frames with, so the host aborts the matching generator\n // (LLM_AND_AGENTS_SPEC §3.3 \"abort the in-flight LLM request\"). Optional — a\n // transport predating the cancel frame simply omits it and the old behavior\n // (host runs to completion) stands.\n cancel?: (msg: { type: string; msgId: number; cancel: true }) => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n *\n * `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it\n * fires (or the consumer `break`s before a terminal frame), the `finally` sends a\n * `cancel` frame back over `transport.cancel` so the HOST stops generating — without\n * it, aborting only stops the app-side iterator while the upstream provider keeps\n * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId(),\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number }\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n // True once a terminal (`done`/`error`) frame arrived — so the `finally` knows the\n // host already stopped and must NOT send a redundant cancel. Left false when the\n // consumer bails early or the signal aborts (the two cases that DO need a cancel).\n let settled = false;\n // True once the request frame went out. A cancel is only meaningful for a stream\n // the host actually started — an abort BEFORE `send` sends nothing to cancel.\n let started = false;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n // An abort wakes the pull loop out of its idle `await`; the loop then throws.\n const onAbort = () => {\n const w = wake;\n wake = null;\n w?.();\n };\n if (signal) signal.addEventListener('abort', onAbort);\n\n // R3-298 — a stream is bounded by SILENCE, not by duration.\n //\n // A total-duration deadline would be wrong here: a long generation that is streaming\n // normally is healthy, and aborting it would be a worse bug than the hang. The hang has a\n // distinct shape — no frames at all — so that is what is bounded: a time-to-FIRST-FRAME\n // deadline, then an idle-gap deadline once frames are flowing. Together they fire exactly\n // on a wedged stream and never on a slow one.\n //\n // The scheme's `type` is `protocol-llm`; the classification table is keyed on the bare\n // scheme, so strip the prefix. The chat stream is classified ATTENDED because its first\n // frame can sit behind the session's first passkey unseal — the dogfood hang.\n const scheme = type.startsWith('protocol-') ? type.slice('protocol-'.length) : type;\n const call = `${scheme}:${method}`;\n const attendance = attendanceOf(scheme, method);\n const firstFrameMs = opts?.timeoutMs ?? firstFrameTimeoutFor(scheme, method);\n const idleMs = opts?.idleTimeoutMs ?? STREAM_IDLE_TIMEOUT_MS;\n let sawFrame = false;\n let noticed = false;\n\n /** Wait for the next frame, bounded. Rejects with a typed timeout rather than parking. */\n const awaitFrame = async (): Promise<void> => {\n const budget = sawFrame ? idleMs : firstFrameMs;\n if (!Number.isFinite(budget)) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n return;\n }\n const startedAt = Date.now();\n let deadline: ReturnType<typeof setTimeout> | undefined;\n let notice: ReturnType<typeof setTimeout> | undefined;\n try {\n await new Promise<void>((resolve, reject) => {\n wake = resolve;\n deadline = setTimeout(\n () => reject(new ProtocolTimeoutError(call, budget, attendance)),\n budget,\n );\n if (opts?.onPending && !noticed) {\n notice = setTimeout(() => {\n noticed = true;\n try {\n opts.onPending?.({\n call,\n attendance,\n elapsedMs: Date.now() - startedAt,\n ...(attendanceReason(scheme, method)\n ? { reason: attendanceReason(scheme, method) as string }\n : {}),\n });\n } catch {\n /* a caller's render callback must never break the stream it describes */\n }\n }, PENDING_NOTICE_MS);\n }\n });\n } finally {\n if (deadline !== undefined) clearTimeout(deadline);\n if (notice !== undefined) clearTimeout(notice);\n wake = null;\n }\n };\n\n try {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted before start');\n transport.send({ type, method, params, msgId, stream: true });\n started = true;\n while (true) {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted');\n if (queue.length === 0) {\n // A timeout throws out of here into the `finally`, which sends the host a real\n // cancel frame — so a wedged stream stops generating and BILLING rather than being\n // merely abandoned by the app. Streams can do this because they own their `msgId`.\n await awaitFrame();\n continue;\n }\n sawFrame = true;\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n settled = true;\n return frame.value as R;\n } else {\n settled = true;\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n if (signal) signal.removeEventListener('abort', onAbort);\n // Consumer stopped pulling before a terminal frame (early break/return, or the\n // signal aborted): tell the host to stop the in-flight generation + billing.\n if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers. `cancel`\n// rides the same messageBus as `send` — a `{type, msgId, cancel:true}` frame the\n// host dispatcher routes to the in-flight generator's AbortController.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n *\n * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[],\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number }\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(\n bundlerTransport,\n protocolName,\n method,\n params,\n undefined,\n signal,\n opts\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\nimport {\n attendanceOf,\n attendanceReason,\n firstFrameTimeoutFor,\n PENDING_NOTICE_MS,\n ProtocolTimeoutError,\n STREAM_IDLE_TIMEOUT_MS,\n type BoundedCallOptions,\n} from './protocolDeadline';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (type: string, handler: (msg: { msgId?: number; stream?: StreamFrame }) => void) => () => void;\n // Tell the host to STOP the stream early — abort the in-flight generation (and,\n // for `llm:chat`, the upstream provider fetch so it stops BILLING). Sent when the\n // consumer bails before a terminal frame: an early `break`/`return` out of the\n // `for await`, or an `AbortSignal` firing. Carries the same `(type, msgId)` the\n // host tagged its frames with, so the host aborts the matching generator\n // (LLM_AND_AGENTS_SPEC §3.3 \"abort the in-flight LLM request\"). Optional — a\n // transport predating the cancel frame simply omits it and the old behavior\n // (host runs to completion) stands.\n cancel?: (msg: { type: string; msgId: number; cancel: true }) => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n *\n * `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it\n * fires (or the consumer `break`s before a terminal frame), the `finally` sends a\n * `cancel` frame back over `transport.cancel` so the HOST stops generating — without\n * it, aborting only stops the app-side iterator while the upstream provider keeps\n * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId(),\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number },\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n // True once a terminal (`done`/`error`) frame arrived — so the `finally` knows the\n // host already stopped and must NOT send a redundant cancel. Left false when the\n // consumer bails early or the signal aborts (the two cases that DO need a cancel).\n let settled = false;\n // True once the request frame went out. A cancel is only meaningful for a stream\n // the host actually started — an abort BEFORE `send` sends nothing to cancel.\n let started = false;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n // An abort wakes the pull loop out of its idle `await`; the loop then throws.\n const onAbort = () => {\n const w = wake;\n wake = null;\n w?.();\n };\n if (signal) signal.addEventListener('abort', onAbort);\n\n // R3-298 — a stream is bounded by SILENCE, not by duration.\n //\n // A total-duration deadline would be wrong here: a long generation that is streaming\n // normally is healthy, and aborting it would be a worse bug than the hang. The hang has a\n // distinct shape — no frames at all — so that is what is bounded: a time-to-FIRST-FRAME\n // deadline, then an idle-gap deadline once frames are flowing. Together they fire exactly\n // on a wedged stream and never on a slow one.\n //\n // The scheme's `type` is `protocol-llm`; the classification table is keyed on the bare\n // scheme, so strip the prefix. The chat stream is classified ATTENDED because its first\n // frame can sit behind the session's first passkey unseal — the dogfood hang.\n const scheme = type.startsWith('protocol-') ? type.slice('protocol-'.length) : type;\n const call = `${scheme}:${method}`;\n const attendance = attendanceOf(scheme, method);\n const firstFrameMs = opts?.timeoutMs ?? firstFrameTimeoutFor(scheme, method);\n const idleMs = opts?.idleTimeoutMs ?? STREAM_IDLE_TIMEOUT_MS;\n let sawFrame = false;\n let noticed = false;\n\n /** Wait for the next frame, bounded. Rejects with a typed timeout rather than parking. */\n const awaitFrame = async (): Promise<void> => {\n const budget = sawFrame ? idleMs : firstFrameMs;\n if (!Number.isFinite(budget)) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n return;\n }\n const startedAt = Date.now();\n let deadline: ReturnType<typeof setTimeout> | undefined;\n let notice: ReturnType<typeof setTimeout> | undefined;\n try {\n await new Promise<void>((resolve, reject) => {\n wake = resolve;\n deadline = setTimeout(() => reject(new ProtocolTimeoutError(call, budget, attendance)), budget);\n if (opts?.onPending && !noticed) {\n notice = setTimeout(() => {\n noticed = true;\n try {\n opts.onPending?.({\n call,\n attendance,\n elapsedMs: Date.now() - startedAt,\n ...(attendanceReason(scheme, method) ? { reason: attendanceReason(scheme, method) as string } : {}),\n });\n } catch {\n /* a caller's render callback must never break the stream it describes */\n }\n }, PENDING_NOTICE_MS);\n }\n });\n } finally {\n if (deadline !== undefined) clearTimeout(deadline);\n if (notice !== undefined) clearTimeout(notice);\n wake = null;\n }\n };\n\n try {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted before start');\n transport.send({ type, method, params, msgId, stream: true });\n started = true;\n while (true) {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted');\n if (queue.length === 0) {\n // A timeout throws out of here into the `finally`, which sends the host a real\n // cancel frame — so a wedged stream stops generating and BILLING rather than being\n // merely abandoned by the app. Streams can do this because they own their `msgId`.\n await awaitFrame();\n continue;\n }\n sawFrame = true;\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n settled = true;\n return frame.value as R;\n } else {\n settled = true;\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n if (signal) signal.removeEventListener('abort', onAbort);\n // Consumer stopped pulling before a terminal frame (early break/return, or the\n // signal aborted): tell the host to stop the in-flight generation + billing.\n if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers. `cancel`\n// rides the same messageBus as `send` — a `{type, msgId, cancel:true}` frame the\n// host dispatcher routes to the in-flight generator's AbortController.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) => addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n *\n * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[],\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number },\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params, undefined, signal, opts);\n}\n"],"mappings":"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","names":[]}
@@ -62,10 +62,7 @@ async function* consumeStream(transport, type, method, params, msgId = nextMsgId
62
62
  try {
63
63
  await new Promise((resolve, reject) => {
64
64
  wake = resolve;
65
- deadline = setTimeout(
66
- () => reject(new ProtocolTimeoutError(call, budget, attendance)),
67
- budget
68
- );
65
+ deadline = setTimeout(() => reject(new ProtocolTimeoutError(call, budget, attendance)), budget);
69
66
  if (opts?.onPending && !noticed) {
70
67
  notice = setTimeout(() => {
71
68
  noticed = true;
@@ -121,15 +118,7 @@ const bundlerTransport = {
121
118
  cancel: (msg) => sendMessage(msg.type, msg)
122
119
  };
123
120
  function protocolStream(protocolName, method, params, signal, opts) {
124
- return consumeStream(
125
- bundlerTransport,
126
- protocolName,
127
- method,
128
- params,
129
- void 0,
130
- signal,
131
- opts
132
- );
121
+ return consumeStream(bundlerTransport, protocolName, method, params, void 0, signal, opts);
133
122
  }
134
123
  export {
135
124
  StreamError,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\nimport {\n attendanceOf,\n attendanceReason,\n firstFrameTimeoutFor,\n PENDING_NOTICE_MS,\n ProtocolTimeoutError,\n STREAM_IDLE_TIMEOUT_MS,\n type BoundedCallOptions,\n} from './protocolDeadline';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (\n type: string,\n handler: (msg: { msgId?: number; stream?: StreamFrame }) => void\n ) => () => void;\n // Tell the host to STOP the stream early — abort the in-flight generation (and,\n // for `llm:chat`, the upstream provider fetch so it stops BILLING). Sent when the\n // consumer bails before a terminal frame: an early `break`/`return` out of the\n // `for await`, or an `AbortSignal` firing. Carries the same `(type, msgId)` the\n // host tagged its frames with, so the host aborts the matching generator\n // (LLM_AND_AGENTS_SPEC §3.3 \"abort the in-flight LLM request\"). Optional — a\n // transport predating the cancel frame simply omits it and the old behavior\n // (host runs to completion) stands.\n cancel?: (msg: { type: string; msgId: number; cancel: true }) => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n *\n * `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it\n * fires (or the consumer `break`s before a terminal frame), the `finally` sends a\n * `cancel` frame back over `transport.cancel` so the HOST stops generating — without\n * it, aborting only stops the app-side iterator while the upstream provider keeps\n * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId(),\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number }\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n // True once a terminal (`done`/`error`) frame arrived — so the `finally` knows the\n // host already stopped and must NOT send a redundant cancel. Left false when the\n // consumer bails early or the signal aborts (the two cases that DO need a cancel).\n let settled = false;\n // True once the request frame went out. A cancel is only meaningful for a stream\n // the host actually started — an abort BEFORE `send` sends nothing to cancel.\n let started = false;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n // An abort wakes the pull loop out of its idle `await`; the loop then throws.\n const onAbort = () => {\n const w = wake;\n wake = null;\n w?.();\n };\n if (signal) signal.addEventListener('abort', onAbort);\n\n // R3-298 — a stream is bounded by SILENCE, not by duration.\n //\n // A total-duration deadline would be wrong here: a long generation that is streaming\n // normally is healthy, and aborting it would be a worse bug than the hang. The hang has a\n // distinct shape — no frames at all — so that is what is bounded: a time-to-FIRST-FRAME\n // deadline, then an idle-gap deadline once frames are flowing. Together they fire exactly\n // on a wedged stream and never on a slow one.\n //\n // The scheme's `type` is `protocol-llm`; the classification table is keyed on the bare\n // scheme, so strip the prefix. The chat stream is classified ATTENDED because its first\n // frame can sit behind the session's first passkey unseal — the dogfood hang.\n const scheme = type.startsWith('protocol-') ? type.slice('protocol-'.length) : type;\n const call = `${scheme}:${method}`;\n const attendance = attendanceOf(scheme, method);\n const firstFrameMs = opts?.timeoutMs ?? firstFrameTimeoutFor(scheme, method);\n const idleMs = opts?.idleTimeoutMs ?? STREAM_IDLE_TIMEOUT_MS;\n let sawFrame = false;\n let noticed = false;\n\n /** Wait for the next frame, bounded. Rejects with a typed timeout rather than parking. */\n const awaitFrame = async (): Promise<void> => {\n const budget = sawFrame ? idleMs : firstFrameMs;\n if (!Number.isFinite(budget)) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n return;\n }\n const startedAt = Date.now();\n let deadline: ReturnType<typeof setTimeout> | undefined;\n let notice: ReturnType<typeof setTimeout> | undefined;\n try {\n await new Promise<void>((resolve, reject) => {\n wake = resolve;\n deadline = setTimeout(\n () => reject(new ProtocolTimeoutError(call, budget, attendance)),\n budget,\n );\n if (opts?.onPending && !noticed) {\n notice = setTimeout(() => {\n noticed = true;\n try {\n opts.onPending?.({\n call,\n attendance,\n elapsedMs: Date.now() - startedAt,\n ...(attendanceReason(scheme, method)\n ? { reason: attendanceReason(scheme, method) as string }\n : {}),\n });\n } catch {\n /* a caller's render callback must never break the stream it describes */\n }\n }, PENDING_NOTICE_MS);\n }\n });\n } finally {\n if (deadline !== undefined) clearTimeout(deadline);\n if (notice !== undefined) clearTimeout(notice);\n wake = null;\n }\n };\n\n try {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted before start');\n transport.send({ type, method, params, msgId, stream: true });\n started = true;\n while (true) {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted');\n if (queue.length === 0) {\n // A timeout throws out of here into the `finally`, which sends the host a real\n // cancel frame — so a wedged stream stops generating and BILLING rather than being\n // merely abandoned by the app. Streams can do this because they own their `msgId`.\n await awaitFrame();\n continue;\n }\n sawFrame = true;\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n settled = true;\n return frame.value as R;\n } else {\n settled = true;\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n if (signal) signal.removeEventListener('abort', onAbort);\n // Consumer stopped pulling before a terminal frame (early break/return, or the\n // signal aborted): tell the host to stop the in-flight generation + billing.\n if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers. `cancel`\n// rides the same messageBus as `send` — a `{type, msgId, cancel:true}` frame the\n// host dispatcher routes to the in-flight generator's AbortController.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) =>\n addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n *\n * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[],\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number }\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(\n bundlerTransport,\n protocolName,\n method,\n params,\n undefined,\n signal,\n opts\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/protocolStream.ts"],"sourcesContent":["// SDK-side consumer for the host streaming transport (UI_AS_APPS_SPEC §5.1).\n//\n// The host `pumpGenerator` emits, per request msgId, a run of `stream.event`\n// frames terminated by one `stream.done` (with the return value) or `stream.error`\n// frame. This reassembles that run into an AsyncGenerator: each `event` is a\n// `yield`, the `done` value is the generator's `return`, an `error` is a `throw`.\n//\n// `consumeStream` takes an injected `StreamTransport` so it's unit-tested with a\n// fake send/subscribe — no bundler. `protocolStream`/`contribute` below wire it to\n// the real sandbox messageBus via sandboxUtils.\nimport { addListener, sendMessage } from './sandboxUtils';\nimport {\n attendanceOf,\n attendanceReason,\n firstFrameTimeoutFor,\n PENDING_NOTICE_MS,\n ProtocolTimeoutError,\n STREAM_IDLE_TIMEOUT_MS,\n type BoundedCallOptions,\n} from './protocolDeadline';\n\n/** One frame of a host stream: an `event` value, the terminal `done` value, or an `error`. */\nexport type StreamFrame =\n | { kind: 'event'; value: unknown }\n | { kind: 'done'; value: unknown }\n | { kind: 'error'; code: string; message: string };\n\n/** The send/subscribe transport {@link consumeStream} drives (injected so it can be faked in tests). */\nexport interface StreamTransport {\n // Fire the request that starts the stream. The host replies with frames tagged\n // by the same `msgId`.\n send: (msg: { type: string; method: string; params: unknown[]; msgId: number; stream: true }) => void;\n // Subscribe to inbound frames for `type`; returns an unsubscribe.\n subscribe: (type: string, handler: (msg: { msgId?: number; stream?: StreamFrame }) => void) => () => void;\n // Tell the host to STOP the stream early — abort the in-flight generation (and,\n // for `llm:chat`, the upstream provider fetch so it stops BILLING). Sent when the\n // consumer bails before a terminal frame: an early `break`/`return` out of the\n // `for await`, or an `AbortSignal` firing. Carries the same `(type, msgId)` the\n // host tagged its frames with, so the host aborts the matching generator\n // (LLM_AND_AGENTS_SPEC §3.3 \"abort the in-flight LLM request\"). Optional — a\n // transport predating the cancel frame simply omits it and the old behavior\n // (host runs to completion) stands.\n cancel?: (msg: { type: string; msgId: number; cancel: true }) => void;\n}\n\n/** Thrown when a stream ends in an `error` frame; carries the host's `code`. */\nexport class StreamError extends Error {\n code: string;\n constructor(code: string, message: string) {\n super(message);\n this.name = 'StreamError';\n this.code = code;\n }\n}\n\nlet streamCounter = 0;\nconst nextMsgId = (): number => {\n // Distinct from the bundler's own protocolRequest counter space is unnecessary —\n // frames are filtered by (type, msgId, stream) so a collision with a one-shot\n // reply (which has `result`, not `stream`) can't be misread.\n streamCounter = (streamCounter + 1) % Number.MAX_SAFE_INTEGER;\n return streamCounter;\n};\n\n/**\n * Drive one streamed request to completion over an injected transport.\n *\n * Yields each event value; returns the `done` value; throws `StreamError` on an\n * error frame. Always unsubscribes (via the generator's `finally`) so an early\n * `break` in the consumer doesn't leak the listener.\n *\n * `signal` wires a caller {@link AbortSignal} to mid-stream cancellation: when it\n * fires (or the consumer `break`s before a terminal frame), the `finally` sends a\n * `cancel` frame back over `transport.cancel` so the HOST stops generating — without\n * it, aborting only stops the app-side iterator while the upstream provider keeps\n * streaming and BILLING (LLM_AND_AGENTS_SPEC §3.3, R3-224 / adversarial F2).\n */\nexport async function* consumeStream<T = unknown, R = unknown>(\n transport: StreamTransport,\n type: string,\n method: string,\n params: unknown[],\n msgId: number = nextMsgId(),\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number },\n): AsyncGenerator<T, R, void> {\n const queue: StreamFrame[] = [];\n let wake: (() => void) | null = null;\n // True once a terminal (`done`/`error`) frame arrived — so the `finally` knows the\n // host already stopped and must NOT send a redundant cancel. Left false when the\n // consumer bails early or the signal aborts (the two cases that DO need a cancel).\n let settled = false;\n // True once the request frame went out. A cancel is only meaningful for a stream\n // the host actually started — an abort BEFORE `send` sends nothing to cancel.\n let started = false;\n const push = (frame: StreamFrame) => {\n queue.push(frame);\n const w = wake;\n wake = null;\n w?.();\n };\n\n const unsubscribe = transport.subscribe(type, (msg) => {\n if (msg.msgId !== msgId || !msg.stream) return;\n push(msg.stream);\n });\n\n // An abort wakes the pull loop out of its idle `await`; the loop then throws.\n const onAbort = () => {\n const w = wake;\n wake = null;\n w?.();\n };\n if (signal) signal.addEventListener('abort', onAbort);\n\n // R3-298 — a stream is bounded by SILENCE, not by duration.\n //\n // A total-duration deadline would be wrong here: a long generation that is streaming\n // normally is healthy, and aborting it would be a worse bug than the hang. The hang has a\n // distinct shape — no frames at all — so that is what is bounded: a time-to-FIRST-FRAME\n // deadline, then an idle-gap deadline once frames are flowing. Together they fire exactly\n // on a wedged stream and never on a slow one.\n //\n // The scheme's `type` is `protocol-llm`; the classification table is keyed on the bare\n // scheme, so strip the prefix. The chat stream is classified ATTENDED because its first\n // frame can sit behind the session's first passkey unseal — the dogfood hang.\n const scheme = type.startsWith('protocol-') ? type.slice('protocol-'.length) : type;\n const call = `${scheme}:${method}`;\n const attendance = attendanceOf(scheme, method);\n const firstFrameMs = opts?.timeoutMs ?? firstFrameTimeoutFor(scheme, method);\n const idleMs = opts?.idleTimeoutMs ?? STREAM_IDLE_TIMEOUT_MS;\n let sawFrame = false;\n let noticed = false;\n\n /** Wait for the next frame, bounded. Rejects with a typed timeout rather than parking. */\n const awaitFrame = async (): Promise<void> => {\n const budget = sawFrame ? idleMs : firstFrameMs;\n if (!Number.isFinite(budget)) {\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n return;\n }\n const startedAt = Date.now();\n let deadline: ReturnType<typeof setTimeout> | undefined;\n let notice: ReturnType<typeof setTimeout> | undefined;\n try {\n await new Promise<void>((resolve, reject) => {\n wake = resolve;\n deadline = setTimeout(() => reject(new ProtocolTimeoutError(call, budget, attendance)), budget);\n if (opts?.onPending && !noticed) {\n notice = setTimeout(() => {\n noticed = true;\n try {\n opts.onPending?.({\n call,\n attendance,\n elapsedMs: Date.now() - startedAt,\n ...(attendanceReason(scheme, method) ? { reason: attendanceReason(scheme, method) as string } : {}),\n });\n } catch {\n /* a caller's render callback must never break the stream it describes */\n }\n }, PENDING_NOTICE_MS);\n }\n });\n } finally {\n if (deadline !== undefined) clearTimeout(deadline);\n if (notice !== undefined) clearTimeout(notice);\n wake = null;\n }\n };\n\n try {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted before start');\n transport.send({ type, method, params, msgId, stream: true });\n started = true;\n while (true) {\n if (signal?.aborted) throw new StreamError('aborted', 'stream aborted');\n if (queue.length === 0) {\n // A timeout throws out of here into the `finally`, which sends the host a real\n // cancel frame — so a wedged stream stops generating and BILLING rather than being\n // merely abandoned by the app. Streams can do this because they own their `msgId`.\n await awaitFrame();\n continue;\n }\n sawFrame = true;\n const frame = queue.shift() as StreamFrame;\n if (frame.kind === 'event') {\n yield frame.value as T;\n } else if (frame.kind === 'done') {\n settled = true;\n return frame.value as R;\n } else {\n settled = true;\n throw new StreamError(frame.code, frame.message);\n }\n }\n } finally {\n unsubscribe();\n if (signal) signal.removeEventListener('abort', onAbort);\n // Consumer stopped pulling before a terminal frame (early break/return, or the\n // signal aborted): tell the host to stop the in-flight generation + billing.\n if (started && !settled) transport.cancel?.({ type, msgId, cancel: true });\n }\n}\n\n// The real sandbox transport, built from the bundler messageBus helpers. `cancel`\n// rides the same messageBus as `send` — a `{type, msgId, cancel:true}` frame the\n// host dispatcher routes to the in-flight generator's AbortController.\nconst bundlerTransport: StreamTransport = {\n send: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n subscribe: (type, handler) => addListener(type, (msg) => handler(msg as { msgId?: number; stream?: StreamFrame })),\n cancel: (msg) => sendMessage(msg.type, msg as unknown as Record<string, unknown>),\n};\n\n/**\n * Consume an elevated streaming protocol method from app code.\n *\n * `for await (const ev of protocolStream('protocol-contribute', 'run', [opts])) …`\n *\n * Pass `signal` to abort the stream (and the host's in-flight work) mid-flight.\n */\nexport function protocolStream<T = unknown, R = unknown>(\n protocolName: string,\n method: string,\n params: unknown[],\n signal?: AbortSignal,\n opts?: BoundedCallOptions & { idleTimeoutMs?: number },\n): AsyncGenerator<T, R, void> {\n return consumeStream<T, R>(bundlerTransport, protocolName, method, params, undefined, signal, opts);\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ready.ts"],"sourcesContent":["// The `ir.interactive` boot signal — the app-facing `reportReady()` / `onReady()` /\n// `getReadyState()` surface (LOAD_PROFILING_SPEC §3.1, R3-46). This closes the\n// \"existing SDK boot signal\" that `UI_AS_APPS_SPEC §6.2` referenced but never\n// defined.\n//\n// The runtime marks `ir.interactive` when the app's root render commits. An app\n// whose USEFULLY-interactive moment is later than first commit (e.g. after an\n// initial data load) calls `reportReady()` to DELAY the signal — which, per LP2-3,\n// can only ever push interactive later, never earlier than the commit (the host\n// resolves `max(commit, reportReady)`; see `resolveInteractive`). `onReady` /\n// `getReadyState` expose the report state in the same poll+subscribe shape as\n// `auth` / `mounts`.\n\nimport { sendMessage as defaultSend } from \"./sandboxUtils\";\n\n/** The app's `reportReady()` state, mirrored by {@link onReady}/{@link getReadyState}. */\nexport interface ReadyState {\n /** Whether the app has called `reportReady()`. */\n reported: boolean;\n /** The app-reported timestamp (`performance.now()`), if it has reported. */\n reportedAt?: number;\n}\n\ninterface ReadyDeps {\n send: (type: string, data?: Record<string, unknown>) => void;\n now: () => number;\n}\n\nconst realNow = (): number =>\n typeof performance !== \"undefined\" && typeof performance.now === \"function\"\n ? performance.now()\n : Date.now();\n\nconst defaultDeps: ReadyDeps = { send: defaultSend, now: realNow };\n\nlet deps: ReadyDeps = defaultDeps;\nlet state: ReadyState = { reported: false };\nconst listeners = new Set<(s: ReadyState) => void>();\n\n/**\n * Signal that the app is usefully interactive (e.g. after an initial data load).\n * IDEMPOTENT — only the FIRST call counts; later calls are ignored. Forwards the\n * report to the runtime (`ir-report-ready`) so the host can resolve\n * `ir.interactive = max(rootRenderCommit, reportedAt)` (LP2-3) — calling it before\n * the root render commits can only delay the signal, never advance it.\n *\n * UX contract (LOADING_UX_SPEC §9.1): calling this tells the host *\"keep your\n * loading skeleton up; I am not done yet\"* — the host holds the §3 reveal until\n * this call (or the load budget). Call it ONCE, when the first USEFULLY-interactive\n * frame is on screen — not at mount, and not after every async settle. An app that\n * never calls it reveals automatically at the root-render commit (the default path).\n */\nexport function reportReady(): void {\n if (state.reported) return;\n state = { reported: true, reportedAt: deps.now() };\n try {\n deps.send(\"ir-report-ready\", { at: state.reportedAt });\n } catch {\n /* transport not ready — the runtime still marks interactive at root commit */\n }\n for (const l of listeners) l(state);\n}\n\n/** Pollable snapshot of the report state. */\nexport function getReadyState(): ReadyState {\n return state;\n}\n\n/**\n * Subscribe to the ready signal. Invoked immediately with the current state (so a\n * late subscriber after `reportReady()` still fires) and again whenever it reports.\n * Returns an unsubscribe.\n */\nexport function onReady(listener: (s: ReadyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Test seam: override the transport/clock. */\nexport function __setReadyDeps(d: Partial<ReadyDeps>): void {\n deps = { ...defaultDeps, ...d };\n}\n\n/** Test seam: reset module state between cases. */\nexport function __resetReady(): void {\n deps = defaultDeps;\n state = { reported: false };\n listeners.clear();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,0BAA2C;AAe3C,MAAM,UAAU,MACd,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,aAC7D,YAAY,IAAI,IAChB,KAAK,IAAI;AAEf,MAAM,cAAyB,EAAE,MAAM,oBAAAA,aAAa,KAAK,QAAQ;AAEjE,IAAI,OAAkB;AACtB,IAAI,QAAoB,EAAE,UAAU,MAAM;AAC1C,MAAM,YAAY,oBAAI,IAA6B;AAe5C,SAAS,cAAoB;AAClC,MAAI,MAAM,SAAU;AACpB,UAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,EAAE;AACjD,MAAI;AACF,SAAK,KAAK,mBAAmB,EAAE,IAAI,MAAM,WAAW,CAAC;AAAA,EACvD,QAAQ;AAAA,EAER;AACA,aAAW,KAAK,UAAW,GAAE,KAAK;AACpC;AAGO,SAAS,gBAA4B;AAC1C,SAAO;AACT;AAOO,SAAS,QAAQ,UAA+C;AACrE,YAAU,IAAI,QAAQ;AACtB,WAAS,KAAK;AACd,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAGO,SAAS,eAAe,GAA6B;AAC1D,SAAO,EAAE,GAAG,aAAa,GAAG,EAAE;AAChC;AAGO,SAAS,eAAqB;AACnC,SAAO;AACP,UAAQ,EAAE,UAAU,MAAM;AAC1B,YAAU,MAAM;AAClB;","names":["defaultSend"]}
1
+ {"version":3,"sources":["../src/ready.ts"],"sourcesContent":["// The `ir.interactive` boot signal — the app-facing `reportReady()` / `onReady()` /\n// `getReadyState()` surface (LOAD_PROFILING_SPEC §3.1, R3-46). This closes the\n// \"existing SDK boot signal\" that `UI_AS_APPS_SPEC §6.2` referenced but never\n// defined.\n//\n// The runtime marks `ir.interactive` when the app's root render commits. An app\n// whose USEFULLY-interactive moment is later than first commit (e.g. after an\n// initial data load) calls `reportReady()` to DELAY the signal — which, per LP2-3,\n// can only ever push interactive later, never earlier than the commit (the host\n// resolves `max(commit, reportReady)`; see `resolveInteractive`). `onReady` /\n// `getReadyState` expose the report state in the same poll+subscribe shape as\n// `auth` / `mounts`.\n\nimport { sendMessage as defaultSend } from './sandboxUtils';\n\n/** The app's `reportReady()` state, mirrored by {@link onReady}/{@link getReadyState}. */\nexport interface ReadyState {\n /** Whether the app has called `reportReady()`. */\n reported: boolean;\n /** The app-reported timestamp (`performance.now()`), if it has reported. */\n reportedAt?: number;\n}\n\ninterface ReadyDeps {\n send: (type: string, data?: Record<string, unknown>) => void;\n now: () => number;\n}\n\nconst realNow = (): number =>\n typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();\n\nconst defaultDeps: ReadyDeps = { send: defaultSend, now: realNow };\n\nlet deps: ReadyDeps = defaultDeps;\nlet state: ReadyState = { reported: false };\nconst listeners = new Set<(s: ReadyState) => void>();\n\n/**\n * Signal that the app is usefully interactive (e.g. after an initial data load).\n * IDEMPOTENT — only the FIRST call counts; later calls are ignored. Forwards the\n * report to the runtime (`ir-report-ready`) so the host can resolve\n * `ir.interactive = max(rootRenderCommit, reportedAt)` (LP2-3) — calling it before\n * the root render commits can only delay the signal, never advance it.\n *\n * UX contract (LOADING_UX_SPEC §9.1): calling this tells the host *\"keep your\n * loading skeleton up; I am not done yet\"* — the host holds the §3 reveal until\n * this call (or the load budget). Call it ONCE, when the first USEFULLY-interactive\n * frame is on screen — not at mount, and not after every async settle. An app that\n * never calls it reveals automatically at the root-render commit (the default path).\n */\nexport function reportReady(): void {\n if (state.reported) return;\n state = { reported: true, reportedAt: deps.now() };\n try {\n deps.send('ir-report-ready', { at: state.reportedAt });\n } catch {\n /* transport not ready — the runtime still marks interactive at root commit */\n }\n for (const l of listeners) l(state);\n}\n\n/** Pollable snapshot of the report state. */\nexport function getReadyState(): ReadyState {\n return state;\n}\n\n/**\n * Subscribe to the ready signal. Invoked immediately with the current state (so a\n * late subscriber after `reportReady()` still fires) and again whenever it reports.\n * Returns an unsubscribe.\n */\nexport function onReady(listener: (s: ReadyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Test seam: override the transport/clock. */\nexport function __setReadyDeps(d: Partial<ReadyDeps>): void {\n deps = { ...defaultDeps, ...d };\n}\n\n/** Test seam: reset module state between cases. */\nexport function __resetReady(): void {\n deps = defaultDeps;\n state = { reported: false };\n listeners.clear();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,0BAA2C;AAe3C,MAAM,UAAU,MACd,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,aAAa,YAAY,IAAI,IAAI,KAAK,IAAI;AAE7G,MAAM,cAAyB,EAAE,MAAM,oBAAAA,aAAa,KAAK,QAAQ;AAEjE,IAAI,OAAkB;AACtB,IAAI,QAAoB,EAAE,UAAU,MAAM;AAC1C,MAAM,YAAY,oBAAI,IAA6B;AAe5C,SAAS,cAAoB;AAClC,MAAI,MAAM,SAAU;AACpB,UAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,EAAE;AACjD,MAAI;AACF,SAAK,KAAK,mBAAmB,EAAE,IAAI,MAAM,WAAW,CAAC;AAAA,EACvD,QAAQ;AAAA,EAER;AACA,aAAW,KAAK,UAAW,GAAE,KAAK;AACpC;AAGO,SAAS,gBAA4B;AAC1C,SAAO;AACT;AAOO,SAAS,QAAQ,UAA+C;AACrE,YAAU,IAAI,QAAQ;AACtB,WAAS,KAAK;AACd,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAGO,SAAS,eAAe,GAA6B;AAC1D,SAAO,EAAE,GAAG,aAAa,GAAG,EAAE;AAChC;AAGO,SAAS,eAAqB;AACnC,SAAO;AACP,UAAQ,EAAE,UAAU,MAAM;AAC1B,YAAU,MAAM;AAClB;","names":["defaultSend"]}
package/dist/ready.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ready.ts"],"sourcesContent":["// The `ir.interactive` boot signal — the app-facing `reportReady()` / `onReady()` /\n// `getReadyState()` surface (LOAD_PROFILING_SPEC §3.1, R3-46). This closes the\n// \"existing SDK boot signal\" that `UI_AS_APPS_SPEC §6.2` referenced but never\n// defined.\n//\n// The runtime marks `ir.interactive` when the app's root render commits. An app\n// whose USEFULLY-interactive moment is later than first commit (e.g. after an\n// initial data load) calls `reportReady()` to DELAY the signal — which, per LP2-3,\n// can only ever push interactive later, never earlier than the commit (the host\n// resolves `max(commit, reportReady)`; see `resolveInteractive`). `onReady` /\n// `getReadyState` expose the report state in the same poll+subscribe shape as\n// `auth` / `mounts`.\n\nimport { sendMessage as defaultSend } from \"./sandboxUtils\";\n\n/** The app's `reportReady()` state, mirrored by {@link onReady}/{@link getReadyState}. */\nexport interface ReadyState {\n /** Whether the app has called `reportReady()`. */\n reported: boolean;\n /** The app-reported timestamp (`performance.now()`), if it has reported. */\n reportedAt?: number;\n}\n\ninterface ReadyDeps {\n send: (type: string, data?: Record<string, unknown>) => void;\n now: () => number;\n}\n\nconst realNow = (): number =>\n typeof performance !== \"undefined\" && typeof performance.now === \"function\"\n ? performance.now()\n : Date.now();\n\nconst defaultDeps: ReadyDeps = { send: defaultSend, now: realNow };\n\nlet deps: ReadyDeps = defaultDeps;\nlet state: ReadyState = { reported: false };\nconst listeners = new Set<(s: ReadyState) => void>();\n\n/**\n * Signal that the app is usefully interactive (e.g. after an initial data load).\n * IDEMPOTENT — only the FIRST call counts; later calls are ignored. Forwards the\n * report to the runtime (`ir-report-ready`) so the host can resolve\n * `ir.interactive = max(rootRenderCommit, reportedAt)` (LP2-3) — calling it before\n * the root render commits can only delay the signal, never advance it.\n *\n * UX contract (LOADING_UX_SPEC §9.1): calling this tells the host *\"keep your\n * loading skeleton up; I am not done yet\"* — the host holds the §3 reveal until\n * this call (or the load budget). Call it ONCE, when the first USEFULLY-interactive\n * frame is on screen — not at mount, and not after every async settle. An app that\n * never calls it reveals automatically at the root-render commit (the default path).\n */\nexport function reportReady(): void {\n if (state.reported) return;\n state = { reported: true, reportedAt: deps.now() };\n try {\n deps.send(\"ir-report-ready\", { at: state.reportedAt });\n } catch {\n /* transport not ready — the runtime still marks interactive at root commit */\n }\n for (const l of listeners) l(state);\n}\n\n/** Pollable snapshot of the report state. */\nexport function getReadyState(): ReadyState {\n return state;\n}\n\n/**\n * Subscribe to the ready signal. Invoked immediately with the current state (so a\n * late subscriber after `reportReady()` still fires) and again whenever it reports.\n * Returns an unsubscribe.\n */\nexport function onReady(listener: (s: ReadyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Test seam: override the transport/clock. */\nexport function __setReadyDeps(d: Partial<ReadyDeps>): void {\n deps = { ...defaultDeps, ...d };\n}\n\n/** Test seam: reset module state between cases. */\nexport function __resetReady(): void {\n deps = defaultDeps;\n state = { reported: false };\n listeners.clear();\n}\n"],"mappings":";AAaA,SAAS,eAAe,mBAAmB;AAe3C,MAAM,UAAU,MACd,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,aAC7D,YAAY,IAAI,IAChB,KAAK,IAAI;AAEf,MAAM,cAAyB,EAAE,MAAM,aAAa,KAAK,QAAQ;AAEjE,IAAI,OAAkB;AACtB,IAAI,QAAoB,EAAE,UAAU,MAAM;AAC1C,MAAM,YAAY,oBAAI,IAA6B;AAe5C,SAAS,cAAoB;AAClC,MAAI,MAAM,SAAU;AACpB,UAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,EAAE;AACjD,MAAI;AACF,SAAK,KAAK,mBAAmB,EAAE,IAAI,MAAM,WAAW,CAAC;AAAA,EACvD,QAAQ;AAAA,EAER;AACA,aAAW,KAAK,UAAW,GAAE,KAAK;AACpC;AAGO,SAAS,gBAA4B;AAC1C,SAAO;AACT;AAOO,SAAS,QAAQ,UAA+C;AACrE,YAAU,IAAI,QAAQ;AACtB,WAAS,KAAK;AACd,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAGO,SAAS,eAAe,GAA6B;AAC1D,SAAO,EAAE,GAAG,aAAa,GAAG,EAAE;AAChC;AAGO,SAAS,eAAqB;AACnC,SAAO;AACP,UAAQ,EAAE,UAAU,MAAM;AAC1B,YAAU,MAAM;AAClB;","names":[]}
1
+ {"version":3,"sources":["../src/ready.ts"],"sourcesContent":["// The `ir.interactive` boot signal — the app-facing `reportReady()` / `onReady()` /\n// `getReadyState()` surface (LOAD_PROFILING_SPEC §3.1, R3-46). This closes the\n// \"existing SDK boot signal\" that `UI_AS_APPS_SPEC §6.2` referenced but never\n// defined.\n//\n// The runtime marks `ir.interactive` when the app's root render commits. An app\n// whose USEFULLY-interactive moment is later than first commit (e.g. after an\n// initial data load) calls `reportReady()` to DELAY the signal — which, per LP2-3,\n// can only ever push interactive later, never earlier than the commit (the host\n// resolves `max(commit, reportReady)`; see `resolveInteractive`). `onReady` /\n// `getReadyState` expose the report state in the same poll+subscribe shape as\n// `auth` / `mounts`.\n\nimport { sendMessage as defaultSend } from './sandboxUtils';\n\n/** The app's `reportReady()` state, mirrored by {@link onReady}/{@link getReadyState}. */\nexport interface ReadyState {\n /** Whether the app has called `reportReady()`. */\n reported: boolean;\n /** The app-reported timestamp (`performance.now()`), if it has reported. */\n reportedAt?: number;\n}\n\ninterface ReadyDeps {\n send: (type: string, data?: Record<string, unknown>) => void;\n now: () => number;\n}\n\nconst realNow = (): number =>\n typeof performance !== 'undefined' && typeof performance.now === 'function' ? performance.now() : Date.now();\n\nconst defaultDeps: ReadyDeps = { send: defaultSend, now: realNow };\n\nlet deps: ReadyDeps = defaultDeps;\nlet state: ReadyState = { reported: false };\nconst listeners = new Set<(s: ReadyState) => void>();\n\n/**\n * Signal that the app is usefully interactive (e.g. after an initial data load).\n * IDEMPOTENT — only the FIRST call counts; later calls are ignored. Forwards the\n * report to the runtime (`ir-report-ready`) so the host can resolve\n * `ir.interactive = max(rootRenderCommit, reportedAt)` (LP2-3) — calling it before\n * the root render commits can only delay the signal, never advance it.\n *\n * UX contract (LOADING_UX_SPEC §9.1): calling this tells the host *\"keep your\n * loading skeleton up; I am not done yet\"* — the host holds the §3 reveal until\n * this call (or the load budget). Call it ONCE, when the first USEFULLY-interactive\n * frame is on screen — not at mount, and not after every async settle. An app that\n * never calls it reveals automatically at the root-render commit (the default path).\n */\nexport function reportReady(): void {\n if (state.reported) return;\n state = { reported: true, reportedAt: deps.now() };\n try {\n deps.send('ir-report-ready', { at: state.reportedAt });\n } catch {\n /* transport not ready — the runtime still marks interactive at root commit */\n }\n for (const l of listeners) l(state);\n}\n\n/** Pollable snapshot of the report state. */\nexport function getReadyState(): ReadyState {\n return state;\n}\n\n/**\n * Subscribe to the ready signal. Invoked immediately with the current state (so a\n * late subscriber after `reportReady()` still fires) and again whenever it reports.\n * Returns an unsubscribe.\n */\nexport function onReady(listener: (s: ReadyState) => void): () => void {\n listeners.add(listener);\n listener(state);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Test seam: override the transport/clock. */\nexport function __setReadyDeps(d: Partial<ReadyDeps>): void {\n deps = { ...defaultDeps, ...d };\n}\n\n/** Test seam: reset module state between cases. */\nexport function __resetReady(): void {\n deps = defaultDeps;\n state = { reported: false };\n listeners.clear();\n}\n"],"mappings":";AAaA,SAAS,eAAe,mBAAmB;AAe3C,MAAM,UAAU,MACd,OAAO,gBAAgB,eAAe,OAAO,YAAY,QAAQ,aAAa,YAAY,IAAI,IAAI,KAAK,IAAI;AAE7G,MAAM,cAAyB,EAAE,MAAM,aAAa,KAAK,QAAQ;AAEjE,IAAI,OAAkB;AACtB,IAAI,QAAoB,EAAE,UAAU,MAAM;AAC1C,MAAM,YAAY,oBAAI,IAA6B;AAe5C,SAAS,cAAoB;AAClC,MAAI,MAAM,SAAU;AACpB,UAAQ,EAAE,UAAU,MAAM,YAAY,KAAK,IAAI,EAAE;AACjD,MAAI;AACF,SAAK,KAAK,mBAAmB,EAAE,IAAI,MAAM,WAAW,CAAC;AAAA,EACvD,QAAQ;AAAA,EAER;AACA,aAAW,KAAK,UAAW,GAAE,KAAK;AACpC;AAGO,SAAS,gBAA4B;AAC1C,SAAO;AACT;AAOO,SAAS,QAAQ,UAA+C;AACrE,YAAU,IAAI,QAAQ;AACtB,WAAS,KAAK;AACd,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAGO,SAAS,eAAe,GAA6B;AAC1D,SAAO,EAAE,GAAG,aAAa,GAAG,EAAE;AAChC;AAGO,SAAS,eAAqB;AACnC,SAAO;AACP,UAAQ,EAAE,UAAU,MAAM;AAC1B,YAAU,MAAM;AAClB;","names":[]}
package/dist/routing.cjs CHANGED
@@ -65,7 +65,9 @@ const renderRoute = (routingRule, params) => {
65
65
  };
66
66
  const Router = () => {
67
67
  const context = (0, import_react.useContext)(import_TinkerableContext.TinkerableContext);
68
- const { navigationState: { routingRule, pathParameters } } = context;
68
+ const {
69
+ navigationState: { routingRule, pathParameters }
70
+ } = context;
69
71
  if (!routingRule) {
70
72
  throw new Error(`No route registered for path ${context.navigationState.sandboxPath}!`);
71
73
  }