oysterun 1.6.0-beta.2 → 1.6.0-beta.21

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 (242) hide show
  1. package/bin/oysterun.mjs +64 -17
  2. package/dev/client/connectivity/completed-message-neighbor-click.mjs +78 -0
  3. package/dev/client/connectivity/completed-message-neighbor-contract.mjs +205 -0
  4. package/dev/client/connectivity/completed-message-visibility.d.mts +4 -4
  5. package/dev/client/connectivity/completed-message-visibility.mjs +14 -11
  6. package/dev/client/connectivity/desktop-chat-sidebar-preference.mjs +57 -0
  7. package/dev/client/connectivity/host-connection-feedback.mjs +12 -1
  8. package/dev/client/connectivity/managed-host-browser.mjs +10 -0
  9. package/dev/client/connectivity/session-navigation-intent.mjs +189 -0
  10. package/dev/client/connectivity/session-transition-state.mjs +18 -6
  11. package/dev/client/connectivity/test-completed-message-neighbor-click.mjs +180 -0
  12. package/dev/client/web/index.html +5367 -609
  13. package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js → ReactPrism-C6YiTZTC.js} +1 -1
  14. package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js.map → ReactPrism-C6YiTZTC.js.map} +1 -1
  15. package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js +6442 -0
  16. package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js.map +1 -0
  17. package/dev/client/web-chat/dist/assets/SessionsSetupApp-Dx09Uz-6.css +1 -0
  18. package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js +9 -0
  19. package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js.map +1 -0
  20. package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js +2 -0
  21. package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js.map +1 -0
  22. package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js → index-DCQFwgGf.js} +2 -2
  23. package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js.map → index-DCQFwgGf.js.map} +1 -1
  24. package/dev/client/web-chat/dist/assets/matrix-entry-BV7G38kH.css +10 -0
  25. package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js +594 -0
  26. package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js.map +1 -0
  27. package/dev/client/web-chat/dist/assets/{pdf-ZjtalfgI.js → pdf-DN7oVvM6.js} +5 -5
  28. package/dev/client/web-chat/dist/assets/pdf-DN7oVvM6.js.map +1 -0
  29. package/dev/client/web-chat/dist/assets/session-notice-DlyODy1z.css +1 -0
  30. package/dev/client/web-chat/dist/assets/web-BlYuDlOV.js +1 -0
  31. package/dev/client/web-chat/dist/assets/{web-c70YaskK.js.map → web-BlYuDlOV.js.map} +1 -1
  32. package/dev/client/web-chat/dist/assets/web-Dabw6cJi.js +1 -0
  33. package/dev/client/web-chat/dist/assets/{web-BzHcZK2M.js.map → web-Dabw6cJi.js.map} +1 -1
  34. package/dev/client/web-chat/dist/index.html +1 -2
  35. package/docs/routec/dev/cloud_deplyments.md +5 -0
  36. package/docs/routec/dev/npm_native_dependency_install.md +6 -2
  37. package/docs/routec/dev/npmjs_publish.md +55 -19
  38. package/docs/routec/dev/p262_setup_input_file.md +117 -0
  39. package/docs/routec/host2_turn_started_idempotency_diagnostics.md +8 -7
  40. package/docs/routec/host_setup.md +39 -10
  41. package/docs/routec/oysterun_operation_interface_spec.md +78 -2
  42. package/docs/routec/provider_contract.md +35 -5
  43. package/host-service/README.md +36 -0
  44. package/host-service/adapters/claude-code-adapter.mjs +48 -5
  45. package/host-service/adapters/codex-app-server-adapter.mjs +320 -18
  46. package/host-service/adapters/debug-fixture-provider-adapter.mjs +102 -1
  47. package/host-service/adapters/debug-large-tool-spillover-provider-adapter.mjs +2 -0
  48. package/host-service/adapters/debug-p135-codex-replay-provider-adapter.mjs +2 -0
  49. package/host-service/adapters/debug-routec-structural-replay-provider-adapter.mjs +2 -0
  50. package/host-service/agent-config.mjs +16 -5
  51. package/host-service/agent-folder-availability.mjs +41 -0
  52. package/host-service/apns-complete-message-dispatcher.mjs +1 -0
  53. package/host-service/assets/product-skills/Oysterun/modules/oysterun-find-context/SKILL.md +24 -0
  54. package/host-service/assets/product-skills/Oysterun/modules/oysterun-session-chat/SKILL.md +94 -5
  55. package/host-service/assets/product-skills/Oysterun/modules/oysterun-sessions/SKILL.md +103 -0
  56. package/host-service/assets/product-skills/Oysterun/modules/oysterun-website/SKILL.md +7 -0
  57. package/host-service/assets/product-skills/oysterun-find-context/SKILL.md +24 -0
  58. package/host-service/assets/product-skills/oysterun-session-chat/SKILL.md +94 -5
  59. package/host-service/assets/product-skills/oysterun-sessions/SKILL.md +103 -0
  60. package/host-service/assets/product-skills/oysterun-website/SKILL.md +7 -0
  61. package/host-service/callback-durable-store.mjs +354 -47
  62. package/host-service/callback-foundation.mjs +21 -1
  63. package/host-service/callback-host-integration.mjs +345 -30
  64. package/host-service/claude-acp-model-discovery.mjs +23 -18
  65. package/host-service/cli/assert-installer-reinstall-target.mjs +9 -0
  66. package/host-service/cli/installer-recovery-guidance.mjs +20 -0
  67. package/host-service/cli/prepare-restart-restore.mjs +2 -0
  68. package/host-service/cli/product-cli.mjs +34 -33
  69. package/host-service/cli/reconcile-initial-preflight.mjs +19 -0
  70. package/host-service/cli/recover-installer-lifecycle.mjs +24 -0
  71. package/host-service/cli/run-host-lifecycle.mjs +2 -0
  72. package/host-service/cli/run-staged-lifecycle-recovery.mjs +161 -0
  73. package/host-service/codex-model-discovery.mjs +23 -18
  74. package/host-service/complete-cli-uninstall.mjs +176 -0
  75. package/host-service/complete-message-notification-predicate.mjs +8 -6
  76. package/host-service/complete-message-notification-title.mjs +4 -0
  77. package/host-service/completed-session-unread.mjs +425 -42
  78. package/host-service/completed-turn-navigation.mjs +444 -144
  79. package/host-service/completed-turn-notification-observer.mjs +116 -0
  80. package/host-service/completed-turn-service.mjs +445 -0
  81. package/host-service/config.mjs +115 -9
  82. package/host-service/desired-open-session-state.mjs +331 -0
  83. package/host-service/fixtures/p218-completed-turn-notification.json +11 -0
  84. package/host-service/frp-agent.mjs +9 -9
  85. package/host-service/grok-acp-model-discovery.mjs +31 -33
  86. package/host-service/host-lifecycle-recovery.mjs +37 -0
  87. package/host-service/host-port-resolution.mjs +98 -0
  88. package/host-service/host-runtime-diagnostics.mjs +18 -1
  89. package/host-service/initial-preflight-recovery.mjs +71 -0
  90. package/host-service/installer-recovery-guidance.mjs +85 -0
  91. package/host-service/installer-reinstall-bindings.mjs +49 -0
  92. package/host-service/installer-reinstall-target.mjs +49 -0
  93. package/host-service/legacy-continuity-observation.mjs +33 -0
  94. package/host-service/lifecycle-compat-cli.mjs +13 -0
  95. package/host-service/lifecycle-compat-preload.mjs +89 -0
  96. package/host-service/lifecycle-compatibility.mjs +164 -0
  97. package/host-service/lifecycle-continuity-assets.mjs +222 -0
  98. package/host-service/lifecycle-continuity-probe.mjs +97 -0
  99. package/host-service/lifecycle-continuity-projection.mjs +29 -0
  100. package/host-service/lifecycle-continuity-runner.mjs +192 -0
  101. package/host-service/lifecycle-continuity.mjs +493 -0
  102. package/host-service/lifecycle-service-render-bundle.mjs +120 -0
  103. package/host-service/lifecycle-user-path.mjs +34 -0
  104. package/host-service/link-annotations.mjs +10 -0
  105. package/host-service/matrix-credential-issuer.mjs +32 -1
  106. package/host-service/matrix-event-correlator.mjs +166 -27
  107. package/host-service/matrix-facade.mjs +96 -19
  108. package/host-service/matrix-room-binding.mjs +24 -0
  109. package/host-service/matrix-transcript-read-adapter.mjs +17 -3
  110. package/host-service/model-discovery-guardian.mjs +40 -0
  111. package/host-service/model-discovery-process.mjs +141 -0
  112. package/host-service/onboard-lifecycle-status.mjs +28 -0
  113. package/host-service/onboard.mjs +479 -0
  114. package/host-service/package.json +2 -1
  115. package/host-service/portable-scheduler-definitions.mjs +28 -0
  116. package/host-service/product-runtime-host-origin.mjs +22 -0
  117. package/host-service/provider-model-params.mjs +14 -17
  118. package/host-service/provider-model-refresh-runner.mjs +124 -169
  119. package/host-service/provider-model-refresh-schedule.mjs +11 -0
  120. package/host-service/provider-resume-contract.mjs +9 -3
  121. package/host-service/restart-restore-state.mjs +25 -0
  122. package/host-service/retained-host-state-holder.mjs +57 -0
  123. package/host-service/routec-matrix-sqlite-store.mjs +155 -10
  124. package/host-service/routec-matrix-storage-adapter.mjs +208 -25
  125. package/host-service/routec-matrix-storage-paths.mjs +12 -0
  126. package/host-service/routec-request-delay-window.mjs +55 -0
  127. package/host-service/scheduler-acp-provider-runner.mjs +2 -1
  128. package/host-service/scheduler-direct-provider-runner.mjs +71 -9
  129. package/host-service/scheduler-run-timeout-policy.mjs +85 -0
  130. package/host-service/scheduler-service.mjs +96 -9
  131. package/host-service/scheduler-store.mjs +9 -2
  132. package/host-service/server.mjs +2202 -552
  133. package/host-service/service-definition-migration.mjs +10 -3
  134. package/host-service/session-collection-service.mjs +320 -0
  135. package/host-service/session-history.mjs +99 -15
  136. package/host-service/session-list-preview-service.mjs +87 -12
  137. package/host-service/session-list-projection-service.mjs +182 -0
  138. package/host-service/session-loop-definitions.mjs +702 -75
  139. package/host-service/session-manager.mjs +473 -38
  140. package/host-service/session-switcher-projection.mjs +11 -3
  141. package/host-service/session-tags.mjs +451 -0
  142. package/host-service/session-transcript-target.mjs +103 -0
  143. package/host-service/setup-input-file.mjs +91 -0
  144. package/host-service/setup-provider-model-defaults.mjs +53 -0
  145. package/host-service/setup.mjs +133 -101
  146. package/host-service/stopped-user-host-package.mjs +60 -0
  147. package/host-service/test-fixtures/p236-navigation-original.mjs +500 -0
  148. package/host-service/update-reminder-state.mjs +7 -0
  149. package/host-service/voice-input-receipt-store.mjs +60 -0
  150. package/package.json +1 -1
  151. package/static/oysterun-social-preview.png +0 -0
  152. package/static/site/assets/oysterun-social-preview.png +0 -0
  153. package/static/site/blog/en/index.html +17 -0
  154. package/static/site/blog/generated_manifest.json +9 -9
  155. package/static/site/blog/index.html +18 -0
  156. package/static/site/blog/voice-input-model-experiment/en/index.html +17 -0
  157. package/static/site/blog/voice-input-model-experiment/zh/index.html +17 -0
  158. package/static/site/blog/zh/index.html +17 -0
  159. package/static/site/docs/choose-and-recover-a-provider/en/index.html +17 -0
  160. package/static/site/docs/choose-and-recover-a-provider/zh/index.html +17 -0
  161. package/static/site/docs/configure-session-files-and-settings/en/index.html +17 -0
  162. package/static/site/docs/configure-session-files-and-settings/zh/index.html +17 -0
  163. package/static/site/docs/en/index.html +17 -0
  164. package/static/site/docs/generated_manifest.json +60 -60
  165. package/static/site/docs/host-authentication-and-connection/en/index.html +17 -0
  166. package/static/site/docs/host-authentication-and-connection/zh/index.html +17 -0
  167. package/static/site/docs/index.html +18 -0
  168. package/static/site/docs/install-and-use-a-skill/en/index.html +17 -0
  169. package/static/site/docs/install-and-use-a-skill/zh/index.html +17 -0
  170. package/static/site/docs/read-large-tool-results/en/index.html +17 -0
  171. package/static/site/docs/read-large-tool-results/zh/index.html +17 -0
  172. package/static/site/docs/recover-from-a-host-runtime-error/en/index.html +17 -0
  173. package/static/site/docs/recover-from-a-host-runtime-error/zh/index.html +17 -0
  174. package/static/site/docs/restore-work-after-a-host-restart/en/index.html +17 -0
  175. package/static/site/docs/restore-work-after-a-host-restart/zh/index.html +17 -0
  176. package/static/site/docs/send-images-and-media/en/index.html +17 -0
  177. package/static/site/docs/send-images-and-media/zh/index.html +17 -0
  178. package/static/site/docs/set-up-and-understand-a-host/en/index.html +17 -0
  179. package/static/site/docs/set-up-and-understand-a-host/zh/index.html +17 -0
  180. package/static/site/docs/understand-agent-activity/en/index.html +19 -2
  181. package/static/site/docs/understand-agent-activity/en/index.md +2 -2
  182. package/static/site/docs/understand-agent-activity/zh/index.html +19 -2
  183. package/static/site/docs/understand-agent-activity/zh/index.md +2 -2
  184. package/static/site/docs/use-the-oysterun-cli-safely/en/index.html +17 -0
  185. package/static/site/docs/use-the-oysterun-cli-safely/zh/index.html +17 -0
  186. package/static/site/docs/zh/index.html +17 -0
  187. package/static/site/en/index.html +18 -1
  188. package/static/site/en/index.md +1 -1
  189. package/static/site/faq/en/index.html +17 -0
  190. package/static/site/faq/index.html +18 -0
  191. package/static/site/faq/zh/index.html +17 -0
  192. package/static/site/getting-started/en/index.html +23 -1
  193. package/static/site/getting-started/en/index.md +7 -1
  194. package/static/site/getting-started/index.html +18 -0
  195. package/static/site/getting-started/zh/index.html +23 -1
  196. package/static/site/getting-started/zh/index.md +7 -1
  197. package/static/site/index.html +18 -0
  198. package/static/site/install.sh +676 -127
  199. package/static/site/overview/en/index.html +18 -1
  200. package/static/site/overview/en/index.md +2 -2
  201. package/static/site/overview/index.html +18 -0
  202. package/static/site/overview/zh/index.html +18 -1
  203. package/static/site/overview/zh/index.md +2 -2
  204. package/static/site/privacy/en/index.html +17 -0
  205. package/static/site/privacy/index.html +18 -0
  206. package/static/site/privacy/zh/index.html +17 -0
  207. package/static/site/zh/index.html +18 -1
  208. package/static/site/zh/index.md +1 -1
  209. package/static/site_source/content/tutorials/published/understand-agent-activity/en/article.html +2 -2
  210. package/static/site_source/content/tutorials/published/understand-agent-activity/zh/article.html +2 -2
  211. package/static/site_source/downloads/install.sh +676 -127
  212. package/static/site_source/manifests/host_install.json +2 -10
  213. package/static/site_source/pages/getting-started/en.html +6 -1
  214. package/static/site_source/pages/getting-started/zh.html +6 -1
  215. package/static/site_source/pages/home/en.html +1 -1
  216. package/static/site_source/pages/home/zh.html +1 -1
  217. package/static/site_source/pages/home_main.html +3 -2
  218. package/static/site_source/pages/overview/en.html +1 -1
  219. package/static/site_source/pages/overview/zh.html +1 -1
  220. package/tool_scripts/install_oysterun_update.mjs +78 -13
  221. package/tool_scripts/oysterun_continuity_transaction.mjs +438 -0
  222. package/tool_scripts/oysterun_npm_package_manifest.mjs +32 -0
  223. package/tool_scripts/oysterun_release_service.sh +5 -0
  224. package/tool_scripts/oysterun_service_common.sh +59 -24
  225. package/tool_scripts/oysterun_setup_input.sh +97 -0
  226. package/tool_scripts/oysterun_update_restart_runner.mjs +19 -3
  227. package/tool_scripts/p233_legacy_bootstrap.mjs +254 -0
  228. package/tool_scripts/resolve_user_host_port.mjs +15 -0
  229. package/tool_scripts/restart_oysterun.sh +8 -2
  230. package/tool_scripts/stack_dashboard_credentials.mjs +2 -1
  231. package/tool_scripts/status_oysterun_launch_agents.sh +13 -1
  232. package/tool_scripts/sync_host_stack_config.mjs +9 -6
  233. package/tool_scripts/test_environment_remote_cloud_deprovision.mjs +419 -0
  234. package/tool_scripts/uninstall_oysterun_launch_agents.sh +3 -5
  235. package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js +0 -129
  236. package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js.map +0 -1
  237. package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js +0 -2
  238. package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js.map +0 -1
  239. package/dev/client/web-chat/dist/assets/index-NYnaSGBb.css +0 -1
  240. package/dev/client/web-chat/dist/assets/pdf-ZjtalfgI.js.map +0 -1
  241. package/dev/client/web-chat/dist/assets/web-BzHcZK2M.js +0 -1
  242. package/dev/client/web-chat/dist/assets/web-c70YaskK.js +0 -1
@@ -0,0 +1,189 @@
1
+ export const SESSION_CHAT_NAVIGATION_OUTCOMES = Object.freeze({
2
+ NOOP_EQUIVALENT: "noop_equivalent",
3
+ SAME_SESSION_CONTEXT_NAVIGATION: "same_session_context_navigation",
4
+ CROSS_SESSION_TRANSITION: "cross_session_transition",
5
+ DOCUMENT_HANDOFF: "document_handoff",
6
+ INVALID_OR_STALE: "invalid_or_stale",
7
+ });
8
+
9
+ const SESSION_APP_PREFIX = "/app/sessions";
10
+ const DEFAULT_PAGE = "chat";
11
+ const SUPPORTED_SESSION_PAGES = new Set([
12
+ "chat",
13
+ "profile",
14
+ "loop",
15
+ "explorer",
16
+ "file-preview",
17
+ ]);
18
+ const SEMANTIC_QUERY_KEYS = new Set(["focus_event_id"]);
19
+
20
+ function normalizedText(value) {
21
+ return typeof value === "string" ? value.trim() : "";
22
+ }
23
+
24
+ function decodePathPart(value) {
25
+ try {
26
+ return normalizedText(decodeURIComponent(value));
27
+ } catch {
28
+ return "";
29
+ }
30
+ }
31
+
32
+ export function normalizeSessionChatNavigationIntent(input = {}) {
33
+ const sessionId = normalizedText(input.sessionId);
34
+ const page = normalizedText(input.page) || DEFAULT_PAGE;
35
+ const source = normalizedText(input.source) || "unknown";
36
+ const historyPolicy = normalizedText(input.historyPolicy) || "push";
37
+ const documentHandoff = input.documentHandoff === true;
38
+ if (documentHandoff) {
39
+ return {
40
+ valid: true,
41
+ documentHandoff: true,
42
+ sessionId,
43
+ page,
44
+ semantic: {},
45
+ explicitSemanticKeys: [],
46
+ source,
47
+ historyPolicy,
48
+ };
49
+ }
50
+ if (!sessionId || !SUPPORTED_SESSION_PAGES.has(page)) {
51
+ return {
52
+ valid: false,
53
+ documentHandoff: false,
54
+ sessionId,
55
+ page,
56
+ semantic: {},
57
+ explicitSemanticKeys: [],
58
+ source,
59
+ historyPolicy,
60
+ };
61
+ }
62
+
63
+ const semantic = {};
64
+ const explicitSemanticKeys = [];
65
+ const semanticInput =
66
+ input.semantic && typeof input.semantic === "object" ? input.semantic : {};
67
+ for (const key of SEMANTIC_QUERY_KEYS) {
68
+ const value = normalizedText(semanticInput[key]);
69
+ if (!value) continue;
70
+ semantic[key] = value;
71
+ explicitSemanticKeys.push(key);
72
+ }
73
+ return {
74
+ valid: true,
75
+ documentHandoff: false,
76
+ sessionId,
77
+ page,
78
+ semantic,
79
+ explicitSemanticKeys,
80
+ source,
81
+ historyPolicy,
82
+ };
83
+ }
84
+
85
+ export function parseSessionChatNavigationIntent(
86
+ urlValue,
87
+ { source = "route", historyPolicy = "push", documentHandoff = false } = {}
88
+ ) {
89
+ let url;
90
+ try {
91
+ url = new URL(urlValue, "http://oysterun.invalid");
92
+ } catch {
93
+ return normalizeSessionChatNavigationIntent({ source, historyPolicy });
94
+ }
95
+ const parts = url.pathname.split("/").filter(Boolean);
96
+ if (parts.length !== 4 || parts[0] !== "app" || parts[1] !== "sessions") {
97
+ return normalizeSessionChatNavigationIntent({
98
+ source,
99
+ historyPolicy,
100
+ documentHandoff,
101
+ });
102
+ }
103
+ const sessionId = decodePathPart(parts[2]);
104
+ const page = normalizedText(parts[3]);
105
+ const semantic = {};
106
+ for (const key of SEMANTIC_QUERY_KEYS) {
107
+ const value = normalizedText(url.searchParams.get(key));
108
+ if (value) semantic[key] = value;
109
+ }
110
+ return normalizeSessionChatNavigationIntent({
111
+ sessionId,
112
+ page,
113
+ semantic,
114
+ source,
115
+ historyPolicy,
116
+ documentHandoff,
117
+ });
118
+ }
119
+
120
+ export function classifySessionChatNavigation({ committed, target } = {}) {
121
+ if (!committed?.valid || !target?.valid) {
122
+ return SESSION_CHAT_NAVIGATION_OUTCOMES.INVALID_OR_STALE;
123
+ }
124
+ if (committed.documentHandoff || target.documentHandoff) {
125
+ return SESSION_CHAT_NAVIGATION_OUTCOMES.DOCUMENT_HANDOFF;
126
+ }
127
+ if (committed.sessionId !== target.sessionId) {
128
+ return SESSION_CHAT_NAVIGATION_OUTCOMES.CROSS_SESSION_TRANSITION;
129
+ }
130
+ if (committed.page !== target.page) {
131
+ return SESSION_CHAT_NAVIGATION_OUTCOMES.SAME_SESSION_CONTEXT_NAVIGATION;
132
+ }
133
+ for (const key of target.explicitSemanticKeys) {
134
+ if (target.semantic[key] !== committed.semantic[key]) {
135
+ return SESSION_CHAT_NAVIGATION_OUTCOMES.SAME_SESSION_CONTEXT_NAVIGATION;
136
+ }
137
+ }
138
+ return SESSION_CHAT_NAVIGATION_OUTCOMES.NOOP_EQUIVALENT;
139
+ }
140
+
141
+ export function classifySessionChatNavigationFromUrls({
142
+ committedSessionId,
143
+ currentUrl,
144
+ targetUrl,
145
+ source = "route",
146
+ historyPolicy = "push",
147
+ documentHandoff = false,
148
+ } = {}) {
149
+ const committedFromUrl = parseSessionChatNavigationIntent(currentUrl, {
150
+ source: "committed_binding",
151
+ historyPolicy: "preserve",
152
+ });
153
+ const committed = normalizeSessionChatNavigationIntent({
154
+ ...committedFromUrl,
155
+ sessionId: normalizedText(committedSessionId),
156
+ });
157
+ const target = parseSessionChatNavigationIntent(targetUrl, {
158
+ source,
159
+ historyPolicy,
160
+ documentHandoff,
161
+ });
162
+ return classifySessionChatNavigation({ committed, target });
163
+ }
164
+
165
+ export function buildNormalizedSessionChatPath(
166
+ sessionId,
167
+ { focusEventId } = {}
168
+ ) {
169
+ const intent = normalizeSessionChatNavigationIntent({
170
+ sessionId,
171
+ page: DEFAULT_PAGE,
172
+ semantic: { focus_event_id: focusEventId },
173
+ source: "route_builder",
174
+ historyPolicy: "push",
175
+ });
176
+ if (!intent.valid) {
177
+ throw new Error("Session Chat path requires a valid Session id.");
178
+ }
179
+ const url = new URL(
180
+ `${SESSION_APP_PREFIX}/${encodeURIComponent(intent.sessionId)}/${
181
+ intent.page
182
+ }`,
183
+ "http://oysterun.invalid"
184
+ );
185
+ if (intent.semantic.focus_event_id) {
186
+ url.searchParams.set("focus_event_id", intent.semantic.focus_event_id);
187
+ }
188
+ return `${url.pathname}${url.search}`;
189
+ }
@@ -1,3 +1,9 @@
1
+ import {
2
+ buildNormalizedSessionChatPath,
3
+ classifySessionChatNavigationFromUrls,
4
+ SESSION_CHAT_NAVIGATION_OUTCOMES,
5
+ } from "./session-navigation-intent.mjs";
6
+
1
7
  export const OYSTERUN_SESSION_TRANSITION_SCHEMA =
2
8
  "routec.p217_session_transition.v1";
3
9
  export const OYSTERUN_SESSION_TRANSITION_VERIFICATION_DELAY_PARAM =
@@ -89,18 +95,24 @@ export function sessionIdFromCleanChatPath(pathname) {
89
95
  }
90
96
 
91
97
  export function cleanSessionChatPath(sessionId) {
92
- const normalized = String(sessionId || "").trim();
93
- if (!normalized)
94
- throw new Error("Session transition path requires a Session id.");
95
- return `/app/sessions/${encodeURIComponent(normalized)}/chat`;
98
+ return buildNormalizedSessionChatPath(sessionId);
96
99
  }
97
100
 
98
101
  export function shouldBlockSessionHistoryTransition({
99
102
  committedSessionId,
100
103
  nextPathname,
104
+ currentPathname = cleanSessionChatPath(committedSessionId),
105
+ currentSearch = "",
106
+ nextSearch = "",
101
107
  }) {
102
- const nextSessionId = sessionIdFromCleanChatPath(nextPathname);
103
- return Boolean(nextSessionId && nextSessionId !== committedSessionId);
108
+ const outcome = classifySessionChatNavigationFromUrls({
109
+ committedSessionId,
110
+ currentUrl: `${currentPathname}${currentSearch}`,
111
+ targetUrl: `${nextPathname}${nextSearch}`,
112
+ source: "browser_history",
113
+ historyPolicy: "pop",
114
+ });
115
+ return outcome === SESSION_CHAT_NAVIGATION_OUTCOMES.CROSS_SESSION_TRANSITION;
104
116
  }
105
117
 
106
118
  export function sessionTransitionVerificationDelayMs({ hostname, search }) {
@@ -0,0 +1,180 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { test } from "node:test";
5
+
6
+ import { createCompletedMessageNeighborClickCoordinator } from "./completed-message-neighbor-click.mjs";
7
+
8
+ const response = (previous, next) => ({ previous, next });
9
+ const selectTarget = (value, direction) => value[direction]?.event_id;
10
+
11
+ function deferred() {
12
+ let resolve;
13
+ let reject;
14
+ const promise = new Promise((yes, no) => {
15
+ resolve = yes;
16
+ reject = no;
17
+ });
18
+ return { promise, resolve, reject };
19
+ }
20
+
21
+ test("mount, refresh, anchor and 100 local focus/scroll changes do not read", async () => {
22
+ let reads = 0;
23
+ const coordinator = createCompletedMessageNeighborClickCoordinator({
24
+ readNeighbors: async () => {
25
+ reads += 1;
26
+ return response(null, { event_id: "$next" });
27
+ },
28
+ selectTarget,
29
+ });
30
+ coordinator.setScope("session:room");
31
+ for (const signal of ["mount", "anchor-change", "session-data-refresh-after-five-seconds"]) {
32
+ assert.ok(signal);
33
+ assert.equal(coordinator.isLoading(), false);
34
+ }
35
+ for (let index = 0; index < 100; index += 1) {
36
+ // These represent timeline-local anchor/focus/scroll changes. The
37
+ // coordinator intentionally exposes no background-read operation.
38
+ assert.equal(coordinator.isLoading(), false);
39
+ }
40
+ assert.equal(reads, 0);
41
+
42
+ const result = await coordinator.click({
43
+ scope: "session:room",
44
+ roomId: "room",
45
+ direction: "next",
46
+ anchorPosition: "latest",
47
+ });
48
+ assert.equal(result.status, "target");
49
+ assert.equal(result.targetEventId, "$next");
50
+ assert.equal(reads, 1);
51
+ });
52
+
53
+ test("each direction uses exactly one request and preserves click-time anchor", async () => {
54
+ const requests = [];
55
+ const coordinator = createCompletedMessageNeighborClickCoordinator({
56
+ readNeighbors: async (request) => {
57
+ requests.push(request);
58
+ return response({ event_id: "$previous" }, { event_id: "$next" });
59
+ },
60
+ selectTarget,
61
+ });
62
+ coordinator.setScope("session:room");
63
+ const previous = await coordinator.click({
64
+ scope: "session:room",
65
+ roomId: "room",
66
+ direction: "previous",
67
+ anchorEventId: "$visible-anchor",
68
+ });
69
+ const next = await coordinator.click({
70
+ scope: "session:room",
71
+ roomId: "room",
72
+ direction: "next",
73
+ anchorPosition: "latest",
74
+ });
75
+ assert.deepEqual(
76
+ [previous.status, previous.targetEventId, next.status, next.targetEventId],
77
+ ["target", "$previous", "target", "$next"]
78
+ );
79
+ assert.deepEqual(requests, [
80
+ {
81
+ roomId: "room",
82
+ anchorEventId: "$visible-anchor",
83
+ anchorPosition: undefined,
84
+ },
85
+ { roomId: "room", anchorEventId: undefined, anchorPosition: "latest" },
86
+ ]);
87
+ });
88
+
89
+ test("double click is rejected while the first transaction owns its response", async () => {
90
+ const pending = deferred();
91
+ let reads = 0;
92
+ const coordinator = createCompletedMessageNeighborClickCoordinator({
93
+ readNeighbors: () => {
94
+ reads += 1;
95
+ return pending.promise;
96
+ },
97
+ selectTarget,
98
+ });
99
+ coordinator.setScope("session:room");
100
+ const first = coordinator.click({
101
+ scope: "session:room",
102
+ roomId: "room",
103
+ direction: "next",
104
+ anchorPosition: "latest",
105
+ });
106
+ const second = await coordinator.click({
107
+ scope: "session:room",
108
+ roomId: "room",
109
+ direction: "next",
110
+ anchorPosition: "latest",
111
+ });
112
+ assert.equal(second.status, "busy");
113
+ assert.equal(reads, 1);
114
+ pending.resolve(response(null, { event_id: "$fixed" }));
115
+ assert.equal((await first).targetEventId, "$fixed");
116
+ });
117
+
118
+ test("no-result and error release the next click", async () => {
119
+ const replies = [
120
+ response(null, null),
121
+ new Error("offline"),
122
+ response(null, { event_id: "$new" }),
123
+ ];
124
+ let reads = 0;
125
+ const coordinator = createCompletedMessageNeighborClickCoordinator({
126
+ readNeighbors: async () => {
127
+ const reply = replies[reads++];
128
+ if (reply instanceof Error) throw reply;
129
+ return reply;
130
+ },
131
+ selectTarget,
132
+ });
133
+ coordinator.setScope("session:room");
134
+ const click = () =>
135
+ coordinator.click({
136
+ scope: "session:room",
137
+ roomId: "room",
138
+ direction: "next",
139
+ anchorPosition: "latest",
140
+ });
141
+ assert.equal((await click()).status, "no_target");
142
+ assert.equal((await click()).status, "error");
143
+ const recovered = await click();
144
+ assert.equal(recovered.status, "target");
145
+ assert.equal(recovered.targetEventId, "$new");
146
+ assert.equal(reads, 3);
147
+ });
148
+
149
+ test("scope change rejects a late response and never substitutes a new target", async () => {
150
+ const pending = deferred();
151
+ const coordinator = createCompletedMessageNeighborClickCoordinator({
152
+ readNeighbors: () => pending.promise,
153
+ selectTarget,
154
+ });
155
+ coordinator.setScope("old-session:old-room");
156
+ const oldClick = coordinator.click({
157
+ scope: "old-session:old-room",
158
+ roomId: "old-room",
159
+ direction: "next",
160
+ anchorPosition: "latest",
161
+ });
162
+ coordinator.setScope("new-session:new-room");
163
+ pending.resolve(response(null, { event_id: "$old-target" }));
164
+ assert.equal((await oldClick).status, "stale");
165
+ });
166
+
167
+ test("RoomTimeline keeps the established focus and cold-load paths", () => {
168
+ const clientRoot = join(import.meta.dirname, "..");
169
+ const source = readFileSync(
170
+ join(clientRoot, "web-chat/src/app/features/room/RoomTimeline.tsx"),
171
+ "utf8"
172
+ );
173
+ assert.match(source, /requestOysterunActiveRoomTimelineFocus\(\{/);
174
+ assert.match(source, /void handleOpenEvent\(targetEventId\)/);
175
+ assert.match(source, /loadEventTimeline\(evtId\)/);
176
+ assert.match(source, /getOysterunHostSessionChatFocusPath\(targetEventId\)/);
177
+ assert.doesNotMatch(source, /debouncedRequestRouteCHostOwnerNeighbors/);
178
+ assert.doesNotMatch(source, /routeCHostOwnerNeighborPendingRequestRef/);
179
+ assert.doesNotMatch(source, /routeCHostOwnerNeighborInvalidationSeq/);
180
+ });