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,8 +1,11 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "crypto";
|
|
2
2
|
import {
|
|
3
3
|
chmodSync,
|
|
4
|
+
closeSync,
|
|
4
5
|
existsSync,
|
|
6
|
+
fsyncSync,
|
|
5
7
|
mkdirSync,
|
|
8
|
+
openSync,
|
|
6
9
|
readFileSync,
|
|
7
10
|
realpathSync,
|
|
8
11
|
renameSync,
|
|
@@ -32,6 +35,8 @@ export const SESSION_LOOP_LIFECYCLE_FILENAME =
|
|
|
32
35
|
export const SESSION_LOOP_RECONCILIATION_ERROR_CODE =
|
|
33
36
|
"session_loop_lifecycle_reconciliation_failed";
|
|
34
37
|
export const SESSION_LOOP_DEFINITION_REVISION_ALGORITHM = "sha256";
|
|
38
|
+
export const SESSION_LOOP_DEFINITION_UNAVAILABLE_GRACE_MS = 10_000;
|
|
39
|
+
export const SESSION_LOOP_INCIDENT_HISTORY_LIMIT = 16;
|
|
35
40
|
|
|
36
41
|
const LOOP_DEFINITION_ROOT_KEYS = new Set(["version", "loops"]);
|
|
37
42
|
const LOOP_DEFINITION_ALLOWED_KEYS = new Set([
|
|
@@ -81,6 +86,15 @@ const LOOP_LIFECYCLE_STATE_ALLOWED_KEYS = new Set([
|
|
|
81
86
|
"dispatch_count",
|
|
82
87
|
"skip_count",
|
|
83
88
|
"last_dispatched_message_id",
|
|
89
|
+
"definition_snapshot",
|
|
90
|
+
"definition_generation",
|
|
91
|
+
"availability_state",
|
|
92
|
+
"unavailable_since",
|
|
93
|
+
"unavailable_observations",
|
|
94
|
+
"candidate_definition",
|
|
95
|
+
"candidate_revision",
|
|
96
|
+
"incidents",
|
|
97
|
+
"migration_receipt",
|
|
84
98
|
"updated_at",
|
|
85
99
|
"evidence",
|
|
86
100
|
]);
|
|
@@ -104,6 +118,18 @@ const LOOP_PENDING_OCCURRENCE_ALLOWED_KEYS = new Set([
|
|
|
104
118
|
]);
|
|
105
119
|
const ACTIVE_AGENT_FOLDER_MUTATIONS = new Set();
|
|
106
120
|
|
|
121
|
+
const LOOP_INCIDENT_ALLOWED_KEYS = new Set([
|
|
122
|
+
"id",
|
|
123
|
+
"generation",
|
|
124
|
+
"reason",
|
|
125
|
+
"definition_revision",
|
|
126
|
+
"candidate_revision",
|
|
127
|
+
"created_at",
|
|
128
|
+
"resolved_at",
|
|
129
|
+
"recovery_options",
|
|
130
|
+
"notification_key",
|
|
131
|
+
]);
|
|
132
|
+
|
|
107
133
|
const INTERVAL_UNITS_MS = Object.freeze({
|
|
108
134
|
s: 1000,
|
|
109
135
|
m: 60 * 1000,
|
|
@@ -213,6 +239,60 @@ function normalizeNullableString(value, fieldName) {
|
|
|
213
239
|
return value.trim() || null;
|
|
214
240
|
}
|
|
215
241
|
|
|
242
|
+
function normalizeOptionalNonNegativeInteger(value, fieldName, fallback = 0) {
|
|
243
|
+
if (value === undefined || value === null) return fallback;
|
|
244
|
+
return normalizeNonNegativeInteger(value, fieldName);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function safeDefinitionCommandSummary(commandText) {
|
|
248
|
+
const normalized = String(commandText || "").replace(/\s+/g, " ").trim();
|
|
249
|
+
if (!normalized) return "[command unavailable]";
|
|
250
|
+
return `[redacted command sha256:${hashText(normalized).slice(0, 16)}]`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function snapshotDefinition(definition) {
|
|
254
|
+
return normalizeLoopDefinition(
|
|
255
|
+
{
|
|
256
|
+
...definition,
|
|
257
|
+
command_text: safeDefinitionCommandSummary(definition.command_text),
|
|
258
|
+
},
|
|
259
|
+
{ existing: definition }
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function normalizeDefinitionSnapshot(value, fieldName) {
|
|
264
|
+
if (value === null || value === undefined) return null;
|
|
265
|
+
try {
|
|
266
|
+
return snapshotDefinition(value);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
throw new Error(`${fieldName} is invalid: ${err.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeLoopIncident(value) {
|
|
273
|
+
if (!isObjectRecord(value)) throw new Error("Loop incident must be an object");
|
|
274
|
+
assertAllowedObjectKeys(value, LOOP_INCIDENT_ALLOWED_KEYS, "Loop incident");
|
|
275
|
+
const recoveryOptions = Array.isArray(value.recovery_options)
|
|
276
|
+
? value.recovery_options.map((entry) =>
|
|
277
|
+
normalizeRequiredString(entry, "loop incident recovery option")
|
|
278
|
+
)
|
|
279
|
+
: [];
|
|
280
|
+
return {
|
|
281
|
+
id: normalizeRequiredString(value.id, "loop incident id"),
|
|
282
|
+
generation: normalizeNonNegativeInteger(value.generation, "loop incident generation"),
|
|
283
|
+
reason: normalizeRequiredString(value.reason, "loop incident reason"),
|
|
284
|
+
definition_revision: normalizeDefinitionRevision(value.definition_revision),
|
|
285
|
+
candidate_revision: normalizeDefinitionRevision(value.candidate_revision),
|
|
286
|
+
created_at: normalizeOptionalIsoTimestamp(value.created_at, "loop incident created_at"),
|
|
287
|
+
resolved_at: normalizeOptionalIsoTimestamp(value.resolved_at, "loop incident resolved_at"),
|
|
288
|
+
recovery_options: recoveryOptions,
|
|
289
|
+
notification_key: normalizeRequiredString(
|
|
290
|
+
value.notification_key,
|
|
291
|
+
"loop incident notification_key"
|
|
292
|
+
),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
216
296
|
export function normalizeSessionLoopCommandText(value) {
|
|
217
297
|
return normalizeRequiredString(value, "loop command text").replace(/\s+/g, " ");
|
|
218
298
|
}
|
|
@@ -661,6 +741,7 @@ function normalizeLifecycleState(value) {
|
|
|
661
741
|
"active",
|
|
662
742
|
"disabled",
|
|
663
743
|
"suspended",
|
|
744
|
+
"provisional",
|
|
664
745
|
"expired",
|
|
665
746
|
"cancelled",
|
|
666
747
|
].includes(status)) {
|
|
@@ -701,6 +782,38 @@ function normalizeLifecycleState(value) {
|
|
|
701
782
|
value.last_dispatched_message_id,
|
|
702
783
|
"loop last_dispatched_message_id"
|
|
703
784
|
),
|
|
785
|
+
definition_snapshot: normalizeDefinitionSnapshot(
|
|
786
|
+
value.definition_snapshot,
|
|
787
|
+
"loop definition_snapshot"
|
|
788
|
+
),
|
|
789
|
+
definition_generation: normalizeOptionalNonNegativeInteger(
|
|
790
|
+
value.definition_generation,
|
|
791
|
+
"loop definition_generation"
|
|
792
|
+
),
|
|
793
|
+
availability_state: normalizeRequiredString(
|
|
794
|
+
value.availability_state || "available",
|
|
795
|
+
"loop availability_state"
|
|
796
|
+
),
|
|
797
|
+
unavailable_since: normalizeOptionalIsoTimestamp(
|
|
798
|
+
value.unavailable_since,
|
|
799
|
+
"loop unavailable_since"
|
|
800
|
+
),
|
|
801
|
+
unavailable_observations: normalizeOptionalNonNegativeInteger(
|
|
802
|
+
value.unavailable_observations,
|
|
803
|
+
"loop unavailable_observations"
|
|
804
|
+
),
|
|
805
|
+
candidate_definition: normalizeDefinitionSnapshot(
|
|
806
|
+
value.candidate_definition,
|
|
807
|
+
"loop candidate_definition"
|
|
808
|
+
),
|
|
809
|
+
candidate_revision: normalizeDefinitionRevision(value.candidate_revision),
|
|
810
|
+
incidents: Array.isArray(value.incidents)
|
|
811
|
+
? value.incidents.map(normalizeLoopIncident).slice(-SESSION_LOOP_INCIDENT_HISTORY_LIMIT)
|
|
812
|
+
: [],
|
|
813
|
+
migration_receipt: normalizeNullableString(
|
|
814
|
+
value.migration_receipt,
|
|
815
|
+
"loop migration_receipt"
|
|
816
|
+
),
|
|
704
817
|
updated_at: normalizeOptionalIsoTimestamp(value.updated_at, "loop updated_at"),
|
|
705
818
|
evidence,
|
|
706
819
|
};
|
|
@@ -849,8 +962,20 @@ export class SessionLoopLifecycleStore {
|
|
|
849
962
|
writeFileSync(temporaryPath, serializeLifecyclePayload(normalized), {
|
|
850
963
|
mode: 0o600,
|
|
851
964
|
});
|
|
965
|
+
const temporaryFd = openSync(temporaryPath, "r");
|
|
966
|
+
try {
|
|
967
|
+
fsyncSync(temporaryFd);
|
|
968
|
+
} finally {
|
|
969
|
+
closeSync(temporaryFd);
|
|
970
|
+
}
|
|
852
971
|
chmodSync(temporaryPath, 0o600);
|
|
853
972
|
renameSync(temporaryPath, this.lifecyclePath);
|
|
973
|
+
const directoryFd = openSync(this.configDir, "r");
|
|
974
|
+
try {
|
|
975
|
+
fsyncSync(directoryFd);
|
|
976
|
+
} finally {
|
|
977
|
+
closeSync(directoryFd);
|
|
978
|
+
}
|
|
854
979
|
chmodSync(this.lifecyclePath, 0o600);
|
|
855
980
|
return this.read();
|
|
856
981
|
}
|
|
@@ -898,6 +1023,15 @@ function createRuntimeState(
|
|
|
898
1023
|
dispatch_count: 0,
|
|
899
1024
|
skip_count: 0,
|
|
900
1025
|
last_dispatched_message_id: null,
|
|
1026
|
+
definition_snapshot: snapshotDefinition(definition),
|
|
1027
|
+
definition_generation: 0,
|
|
1028
|
+
availability_state: "available",
|
|
1029
|
+
unavailable_since: null,
|
|
1030
|
+
unavailable_observations: 0,
|
|
1031
|
+
candidate_definition: null,
|
|
1032
|
+
candidate_revision: null,
|
|
1033
|
+
incidents: [],
|
|
1034
|
+
migration_receipt: null,
|
|
901
1035
|
updated_at: timestamp,
|
|
902
1036
|
evidence: [
|
|
903
1037
|
{
|
|
@@ -918,12 +1052,22 @@ export class SessionLoopDefinitionService {
|
|
|
918
1052
|
lifecycleStore = new SessionLoopLifecycleStore(),
|
|
919
1053
|
clock = () => new Date(),
|
|
920
1054
|
recheckMs = 5000,
|
|
1055
|
+
unavailableGraceMs = SESSION_LOOP_DEFINITION_UNAVAILABLE_GRACE_MS,
|
|
921
1056
|
} = {}) {
|
|
922
1057
|
this.store = store;
|
|
923
1058
|
this.lifecycleStore = lifecycleStore;
|
|
924
1059
|
this.clock = clock;
|
|
925
1060
|
this.recheckMs = recheckMs;
|
|
1061
|
+
if (
|
|
1062
|
+
!Number.isInteger(unavailableGraceMs) ||
|
|
1063
|
+
unavailableGraceMs < 5_000 ||
|
|
1064
|
+
unavailableGraceMs > 30_000
|
|
1065
|
+
) {
|
|
1066
|
+
throw new Error("Loop definition unavailable grace must be 5000..30000ms");
|
|
1067
|
+
}
|
|
1068
|
+
this.unavailableGraceMs = unavailableGraceMs;
|
|
926
1069
|
this.sessions = new Map();
|
|
1070
|
+
this.folderSnapshots = new Map();
|
|
927
1071
|
}
|
|
928
1072
|
|
|
929
1073
|
createReconciliationError(
|
|
@@ -986,6 +1130,21 @@ export class SessionLoopDefinitionService {
|
|
|
986
1130
|
}
|
|
987
1131
|
}
|
|
988
1132
|
|
|
1133
|
+
readDefinitionsForReconciliation(agentFolder) {
|
|
1134
|
+
try {
|
|
1135
|
+
return this.store.readDefinitions(agentFolder);
|
|
1136
|
+
} catch (err) {
|
|
1137
|
+
const paths = this.store.resolveLoopsPath(agentFolder);
|
|
1138
|
+
return {
|
|
1139
|
+
...paths,
|
|
1140
|
+
version: SESSION_LOOP_DEFINITIONS_VERSION,
|
|
1141
|
+
loops: [],
|
|
1142
|
+
file_exists: existsSync(paths.loopsPath),
|
|
1143
|
+
read_error: err?.message || String(err),
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
989
1148
|
appendTerminalEvidence(session, entry) {
|
|
990
1149
|
session.terminal_evidence = [
|
|
991
1150
|
...(session.terminal_evidence || []),
|
|
@@ -1019,32 +1178,156 @@ export class SessionLoopDefinitionService {
|
|
|
1019
1178
|
});
|
|
1020
1179
|
}
|
|
1021
1180
|
|
|
1022
|
-
|
|
1181
|
+
adoptDefinitionRevision({ state, definition, generation, timestamp }) {
|
|
1023
1182
|
const occurrenceId = state.pending_occurrence?.occurrence_id || null;
|
|
1024
1183
|
if (occurrenceId) {
|
|
1025
1184
|
this.appendEvidence(state, "cancelled", {
|
|
1026
1185
|
occurrenceId,
|
|
1027
|
-
reason:
|
|
1186
|
+
reason: "definition_updated_pending_cancelled",
|
|
1028
1187
|
recordedAt: timestamp,
|
|
1029
1188
|
});
|
|
1030
1189
|
}
|
|
1031
1190
|
state.definition_revision = definitionRevision(definition);
|
|
1032
|
-
state.
|
|
1033
|
-
state.
|
|
1034
|
-
state.
|
|
1035
|
-
state.
|
|
1191
|
+
state.definition_snapshot = snapshotDefinition(definition);
|
|
1192
|
+
state.definition_generation = generation;
|
|
1193
|
+
state.availability_state = "available";
|
|
1194
|
+
state.unavailable_since = null;
|
|
1195
|
+
state.unavailable_observations = 0;
|
|
1196
|
+
state.candidate_definition = null;
|
|
1197
|
+
state.candidate_revision = null;
|
|
1036
1198
|
state.pending_occurrence = null;
|
|
1037
|
-
state.suspension_reason = reason;
|
|
1038
1199
|
state.last_error = null;
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1200
|
+
state.status = state.enabled ? "active" : "disabled";
|
|
1201
|
+
state.suspension_reason = state.enabled ? null : "owner_disabled";
|
|
1202
|
+
state.cadence_anchor_at = state.enabled ? definition.start_at || timestamp : null;
|
|
1203
|
+
state.next_run_at = state.enabled
|
|
1204
|
+
? Date.parse(definition.start_at || "") > Date.parse(timestamp)
|
|
1205
|
+
? definition.start_at
|
|
1206
|
+
: computeFirstCadenceBoundaryAfter({
|
|
1207
|
+
anchorAt: state.cadence_anchor_at,
|
|
1208
|
+
intervalMs: definition.interval_ms,
|
|
1209
|
+
after: timestamp,
|
|
1210
|
+
})
|
|
1211
|
+
: null;
|
|
1212
|
+
// Historical review incidents are evidence, never ongoing execution authority.
|
|
1213
|
+
for (const incident of state.incidents || []) {
|
|
1214
|
+
if (!incident.resolved_at) incident.resolved_at = timestamp;
|
|
1215
|
+
}
|
|
1216
|
+
this.appendEvidence(state, state.enabled ? "session-resumed" : "disabled", {
|
|
1217
|
+
reason: "definition_updated_intent_preserved_fresh_cadence",
|
|
1042
1218
|
recordedAt: timestamp,
|
|
1043
1219
|
});
|
|
1044
|
-
return state;
|
|
1045
1220
|
}
|
|
1046
1221
|
|
|
1047
|
-
|
|
1222
|
+
definitionSetSignature(storage) {
|
|
1223
|
+
return hashText(
|
|
1224
|
+
JSON.stringify(
|
|
1225
|
+
{
|
|
1226
|
+
file_exists: storage.file_exists === true,
|
|
1227
|
+
read_error: storage.read_error ? hashText(storage.read_error) : null,
|
|
1228
|
+
definitions: storage.loops
|
|
1229
|
+
.map((definition) => [definition.id, definitionRevision(definition)])
|
|
1230
|
+
.sort((left, right) => left[0].localeCompare(right[0])),
|
|
1231
|
+
}
|
|
1232
|
+
)
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
nextFolderGeneration(storage, lifecycle) {
|
|
1237
|
+
const signature = this.definitionSetSignature(storage);
|
|
1238
|
+
const current = this.folderSnapshots.get(storage.agentFolder);
|
|
1239
|
+
if (current?.signature === signature) return current.generation;
|
|
1240
|
+
const durableGeneration = lifecycle.sessions
|
|
1241
|
+
.filter((session) => session.agent_folder === storage.agentFolder)
|
|
1242
|
+
.flatMap((session) => session.loops)
|
|
1243
|
+
.reduce((maximum, state) => Math.max(maximum, state.definition_generation || 0), 0);
|
|
1244
|
+
const generation = Math.max(current?.generation || 0, durableGeneration) + 1;
|
|
1245
|
+
this.folderSnapshots.set(storage.agentFolder, { signature, generation });
|
|
1246
|
+
return generation;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
currentIncident(state) {
|
|
1250
|
+
return [...(state.incidents || [])].reverse().find((incident) => !incident.resolved_at) || null;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
resolveCurrentIncident(state, timestamp) {
|
|
1254
|
+
const incident = this.currentIncident(state);
|
|
1255
|
+
if (incident) incident.resolved_at = timestamp;
|
|
1256
|
+
return incident;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
createIncident({ session, state, reason, generation, candidateRevision = null, timestamp }) {
|
|
1260
|
+
const unresolvedEquivalent = (state.incidents || []).find(
|
|
1261
|
+
(incident) =>
|
|
1262
|
+
!incident.resolved_at &&
|
|
1263
|
+
incident.reason === reason &&
|
|
1264
|
+
incident.definition_revision === state.definition_revision &&
|
|
1265
|
+
incident.candidate_revision === candidateRevision
|
|
1266
|
+
);
|
|
1267
|
+
if (unresolvedEquivalent) {
|
|
1268
|
+
return { incident: unresolvedEquivalent, created: false };
|
|
1269
|
+
}
|
|
1270
|
+
const notificationKey = hashText(
|
|
1271
|
+
[generation, session.agent_folder, session.host_session_id, state.id, reason,
|
|
1272
|
+
candidateRevision || state.definition_revision || "none"].join(":")
|
|
1273
|
+
);
|
|
1274
|
+
const existing = (state.incidents || []).find(
|
|
1275
|
+
(incident) => incident.notification_key === notificationKey
|
|
1276
|
+
);
|
|
1277
|
+
if (existing) return { incident: existing, created: false };
|
|
1278
|
+
const incident = normalizeLoopIncident({
|
|
1279
|
+
id: `loop_incident_${notificationKey.slice(0, 24)}`,
|
|
1280
|
+
generation,
|
|
1281
|
+
reason,
|
|
1282
|
+
definition_revision: state.definition_revision,
|
|
1283
|
+
candidate_revision: candidateRevision,
|
|
1284
|
+
created_at: timestamp,
|
|
1285
|
+
resolved_at: null,
|
|
1286
|
+
recovery_options:
|
|
1287
|
+
reason === "legacy_reconciliation_intent_loss"
|
|
1288
|
+
? ["restore_previous_enabled_state", "keep_disabled", "open_loop"]
|
|
1289
|
+
: ["restore", "keep_disabled", "open_loop"],
|
|
1290
|
+
notification_key: notificationKey,
|
|
1291
|
+
});
|
|
1292
|
+
state.incidents = [...(state.incidents || []), incident].slice(
|
|
1293
|
+
-SESSION_LOOP_INCIDENT_HISTORY_LIMIT
|
|
1294
|
+
);
|
|
1295
|
+
return { incident, created: true };
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
classifyLegacyIntentLoss({ session, state, timestamp, generation }) {
|
|
1299
|
+
if (state.migration_receipt) return null;
|
|
1300
|
+
const discovered = state.evidence?.some(
|
|
1301
|
+
(entry) => entry.reason === "shared_agent_folder_definition_discovered_default_off"
|
|
1302
|
+
);
|
|
1303
|
+
const removed = session.terminal_evidence?.some(
|
|
1304
|
+
(entry) =>
|
|
1305
|
+
entry.loop_id === state.id &&
|
|
1306
|
+
entry.reason === "shared_agent_folder_definition_removed"
|
|
1307
|
+
);
|
|
1308
|
+
if (!discovered || !removed) return null;
|
|
1309
|
+
state.migration_receipt = `p254:${hashText(
|
|
1310
|
+
`${session.host_session_id}:${state.id}:legacy_reconciliation_intent_loss`
|
|
1311
|
+
).slice(0, 24)}`;
|
|
1312
|
+
state.status = "suspended";
|
|
1313
|
+
state.availability_state = "available";
|
|
1314
|
+
state.suspension_reason = "legacy_reconciliation_intent_loss";
|
|
1315
|
+
const created = this.createIncident({
|
|
1316
|
+
session,
|
|
1317
|
+
state,
|
|
1318
|
+
reason: "legacy_reconciliation_intent_loss",
|
|
1319
|
+
generation,
|
|
1320
|
+
timestamp,
|
|
1321
|
+
});
|
|
1322
|
+
return created.created ? created.incident : null;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
reconcileLifecycleSession({
|
|
1326
|
+
session,
|
|
1327
|
+
storage,
|
|
1328
|
+
generation,
|
|
1329
|
+
explicitDeletedLoopId = null,
|
|
1330
|
+
}) {
|
|
1048
1331
|
let storedCanonicalFolder = session.agent_folder;
|
|
1049
1332
|
if (storedCanonicalFolder !== storage.agentFolder) {
|
|
1050
1333
|
try {
|
|
@@ -1060,6 +1343,7 @@ export class SessionLoopDefinitionService {
|
|
|
1060
1343
|
const timestamp = nowIso(this.clock);
|
|
1061
1344
|
const statesById = new Map(session.loops.map((state) => [state.id, state]));
|
|
1062
1345
|
const nextStates = [];
|
|
1346
|
+
const createdIncidents = [];
|
|
1063
1347
|
let mutated = false;
|
|
1064
1348
|
if (session.agent_folder !== storage.agentFolder) {
|
|
1065
1349
|
session.agent_folder = storage.agentFolder;
|
|
@@ -1074,17 +1358,81 @@ export class SessionLoopDefinitionService {
|
|
|
1074
1358
|
timestamp,
|
|
1075
1359
|
reason: "shared_agent_folder_definition_discovered_default_off",
|
|
1076
1360
|
});
|
|
1361
|
+
state.definition_generation = generation;
|
|
1077
1362
|
mutated = true;
|
|
1078
|
-
} else if (state.definition_revision !== revision
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
state
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
}
|
|
1363
|
+
} else if (state.definition_revision !== revision ||
|
|
1364
|
+
state.availability_state === "changed_review" ||
|
|
1365
|
+
state.suspension_reason === "definition_changed_requires_review") {
|
|
1366
|
+
if (!state.definition_revision) {
|
|
1367
|
+
state.definition_revision = revision;
|
|
1368
|
+
state.definition_snapshot = snapshotDefinition(definition);
|
|
1369
|
+
state.definition_generation = generation;
|
|
1370
|
+
state.availability_state = "available";
|
|
1371
|
+
state.suspension_reason = state.enabled ? null : state.suspension_reason;
|
|
1372
|
+
} else {
|
|
1373
|
+
this.adoptDefinitionRevision({ state, definition, generation, timestamp });
|
|
1374
|
+
}
|
|
1375
|
+
mutated = true;
|
|
1376
|
+
} else {
|
|
1377
|
+
const wasUnavailable = state.availability_state !== "available";
|
|
1378
|
+
const generationChanged = state.definition_generation !== generation;
|
|
1379
|
+
const snapshotMissing = !state.definition_snapshot;
|
|
1380
|
+
state.definition_snapshot = snapshotDefinition(definition);
|
|
1381
|
+
state.definition_generation = Math.max(state.definition_generation || 0, generation);
|
|
1382
|
+
state.availability_state = "available";
|
|
1383
|
+
state.unavailable_since = null;
|
|
1384
|
+
state.unavailable_observations = 0;
|
|
1385
|
+
state.candidate_definition = null;
|
|
1386
|
+
state.candidate_revision = null;
|
|
1387
|
+
state.last_error = null;
|
|
1388
|
+
if (generationChanged || snapshotMissing) mutated = true;
|
|
1389
|
+
if (wasUnavailable && state.enabled === true) {
|
|
1390
|
+
const now = nowIso(this.clock);
|
|
1391
|
+
state.status = "active";
|
|
1392
|
+
state.suspension_reason = null;
|
|
1393
|
+
state.cadence_anchor_at = now;
|
|
1394
|
+
state.next_run_at = computeFirstCadenceBoundaryAfter({
|
|
1395
|
+
anchorAt: now,
|
|
1396
|
+
intervalMs: definition.interval_ms,
|
|
1397
|
+
after: now,
|
|
1398
|
+
});
|
|
1399
|
+
state.pending_occurrence = null;
|
|
1400
|
+
this.resolveCurrentIncident(state, timestamp);
|
|
1401
|
+
this.appendEvidence(state, "session-resumed", {
|
|
1402
|
+
reason: "definition_returned_same_revision_fresh_cadence",
|
|
1403
|
+
recordedAt: timestamp,
|
|
1404
|
+
});
|
|
1405
|
+
mutated = true;
|
|
1406
|
+
} else if (
|
|
1407
|
+
state.enabled !== true &&
|
|
1408
|
+
state.status !== "expired" &&
|
|
1409
|
+
state.status !== "cancelled" &&
|
|
1410
|
+
state.evidence.some((entry) =>
|
|
1411
|
+
["owner_kept_disabled", "owner_disabled"].includes(entry.reason)
|
|
1412
|
+
) &&
|
|
1413
|
+
(state.status !== "disabled" || state.suspension_reason !== "owner_disabled")
|
|
1414
|
+
) {
|
|
1415
|
+
state.status = "disabled";
|
|
1416
|
+
state.suspension_reason = "owner_disabled";
|
|
1417
|
+
state.cadence_anchor_at = null;
|
|
1418
|
+
state.next_run_at = null;
|
|
1419
|
+
state.pending_occurrence = null;
|
|
1420
|
+
this.resolveCurrentIncident(state, timestamp);
|
|
1421
|
+
this.appendEvidence(state, "disabled", {
|
|
1422
|
+
reason: "owner_disabled_state_reconciled",
|
|
1423
|
+
recordedAt: timestamp,
|
|
1424
|
+
});
|
|
1425
|
+
mutated = true;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
const migratedIncident = this.classifyLegacyIntentLoss({
|
|
1429
|
+
session,
|
|
1430
|
+
state,
|
|
1431
|
+
timestamp,
|
|
1432
|
+
generation,
|
|
1433
|
+
});
|
|
1434
|
+
if (migratedIncident) {
|
|
1435
|
+
createdIncidents.push(migratedIncident);
|
|
1088
1436
|
mutated = true;
|
|
1089
1437
|
}
|
|
1090
1438
|
nextStates.push(state);
|
|
@@ -1092,13 +1440,73 @@ export class SessionLoopDefinitionService {
|
|
|
1092
1440
|
}
|
|
1093
1441
|
|
|
1094
1442
|
for (const staleState of statesById.values()) {
|
|
1443
|
+
if (explicitDeletedLoopId === staleState.id) {
|
|
1444
|
+
const occurrenceId = staleState.pending_occurrence?.occurrence_id || null;
|
|
1445
|
+
this.appendEvidence(staleState, "cancelled", {
|
|
1446
|
+
occurrenceId,
|
|
1447
|
+
reason: "deleted_by_owner",
|
|
1448
|
+
recordedAt: timestamp,
|
|
1449
|
+
});
|
|
1450
|
+
this.appendTerminalEvidence(session, staleState.evidence.at(-1));
|
|
1451
|
+
mutated = true;
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1095
1454
|
const occurrenceId = staleState.pending_occurrence?.occurrence_id || null;
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1455
|
+
const firstUnavailable = !staleState.unavailable_since;
|
|
1456
|
+
staleState.unavailable_since ||= timestamp;
|
|
1457
|
+
staleState.unavailable_observations =
|
|
1458
|
+
(staleState.unavailable_observations || 0) + 1;
|
|
1459
|
+
staleState.availability_state = "provisional";
|
|
1460
|
+
staleState.definition_generation = generation;
|
|
1461
|
+
staleState.status = "provisional";
|
|
1462
|
+
staleState.suspension_reason = "checking_definition_availability";
|
|
1463
|
+
staleState.last_error = storage.read_error || null;
|
|
1464
|
+
staleState.next_run_at = null;
|
|
1465
|
+
if (occurrenceId) {
|
|
1466
|
+
this.appendEvidence(staleState, "cancelled", {
|
|
1467
|
+
occurrenceId,
|
|
1468
|
+
reason: "provisional_definition_unavailable_pending_cancelled",
|
|
1469
|
+
recordedAt: timestamp,
|
|
1470
|
+
});
|
|
1471
|
+
staleState.pending_occurrence = null;
|
|
1472
|
+
}
|
|
1473
|
+
const elapsed = Date.parse(timestamp) - Date.parse(staleState.unavailable_since);
|
|
1474
|
+
if (staleState.enabled !== true) {
|
|
1475
|
+
staleState.status = "disabled";
|
|
1476
|
+
staleState.suspension_reason = "owner_disabled";
|
|
1477
|
+
staleState.availability_state =
|
|
1478
|
+
staleState.unavailable_observations >= 2 && elapsed >= this.unavailableGraceMs
|
|
1479
|
+
? "unavailable"
|
|
1480
|
+
: "provisional";
|
|
1481
|
+
this.resolveCurrentIncident(staleState, timestamp);
|
|
1482
|
+
nextStates.push(staleState);
|
|
1483
|
+
mutated = true;
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
if (
|
|
1487
|
+
staleState.unavailable_observations >= 2 &&
|
|
1488
|
+
elapsed >= this.unavailableGraceMs
|
|
1489
|
+
) {
|
|
1490
|
+
staleState.availability_state = "unavailable";
|
|
1491
|
+
staleState.status = "suspended";
|
|
1492
|
+
staleState.suspension_reason = "definition_unavailable_confirmed";
|
|
1493
|
+
const created = this.createIncident({
|
|
1494
|
+
session,
|
|
1495
|
+
state: staleState,
|
|
1496
|
+
reason: "definition_unavailable_confirmed",
|
|
1497
|
+
generation,
|
|
1498
|
+
timestamp,
|
|
1499
|
+
});
|
|
1500
|
+
if (created.created) createdIncidents.push(created.incident);
|
|
1501
|
+
if (firstUnavailable || created.created) {
|
|
1502
|
+
this.appendEvidence(staleState, "session-suspended", {
|
|
1503
|
+
occurrenceId,
|
|
1504
|
+
reason: "definition_unavailable_confirmed",
|
|
1505
|
+
recordedAt: timestamp,
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
nextStates.push(staleState);
|
|
1102
1510
|
mutated = true;
|
|
1103
1511
|
}
|
|
1104
1512
|
|
|
@@ -1106,7 +1514,7 @@ export class SessionLoopDefinitionService {
|
|
|
1106
1514
|
session.loops = nextStates;
|
|
1107
1515
|
session.updated_at = timestamp;
|
|
1108
1516
|
}
|
|
1109
|
-
return { applies: true, mutated, session };
|
|
1517
|
+
return { applies: true, mutated, session, createdIncidents };
|
|
1110
1518
|
}
|
|
1111
1519
|
|
|
1112
1520
|
syncLiveRuntimeFromLifecycleSession(session) {
|
|
@@ -1123,15 +1531,35 @@ export class SessionLoopDefinitionService {
|
|
|
1123
1531
|
runtime.updated_at = session.updated_at;
|
|
1124
1532
|
}
|
|
1125
1533
|
|
|
1126
|
-
reconcileLifecycleForFolder({ storage }) {
|
|
1534
|
+
reconcileLifecycleForFolder({ storage, explicitDeletedLoopId = null }) {
|
|
1127
1535
|
const lifecycle = this.lifecycleStore.read();
|
|
1536
|
+
const generation = this.nextFolderGeneration(storage, lifecycle);
|
|
1128
1537
|
let mutated = false;
|
|
1129
1538
|
let matchedSessionCount = 0;
|
|
1539
|
+
const createdIncidents = [];
|
|
1130
1540
|
for (const session of lifecycle.sessions) {
|
|
1131
|
-
const result = this.reconcileLifecycleSession({
|
|
1541
|
+
const result = this.reconcileLifecycleSession({
|
|
1542
|
+
session,
|
|
1543
|
+
storage,
|
|
1544
|
+
generation,
|
|
1545
|
+
explicitDeletedLoopId,
|
|
1546
|
+
});
|
|
1132
1547
|
if (!result.applies) continue;
|
|
1133
1548
|
matchedSessionCount += 1;
|
|
1134
1549
|
mutated = result.mutated || mutated;
|
|
1550
|
+
for (const incident of result.createdIncidents || []) {
|
|
1551
|
+
createdIncidents.push({
|
|
1552
|
+
incident,
|
|
1553
|
+
session: {
|
|
1554
|
+
host_session_id: session.host_session_id,
|
|
1555
|
+
agent_id: session.agent_id,
|
|
1556
|
+
agent_folder: session.agent_folder,
|
|
1557
|
+
},
|
|
1558
|
+
state: session.loops.find((state) =>
|
|
1559
|
+
(state.incidents || []).some((candidate) => candidate.id === incident.id)
|
|
1560
|
+
) || null,
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1135
1563
|
}
|
|
1136
1564
|
const committed = mutated
|
|
1137
1565
|
? this.lifecycleStore.writeSessions(lifecycle.sessions)
|
|
@@ -1145,6 +1573,8 @@ export class SessionLoopDefinitionService {
|
|
|
1145
1573
|
lifecycle: committed,
|
|
1146
1574
|
mutated,
|
|
1147
1575
|
matched_session_count: matchedSessionCount,
|
|
1576
|
+
definition_generation: generation,
|
|
1577
|
+
incident_count: createdIncidents.length,
|
|
1148
1578
|
};
|
|
1149
1579
|
}
|
|
1150
1580
|
|
|
@@ -1204,6 +1634,14 @@ export class SessionLoopDefinitionService {
|
|
|
1204
1634
|
mutatedCount += 1;
|
|
1205
1635
|
continue;
|
|
1206
1636
|
}
|
|
1637
|
+
const alreadySuspendedForLifecycle =
|
|
1638
|
+
state.enabled === true &&
|
|
1639
|
+
state.status === "suspended" &&
|
|
1640
|
+
state.suspension_reason === resetReason &&
|
|
1641
|
+
state.cadence_anchor_at === null &&
|
|
1642
|
+
state.next_run_at === null &&
|
|
1643
|
+
state.pending_occurrence === null;
|
|
1644
|
+
if (alreadySuspendedForLifecycle) continue;
|
|
1207
1645
|
const hadRuntimeAuthority =
|
|
1208
1646
|
state.enabled === true ||
|
|
1209
1647
|
state.status === "active" ||
|
|
@@ -1220,28 +1658,33 @@ export class SessionLoopDefinitionService {
|
|
|
1220
1658
|
});
|
|
1221
1659
|
cancelledCount += 1;
|
|
1222
1660
|
}
|
|
1223
|
-
state.enabled
|
|
1224
|
-
state.
|
|
1661
|
+
const enabledIntent = state.enabled === true;
|
|
1662
|
+
state.enabled = enabledIntent;
|
|
1663
|
+
state.status = enabledIntent ? "suspended" : "disabled";
|
|
1225
1664
|
state.cadence_anchor_at = null;
|
|
1226
1665
|
state.next_run_at = null;
|
|
1227
1666
|
state.pending_occurrence = null;
|
|
1228
|
-
state.suspension_reason = resetReason;
|
|
1667
|
+
state.suspension_reason = enabledIntent ? resetReason : state.suspension_reason;
|
|
1229
1668
|
state.last_error = null;
|
|
1230
|
-
this.appendEvidence(state, "disabled", {
|
|
1669
|
+
this.appendEvidence(state, enabledIntent ? "session-suspended" : "disabled", {
|
|
1231
1670
|
occurrenceId,
|
|
1232
1671
|
reason: resetReason,
|
|
1233
1672
|
recordedAt: timestamp,
|
|
1234
1673
|
});
|
|
1235
1674
|
runtime.states.set(state.id, state);
|
|
1236
|
-
disabledCount += 1;
|
|
1675
|
+
if (!enabledIntent) disabledCount += 1;
|
|
1237
1676
|
mutatedCount += 1;
|
|
1238
1677
|
}
|
|
1239
1678
|
if (mutatedCount > 0) this.persistRuntime(runtime);
|
|
1679
|
+
this.sessions.delete(runtime.host_session_id);
|
|
1240
1680
|
return {
|
|
1241
|
-
status: "
|
|
1681
|
+
status: "intent_preserved_runtime_suspended",
|
|
1242
1682
|
host_session_id: runtime.host_session_id,
|
|
1243
1683
|
runtime_state_count: runtime.states.size,
|
|
1244
1684
|
disabled_count: disabledCount,
|
|
1685
|
+
enabled_intent_preserved_count: [...runtime.states.values()].filter(
|
|
1686
|
+
(state) => state.enabled === true
|
|
1687
|
+
).length,
|
|
1245
1688
|
cancelled_count: cancelledCount,
|
|
1246
1689
|
idempotent: mutatedCount === 0,
|
|
1247
1690
|
reason: resetReason,
|
|
@@ -1364,7 +1807,7 @@ export class SessionLoopDefinitionService {
|
|
|
1364
1807
|
}) {
|
|
1365
1808
|
const sessionId = normalizeRequiredString(hostSessionId, "hostSessionId");
|
|
1366
1809
|
const normalizedAgentId = normalizeRequiredString(agentId, "agentId");
|
|
1367
|
-
const storage = this.
|
|
1810
|
+
const storage = this.readDefinitionsForReconciliation(agentFolder);
|
|
1368
1811
|
const hadLiveRuntime = this.sessions.has(sessionId);
|
|
1369
1812
|
const runtime = this.buildRuntimeFromLifecycle({
|
|
1370
1813
|
sessionId,
|
|
@@ -1372,14 +1815,44 @@ export class SessionLoopDefinitionService {
|
|
|
1372
1815
|
agentFolder,
|
|
1373
1816
|
storage,
|
|
1374
1817
|
});
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1818
|
+
let reconstructed = null;
|
|
1819
|
+
if (!hadLiveRuntime) {
|
|
1820
|
+
const timestamp = nowIso(this.clock);
|
|
1821
|
+
let mutated = false;
|
|
1822
|
+
for (const state of runtime.states.values()) {
|
|
1823
|
+
if (
|
|
1824
|
+
state.enabled !== true ||
|
|
1825
|
+
!["active", "suspended"].includes(state.status) ||
|
|
1826
|
+
state.availability_state !== "available"
|
|
1827
|
+
) continue;
|
|
1828
|
+
const definition = storage.loops.find((entry) => entry.id === state.id);
|
|
1829
|
+
if (!definition || state.definition_revision !== definitionRevision(definition)) {
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
state.cadence_anchor_at = timestamp;
|
|
1833
|
+
state.status = "active";
|
|
1834
|
+
state.suspension_reason = null;
|
|
1835
|
+
state.next_run_at = computeFirstCadenceBoundaryAfter({
|
|
1836
|
+
anchorAt: timestamp,
|
|
1837
|
+
intervalMs: definition.interval_ms,
|
|
1838
|
+
after: timestamp,
|
|
1839
|
+
});
|
|
1840
|
+
state.pending_occurrence = null;
|
|
1841
|
+
this.appendEvidence(state, "session-resumed", {
|
|
1842
|
+
reason: "durable_enabled_intent_reconstructed_fresh_cadence",
|
|
1843
|
+
recordedAt: timestamp,
|
|
1844
|
+
});
|
|
1845
|
+
mutated = true;
|
|
1846
|
+
}
|
|
1847
|
+
if (mutated) this.persistRuntime(runtime);
|
|
1848
|
+
reconstructed = {
|
|
1849
|
+
status: "intent_reconstructed",
|
|
1850
|
+
reason: defaultOffReason,
|
|
1851
|
+
lifecycle_mutated: mutated,
|
|
1852
|
+
};
|
|
1853
|
+
}
|
|
1381
1854
|
this.sessions.set(sessionId, runtime);
|
|
1382
|
-
return { runtime, storage, defaultOffReset };
|
|
1855
|
+
return { runtime, storage, defaultOffReset: reconstructed };
|
|
1383
1856
|
}
|
|
1384
1857
|
|
|
1385
1858
|
ensureRuntimeSession(options) {
|
|
@@ -1406,7 +1879,13 @@ export class SessionLoopDefinitionService {
|
|
|
1406
1879
|
agentId,
|
|
1407
1880
|
agentFolder,
|
|
1408
1881
|
});
|
|
1409
|
-
|
|
1882
|
+
const definitions = new Map(storage.loops.map((definition) => [definition.id, definition]));
|
|
1883
|
+
for (const state of runtime.states.values()) {
|
|
1884
|
+
if (!definitions.has(state.id) && state.definition_snapshot) {
|
|
1885
|
+
definitions.set(state.id, state.definition_snapshot);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return [...definitions.values()].map((definition) =>
|
|
1410
1889
|
this.serializeForGui({ definition, runtime })
|
|
1411
1890
|
);
|
|
1412
1891
|
}
|
|
@@ -1599,14 +2078,12 @@ export class SessionLoopDefinitionService {
|
|
|
1599
2078
|
}
|
|
1600
2079
|
const refreshed = { runtime, storage: result.storage };
|
|
1601
2080
|
if (typeof enabled === "boolean") {
|
|
1602
|
-
this.setRuntimeEnabled({
|
|
1603
|
-
runtime: refreshed.runtime,
|
|
1604
|
-
definition: result.definition,
|
|
1605
|
-
enabled,
|
|
1606
|
-
rebase: result.status === "updated",
|
|
1607
|
-
});
|
|
2081
|
+
this.setRuntimeEnabled({ runtime, definition: result.definition, enabled });
|
|
1608
2082
|
result.desired_enabled = enabled;
|
|
1609
2083
|
result.previous_enabled = previousEnabled;
|
|
2084
|
+
} else if (result.status === "updated") {
|
|
2085
|
+
result.desired_enabled = null;
|
|
2086
|
+
result.previous_enabled = previousEnabled;
|
|
1610
2087
|
}
|
|
1611
2088
|
return {
|
|
1612
2089
|
status: result.status,
|
|
@@ -1675,7 +2152,11 @@ export class SessionLoopDefinitionService {
|
|
|
1675
2152
|
agentFolder,
|
|
1676
2153
|
loopId: scheduleId,
|
|
1677
2154
|
},
|
|
1678
|
-
() =>
|
|
2155
|
+
() =>
|
|
2156
|
+
this.reconcileLifecycleForFolder({
|
|
2157
|
+
storage: result.storage,
|
|
2158
|
+
explicitDeletedLoopId: scheduleId,
|
|
2159
|
+
})
|
|
1679
2160
|
);
|
|
1680
2161
|
return {
|
|
1681
2162
|
status: "deleted",
|
|
@@ -1722,7 +2203,7 @@ export class SessionLoopDefinitionService {
|
|
|
1722
2203
|
};
|
|
1723
2204
|
const removed = this.sessions.delete(sessionId);
|
|
1724
2205
|
return {
|
|
1725
|
-
status: runtime ? "
|
|
2206
|
+
status: runtime ? "intent_preserved_runtime_suspended" : "already_clear",
|
|
1726
2207
|
host_session_id: sessionId,
|
|
1727
2208
|
removed,
|
|
1728
2209
|
runtime_state_count: runtimeStateCount,
|
|
@@ -1854,6 +2335,12 @@ export class SessionLoopDefinitionService {
|
|
|
1854
2335
|
state.pending_occurrence = null;
|
|
1855
2336
|
}
|
|
1856
2337
|
state.last_error = null;
|
|
2338
|
+
state.availability_state = "available";
|
|
2339
|
+
state.unavailable_since = null;
|
|
2340
|
+
state.unavailable_observations = 0;
|
|
2341
|
+
state.candidate_definition = null;
|
|
2342
|
+
state.candidate_revision = null;
|
|
2343
|
+
this.resolveCurrentIncident(state, now);
|
|
1857
2344
|
this.appendEvidence(state, "enabled", {
|
|
1858
2345
|
reason: wasEnabled && !rebase ? "idempotent_enabled_intent" : "owner_enabled",
|
|
1859
2346
|
});
|
|
@@ -1866,6 +2353,7 @@ export class SessionLoopDefinitionService {
|
|
|
1866
2353
|
state.next_run_at = null;
|
|
1867
2354
|
if (!occurrenceWasDispatching) state.pending_occurrence = null;
|
|
1868
2355
|
state.suspension_reason = "owner_disabled";
|
|
2356
|
+
this.resolveCurrentIncident(state, nowIso(this.clock));
|
|
1869
2357
|
if (pendingOccurrenceId && !occurrenceWasDispatching) {
|
|
1870
2358
|
this.appendEvidence(state, "cancelled", {
|
|
1871
2359
|
occurrenceId: pendingOccurrenceId,
|
|
@@ -1884,6 +2372,63 @@ export class SessionLoopDefinitionService {
|
|
|
1884
2372
|
return state;
|
|
1885
2373
|
}
|
|
1886
2374
|
|
|
2375
|
+
resolveIncidentForGui({
|
|
2376
|
+
hostSessionId,
|
|
2377
|
+
agentId,
|
|
2378
|
+
agentFolder,
|
|
2379
|
+
scheduleId,
|
|
2380
|
+
action,
|
|
2381
|
+
}) {
|
|
2382
|
+
return this.withAgentFolderMutation(agentFolder, (canonicalFolder) => {
|
|
2383
|
+
const { runtime, storage } = this.ensureRuntimeSessionUnlocked({
|
|
2384
|
+
hostSessionId,
|
|
2385
|
+
agentId,
|
|
2386
|
+
agentFolder: canonicalFolder,
|
|
2387
|
+
});
|
|
2388
|
+
const state = runtime.states.get(scheduleId);
|
|
2389
|
+
if (!state) throw new Error("Loop lifecycle state not found for recovery");
|
|
2390
|
+
const incident = this.currentIncident(state);
|
|
2391
|
+
if (!incident) throw new Error("Loop has no unresolved incident");
|
|
2392
|
+
const normalizedAction = normalizeRequiredString(action, "loop recovery action");
|
|
2393
|
+
const definition = storage.loops.find((entry) => entry.id === scheduleId) || null;
|
|
2394
|
+
if (normalizedAction === "keep_disabled") {
|
|
2395
|
+
if (definition) {
|
|
2396
|
+
state.definition_revision = definitionRevision(definition);
|
|
2397
|
+
state.definition_snapshot = snapshotDefinition(definition);
|
|
2398
|
+
}
|
|
2399
|
+
state.availability_state = definition ? "available" : state.availability_state;
|
|
2400
|
+
state.candidate_definition = null;
|
|
2401
|
+
state.candidate_revision = null;
|
|
2402
|
+
state.enabled = false;
|
|
2403
|
+
state.status = "disabled";
|
|
2404
|
+
state.cadence_anchor_at = null;
|
|
2405
|
+
state.next_run_at = null;
|
|
2406
|
+
state.pending_occurrence = null;
|
|
2407
|
+
state.suspension_reason = "owner_disabled";
|
|
2408
|
+
this.resolveCurrentIncident(state, nowIso(this.clock));
|
|
2409
|
+
this.appendEvidence(state, "disabled", { reason: "owner_kept_disabled" });
|
|
2410
|
+
this.persistRuntime(runtime);
|
|
2411
|
+
} else if (["restore", "restore_previous_enabled_state"].includes(normalizedAction)) {
|
|
2412
|
+
if (!definition || definitionRevision(definition) !== state.definition_revision) {
|
|
2413
|
+
throw new Error("The original Loop definition must return before it can be restored");
|
|
2414
|
+
}
|
|
2415
|
+
state.availability_state = "available";
|
|
2416
|
+
state.unavailable_since = null;
|
|
2417
|
+
state.unavailable_observations = 0;
|
|
2418
|
+
this.setRuntimeEnabled({ runtime, definition, enabled: true, rebase: true });
|
|
2419
|
+
} else {
|
|
2420
|
+
throw new Error(`Unsupported Loop recovery action ${normalizedAction}`);
|
|
2421
|
+
}
|
|
2422
|
+
const projectionDefinition = definition || state.definition_snapshot;
|
|
2423
|
+
return {
|
|
2424
|
+
status: "incident_resolved",
|
|
2425
|
+
action: normalizedAction,
|
|
2426
|
+
incident_id: incident.id,
|
|
2427
|
+
schedule: this.serializeForGui({ definition: projectionDefinition, runtime }),
|
|
2428
|
+
};
|
|
2429
|
+
});
|
|
2430
|
+
}
|
|
2431
|
+
|
|
1887
2432
|
buildOccurrence(definition, state) {
|
|
1888
2433
|
const dueAt = normalizeOptionalIsoTimestamp(
|
|
1889
2434
|
state.next_run_at,
|
|
@@ -1931,7 +2476,7 @@ export class SessionLoopDefinitionService {
|
|
|
1931
2476
|
agentFolder: canonicalFolder,
|
|
1932
2477
|
},
|
|
1933
2478
|
() => {
|
|
1934
|
-
const currentStorage = this.
|
|
2479
|
+
const currentStorage = this.readDefinitionsForReconciliation(canonicalFolder);
|
|
1935
2480
|
this.reconcileLifecycleForFolder({ storage: currentStorage });
|
|
1936
2481
|
return currentStorage;
|
|
1937
2482
|
}
|
|
@@ -1987,6 +2532,23 @@ export class SessionLoopDefinitionService {
|
|
|
1987
2532
|
return due;
|
|
1988
2533
|
}
|
|
1989
2534
|
|
|
2535
|
+
refreshDueLoopForDispatch({ runtime, definition, state }) {
|
|
2536
|
+
const occurrenceId = state.pending_occurrence?.occurrence_id;
|
|
2537
|
+
return this.withAgentFolderMutation(runtime.agent_folder, (canonicalFolder) => {
|
|
2538
|
+
const storage = this.readDefinitionsForReconciliation(canonicalFolder);
|
|
2539
|
+
this.reconcileLifecycleForFolder({ storage });
|
|
2540
|
+
const currentDefinition = storage.loops.find((entry) => entry.id === definition.id);
|
|
2541
|
+
const currentState = runtime.states.get(definition.id);
|
|
2542
|
+
if (!currentDefinition || !currentState || !occurrenceId ||
|
|
2543
|
+
definitionRevision(currentDefinition) !== definitionRevision(definition) ||
|
|
2544
|
+
currentState.definition_revision !== definitionRevision(definition) ||
|
|
2545
|
+
currentState.enabled !== true || currentState.status !== "active" ||
|
|
2546
|
+
currentState.pending_occurrence?.occurrence_id !== occurrenceId ||
|
|
2547
|
+
currentState.pending_occurrence.state === "dispatching") return null;
|
|
2548
|
+
return { runtime, definition: currentDefinition, state: currentState };
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
|
|
1990
2552
|
recordSkipped({ runtime, definition, state, triggeredAt, reason }) {
|
|
1991
2553
|
if (!state.pending_occurrence) {
|
|
1992
2554
|
throw new Error("Busy Loop retry requires a pending occurrence");
|
|
@@ -2065,26 +2627,34 @@ export class SessionLoopDefinitionService {
|
|
|
2065
2627
|
!durableState.pending_occurrence ||
|
|
2066
2628
|
durableState.pending_occurrence.occurrence_id !== expectedOccurrenceId
|
|
2067
2629
|
) {
|
|
2068
|
-
if (
|
|
2069
|
-
durableState.enabled !== true &&
|
|
2070
|
-
durableState.pending_occurrence === null
|
|
2071
|
-
) {
|
|
2630
|
+
if (durableState.pending_occurrence === null) {
|
|
2072
2631
|
Object.assign(state, durableState);
|
|
2073
2632
|
state.dispatch_count += 1;
|
|
2074
2633
|
state.last_run_at = triggeredAt;
|
|
2075
|
-
state.last_status =
|
|
2634
|
+
state.last_status = durableState.enabled === true
|
|
2635
|
+
? "dispatched-after-lifecycle-reconstruction"
|
|
2636
|
+
: "dispatched-after-lifecycle-disable";
|
|
2076
2637
|
state.last_error = null;
|
|
2077
2638
|
state.last_dispatched_message_id = dispatchedMessageId;
|
|
2078
2639
|
this.appendEvidence(state, "succeeded", {
|
|
2079
2640
|
occurrenceId: expectedOccurrenceId,
|
|
2080
|
-
reason:
|
|
2641
|
+
reason: durableState.enabled === true
|
|
2642
|
+
? "matrix_append_committed_after_intent_preserving_reconstruction"
|
|
2643
|
+
: "matrix_append_committed_after_lifecycle_default_off",
|
|
2081
2644
|
appendIdentity: matrixEventId || dispatchedMessageId,
|
|
2082
2645
|
recordedAt: triggeredAt,
|
|
2083
2646
|
});
|
|
2084
2647
|
this.persistState(runtime, state);
|
|
2085
2648
|
return state;
|
|
2086
2649
|
}
|
|
2087
|
-
|
|
2650
|
+
this.persistTerminalEvidence(runtime, {
|
|
2651
|
+
transition: "succeeded", recorded_at: triggeredAt, loop_id: definition.id,
|
|
2652
|
+
occurrence_id: expectedOccurrenceId,
|
|
2653
|
+
reason: "matrix_append_committed_after_occurrence_superseded",
|
|
2654
|
+
append_identity_sha256: hashText(matrixEventId || dispatchedMessageId),
|
|
2655
|
+
});
|
|
2656
|
+
Object.assign(state, durableState);
|
|
2657
|
+
return state;
|
|
2088
2658
|
}
|
|
2089
2659
|
Object.assign(state, durableState);
|
|
2090
2660
|
if (!state.pending_occurrence) {
|
|
@@ -2139,13 +2709,12 @@ export class SessionLoopDefinitionService {
|
|
|
2139
2709
|
!durableState.pending_occurrence ||
|
|
2140
2710
|
durableState.pending_occurrence.occurrence_id !== expectedOccurrenceId
|
|
2141
2711
|
) {
|
|
2142
|
-
if (
|
|
2143
|
-
durableState.enabled !== true &&
|
|
2144
|
-
durableState.pending_occurrence === null
|
|
2145
|
-
) {
|
|
2712
|
+
if (durableState.pending_occurrence === null) {
|
|
2146
2713
|
Object.assign(state, durableState);
|
|
2147
2714
|
state.last_run_at = triggeredAt;
|
|
2148
|
-
state.last_status =
|
|
2715
|
+
state.last_status = durableState.enabled === true
|
|
2716
|
+
? "failed-after-lifecycle-reconstruction"
|
|
2717
|
+
: "failed-after-lifecycle-disable";
|
|
2149
2718
|
state.last_error = reason;
|
|
2150
2719
|
this.appendEvidence(state, "failed", {
|
|
2151
2720
|
occurrenceId: expectedOccurrenceId,
|
|
@@ -2155,7 +2724,13 @@ export class SessionLoopDefinitionService {
|
|
|
2155
2724
|
this.persistState(runtime, state);
|
|
2156
2725
|
return state;
|
|
2157
2726
|
}
|
|
2158
|
-
|
|
2727
|
+
this.persistTerminalEvidence(runtime, {
|
|
2728
|
+
transition: "failed", recorded_at: triggeredAt, loop_id: definition.id,
|
|
2729
|
+
occurrence_id: expectedOccurrenceId, reason,
|
|
2730
|
+
append_identity_sha256: null,
|
|
2731
|
+
});
|
|
2732
|
+
Object.assign(state, durableState);
|
|
2733
|
+
return state;
|
|
2159
2734
|
}
|
|
2160
2735
|
Object.assign(state, durableState);
|
|
2161
2736
|
if (!state.pending_occurrence) {
|
|
@@ -2209,7 +2784,7 @@ export class SessionLoopDefinitionService {
|
|
|
2209
2784
|
agentFolder: canonicalFolder,
|
|
2210
2785
|
},
|
|
2211
2786
|
() => {
|
|
2212
|
-
const currentStorage = this.
|
|
2787
|
+
const currentStorage = this.readDefinitionsForReconciliation(canonicalFolder);
|
|
2213
2788
|
this.reconcileLifecycleForFolder({ storage: currentStorage });
|
|
2214
2789
|
return currentStorage;
|
|
2215
2790
|
}
|
|
@@ -2221,7 +2796,7 @@ export class SessionLoopDefinitionService {
|
|
|
2221
2796
|
const loops = [];
|
|
2222
2797
|
for (const [loopId, state] of runtime.states.entries()) {
|
|
2223
2798
|
const definition = definitionsById.get(loopId);
|
|
2224
|
-
if (!definition || state.enabled !== true) continue;
|
|
2799
|
+
if (!definition || state.enabled !== true || state.status !== "active") continue;
|
|
2225
2800
|
loops.push({
|
|
2226
2801
|
loop_id: loopId,
|
|
2227
2802
|
enabled: true,
|
|
@@ -2235,6 +2810,7 @@ export class SessionLoopDefinitionService {
|
|
|
2235
2810
|
cadence_anchor_at: state.cadence_anchor_at || null,
|
|
2236
2811
|
pending_occurrence: state.pending_occurrence || null,
|
|
2237
2812
|
interval_ms: definition.interval_ms,
|
|
2813
|
+
definition_revision: state.definition_revision,
|
|
2238
2814
|
runtime_state_owner: SESSION_LOOP_RUNTIME_OWNER,
|
|
2239
2815
|
definition_storage_owner: SESSION_LOOP_DEFINITION_STORAGE_OWNER,
|
|
2240
2816
|
});
|
|
@@ -2258,7 +2834,12 @@ export class SessionLoopDefinitionService {
|
|
|
2258
2834
|
|
|
2259
2835
|
restoreHostRestartRuntimeState(
|
|
2260
2836
|
snapshot = {},
|
|
2261
|
-
{
|
|
2837
|
+
{
|
|
2838
|
+
restartId,
|
|
2839
|
+
restoredAt,
|
|
2840
|
+
restoredSessionIds = null,
|
|
2841
|
+
loopRestorePolicy = "default_off",
|
|
2842
|
+
} = {}
|
|
2262
2843
|
) {
|
|
2263
2844
|
const runtimeSessions = Array.isArray(snapshot.runtime_sessions)
|
|
2264
2845
|
? snapshot.runtime_sessions
|
|
@@ -2270,6 +2851,7 @@ export class SessionLoopDefinitionService {
|
|
|
2270
2851
|
)
|
|
2271
2852
|
)
|
|
2272
2853
|
: null;
|
|
2854
|
+
const restored = [];
|
|
2273
2855
|
const disabled = [];
|
|
2274
2856
|
const skipped = [];
|
|
2275
2857
|
const timestamp = restoredAt || nowIso(this.clock);
|
|
@@ -2309,6 +2891,42 @@ export class SessionLoopDefinitionService {
|
|
|
2309
2891
|
});
|
|
2310
2892
|
continue;
|
|
2311
2893
|
}
|
|
2894
|
+
const revisionMatches =
|
|
2895
|
+
typeof loop.definition_revision === "string" &&
|
|
2896
|
+
loop.definition_revision === definitionRevision(definition);
|
|
2897
|
+
if (
|
|
2898
|
+
loopRestorePolicy === "restore_enabled_with_fresh_cadence" &&
|
|
2899
|
+
sessionRuntimeRestored &&
|
|
2900
|
+
revisionMatches
|
|
2901
|
+
) {
|
|
2902
|
+
this.setRuntimeEnabled({
|
|
2903
|
+
runtime,
|
|
2904
|
+
definition,
|
|
2905
|
+
enabled: true,
|
|
2906
|
+
rebase: true,
|
|
2907
|
+
});
|
|
2908
|
+
restored.push({
|
|
2909
|
+
host_session_id: entry.host_session_id,
|
|
2910
|
+
loop_id: definition.id,
|
|
2911
|
+
restart_id: restartId || null,
|
|
2912
|
+
enabled: true,
|
|
2913
|
+
next_run_at: runtime.states.get(definition.id)?.next_run_at || null,
|
|
2914
|
+
reason: "restore_enabled_with_fresh_cadence",
|
|
2915
|
+
});
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
const reason = loopRestorePolicy !== "restore_enabled_with_fresh_cadence"
|
|
2919
|
+
? "legacy_policy_default_off"
|
|
2920
|
+
: !sessionRuntimeRestored
|
|
2921
|
+
? "session_restore_failed"
|
|
2922
|
+
: "definition_revision_changed";
|
|
2923
|
+
state.enabled = false;
|
|
2924
|
+
state.status = "disabled";
|
|
2925
|
+
state.cadence_anchor_at = null;
|
|
2926
|
+
state.next_run_at = null;
|
|
2927
|
+
state.pending_occurrence = null;
|
|
2928
|
+
state.suspension_reason = reason;
|
|
2929
|
+
this.persistRuntime(runtime);
|
|
2312
2930
|
disabled.push({
|
|
2313
2931
|
host_session_id: entry.host_session_id,
|
|
2314
2932
|
loop_id: definition.id,
|
|
@@ -2317,7 +2935,7 @@ export class SessionLoopDefinitionService {
|
|
|
2317
2935
|
enabled: false,
|
|
2318
2936
|
pending_occurrence_id: null,
|
|
2319
2937
|
next_run_at: null,
|
|
2320
|
-
reason
|
|
2938
|
+
reason,
|
|
2321
2939
|
loops_json_mutated: false,
|
|
2322
2940
|
});
|
|
2323
2941
|
}
|
|
@@ -2332,11 +2950,12 @@ export class SessionLoopDefinitionService {
|
|
|
2332
2950
|
}
|
|
2333
2951
|
}
|
|
2334
2952
|
return {
|
|
2335
|
-
restored
|
|
2953
|
+
restored,
|
|
2336
2954
|
disabled,
|
|
2337
2955
|
skipped,
|
|
2338
2956
|
restored_at: timestamp,
|
|
2339
|
-
default_off:
|
|
2957
|
+
default_off: loopRestorePolicy !== "restore_enabled_with_fresh_cadence",
|
|
2958
|
+
loop_restore_policy: loopRestorePolicy,
|
|
2340
2959
|
};
|
|
2341
2960
|
}
|
|
2342
2961
|
|
|
@@ -2436,11 +3055,19 @@ export class SessionLoopDefinitionService {
|
|
|
2436
3055
|
definition_revision: state.definition_revision,
|
|
2437
3056
|
definition_revision_algorithm:
|
|
2438
3057
|
SESSION_LOOP_DEFINITION_REVISION_ALGORITHM,
|
|
2439
|
-
enabled: state.enabled === true
|
|
3058
|
+
enabled: state.enabled === true,
|
|
2440
3059
|
enabled_intent: state.enabled === true,
|
|
2441
3060
|
runnable: state.enabled === true && state.status === "active",
|
|
2442
3061
|
status: state.status,
|
|
2443
3062
|
suspension_reason: state.suspension_reason,
|
|
3063
|
+
definition_generation: state.definition_generation || 0,
|
|
3064
|
+
definition_availability: state.availability_state || "available",
|
|
3065
|
+
unavailable_since: state.unavailable_since || null,
|
|
3066
|
+
unavailable_observations: state.unavailable_observations || 0,
|
|
3067
|
+
candidate_definition_revision: state.candidate_revision || null,
|
|
3068
|
+
incidents: (state.incidents || []).map((incident) => ({ ...incident })),
|
|
3069
|
+
active_incident: this.currentIncident(state),
|
|
3070
|
+
recovery_options: this.currentIncident(state)?.recovery_options || [],
|
|
2444
3071
|
cadence_anchor_at: state.cadence_anchor_at,
|
|
2445
3072
|
next_run_at: state.next_run_at,
|
|
2446
3073
|
pending_occurrence: state.pending_occurrence
|