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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oysterun-host-service",
|
|
3
|
-
"version": "1.6.0-beta.
|
|
3
|
+
"version": "1.6.0-beta.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Oysterun host service — runs on Mac mini, manages Claude Code sessions",
|
|
6
6
|
"scripts": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"test:p118": "node test-p118-exact-matrix-event-context.mjs",
|
|
46
46
|
"test:p119": "node test-p119-telegram-multi-chat-origin-routing.mjs",
|
|
47
47
|
"test:p208": "node --test test-p208-reasoning-effort-configuration-precedence.mjs",
|
|
48
|
+
"test:p220": "node test-p220-session-tags.mjs && node test-p220-session-tags-lifecycle.mjs",
|
|
48
49
|
"test:p226": "node --test test-p226-lifecycle-diagnostics.mjs test-p226-service-definition-migration.mjs"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getHostSystemTimezone,
|
|
17
17
|
normalizeScheduleRule,
|
|
18
18
|
} from "./scheduler-rule-model.mjs";
|
|
19
|
+
import { resolveSchedulerRunTimeout } from "./scheduler-run-timeout-policy.mjs";
|
|
19
20
|
|
|
20
21
|
export const PORTABLE_SCHEDULER_STORAGE_OWNER =
|
|
21
22
|
"agent_folder_oysterun_schedulers_json";
|
|
@@ -200,6 +201,14 @@ function normalizePortableScheduleForFile(input, { now, agentFolder }) {
|
|
|
200
201
|
const metadata = isObjectRecord(input.metadata)
|
|
201
202
|
? normalizePortableMetadata(input.metadata)
|
|
202
203
|
: {};
|
|
204
|
+
const timeoutPresent = Object.prototype.hasOwnProperty.call(
|
|
205
|
+
input,
|
|
206
|
+
"run_timeout_seconds"
|
|
207
|
+
);
|
|
208
|
+
const timeout = resolveSchedulerRunTimeout(input.run_timeout_seconds, {
|
|
209
|
+
present: timeoutPresent,
|
|
210
|
+
source: normalizeOptionalString(input.run_timeout_source),
|
|
211
|
+
});
|
|
203
212
|
return {
|
|
204
213
|
id,
|
|
205
214
|
agent_id: projectId,
|
|
@@ -217,6 +226,12 @@ function normalizePortableScheduleForFile(input, { now, agentFolder }) {
|
|
|
217
226
|
setup_snapshot: setupSnapshot,
|
|
218
227
|
portable_setup_snapshot: portableSetupSnapshot,
|
|
219
228
|
metadata,
|
|
229
|
+
...(timeoutPresent
|
|
230
|
+
? {
|
|
231
|
+
run_timeout_seconds: timeout.seconds,
|
|
232
|
+
run_timeout_source: timeout.source,
|
|
233
|
+
}
|
|
234
|
+
: {}),
|
|
220
235
|
created_at: createdAt,
|
|
221
236
|
updated_at: normalizeOptionalString(input.updated_at) || now,
|
|
222
237
|
storage_owner: PORTABLE_SCHEDULER_STORAGE_OWNER,
|
|
@@ -772,6 +787,16 @@ export class PortableSchedulerDefinitionStore {
|
|
|
772
787
|
const validationFailed =
|
|
773
788
|
definition.metadata?.portable_scheduler_validation_failed === true;
|
|
774
789
|
const effectiveEnabled = definition.enabled === true;
|
|
790
|
+
const runTimeout = resolveSchedulerRunTimeout(
|
|
791
|
+
definition.run_timeout_seconds,
|
|
792
|
+
{
|
|
793
|
+
present: Object.prototype.hasOwnProperty.call(
|
|
794
|
+
definition,
|
|
795
|
+
"run_timeout_seconds"
|
|
796
|
+
),
|
|
797
|
+
source: definition.run_timeout_source,
|
|
798
|
+
}
|
|
799
|
+
);
|
|
775
800
|
const normalizedRule =
|
|
776
801
|
validationFailed && !isObjectRecord(definition.schedule_rule)
|
|
777
802
|
? null
|
|
@@ -815,6 +840,9 @@ export class PortableSchedulerDefinitionStore {
|
|
|
815
840
|
next_run_at: nextRunAt,
|
|
816
841
|
availability_state: availabilityState,
|
|
817
842
|
availability_reason: availabilityReason,
|
|
843
|
+
run_timeout_seconds: runTimeout.seconds,
|
|
844
|
+
run_timeout_source: runTimeout.source,
|
|
845
|
+
run_timeout_explicit: runTimeout.explicit,
|
|
818
846
|
metadata: {
|
|
819
847
|
...definition.metadata,
|
|
820
848
|
source: "browser_root_project_schedulers_json",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function normalizeOrigin(value) {
|
|
2
|
+
return typeof value === "string" ? value.trim().replace(/\/+$/, "") : "";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function resolveProductRuntimeHostOrigin({
|
|
6
|
+
config,
|
|
7
|
+
directHostUrl,
|
|
8
|
+
port,
|
|
9
|
+
}) {
|
|
10
|
+
if (config?.connection_mode === "cloud") {
|
|
11
|
+
return normalizeOrigin(config.managed_endpoint) || `http://127.0.0.1:${port}`;
|
|
12
|
+
}
|
|
13
|
+
if (config?.connection_mode === "direct") {
|
|
14
|
+
return (
|
|
15
|
+
normalizeOrigin(directHostUrl) ||
|
|
16
|
+
normalizeOrigin(config.direct_host_url) ||
|
|
17
|
+
normalizeOrigin(config.public_base_url) ||
|
|
18
|
+
`http://127.0.0.1:${port}`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return `http://127.0.0.1:${port}`;
|
|
22
|
+
}
|
|
@@ -431,25 +431,10 @@ function statusTimestamp(params, providerId) {
|
|
|
431
431
|
normalizeString(providerModels.refreshed_at) ||
|
|
432
432
|
normalizeString(providerModels.last_success_at) ||
|
|
433
433
|
normalizeString(providerRuntime.last_success_at) ||
|
|
434
|
-
normalizeString(providerRuntime.last_attempt_at) ||
|
|
435
|
-
normalizeString(params.refresh?.last_success_at) ||
|
|
436
434
|
null
|
|
437
435
|
);
|
|
438
436
|
}
|
|
439
437
|
|
|
440
|
-
function parseStatusTimestamp(value) {
|
|
441
|
-
const normalized = normalizeString(value);
|
|
442
|
-
if (!normalized) return null;
|
|
443
|
-
const ms = Date.parse(normalized);
|
|
444
|
-
return Number.isFinite(ms) ? ms : null;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function nextProviderRefreshAt(params, providerId) {
|
|
448
|
-
const refreshedAtMs = parseStatusTimestamp(statusTimestamp(params, providerId));
|
|
449
|
-
if (!refreshedAtMs) return null;
|
|
450
|
-
return new Date(refreshedAtMs + PROVIDER_MODEL_REFRESH_INTERVAL_MS).toISOString();
|
|
451
|
-
}
|
|
452
|
-
|
|
453
438
|
export function loadProviderModelCatalogState(providerId, options = {}) {
|
|
454
439
|
if (
|
|
455
440
|
providerId === "debug-fixture" ||
|
|
@@ -500,10 +485,22 @@ export function loadProviderModelCatalogState(providerId, options = {}) {
|
|
|
500
485
|
normalizeString(providerRuntime?.last_error) ||
|
|
501
486
|
null,
|
|
502
487
|
refreshed_at: statusTimestamp(result.params, providerId),
|
|
488
|
+
last_success_at: statusTimestamp(result.params, providerId),
|
|
489
|
+
last_attempt_at: normalizeString(providerRuntime.last_attempt_at),
|
|
490
|
+
last_attempt_source: normalizeString(providerRuntime.last_attempt_source),
|
|
491
|
+
last_attempt_status: normalizeString(providerRuntime.probe_status),
|
|
492
|
+
process_cleanup: cloneValue(providerRuntime.process_cleanup || null),
|
|
493
|
+
last_attempt_reason: [
|
|
494
|
+
normalizeString(providerRuntime.refresh_skip_reason) || normalizeString(providerRuntime.last_error),
|
|
495
|
+
["unknown", "failed"].includes(providerRuntime.process_cleanup?.status)
|
|
496
|
+
? `Process cleanup ${providerRuntime.process_cleanup.status}: owned descendant absence is not confirmed.`
|
|
497
|
+
: null,
|
|
498
|
+
].filter(Boolean).join(" ") || null,
|
|
499
|
+
using_previous_catalog: models.length > 0 && ["error", "skipped"].includes(providerRuntime.probe_status),
|
|
503
500
|
next_refresh_at:
|
|
504
|
-
nextProviderRefreshAt(result.params, providerId) ||
|
|
505
501
|
normalizeString(result.params.refresh?.next_refresh_at),
|
|
506
|
-
refresh_scheduling_authority:
|
|
502
|
+
refresh_scheduling_authority: result.params.refresh?.scheduling_authority || null,
|
|
503
|
+
refresh_time_zone: result.params.refresh?.time_zone || null,
|
|
507
504
|
runtime: cloneValue(providerRuntime),
|
|
508
505
|
},
|
|
509
506
|
};
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
nextProviderModelRefreshTime,
|
|
3
|
+
providerModelRefreshZone,
|
|
4
|
+
PROVIDER_MODEL_REFRESH_SCHEDULING_AUTHORITY,
|
|
5
|
+
} from "./provider-model-refresh-schedule.mjs";
|
|
1
6
|
import { appendFileSync, mkdirSync } from "fs";
|
|
2
7
|
import { dirname, join } from "path";
|
|
3
8
|
import {
|
|
@@ -9,7 +14,6 @@ import { getConfigDir, readConfig } from "./config.mjs";
|
|
|
9
14
|
import { discoverCodexProviderModels } from "./codex-model-discovery.mjs";
|
|
10
15
|
import { discoverGrokProviderModels } from "./grok-acp-model-discovery.mjs";
|
|
11
16
|
import {
|
|
12
|
-
PROVIDER_MODEL_REFRESH_INTERVAL_MS,
|
|
13
17
|
readProviderModelParams,
|
|
14
18
|
writeProviderModelParams,
|
|
15
19
|
} from "./provider-model-params.mjs";
|
|
@@ -20,7 +24,6 @@ import {
|
|
|
20
24
|
requireProvider,
|
|
21
25
|
} from "./provider-registry.mjs";
|
|
22
26
|
|
|
23
|
-
const DEFAULT_STARTUP_DELAY_MS = 5 * 60 * 1000;
|
|
24
27
|
const MAX_ERROR_LENGTH = 500;
|
|
25
28
|
const PROVIDER_MODEL_REFRESH_OPERATION_LOG_NAME =
|
|
26
29
|
"provider_model_refresh.jsonl";
|
|
@@ -67,12 +70,6 @@ function appendProviderModelRefreshOperationLog(row, options = {}) {
|
|
|
67
70
|
return logPath;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
function parseTime(value) {
|
|
71
|
-
if (typeof value !== "string" || !value.trim()) return null;
|
|
72
|
-
const ms = Date.parse(value);
|
|
73
|
-
return Number.isFinite(ms) ? ms : null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
73
|
function normalizeRefreshProviderId(providerId) {
|
|
77
74
|
if (providerId === undefined || providerId === null) return null;
|
|
78
75
|
const normalized = String(providerId).trim().toLowerCase();
|
|
@@ -82,29 +79,6 @@ function normalizeRefreshProviderId(providerId) {
|
|
|
82
79
|
return normalized;
|
|
83
80
|
}
|
|
84
81
|
|
|
85
|
-
function providerRefreshTimestamp(params, providerId) {
|
|
86
|
-
const providerModels = params?.provider_models?.[providerId] || {};
|
|
87
|
-
const providerRuntime = params?.provider_runtime?.[providerId] || {};
|
|
88
|
-
return (
|
|
89
|
-
parseTime(providerModels.refreshed_at) ||
|
|
90
|
-
parseTime(providerModels.last_success_at) ||
|
|
91
|
-
parseTime(providerRuntime.last_success_at) ||
|
|
92
|
-
parseTime(providerRuntime.last_attempt_at) ||
|
|
93
|
-
null
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function providerNextRefreshAt(params, providerId, refreshIntervalMs) {
|
|
98
|
-
const lastRefreshMs = providerRefreshTimestamp(params, providerId);
|
|
99
|
-
if (!lastRefreshMs) return null;
|
|
100
|
-
return new Date(lastRefreshMs + refreshIntervalMs).toISOString();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function isProviderRefreshDue(params, providerId, nowMs, refreshIntervalMs) {
|
|
104
|
-
const lastRefreshMs = providerRefreshTimestamp(params, providerId);
|
|
105
|
-
return !lastRefreshMs || nowMs - lastRefreshMs >= refreshIntervalMs;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
82
|
export class ProviderModelRefreshRunner {
|
|
109
83
|
constructor(options = {}) {
|
|
110
84
|
this.configDir = options.configDir || null;
|
|
@@ -143,20 +117,18 @@ export class ProviderModelRefreshRunner {
|
|
|
143
117
|
this.setTimeoutFn = options.setTimeoutFn || setTimeout;
|
|
144
118
|
this.clearTimeoutFn = options.clearTimeoutFn || clearTimeout;
|
|
145
119
|
this.nowFn = options.nowFn || Date.now;
|
|
146
|
-
this.startupDelayMs = Number.isInteger(options.startupDelayMs)
|
|
147
|
-
? options.startupDelayMs
|
|
148
|
-
: DEFAULT_STARTUP_DELAY_MS;
|
|
149
|
-
this.refreshIntervalMs = Number.isInteger(options.refreshIntervalMs)
|
|
150
|
-
? options.refreshIntervalMs
|
|
151
|
-
: PROVIDER_MODEL_REFRESH_INTERVAL_MS;
|
|
152
120
|
this.logger = options.logger || console;
|
|
153
121
|
this.timer = null;
|
|
154
122
|
this.started = false;
|
|
155
|
-
this.
|
|
123
|
+
this.activeProviders = new Set();
|
|
124
|
+
this.nextRefreshAt = null;
|
|
125
|
+
this.scheduleZone = providerModelRefreshZone();
|
|
126
|
+
this.clockTimer = null;
|
|
156
127
|
}
|
|
157
128
|
|
|
158
129
|
start() {
|
|
159
130
|
if (!this.backgroundRefreshEnabled) {
|
|
131
|
+
this.persistSchedule();
|
|
160
132
|
return {
|
|
161
133
|
status: "disabled",
|
|
162
134
|
reason: this.backgroundRefreshDisabledReason,
|
|
@@ -167,103 +139,60 @@ export class ProviderModelRefreshRunner {
|
|
|
167
139
|
return { status: "already_started" };
|
|
168
140
|
}
|
|
169
141
|
this.started = true;
|
|
170
|
-
this.scheduleNext(
|
|
142
|
+
this.scheduleNext();
|
|
143
|
+
this.watchClock();
|
|
171
144
|
return { status: "started" };
|
|
172
145
|
}
|
|
173
146
|
|
|
174
147
|
stop() {
|
|
175
148
|
this.started = false;
|
|
149
|
+
if (this.clockTimer) this.clearTimeoutFn(this.clockTimer);
|
|
150
|
+
this.clockTimer = null;
|
|
151
|
+
this.nextRefreshAt = null;
|
|
152
|
+
this.persistSchedule();
|
|
176
153
|
if (this.timer) {
|
|
177
154
|
this.clearTimeoutFn(this.timer);
|
|
178
155
|
this.timer = null;
|
|
179
156
|
}
|
|
180
157
|
}
|
|
181
158
|
|
|
182
|
-
computeNextDelay(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
try {
|
|
187
|
-
const result = this.readParamsFn({ configDir: this.configDir });
|
|
188
|
-
const nowMs = this.nowFn();
|
|
189
|
-
const nextRefreshTimes = PRODUCT_PROVIDER_REFRESH_IDS.map((providerId) =>
|
|
190
|
-
providerRefreshTimestamp(result.params, providerId)
|
|
191
|
-
)
|
|
192
|
-
.filter((timeMs) => Number.isFinite(timeMs))
|
|
193
|
-
.map((timeMs) => timeMs + this.refreshIntervalMs);
|
|
194
|
-
if (nextRefreshTimes.length < PRODUCT_PROVIDER_REFRESH_IDS.length) {
|
|
195
|
-
return 0;
|
|
196
|
-
}
|
|
197
|
-
const dueNow = nextRefreshTimes.some((timeMs) => timeMs <= nowMs);
|
|
198
|
-
if (dueNow) return 0;
|
|
199
|
-
return Math.max(0, Math.min(...nextRefreshTimes) - nowMs);
|
|
200
|
-
} catch {
|
|
201
|
-
// Bad params should not create a timer storm; use the normal interval.
|
|
202
|
-
}
|
|
203
|
-
return Math.max(0, this.refreshIntervalMs);
|
|
159
|
+
computeNextDelay() {
|
|
160
|
+
const now = this.nowFn();
|
|
161
|
+
return nextProviderModelRefreshTime(now) - now;
|
|
204
162
|
}
|
|
205
163
|
|
|
206
164
|
getProviderRefreshStatus(providerId) {
|
|
207
165
|
const provider = normalizeRefreshProviderId(providerId);
|
|
208
|
-
const
|
|
209
|
-
const
|
|
166
|
+
const { params } = this.readParamsFn({ configDir: this.configDir });
|
|
167
|
+
const runtime = params.provider_runtime?.[provider] || {};
|
|
210
168
|
return {
|
|
211
169
|
provider,
|
|
212
|
-
refreshed_at:
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
refresh_due:
|
|
221
|
-
isProviderRefreshDue(
|
|
222
|
-
result.params,
|
|
223
|
-
provider,
|
|
224
|
-
this.nowFn(),
|
|
225
|
-
this.refreshIntervalMs
|
|
226
|
-
) === true,
|
|
227
|
-
scheduling_authority: "provider_last_refresh_plus_interval",
|
|
170
|
+
refreshed_at: params.provider_models?.[provider]?.refreshed_at || runtime.last_success_at || null,
|
|
171
|
+
last_attempt_at: runtime.last_attempt_at || null,
|
|
172
|
+
last_attempt_source: runtime.last_attempt_source || null,
|
|
173
|
+
last_attempt_status: runtime.probe_status || null,
|
|
174
|
+
process_cleanup: runtime.process_cleanup || null,
|
|
175
|
+
next_refresh_at: this.nextRefreshAt,
|
|
176
|
+
refresh_due: this.nextRefreshAt ? Date.parse(this.nextRefreshAt) <= this.nowFn() : false,
|
|
177
|
+
scheduling_authority: PROVIDER_MODEL_REFRESH_SCHEDULING_AUTHORITY,
|
|
228
178
|
};
|
|
229
179
|
}
|
|
230
180
|
|
|
231
181
|
getAllProviderRefreshStatus() {
|
|
232
|
-
|
|
233
|
-
return PRODUCT_PROVIDER_REFRESH_IDS.map((providerId) =>
|
|
234
|
-
this.getProviderRefreshStatus(providerId)
|
|
235
|
-
);
|
|
236
|
-
} catch {
|
|
237
|
-
return PRODUCT_PROVIDER_REFRESH_IDS.map((providerId) => ({
|
|
238
|
-
provider: providerId,
|
|
239
|
-
refreshed_at: null,
|
|
240
|
-
next_refresh_at: null,
|
|
241
|
-
refresh_due: true,
|
|
242
|
-
scheduling_authority: "provider_last_refresh_plus_interval",
|
|
243
|
-
}));
|
|
244
|
-
}
|
|
182
|
+
return PRODUCT_PROVIDER_REFRESH_IDS.map((providerId) => this.getProviderRefreshStatus(providerId));
|
|
245
183
|
}
|
|
246
184
|
|
|
247
|
-
getTargetProviderIds({ provider = null
|
|
185
|
+
getTargetProviderIds({ provider = null } = {}) {
|
|
248
186
|
const normalizedProvider = normalizeRefreshProviderId(provider);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
result.params,
|
|
259
|
-
providerId,
|
|
260
|
-
nowMs,
|
|
261
|
-
this.refreshIntervalMs
|
|
262
|
-
)
|
|
263
|
-
);
|
|
264
|
-
} catch {
|
|
265
|
-
return providerIds;
|
|
266
|
-
}
|
|
187
|
+
return normalizedProvider ? [normalizedProvider] : [...PRODUCT_PROVIDER_REFRESH_IDS];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
persistSchedule() {
|
|
191
|
+
this.writeParamsFn({ refresh: {
|
|
192
|
+
next_refresh_at: this.nextRefreshAt,
|
|
193
|
+
scheduling_authority: PROVIDER_MODEL_REFRESH_SCHEDULING_AUTHORITY,
|
|
194
|
+
time_zone: this.scheduleZone,
|
|
195
|
+
} }, { configDir: this.configDir });
|
|
267
196
|
}
|
|
268
197
|
|
|
269
198
|
assertProviderAvailable(config, providerId) {
|
|
@@ -512,7 +441,8 @@ export class ProviderModelRefreshRunner {
|
|
|
512
441
|
if (outcomes.some((outcome) => outcome.status === "success")) {
|
|
513
442
|
return { status: "refreshed", reason: null };
|
|
514
443
|
}
|
|
515
|
-
return { status: "skipped", reason: "
|
|
444
|
+
return { status: "skipped", reason: outcomes.every((outcome) => outcome.reason === "refresh_already_running")
|
|
445
|
+
? "refresh_already_running" : "all_providers_unavailable" };
|
|
516
446
|
}
|
|
517
447
|
|
|
518
448
|
async refreshProvider(providerId, options = {}) {
|
|
@@ -538,7 +468,6 @@ export class ProviderModelRefreshRunner {
|
|
|
538
468
|
|
|
539
469
|
async runOnce(options = {}) {
|
|
540
470
|
const targetProvider = normalizeRefreshProviderId(options.provider);
|
|
541
|
-
const force = options.force !== false;
|
|
542
471
|
const refreshSource =
|
|
543
472
|
typeof options.source === "string" && options.source.trim()
|
|
544
473
|
? options.source.trim()
|
|
@@ -547,56 +476,50 @@ export class ProviderModelRefreshRunner {
|
|
|
547
476
|
: "explicit";
|
|
548
477
|
const startedMs = this.nowFn();
|
|
549
478
|
const startedAt = operationLogTimestamp(startedMs);
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
provider: targetProvider || "all",
|
|
557
|
-
status: "skipped",
|
|
558
|
-
reason: "refresh_already_running",
|
|
559
|
-
source: refreshSource,
|
|
560
|
-
started_at: startedAt,
|
|
561
|
-
finished_at: operationLogTimestamp(finishedMs),
|
|
562
|
-
duration_ms: durationMs(startedMs, finishedMs),
|
|
563
|
-
},
|
|
564
|
-
{ configDir: this.configDir || getConfigDir() }
|
|
565
|
-
);
|
|
566
|
-
return { status: "skipped", reason: "refresh_already_running" };
|
|
567
|
-
}
|
|
568
|
-
this.running = true;
|
|
479
|
+
// Reserve the batch synchronously: overlapping calls never queue a second
|
|
480
|
+
// discovery, and one busy provider does not suppress the others.
|
|
481
|
+
const targetProviders = this.getTargetProviderIds({ provider: targetProvider });
|
|
482
|
+
const busyProviders = new Set(targetProviders.filter((id) => this.activeProviders.has(id)));
|
|
483
|
+
const reservedProviders = targetProviders.filter((id) => !busyProviders.has(id));
|
|
484
|
+
for (const id of reservedProviders) this.activeProviders.add(id);
|
|
569
485
|
const attemptedAt = startedAt;
|
|
570
486
|
try {
|
|
571
487
|
const config = this.readConfigFn();
|
|
572
|
-
const targetProviders = this.getTargetProviderIds({
|
|
573
|
-
provider: targetProvider,
|
|
574
|
-
force,
|
|
575
|
-
});
|
|
576
|
-
if (targetProviders.length === 0) {
|
|
577
|
-
return {
|
|
578
|
-
status: "skipped",
|
|
579
|
-
reason: "refresh_not_due",
|
|
580
|
-
providers: [],
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
const nextRefreshAt = new Date(
|
|
584
|
-
this.nowFn() + this.refreshIntervalMs
|
|
585
|
-
).toISOString();
|
|
586
488
|
const aggregatePatch = {
|
|
587
489
|
provider_models: {},
|
|
588
490
|
provider_runtime: {},
|
|
589
491
|
refresh: {
|
|
590
492
|
last_attempt_at: attemptedAt,
|
|
591
|
-
next_refresh_at: nextRefreshAt,
|
|
592
|
-
scheduling_authority: "provider_last_refresh_plus_interval",
|
|
593
493
|
},
|
|
594
494
|
};
|
|
495
|
+
// A skip owns admission evidence, not a later discovery completion.
|
|
496
|
+
// Publish all skips before the first await (including providers later in
|
|
497
|
+
// batch order), preserve their existing discovery metadata, and never
|
|
498
|
+
// carry those non-owned runtime blocks into the delayed batch write.
|
|
499
|
+
if (busyProviders.size) {
|
|
500
|
+
const { params } = this.readParamsFn({ configDir: this.configDir });
|
|
501
|
+
const skippedRuntime = {};
|
|
502
|
+
for (const providerId of busyProviders) {
|
|
503
|
+
skippedRuntime[providerId] = {
|
|
504
|
+
...params.provider_runtime?.[providerId],
|
|
505
|
+
probe_status: 'skipped', refresh_skip_reason: 'refresh_already_running',
|
|
506
|
+
last_attempt_at: attemptedAt, last_attempt_source: refreshSource,
|
|
507
|
+
last_error: null,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
this.writeParamsFn({ provider_runtime: skippedRuntime }, { configDir: this.configDir });
|
|
511
|
+
}
|
|
595
512
|
const specs = this.buildProviderSpecs(config, attemptedAt);
|
|
596
513
|
const outcomes = [];
|
|
597
514
|
for (const providerId of targetProviders) {
|
|
598
515
|
const spec = specs.get(providerId);
|
|
599
516
|
const providerStartedMs = this.nowFn();
|
|
517
|
+
if (busyProviders.has(providerId)) {
|
|
518
|
+
outcomes.push({ provider: providerId, status: 'skipped', reason: 'refresh_already_running',
|
|
519
|
+
model_count: 0, started_ms: providerStartedMs });
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
|
|
600
523
|
try {
|
|
601
524
|
this.assertProviderAvailable(config, providerId);
|
|
602
525
|
} catch (err) {
|
|
@@ -619,6 +542,7 @@ export class ProviderModelRefreshRunner {
|
|
|
619
542
|
outcomes.push({
|
|
620
543
|
provider: spec.provider,
|
|
621
544
|
status: "success",
|
|
545
|
+
process_cleanup: discovery.process_cleanup,
|
|
622
546
|
model_count: Array.isArray(discovery.models)
|
|
623
547
|
? discovery.models.length
|
|
624
548
|
: 0,
|
|
@@ -635,12 +559,18 @@ export class ProviderModelRefreshRunner {
|
|
|
635
559
|
outcomes.push({
|
|
636
560
|
provider: spec.provider,
|
|
637
561
|
status: "error",
|
|
562
|
+
process_cleanup: err.details?.process_cleanup,
|
|
638
563
|
model_count: 0,
|
|
639
564
|
error: err,
|
|
640
565
|
started_ms: providerStartedMs,
|
|
641
566
|
});
|
|
642
567
|
}
|
|
643
568
|
}
|
|
569
|
+
for (const outcome of outcomes) {
|
|
570
|
+
if (busyProviders.has(outcome.provider)) continue;
|
|
571
|
+
aggregatePatch.provider_runtime[outcome.provider].last_attempt_source = refreshSource;
|
|
572
|
+
aggregatePatch.provider_runtime[outcome.provider].process_cleanup = outcome.process_cleanup || null;
|
|
573
|
+
}
|
|
644
574
|
const overall = this.buildOverallStatus(outcomes);
|
|
645
575
|
if (overall.status === "refreshed") {
|
|
646
576
|
aggregatePatch.refresh.last_success_at = attemptedAt;
|
|
@@ -663,15 +593,16 @@ export class ProviderModelRefreshRunner {
|
|
|
663
593
|
duration_ms: durationMs(outcome.started_ms, finishedMs),
|
|
664
594
|
model_count: outcome.model_count,
|
|
665
595
|
config_options_count: outcome.config_options_count ?? null,
|
|
596
|
+
process_cleanup: outcome.process_cleanup,
|
|
666
597
|
last_success_at:
|
|
667
598
|
outcome.status === "success" ? attemptedAt : undefined,
|
|
668
|
-
next_refresh_at: nextRefreshAt,
|
|
669
|
-
scheduling_authority:
|
|
599
|
+
next_refresh_at: this.nextRefreshAt,
|
|
600
|
+
scheduling_authority: PROVIDER_MODEL_REFRESH_SCHEDULING_AUTHORITY,
|
|
670
601
|
params_path: writeResult.path,
|
|
671
602
|
error:
|
|
672
603
|
outcome.status === "success"
|
|
673
604
|
? null
|
|
674
|
-
: sanitizeOperationLogError(outcome.error),
|
|
605
|
+
: outcome.error ? sanitizeOperationLogError(outcome.error) : null,
|
|
675
606
|
},
|
|
676
607
|
{ configDir: this.configDir || getConfigDir() }
|
|
677
608
|
);
|
|
@@ -699,14 +630,15 @@ export class ProviderModelRefreshRunner {
|
|
|
699
630
|
status: outcome.status,
|
|
700
631
|
reason: outcome.reason || null,
|
|
701
632
|
model_count: outcome.model_count,
|
|
633
|
+
process_cleanup: outcome.process_cleanup || null,
|
|
702
634
|
error:
|
|
703
635
|
outcome.status === "success"
|
|
704
636
|
? null
|
|
705
|
-
: truncateError(outcome.error),
|
|
637
|
+
: outcome.error ? truncateError(outcome.error) : null,
|
|
706
638
|
})),
|
|
707
639
|
};
|
|
708
640
|
} finally {
|
|
709
|
-
this.
|
|
641
|
+
for (const id of reservedProviders) this.activeProviders.delete(id);
|
|
710
642
|
}
|
|
711
643
|
}
|
|
712
644
|
|
|
@@ -721,28 +653,51 @@ export class ProviderModelRefreshRunner {
|
|
|
721
653
|
background_refresh_started: this.started === true,
|
|
722
654
|
background_refresh_timer_active: Boolean(this.timer),
|
|
723
655
|
explicit_refresh_available: true,
|
|
724
|
-
refresh_running: this.
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
scheduling_authority:
|
|
656
|
+
refresh_running: this.activeProviders.size > 0,
|
|
657
|
+
next_refresh_at: this.nextRefreshAt,
|
|
658
|
+
time_zone: this.scheduleZone,
|
|
659
|
+
scheduling_authority: PROVIDER_MODEL_REFRESH_SCHEDULING_AUTHORITY,
|
|
728
660
|
providers: this.getAllProviderRefreshStatus(),
|
|
729
661
|
};
|
|
730
662
|
}
|
|
731
663
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
664
|
+
// A bounded clock check only recalculates after a zone change. It never
|
|
665
|
+
// retries discovery and does not append an operation result.
|
|
666
|
+
watchClock() {
|
|
667
|
+
if (!this.started) return;
|
|
668
|
+
this.clockTimer = this.setTimeoutFn(() => {
|
|
669
|
+
this.clockTimer = null;
|
|
670
|
+
const zone = providerModelRefreshZone();
|
|
671
|
+
if (zone !== this.scheduleZone) {
|
|
672
|
+
if (this.timer) this.clearTimeoutFn(this.timer);
|
|
673
|
+
this.timer = null;
|
|
740
674
|
this.scheduleNext();
|
|
741
675
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
676
|
+
this.watchClock();
|
|
677
|
+
}, 60_000);
|
|
678
|
+
this.clockTimer?.unref?.();
|
|
746
679
|
}
|
|
747
680
|
|
|
681
|
+
scheduleNext() {
|
|
682
|
+
if (!this.started || this.timer) return;
|
|
683
|
+
const now = this.nowFn();
|
|
684
|
+
const next = nextProviderModelRefreshTime(now);
|
|
685
|
+
this.scheduleZone = providerModelRefreshZone();
|
|
686
|
+
this.nextRefreshAt = new Date(next).toISOString();
|
|
687
|
+
this.persistSchedule();
|
|
688
|
+
const timer = this.setTimeoutFn(async () => {
|
|
689
|
+
if (!this.started || this.timer !== timer) return;
|
|
690
|
+
this.timer = null;
|
|
691
|
+
// Advance the calendar before discovery so results and Preferences share
|
|
692
|
+
// the next real timer even while a provider is unavailable or slow.
|
|
693
|
+
this.scheduleNext();
|
|
694
|
+
// Timers are notifications, not catch-up authority. Permit only normal
|
|
695
|
+
// sub-second dispatch jitter at the scheduled instant; a suspended or
|
|
696
|
+
// delayed Host skips the expired slot and keeps tomorrow's timer.
|
|
697
|
+
if (this.nowFn() - next > 1000 || this.nowFn() < next) return;
|
|
698
|
+
await this.runOnce({ source: "background", force: true });
|
|
699
|
+
}, next - now);
|
|
700
|
+
this.timer = timer;
|
|
701
|
+
this.timer?.unref?.();
|
|
702
|
+
}
|
|
748
703
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Calendar arithmetic intentionally uses the Host's local zone (including DST).
|
|
2
|
+
export const PROVIDER_MODEL_REFRESH_SCHEDULING_AUTHORITY = "host_local_daily_0900";
|
|
3
|
+
export function nextProviderModelRefreshTime(nowMs) {
|
|
4
|
+
const next = new Date(nowMs);
|
|
5
|
+
next.setHours(9, 0, 0, 0);
|
|
6
|
+
if (next.getTime() <= nowMs) next.setDate(next.getDate() + 1);
|
|
7
|
+
return next.getTime();
|
|
8
|
+
}
|
|
9
|
+
export function providerModelRefreshZone() {
|
|
10
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
11
|
+
}
|
|
@@ -39,6 +39,7 @@ export function assertHostSessionFixtureStart({
|
|
|
39
39
|
sessionId,
|
|
40
40
|
resumeSessionId,
|
|
41
41
|
fixtureResumeGeneration,
|
|
42
|
+
fork = false,
|
|
42
43
|
}) {
|
|
43
44
|
const normalizedSessionId = normalizeResumeIdentity(sessionId);
|
|
44
45
|
if (!normalizedSessionId) {
|
|
@@ -49,20 +50,25 @@ export function assertHostSessionFixtureStart({
|
|
|
49
50
|
fixtureResumeGeneration,
|
|
50
51
|
normalizedResumeSessionId ? 1 : 0
|
|
51
52
|
);
|
|
53
|
+
const branchStart = fork === true;
|
|
54
|
+
if (branchStart && !normalizedResumeSessionId) {
|
|
55
|
+
throw new Error("host-session fixture branch requires a source sessionId");
|
|
56
|
+
}
|
|
52
57
|
if (
|
|
53
58
|
normalizedResumeSessionId &&
|
|
54
|
-
normalizedResumeSessionId !== normalizedSessionId
|
|
59
|
+
normalizedResumeSessionId !== normalizedSessionId &&
|
|
60
|
+
!branchStart
|
|
55
61
|
) {
|
|
56
62
|
throw new Error(
|
|
57
63
|
"host-session fixture resume identity must equal sessionId"
|
|
58
64
|
);
|
|
59
65
|
}
|
|
60
|
-
if (normalizedResumeSessionId && generation < 1) {
|
|
66
|
+
if (normalizedResumeSessionId && !branchStart && generation < 1) {
|
|
61
67
|
throw new Error(
|
|
62
68
|
"resumed host-session fixture requires a positive generation"
|
|
63
69
|
);
|
|
64
70
|
}
|
|
65
|
-
if (!normalizedResumeSessionId && generation !== 0) {
|
|
71
|
+
if ((!normalizedResumeSessionId || branchStart) && generation !== 0) {
|
|
66
72
|
throw new Error("fresh host-session fixture requires generation zero");
|
|
67
73
|
}
|
|
68
74
|
return generation;
|