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.
- package/bin/oysterun.mjs +64 -17
- package/dev/client/connectivity/completed-message-neighbor-click.mjs +78 -0
- package/dev/client/connectivity/completed-message-neighbor-contract.mjs +205 -0
- package/dev/client/connectivity/completed-message-visibility.d.mts +4 -4
- package/dev/client/connectivity/completed-message-visibility.mjs +14 -11
- package/dev/client/connectivity/desktop-chat-sidebar-preference.mjs +57 -0
- package/dev/client/connectivity/host-connection-feedback.mjs +12 -1
- package/dev/client/connectivity/managed-host-browser.mjs +10 -0
- package/dev/client/connectivity/session-navigation-intent.mjs +189 -0
- package/dev/client/connectivity/session-transition-state.mjs +18 -6
- package/dev/client/connectivity/test-completed-message-neighbor-click.mjs +180 -0
- package/dev/client/web/index.html +5367 -609
- package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js → ReactPrism-C6YiTZTC.js} +1 -1
- package/dev/client/web-chat/dist/assets/{ReactPrism-BwJhidYf.js.map → ReactPrism-C6YiTZTC.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js +6442 -0
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-CKJOSPFP.js.map +1 -0
- package/dev/client/web-chat/dist/assets/SessionsSetupApp-Dx09Uz-6.css +1 -0
- package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js +9 -0
- package/dev/client/web-chat/dist/assets/index-Ae5I4LHp.js.map +1 -0
- package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js +2 -0
- package/dev/client/web-chat/dist/assets/index-CN_FoTB1.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js → index-DCQFwgGf.js} +2 -2
- package/dev/client/web-chat/dist/assets/{index--1xmQ99x.js.map → index-DCQFwgGf.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/matrix-entry-BV7G38kH.css +10 -0
- package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js +594 -0
- package/dev/client/web-chat/dist/assets/matrix-entry-abAy3aOo.js.map +1 -0
- package/dev/client/web-chat/dist/assets/{pdf-ZjtalfgI.js → pdf-DN7oVvM6.js} +5 -5
- package/dev/client/web-chat/dist/assets/pdf-DN7oVvM6.js.map +1 -0
- package/dev/client/web-chat/dist/assets/session-notice-DlyODy1z.css +1 -0
- package/dev/client/web-chat/dist/assets/web-BlYuDlOV.js +1 -0
- package/dev/client/web-chat/dist/assets/{web-c70YaskK.js.map → web-BlYuDlOV.js.map} +1 -1
- package/dev/client/web-chat/dist/assets/web-Dabw6cJi.js +1 -0
- package/dev/client/web-chat/dist/assets/{web-BzHcZK2M.js.map → web-Dabw6cJi.js.map} +1 -1
- package/dev/client/web-chat/dist/index.html +1 -2
- package/docs/routec/dev/cloud_deplyments.md +5 -0
- package/docs/routec/dev/npm_native_dependency_install.md +6 -2
- package/docs/routec/dev/npmjs_publish.md +55 -19
- package/docs/routec/dev/p262_setup_input_file.md +117 -0
- package/docs/routec/host2_turn_started_idempotency_diagnostics.md +8 -7
- package/docs/routec/host_setup.md +39 -10
- package/docs/routec/oysterun_operation_interface_spec.md +78 -2
- package/docs/routec/provider_contract.md +35 -5
- package/host-service/README.md +36 -0
- package/host-service/adapters/claude-code-adapter.mjs +48 -5
- package/host-service/adapters/codex-app-server-adapter.mjs +320 -18
- package/host-service/adapters/debug-fixture-provider-adapter.mjs +102 -1
- package/host-service/adapters/debug-large-tool-spillover-provider-adapter.mjs +2 -0
- package/host-service/adapters/debug-p135-codex-replay-provider-adapter.mjs +2 -0
- package/host-service/adapters/debug-routec-structural-replay-provider-adapter.mjs +2 -0
- package/host-service/agent-config.mjs +16 -5
- package/host-service/agent-folder-availability.mjs +41 -0
- package/host-service/apns-complete-message-dispatcher.mjs +1 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-find-context/SKILL.md +24 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-session-chat/SKILL.md +94 -5
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-sessions/SKILL.md +103 -0
- package/host-service/assets/product-skills/Oysterun/modules/oysterun-website/SKILL.md +7 -0
- package/host-service/assets/product-skills/oysterun-find-context/SKILL.md +24 -0
- package/host-service/assets/product-skills/oysterun-session-chat/SKILL.md +94 -5
- package/host-service/assets/product-skills/oysterun-sessions/SKILL.md +103 -0
- package/host-service/assets/product-skills/oysterun-website/SKILL.md +7 -0
- package/host-service/callback-durable-store.mjs +354 -47
- package/host-service/callback-foundation.mjs +21 -1
- package/host-service/callback-host-integration.mjs +345 -30
- package/host-service/claude-acp-model-discovery.mjs +23 -18
- package/host-service/cli/assert-installer-reinstall-target.mjs +9 -0
- package/host-service/cli/installer-recovery-guidance.mjs +20 -0
- package/host-service/cli/prepare-restart-restore.mjs +2 -0
- package/host-service/cli/product-cli.mjs +34 -33
- package/host-service/cli/reconcile-initial-preflight.mjs +19 -0
- package/host-service/cli/recover-installer-lifecycle.mjs +24 -0
- package/host-service/cli/run-host-lifecycle.mjs +2 -0
- package/host-service/cli/run-staged-lifecycle-recovery.mjs +161 -0
- package/host-service/codex-model-discovery.mjs +23 -18
- package/host-service/complete-cli-uninstall.mjs +176 -0
- package/host-service/complete-message-notification-predicate.mjs +8 -6
- package/host-service/complete-message-notification-title.mjs +4 -0
- package/host-service/completed-session-unread.mjs +425 -42
- package/host-service/completed-turn-navigation.mjs +444 -144
- package/host-service/completed-turn-notification-observer.mjs +116 -0
- package/host-service/completed-turn-service.mjs +445 -0
- package/host-service/config.mjs +115 -9
- package/host-service/desired-open-session-state.mjs +331 -0
- package/host-service/fixtures/p218-completed-turn-notification.json +11 -0
- package/host-service/frp-agent.mjs +9 -9
- package/host-service/grok-acp-model-discovery.mjs +31 -33
- package/host-service/host-lifecycle-recovery.mjs +37 -0
- package/host-service/host-port-resolution.mjs +98 -0
- package/host-service/host-runtime-diagnostics.mjs +18 -1
- package/host-service/initial-preflight-recovery.mjs +71 -0
- package/host-service/installer-recovery-guidance.mjs +85 -0
- package/host-service/installer-reinstall-bindings.mjs +49 -0
- package/host-service/installer-reinstall-target.mjs +49 -0
- package/host-service/legacy-continuity-observation.mjs +33 -0
- package/host-service/lifecycle-compat-cli.mjs +13 -0
- package/host-service/lifecycle-compat-preload.mjs +89 -0
- package/host-service/lifecycle-compatibility.mjs +164 -0
- package/host-service/lifecycle-continuity-assets.mjs +222 -0
- package/host-service/lifecycle-continuity-probe.mjs +97 -0
- package/host-service/lifecycle-continuity-projection.mjs +29 -0
- package/host-service/lifecycle-continuity-runner.mjs +192 -0
- package/host-service/lifecycle-continuity.mjs +493 -0
- package/host-service/lifecycle-service-render-bundle.mjs +120 -0
- package/host-service/lifecycle-user-path.mjs +34 -0
- package/host-service/link-annotations.mjs +10 -0
- package/host-service/matrix-credential-issuer.mjs +32 -1
- package/host-service/matrix-event-correlator.mjs +166 -27
- package/host-service/matrix-facade.mjs +96 -19
- package/host-service/matrix-room-binding.mjs +24 -0
- package/host-service/matrix-transcript-read-adapter.mjs +17 -3
- package/host-service/model-discovery-guardian.mjs +40 -0
- package/host-service/model-discovery-process.mjs +141 -0
- package/host-service/onboard-lifecycle-status.mjs +28 -0
- package/host-service/onboard.mjs +479 -0
- package/host-service/package.json +2 -1
- package/host-service/portable-scheduler-definitions.mjs +28 -0
- package/host-service/product-runtime-host-origin.mjs +22 -0
- package/host-service/provider-model-params.mjs +14 -17
- package/host-service/provider-model-refresh-runner.mjs +124 -169
- package/host-service/provider-model-refresh-schedule.mjs +11 -0
- package/host-service/provider-resume-contract.mjs +9 -3
- package/host-service/restart-restore-state.mjs +25 -0
- package/host-service/retained-host-state-holder.mjs +57 -0
- package/host-service/routec-matrix-sqlite-store.mjs +155 -10
- package/host-service/routec-matrix-storage-adapter.mjs +208 -25
- package/host-service/routec-matrix-storage-paths.mjs +12 -0
- package/host-service/routec-request-delay-window.mjs +55 -0
- package/host-service/scheduler-acp-provider-runner.mjs +2 -1
- package/host-service/scheduler-direct-provider-runner.mjs +71 -9
- package/host-service/scheduler-run-timeout-policy.mjs +85 -0
- package/host-service/scheduler-service.mjs +96 -9
- package/host-service/scheduler-store.mjs +9 -2
- package/host-service/server.mjs +2202 -552
- package/host-service/service-definition-migration.mjs +10 -3
- package/host-service/session-collection-service.mjs +320 -0
- package/host-service/session-history.mjs +99 -15
- package/host-service/session-list-preview-service.mjs +87 -12
- package/host-service/session-list-projection-service.mjs +182 -0
- package/host-service/session-loop-definitions.mjs +702 -75
- package/host-service/session-manager.mjs +473 -38
- package/host-service/session-switcher-projection.mjs +11 -3
- package/host-service/session-tags.mjs +451 -0
- package/host-service/session-transcript-target.mjs +103 -0
- package/host-service/setup-input-file.mjs +91 -0
- package/host-service/setup-provider-model-defaults.mjs +53 -0
- package/host-service/setup.mjs +133 -101
- package/host-service/stopped-user-host-package.mjs +60 -0
- package/host-service/test-fixtures/p236-navigation-original.mjs +500 -0
- package/host-service/update-reminder-state.mjs +7 -0
- package/host-service/voice-input-receipt-store.mjs +60 -0
- package/package.json +1 -1
- package/static/oysterun-social-preview.png +0 -0
- package/static/site/assets/oysterun-social-preview.png +0 -0
- package/static/site/blog/en/index.html +17 -0
- package/static/site/blog/generated_manifest.json +9 -9
- package/static/site/blog/index.html +18 -0
- package/static/site/blog/voice-input-model-experiment/en/index.html +17 -0
- package/static/site/blog/voice-input-model-experiment/zh/index.html +17 -0
- package/static/site/blog/zh/index.html +17 -0
- package/static/site/docs/choose-and-recover-a-provider/en/index.html +17 -0
- package/static/site/docs/choose-and-recover-a-provider/zh/index.html +17 -0
- package/static/site/docs/configure-session-files-and-settings/en/index.html +17 -0
- package/static/site/docs/configure-session-files-and-settings/zh/index.html +17 -0
- package/static/site/docs/en/index.html +17 -0
- package/static/site/docs/generated_manifest.json +60 -60
- package/static/site/docs/host-authentication-and-connection/en/index.html +17 -0
- package/static/site/docs/host-authentication-and-connection/zh/index.html +17 -0
- package/static/site/docs/index.html +18 -0
- package/static/site/docs/install-and-use-a-skill/en/index.html +17 -0
- package/static/site/docs/install-and-use-a-skill/zh/index.html +17 -0
- package/static/site/docs/read-large-tool-results/en/index.html +17 -0
- package/static/site/docs/read-large-tool-results/zh/index.html +17 -0
- package/static/site/docs/recover-from-a-host-runtime-error/en/index.html +17 -0
- package/static/site/docs/recover-from-a-host-runtime-error/zh/index.html +17 -0
- package/static/site/docs/restore-work-after-a-host-restart/en/index.html +17 -0
- package/static/site/docs/restore-work-after-a-host-restart/zh/index.html +17 -0
- package/static/site/docs/send-images-and-media/en/index.html +17 -0
- package/static/site/docs/send-images-and-media/zh/index.html +17 -0
- package/static/site/docs/set-up-and-understand-a-host/en/index.html +17 -0
- package/static/site/docs/set-up-and-understand-a-host/zh/index.html +17 -0
- package/static/site/docs/understand-agent-activity/en/index.html +19 -2
- package/static/site/docs/understand-agent-activity/en/index.md +2 -2
- package/static/site/docs/understand-agent-activity/zh/index.html +19 -2
- package/static/site/docs/understand-agent-activity/zh/index.md +2 -2
- package/static/site/docs/use-the-oysterun-cli-safely/en/index.html +17 -0
- package/static/site/docs/use-the-oysterun-cli-safely/zh/index.html +17 -0
- package/static/site/docs/zh/index.html +17 -0
- package/static/site/en/index.html +18 -1
- package/static/site/en/index.md +1 -1
- package/static/site/faq/en/index.html +17 -0
- package/static/site/faq/index.html +18 -0
- package/static/site/faq/zh/index.html +17 -0
- package/static/site/getting-started/en/index.html +23 -1
- package/static/site/getting-started/en/index.md +7 -1
- package/static/site/getting-started/index.html +18 -0
- package/static/site/getting-started/zh/index.html +23 -1
- package/static/site/getting-started/zh/index.md +7 -1
- package/static/site/index.html +18 -0
- package/static/site/install.sh +676 -127
- package/static/site/overview/en/index.html +18 -1
- package/static/site/overview/en/index.md +2 -2
- package/static/site/overview/index.html +18 -0
- package/static/site/overview/zh/index.html +18 -1
- package/static/site/overview/zh/index.md +2 -2
- package/static/site/privacy/en/index.html +17 -0
- package/static/site/privacy/index.html +18 -0
- package/static/site/privacy/zh/index.html +17 -0
- package/static/site/zh/index.html +18 -1
- package/static/site/zh/index.md +1 -1
- package/static/site_source/content/tutorials/published/understand-agent-activity/en/article.html +2 -2
- package/static/site_source/content/tutorials/published/understand-agent-activity/zh/article.html +2 -2
- package/static/site_source/downloads/install.sh +676 -127
- package/static/site_source/manifests/host_install.json +2 -10
- package/static/site_source/pages/getting-started/en.html +6 -1
- package/static/site_source/pages/getting-started/zh.html +6 -1
- package/static/site_source/pages/home/en.html +1 -1
- package/static/site_source/pages/home/zh.html +1 -1
- package/static/site_source/pages/home_main.html +3 -2
- package/static/site_source/pages/overview/en.html +1 -1
- package/static/site_source/pages/overview/zh.html +1 -1
- package/tool_scripts/install_oysterun_update.mjs +78 -13
- package/tool_scripts/oysterun_continuity_transaction.mjs +438 -0
- package/tool_scripts/oysterun_npm_package_manifest.mjs +32 -0
- package/tool_scripts/oysterun_release_service.sh +5 -0
- package/tool_scripts/oysterun_service_common.sh +59 -24
- package/tool_scripts/oysterun_setup_input.sh +97 -0
- package/tool_scripts/oysterun_update_restart_runner.mjs +19 -3
- package/tool_scripts/p233_legacy_bootstrap.mjs +254 -0
- package/tool_scripts/resolve_user_host_port.mjs +15 -0
- package/tool_scripts/restart_oysterun.sh +8 -2
- package/tool_scripts/stack_dashboard_credentials.mjs +2 -1
- package/tool_scripts/status_oysterun_launch_agents.sh +13 -1
- package/tool_scripts/sync_host_stack_config.mjs +9 -6
- package/tool_scripts/test_environment_remote_cloud_deprovision.mjs +419 -0
- package/tool_scripts/uninstall_oysterun_launch_agents.sh +3 -5
- package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js +0 -129
- package/dev/client/web-chat/dist/assets/index-Ce9NZBZf.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js +0 -2
- package/dev/client/web-chat/dist/assets/index-DzIClZ3T.js.map +0 -1
- package/dev/client/web-chat/dist/assets/index-NYnaSGBb.css +0 -1
- package/dev/client/web-chat/dist/assets/pdf-ZjtalfgI.js.map +0 -1
- package/dev/client/web-chat/dist/assets/web-BzHcZK2M.js +0 -1
- 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
|
Binary file
|
|
Binary file
|
|
@@ -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": "
|
|
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": "
|
|
35
|
+
"shell_sha256": "59cae2adbf15880e4b558edbf255409493d2a10ae820d10e7bf3ab33f1c78c7a",
|
|
36
36
|
"files": [
|
|
37
37
|
{
|
|
38
38
|
"path": "en/index.html",
|
|
39
|
-
"size":
|
|
40
|
-
"sha256": "
|
|
39
|
+
"size": 99425,
|
|
40
|
+
"sha256": "2912695661fd8ab3184f2676e930f924f78911b9e5f643ca44bd56b0e8616cf4"
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
"path": "zh/index.html",
|
|
44
|
-
"size":
|
|
45
|
-
"sha256": "
|
|
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": "
|
|
100
|
+
"sha256": "4a5c23ee4e46c2609748a188eaa1e480eef93e27800311336ec1aa28cc5d1dae"
|
|
101
101
|
},
|
|
102
102
|
"generated_indexes": [
|
|
103
103
|
{
|
|
104
104
|
"language": "zh",
|
|
105
105
|
"path": "zh/index.html",
|
|
106
|
-
"sha256": "
|
|
106
|
+
"sha256": "16c877bb32e43b2b529020d90b572112b3b7b0f7e3bb6670af1df91593497311"
|
|
107
107
|
},
|
|
108
108
|
{
|
|
109
109
|
"language": "en",
|
|
110
110
|
"path": "en/index.html",
|
|
111
|
-
"sha256": "
|
|
111
|
+
"sha256": "ec6c7b405c88a4fd692b2dfdcc89eb3c3e4537e3f3eaaa6bd9fd8671e0ba3f3c"
|
|
112
112
|
}
|
|
113
113
|
]
|
|
114
114
|
}
|