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,500 @@
1
+ // Test-only frozen oracle from 9fcfdebd9eab499b5aaabe3829acb9c206d33c8a; only the two relative import paths changed.
2
+ import {
3
+ normalizeCommittedProviderOutputMatrixNotificationCandidate,
4
+ normalizeProviderCompletionReleaseMarker,
5
+ } from "../complete-message-notification-predicate.mjs";
6
+ import {
7
+ MATRIX_COMPLETED_TURN_CANDIDATE_INDEX,
8
+ MATRIX_COMPLETED_TURN_COMPLETION_INDEX,
9
+ MATRIX_COMPLETED_TURN_NAVIGATION_INDEX_VERSION,
10
+ assertSchemaCompatible,
11
+ } from "../routec-matrix-sqlite-store.mjs";
12
+
13
+ const SEMANTIC_NAMESPACE = "org.oysterun.semantic.v1";
14
+ const NAVIGATION_SCHEMA_VERSION =
15
+ "routec.p206_completed_turn_final_message_neighbors.v1";
16
+
17
+ export const P203_CALLBACK_COMPLETED_TURN_PROJECTION =
18
+ "p203.exact_final_committed_assistant.v1";
19
+
20
+ function parseJson(value) {
21
+ if (typeof value !== "string") return {};
22
+ const parsed = JSON.parse(value);
23
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
24
+ ? parsed
25
+ : {};
26
+ }
27
+
28
+ function semanticPayload(content) {
29
+ const payload = content?.[SEMANTIC_NAMESPACE];
30
+ return payload && typeof payload === "object" && !Array.isArray(payload)
31
+ ? payload
32
+ : {};
33
+ }
34
+
35
+ export function projectP203CallbackCandidate(
36
+ event,
37
+ { targetSessionId, targetProviderTurnId } = {}
38
+ ) {
39
+ const normalized = normalizeCommittedProviderOutputMatrixNotificationCandidate(
40
+ event
41
+ );
42
+ if (!normalized.accepted) {
43
+ return { accepted: false, disposition: normalized.reason };
44
+ }
45
+ const candidate = normalized.candidate;
46
+ if (candidate.hostSessionId !== targetSessionId) {
47
+ return { accepted: false, disposition: "target_session_mismatch" };
48
+ }
49
+ if (candidate.providerTurnId !== targetProviderTurnId) {
50
+ return { accepted: false, disposition: "provider_turn_mismatch" };
51
+ }
52
+ if (candidate.semanticType !== "message.assistant") {
53
+ return {
54
+ accepted: false,
55
+ disposition: "semantic_type_not_callback_success",
56
+ };
57
+ }
58
+ const body = typeof event?.contentBody === "string" ? event.contentBody : "";
59
+ if (!body) {
60
+ return { accepted: false, disposition: "assistant_body_missing" };
61
+ }
62
+ return {
63
+ accepted: true,
64
+ disposition: "candidate_accepted",
65
+ projection_policy: P203_CALLBACK_COMPLETED_TURN_PROJECTION,
66
+ candidate: {
67
+ matrixEventId: candidate.matrixEventId,
68
+ providerTurnId: candidate.providerTurnId,
69
+ body,
70
+ },
71
+ };
72
+ }
73
+
74
+ export function projectP203CallbackCompletion(
75
+ event,
76
+ { targetSessionId, targetProviderTurnId, targetMatrixRoomId } = {}
77
+ ) {
78
+ const semantic = event?.semanticPayload || {};
79
+ if (
80
+ event?.eventType === "m.room.message" &&
81
+ semantic.semantic_type === "session_lifecycle" &&
82
+ semantic.provider_completion_state === "turn_failed"
83
+ ) {
84
+ if (!event.matrixEventId) {
85
+ return { accepted: false, disposition: "matrix_event_id_missing" };
86
+ }
87
+ if (!targetMatrixRoomId || event.matrixRoomId !== targetMatrixRoomId) {
88
+ return { accepted: false, disposition: "target_room_mismatch" };
89
+ }
90
+ if (semantic.host_session_id !== targetSessionId) {
91
+ return { accepted: false, disposition: "target_session_mismatch" };
92
+ }
93
+ if (
94
+ semantic.provider_turn_id !== targetProviderTurnId ||
95
+ semantic.target_turn_id !== targetProviderTurnId
96
+ ) {
97
+ return { accepted: false, disposition: "provider_turn_mismatch" };
98
+ }
99
+ return {
100
+ accepted: true,
101
+ disposition: "completion_accepted",
102
+ projection_policy: P203_CALLBACK_COMPLETED_TURN_PROJECTION,
103
+ completion: {
104
+ matrixEventId: event.matrixEventId,
105
+ providerTurnId: semantic.provider_turn_id,
106
+ targetOutcome: "failed",
107
+ successful: false,
108
+ status: "failed",
109
+ },
110
+ };
111
+ }
112
+ const normalized = normalizeProviderCompletionReleaseMarker(event);
113
+ if (!normalized.accepted) {
114
+ return { accepted: false, disposition: normalized.reason };
115
+ }
116
+ const release = normalized.release;
117
+ if (release.hostSessionId !== targetSessionId) {
118
+ return { accepted: false, disposition: "target_session_mismatch" };
119
+ }
120
+ if (targetMatrixRoomId && event?.matrixRoomId !== targetMatrixRoomId) {
121
+ return { accepted: false, disposition: "target_room_mismatch" };
122
+ }
123
+ if (release.providerTurnId !== targetProviderTurnId) {
124
+ return { accepted: false, disposition: "provider_turn_mismatch" };
125
+ }
126
+ const status = String(release.status || "").trim().toLowerCase();
127
+ let targetOutcome = "failed";
128
+ if (release.successful) targetOutcome = "completed";
129
+ else if (["interrupted", "interrupt"].includes(status)) {
130
+ targetOutcome = "interrupted";
131
+ } else if (["cancelled", "canceled"].includes(status)) {
132
+ targetOutcome = "cancelled";
133
+ }
134
+ return {
135
+ accepted: true,
136
+ disposition: "completion_accepted",
137
+ projection_policy: P203_CALLBACK_COMPLETED_TURN_PROJECTION,
138
+ completion: {
139
+ matrixEventId: release.matrixEventId,
140
+ providerTurnId: release.providerTurnId,
141
+ targetOutcome,
142
+ successful: release.successful,
143
+ status: release.status,
144
+ },
145
+ };
146
+ }
147
+
148
+ const CANDIDATE_WHERE = `c.room_id = @room_id
149
+ AND c.is_state = 0
150
+ AND c.type = 'm.room.message'
151
+ AND c.semantic_type IN ('message.assistant', 'tool.result', 'tool.failure', 'thinking.reasoning')
152
+ AND json_extract(c.content_json, '$."org.oysterun.semantic.v1".provider_turn_id') IS NOT NULL
153
+ AND json_extract(c.content_json, '$."org.oysterun.semantic.v1".provider_turn_id') <> ''
154
+ AND json_extract(c.content_json, '$."org.oysterun.semantic.v1".provider_completion_marker') =
155
+ 'oysterun_provider_complete_message_notification_v1'
156
+ AND json_extract(c.content_json, '$."org.oysterun.semantic.v1".provider_completion_state') =
157
+ 'pending_turn_completion'
158
+ AND COALESCE(json_extract(c.content_json, '$.delta'), 0) <> 1
159
+ AND COALESCE(json_extract(c.content_json, '$."org.oysterun.semantic.v1".delta'), 0) <> 1`;
160
+
161
+ const COMPLETION_WHERE = `m.room_id = @room_id
162
+ AND m.is_state = 0
163
+ AND m.type = 'm.room.message'
164
+ AND json_extract(m.content_json, '$."org.oysterun.semantic.v1".provider_completion_marker') =
165
+ 'oysterun_provider_complete_message_notification_v1'
166
+ AND json_extract(m.content_json, '$."org.oysterun.semantic.v1".provider_completion_turn_id') IS NOT NULL
167
+ AND json_extract(m.content_json, '$."org.oysterun.semantic.v1".provider_completion_turn_id') <> ''
168
+ AND json_extract(m.content_json, '$."org.oysterun.semantic.v1".provider_completion_state') =
169
+ 'turn_completed'
170
+ AND json_extract(m.content_json, '$."org.oysterun.semantic.v1".provider_completion_success') = 1
171
+ AND LOWER(TRIM(COALESCE(
172
+ json_extract(m.content_json, '$."org.oysterun.semantic.v1".provider_completion_status'),
173
+ 'completed'
174
+ ))) NOT IN ('failed', 'error', 'errored', 'interrupted', 'interrupt', 'canceled', 'cancelled')`;
175
+
176
+ function selectorSql(direction) {
177
+ if (direction !== "previous" && direction !== "next") {
178
+ throw new Error(`Completed-turn navigation direction is invalid: ${direction}`);
179
+ }
180
+ const comparison = direction === "previous" ? "<" : ">";
181
+ const order = direction === "previous" ? "DESC" : "ASC";
182
+ return `WITH candidate_finals AS (
183
+ SELECT
184
+ json_extract(c.content_json, '$."org.oysterun.semantic.v1".provider_turn_id') AS provider_turn_id,
185
+ MAX(c.stream_seq) AS final_seq
186
+ FROM matrix_events AS c
187
+ WHERE ${CANDIDATE_WHERE}
188
+ GROUP BY provider_turn_id
189
+ ), successful_turns AS (
190
+ SELECT
191
+ json_extract(m.content_json, '$."org.oysterun.semantic.v1".provider_completion_turn_id') AS provider_turn_id,
192
+ MAX(m.stream_seq) AS completion_seq
193
+ FROM matrix_events AS m
194
+ WHERE ${COMPLETION_WHERE}
195
+ GROUP BY provider_turn_id
196
+ )
197
+ SELECT c.event_id,
198
+ c.room_id,
199
+ c.type,
200
+ c.stream_seq,
201
+ c.origin_server_ts,
202
+ c.content_json,
203
+ c.unsigned_json,
204
+ m.event_id AS completion_event_id,
205
+ m.content_json AS completion_content_json,
206
+ m.unsigned_json AS completion_unsigned_json
207
+ FROM candidate_finals AS f
208
+ JOIN successful_turns AS s ON s.provider_turn_id = f.provider_turn_id
209
+ JOIN matrix_events AS c ON c.stream_seq = f.final_seq
210
+ JOIN matrix_events AS m ON m.stream_seq = s.completion_seq
211
+ WHERE f.final_seq ${comparison} @anchor_seq
212
+ ORDER BY f.final_seq ${order}
213
+ LIMIT 1`;
214
+ }
215
+
216
+ function exactCandidate(row, hostSessionId) {
217
+ const content = parseJson(row.content_json);
218
+ return normalizeCommittedProviderOutputMatrixNotificationCandidate({
219
+ eventType: row.type,
220
+ matrixEventId: row.event_id,
221
+ matrixRoomId: row.room_id,
222
+ hostSessionId,
223
+ contentBody: typeof content.body === "string" ? content.body : "",
224
+ contentDelta: content.delta === true,
225
+ semanticPayload: semanticPayload(content),
226
+ });
227
+ }
228
+
229
+ function exactCompletion(row, hostSessionId) {
230
+ const content = parseJson(row.completion_content_json);
231
+ return normalizeProviderCompletionReleaseMarker({
232
+ eventType: "m.room.message",
233
+ matrixEventId: row.completion_event_id,
234
+ matrixRoomId: row.room_id,
235
+ hostSessionId,
236
+ semanticPayload: semanticPayload(content),
237
+ });
238
+ }
239
+
240
+ function readDirectionalTarget(db, { roomId, hostSessionId, anchorSeq, direction }) {
241
+ const statement = db.prepare(selectorSql(direction));
242
+ let seekSeq = anchorSeq;
243
+ while (true) {
244
+ const row = statement.get({ room_id: roomId, anchor_seq: seekSeq });
245
+ if (!row) return null;
246
+ const candidate = exactCandidate(row, hostSessionId);
247
+ const completion = exactCompletion(row, hostSessionId);
248
+ if (
249
+ candidate.accepted &&
250
+ completion.accepted &&
251
+ completion.release.successful === true &&
252
+ candidate.candidate.providerTurnId === completion.release.providerTurnId
253
+ ) {
254
+ return {
255
+ event_id: row.event_id,
256
+ routec_stream_seq: row.stream_seq,
257
+ origin_server_ts: Number.isFinite(row.origin_server_ts)
258
+ ? row.origin_server_ts
259
+ : null,
260
+ provider_turn_id: candidate.candidate.providerTurnId,
261
+ };
262
+ }
263
+ seekSeq = row.stream_seq;
264
+ }
265
+ }
266
+
267
+ function exactTargetSql() {
268
+ return `WITH candidate_finals AS (
269
+ SELECT
270
+ json_extract(c.content_json, '$."org.oysterun.semantic.v1".provider_turn_id') AS provider_turn_id,
271
+ MAX(c.stream_seq) AS final_seq
272
+ FROM matrix_events AS c
273
+ WHERE ${CANDIDATE_WHERE}
274
+ GROUP BY provider_turn_id
275
+ ), successful_turns AS (
276
+ SELECT
277
+ json_extract(m.content_json, '$."org.oysterun.semantic.v1".provider_completion_turn_id') AS provider_turn_id,
278
+ MAX(m.stream_seq) AS completion_seq
279
+ FROM matrix_events AS m
280
+ WHERE ${COMPLETION_WHERE}
281
+ GROUP BY provider_turn_id
282
+ )
283
+ SELECT c.event_id,
284
+ c.room_id,
285
+ c.type,
286
+ c.stream_seq,
287
+ c.origin_server_ts,
288
+ c.content_json,
289
+ c.unsigned_json,
290
+ m.event_id AS completion_event_id,
291
+ m.content_json AS completion_content_json,
292
+ m.unsigned_json AS completion_unsigned_json
293
+ FROM candidate_finals AS f
294
+ JOIN successful_turns AS s ON s.provider_turn_id = f.provider_turn_id
295
+ JOIN matrix_events AS c ON c.stream_seq = f.final_seq
296
+ JOIN matrix_events AS m ON m.stream_seq = s.completion_seq
297
+ WHERE c.event_id = @event_id
298
+ LIMIT 1`;
299
+ }
300
+
301
+ /**
302
+ * Resolve one exact P206 successfully-completed final-message identity.
303
+ * This deliberately shares the same indexed SQL predicates and the same
304
+ * candidate/release normalizers as completed-message navigation.
305
+ */
306
+ export function readCompletedTurnFinalMessageByEventIdFromSQLite(
307
+ db,
308
+ { roomId, hostSessionId, eventId }
309
+ ) {
310
+ assertSchemaCompatible(db);
311
+ if (!roomId || !hostSessionId || !eventId) return null;
312
+ const row = db.prepare(exactTargetSql()).get({
313
+ room_id: roomId,
314
+ event_id: eventId,
315
+ });
316
+ if (!row) return null;
317
+ const candidate = exactCandidate(row, hostSessionId);
318
+ const completion = exactCompletion(row, hostSessionId);
319
+ if (
320
+ !candidate.accepted ||
321
+ !completion.accepted ||
322
+ completion.release.successful !== true ||
323
+ candidate.candidate.providerTurnId !== completion.release.providerTurnId
324
+ ) {
325
+ return null;
326
+ }
327
+ return {
328
+ event_id: row.event_id,
329
+ routec_stream_seq: row.stream_seq,
330
+ origin_server_ts: Number.isFinite(row.origin_server_ts)
331
+ ? row.origin_server_ts
332
+ : null,
333
+ provider_turn_id: candidate.candidate.providerTurnId,
334
+ };
335
+ }
336
+
337
+ export function readLatestCompletedTurnFinalMessageFromSQLite(
338
+ db,
339
+ { roomId, hostSessionId }
340
+ ) {
341
+ assertSchemaCompatible(db);
342
+ if (!roomId || !hostSessionId) return null;
343
+ return readDirectionalTarget(db, {
344
+ roomId,
345
+ hostSessionId,
346
+ anchorSeq: latestTimelineSeq(db, roomId) + 1,
347
+ direction: "previous",
348
+ });
349
+ }
350
+
351
+ function anchorRow(db, { roomId, eventId }) {
352
+ return (
353
+ db
354
+ .prepare(
355
+ `SELECT event_id, stream_seq
356
+ FROM matrix_events
357
+ WHERE room_id = ? AND event_id = ? AND is_state = 0`
358
+ )
359
+ .get(roomId, eventId) ?? null
360
+ );
361
+ }
362
+
363
+ function latestTimelineSeq(db, roomId) {
364
+ const row = db
365
+ .prepare(
366
+ `SELECT stream_seq
367
+ FROM matrix_events
368
+ WHERE room_id = ? AND is_state = 0
369
+ ORDER BY stream_seq DESC
370
+ LIMIT 1`
371
+ )
372
+ .get(roomId);
373
+ return Number.isSafeInteger(row?.stream_seq) ? row.stream_seq : 0;
374
+ }
375
+
376
+ export function readCompletedTurnFinalMessageNeighborsFromSQLite(
377
+ db,
378
+ { roomId, hostSessionId, anchorEventId = null, anchorPosition = null }
379
+ ) {
380
+ assertSchemaCompatible(db);
381
+ if (!roomId || !hostSessionId) {
382
+ throw new Error("Completed-turn navigation requires room and Host session identity");
383
+ }
384
+ if (anchorPosition && anchorPosition !== "latest") {
385
+ return { status: "invalid_anchor", error: "anchor_position must be latest" };
386
+ }
387
+ if (!anchorEventId && anchorPosition !== "latest") {
388
+ return {
389
+ status: "missing_identity",
390
+ error: "anchor_event_id or anchor_position=latest required",
391
+ };
392
+ }
393
+
394
+ const anchor = anchorEventId
395
+ ? anchorRow(db, { roomId, eventId: anchorEventId })
396
+ : null;
397
+ if (anchorEventId && !anchor) {
398
+ return {
399
+ status: "anchor_not_found",
400
+ error: "completed-turn navigation anchor event not found in bound room",
401
+ matrix_event_id: anchorEventId,
402
+ };
403
+ }
404
+
405
+ const latestSeq = latestTimelineSeq(db, roomId);
406
+ const anchorSeq = anchor?.stream_seq ?? latestSeq + 1;
407
+ const previous = readDirectionalTarget(db, {
408
+ roomId,
409
+ hostSessionId,
410
+ anchorSeq,
411
+ direction: "previous",
412
+ });
413
+ const next = anchor
414
+ ? readDirectionalTarget(db, {
415
+ roomId,
416
+ hostSessionId,
417
+ anchorSeq,
418
+ direction: "next",
419
+ })
420
+ : null;
421
+ const latestCompleted = readDirectionalTarget(db, {
422
+ roomId,
423
+ hostSessionId,
424
+ anchorSeq: latestSeq + 1,
425
+ direction: "previous",
426
+ });
427
+ const noCompletedTurns = latestCompleted === null;
428
+
429
+ const navigationIndexesPresent = [
430
+ MATRIX_COMPLETED_TURN_CANDIDATE_INDEX,
431
+ MATRIX_COMPLETED_TURN_COMPLETION_INDEX,
432
+ ].every(
433
+ (name) =>
434
+ db
435
+ .prepare(
436
+ "SELECT 1 AS present FROM sqlite_master WHERE type = 'index' AND name = ?"
437
+ )
438
+ .get(name)?.present === 1
439
+ );
440
+
441
+ return {
442
+ status: "ok",
443
+ session_id: hostSessionId,
444
+ matrix_room_id: roomId,
445
+ anchor: anchor
446
+ ? {
447
+ anchor_position: "event",
448
+ event_id: anchor.event_id,
449
+ routec_stream_seq: anchor.stream_seq,
450
+ }
451
+ : {
452
+ anchor_position: "latest",
453
+ event_id: null,
454
+ routec_stream_seq: latestSeq || null,
455
+ },
456
+ previous,
457
+ next,
458
+ boundaries: {
459
+ no_completed_turns: noCompletedTurns,
460
+ at_first_completed_message: previous === null,
461
+ at_latest_completed_message: next === null,
462
+ },
463
+ proof: {
464
+ schema_version: NAVIGATION_SCHEMA_VERSION,
465
+ lookup_source: navigationIndexesPresent
466
+ ? "routec_matrix_events_completed_turn_expression_indexes"
467
+ : "routec_matrix_events_completed_turn_sql",
468
+ lookup_strategy: navigationIndexesPresent
469
+ ? "indexed_completed_turn_final_message"
470
+ : "unindexed_completed_turn_final_message",
471
+ order_by: "final_candidate_stream_seq",
472
+ candidate_predicate: "complete_message_notification_candidate",
473
+ completion_predicate: "complete_message_notification_completion",
474
+ navigation_index_version: navigationIndexesPresent
475
+ ? MATRIX_COMPLETED_TURN_NAVIGATION_INDEX_VERSION
476
+ : null,
477
+ navigation_index_names: navigationIndexesPresent
478
+ ? [
479
+ MATRIX_COMPLETED_TURN_CANDIDATE_INDEX,
480
+ MATRIX_COMPLETED_TURN_COMPLETION_INDEX,
481
+ ]
482
+ : [],
483
+ raw_event_scan_limit: null,
484
+ raw_event_payload_returned: false,
485
+ message_body_returned: false,
486
+ },
487
+ raw_event_payload_returned: false,
488
+ message_body_returned: false,
489
+ };
490
+ }
491
+
492
+ export function explainCompletedTurnFinalMessageSelectorForTest(
493
+ db,
494
+ { roomId, anchorSeq, direction }
495
+ ) {
496
+ assertSchemaCompatible(db);
497
+ return db
498
+ .prepare(`EXPLAIN QUERY PLAN ${selectorSql(direction)}`)
499
+ .all({ room_id: roomId, anchor_seq: anchorSeq });
500
+ }
@@ -26,6 +26,7 @@ const UPDATE_OPERATION_TERMINAL_STATUSES = new Set([
26
26
  "update_check_failed",
27
27
  "version_mismatch",
28
28
  "restart_complete",
29
+ "restart_rejected",
29
30
  "superseded_reconciled",
30
31
  ]);
31
32
 
@@ -462,6 +463,9 @@ export function readUpdateOperationState({ configDir = getConfigDir() } = {}) {
462
463
  state.operation_cleanup_completed === true,
463
464
  rollback_completed: state.rollback_completed === true,
464
465
  restart_attempted: state.restart_attempted === true,
466
+ continuity_path: normalizeString(state.continuity_path),
467
+ continuity_state: normalizeString(state.continuity_state),
468
+ user_path_verification: normalizeStructuredObject(state.user_path_verification),
465
469
  diagnostic_summary: normalizeStructuredObject(state.diagnostic_summary),
466
470
  root_failure: normalizeStructuredObject(state.root_failure),
467
471
  rollback_result: normalizeStructuredObject(state.rollback_result),
@@ -545,6 +549,9 @@ export function writeUpdateOperationState(
545
549
  state.operation_cleanup_completed === true,
546
550
  rollback_completed: state.rollback_completed === true,
547
551
  restart_attempted: state.restart_attempted === true,
552
+ continuity_path: normalizeString(state.continuity_path),
553
+ continuity_state: normalizeString(state.continuity_state),
554
+ user_path_verification: normalizeStructuredObject(state.user_path_verification),
548
555
  diagnostic_summary: normalizeStructuredObject(state.diagnostic_summary),
549
556
  root_failure: normalizeStructuredObject(state.root_failure),
550
557
  rollback_result: normalizeStructuredObject(state.rollback_result),
@@ -0,0 +1,60 @@
1
+ import Database from 'better-sqlite3';
2
+ import { mkdirSync } from 'node:fs';
3
+ import { dirname } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { getHostDbPath } from './config.mjs';
6
+
7
+ // Audio never enters this store. The fingerprint binds the exact finalized segment.
8
+ export const voiceInputFingerprint = (metadata) => createHash('sha256').update(JSON.stringify([
9
+ metadata.recordingId, metadata.segmentIndex, metadata.durationMs,
10
+ metadata.mimeType.split(';')[0].trim().toLowerCase(), metadata.size, metadata.audioSha256,
11
+ ])).digest('hex');
12
+ const failure = (message, code) => Object.assign(new Error(message), { code });
13
+ export class VoiceInputReceiptStore {
14
+ constructor({ dbPath = getHostDbPath() } = {}) {
15
+ mkdirSync(dirname(dbPath), { recursive: true });
16
+ this.db = new Database(dbPath);
17
+ this.db.pragma('journal_mode = WAL');
18
+ this.db.pragma('synchronous = FULL');
19
+ this.db.exec(`CREATE TABLE IF NOT EXISTS voice_input_receipts (
20
+ session_id TEXT NOT NULL, capture_id TEXT NOT NULL, fingerprint TEXT,
21
+ generation INTEGER NOT NULL, state TEXT NOT NULL, result TEXT,
22
+ updated_at TEXT NOT NULL, PRIMARY KEY(session_id,capture_id))`);
23
+ if (!this.db.pragma('table_info(voice_input_receipts)').some(column => column.name === 'identity_json')) this.db.exec('ALTER TABLE voice_input_receipts ADD COLUMN identity_json TEXT');
24
+ // A process loss after paid work started is ambiguous, never safe to replay.
25
+ this.db.prepare("UPDATE voice_input_receipts SET state='outcome_unknown', updated_at=? WHERE state='provider_started'").run(new Date().toISOString());
26
+ }
27
+ get(sessionId, captureId) {
28
+ const row = this.db.prepare('SELECT * FROM voice_input_receipts WHERE session_id=? AND capture_id=?').get(sessionId, captureId);
29
+ return row ? { state: row.state, attempt_generation: row.generation, ...(row.identity_json ? { identity: JSON.parse(row.identity_json) } : {}), ...(row.result ? { result: JSON.parse(row.result) } : {}) } : { state: 'not_received', attempt_generation: 0 };
30
+ }
31
+ begin(sessionId, captureId, fingerprint, generation = 1, explicitRetry = false, identity = null) {
32
+ if (!Number.isSafeInteger(generation) || generation < 1) throw failure('Invalid recording attempt generation.', 'voice_input_attempt_invalid');
33
+ return this.db.transaction(() => {
34
+ const old = this.db.prepare('SELECT * FROM voice_input_receipts WHERE session_id=? AND capture_id=?').get(sessionId, captureId);
35
+ if (old) {
36
+ if (old.state === 'released') throw failure('Recording was removed.', 'voice_input_released');
37
+ if (old.fingerprint !== fingerprint) throw failure('Recording identity conflicts with its saved payload.', 'voice_input_receipt_conflict');
38
+ if (old.generation === generation) return this.get(sessionId, captureId);
39
+ if (!explicitRetry || generation !== old.generation + 1 || !['failed_retryable','failed_terminal','outcome_unknown'].includes(old.state)) throw failure('Recording attempt cannot be replayed.', 'voice_input_attempt_conflict');
40
+ this.db.prepare("UPDATE voice_input_receipts SET generation=?,state='received',result=NULL,updated_at=? WHERE session_id=? AND capture_id=?").run(generation,new Date().toISOString(),sessionId,captureId);
41
+ } else {
42
+ if (generation !== 1 || explicitRetry) throw failure('Recording attempt is unknown.', 'voice_input_attempt_conflict');
43
+ this.db.prepare('INSERT INTO voice_input_receipts (session_id,capture_id,fingerprint,generation,state,result,updated_at,identity_json) VALUES (?,?,?,?,?,?,?,?)').run(sessionId,captureId,fingerprint,generation,'received',null,new Date().toISOString(),identity ? JSON.stringify(identity) : null);
44
+ }
45
+ return this.get(sessionId,captureId);
46
+ })();
47
+ }
48
+ start(sessionId,captureId,generation) {
49
+ return this.db.prepare("UPDATE voice_input_receipts SET state='provider_started',updated_at=? WHERE session_id=? AND capture_id=? AND generation=? AND state='received'").run(new Date().toISOString(),sessionId,captureId,generation).changes === 1;
50
+ }
51
+ complete(sessionId,captureId,generation,state,result) {
52
+ if (!['succeeded','failed_retryable','failed_terminal','outcome_unknown'].includes(state)) throw new Error('Invalid receipt terminal state.');
53
+ return this.db.prepare("UPDATE voice_input_receipts SET state=?,result=?,updated_at=? WHERE session_id=? AND capture_id=? AND generation=? AND state='provider_started'").run(state,JSON.stringify(result),new Date().toISOString(),sessionId,captureId,generation).changes === 1;
54
+ }
55
+ release(sessionId,captureId) {
56
+ this.db.prepare("INSERT INTO voice_input_receipts (session_id,capture_id,fingerprint,generation,state,result,updated_at) VALUES (?,?,NULL,1,'released',NULL,?) ON CONFLICT(session_id,capture_id) DO UPDATE SET state='released',result=NULL,updated_at=excluded.updated_at").run(sessionId,captureId,new Date().toISOString());
57
+ return this.get(sessionId,captureId);
58
+ }
59
+ close() { this.db.close(); }
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oysterun",
3
- "version": "1.6.0-beta.2",
3
+ "version": "1.6.0-beta.21",
4
4
  "type": "module",
5
5
  "description": "Oysterun Host installer and local service",
6
6
  "bin": {
@@ -13,6 +13,23 @@
13
13
  <link rel="alternate" hreflang="zh-Hant" href="https://oysterun.com/blog/zh/index.html" />
14
14
  <link rel="alternate" type="text/markdown" href="/blog/en/index.md" />
15
15
  <link rel="describedby" href="/llms.txt" type="text/plain" />
16
+ <meta property="og:title" content="Blog — Oysterun" />
17
+ <meta property="og:description" content="Read bilingual product and engineering notes from Oysterun." />
18
+ <meta property="og:type" content="website" />
19
+ <meta property="og:url" content="https://oysterun.com/blog/en/index.html" />
20
+ <meta property="og:site_name" content="Oysterun" />
21
+ <meta property="og:locale" content="en_US" />
22
+ <meta property="og:image" content="https://oysterun.com/assets/oysterun-social-preview.png" />
23
+ <meta property="og:image:secure_url" content="https://oysterun.com/assets/oysterun-social-preview.png" />
24
+ <meta property="og:image:type" content="image/png" />
25
+ <meta property="og:image:width" content="1200" />
26
+ <meta property="og:image:height" content="630" />
27
+ <meta property="og:image:alt" content="Oysterun connecting a phone to a computer running AI agents" />
28
+ <meta name="twitter:card" content="summary_large_image" />
29
+ <meta name="twitter:title" content="Blog — Oysterun" />
30
+ <meta name="twitter:description" content="Read bilingual product and engineering notes from Oysterun." />
31
+ <meta name="twitter:image" content="https://oysterun.com/assets/oysterun-social-preview.png" />
32
+ <meta name="twitter:image:alt" content="Oysterun connecting a phone to a computer running AI agents" />
16
33
 
17
34
  </head>
18
35
  <body>
@@ -2,7 +2,7 @@
2
2
  "schema": "oysterun.static_blog_manifest.v3",
3
3
  "content_schema": "oysterun.static_content_bundle.v2",
4
4
  "shell_schema": "oysterun.static_site_shell.v3",
5
- "shell_sha256": "1b499d9ab1382144b285cf0dc75c00da0d3e3095aed750a0110025be3f93caa3",
5
+ "shell_sha256": "59cae2adbf15880e4b558edbf255409493d2a10ae820d10e7bf3ab33f1c78c7a",
6
6
  "generated_slugs": [
7
7
  "voice-input-model-experiment"
8
8
  ],
@@ -32,17 +32,17 @@
32
32
  "type": "svg"
33
33
  },
34
34
  "content_sha256": "49a68eec9d4d83126308962f646d673dacd526b867b2d3818b834c06a893e1a9",
35
- "shell_sha256": "1b499d9ab1382144b285cf0dc75c00da0d3e3095aed750a0110025be3f93caa3",
35
+ "shell_sha256": "59cae2adbf15880e4b558edbf255409493d2a10ae820d10e7bf3ab33f1c78c7a",
36
36
  "files": [
37
37
  {
38
38
  "path": "en/index.html",
39
- "size": 97758,
40
- "sha256": "0fd8d384ddb4fdf6e096c13dd62d4b26122262384645a99336e3c19ecf7e83b2"
39
+ "size": 99425,
40
+ "sha256": "2912695661fd8ab3184f2676e930f924f78911b9e5f643ca44bd56b0e8616cf4"
41
41
  },
42
42
  {
43
43
  "path": "zh/index.html",
44
- "size": 132388,
45
- "sha256": "5b0d1a028d8459d3c4911583e6e4953979d26581bbd575e9bce65554d6060a73"
44
+ "size": 134033,
45
+ "sha256": "d77ae4aea1bed74d79d726e8303c40980b18bbc11bf700eb52282a420bbaa2d2"
46
46
  },
47
47
  {
48
48
  "path": "assets/article.css",
@@ -97,18 +97,18 @@
97
97
  ],
98
98
  "generated_index": {
99
99
  "path": "index.html",
100
- "sha256": "c220a9dfee4da37dc302f56cbdf9fe27c2fdf09b76087f99b25ebcaa7dc670c8"
100
+ "sha256": "4a5c23ee4e46c2609748a188eaa1e480eef93e27800311336ec1aa28cc5d1dae"
101
101
  },
102
102
  "generated_indexes": [
103
103
  {
104
104
  "language": "zh",
105
105
  "path": "zh/index.html",
106
- "sha256": "38ea738b6bf92f75ae63c2a6f105fdb2d645af8673e7a49c5aee08397de09928"
106
+ "sha256": "16c877bb32e43b2b529020d90b572112b3b7b0f7e3bb6670af1df91593497311"
107
107
  },
108
108
  {
109
109
  "language": "en",
110
110
  "path": "en/index.html",
111
- "sha256": "e6ffc934a75bfa64c46df7ec926e8b0567450b595239f8414695ef94b6e243c0"
111
+ "sha256": "ec6c7b405c88a4fd692b2dfdcc89eb3c3e4537e3f3eaaa6bd9fd8671e0ba3f3c"
112
112
  }
113
113
  ]
114
114
  }