@scotthuang/agent-knock-knock 0.12.11 → 0.12.13
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/CHANGELOG.md +40 -0
- package/README.md +43 -30
- package/dist/src/approval-policy.d.ts +11 -2
- package/dist/src/approval-policy.js +33 -7
- package/dist/src/approval-policy.js.map +1 -1
- package/dist/src/callback-cli-adapter.d.ts +47 -0
- package/dist/src/callback-cli-adapter.js +156 -0
- package/dist/src/callback-cli-adapter.js.map +1 -0
- package/dist/src/callback-outbox-policy.d.ts +95 -0
- package/dist/src/callback-outbox-policy.js +149 -0
- package/dist/src/callback-outbox-policy.js.map +1 -0
- package/dist/src/callback-outbox-service.d.ts +247 -0
- package/dist/src/callback-outbox-service.js +1053 -0
- package/dist/src/callback-outbox-service.js.map +1 -0
- package/dist/src/callback-outbox-settlement.d.ts +54 -0
- package/dist/src/callback-outbox-settlement.js +241 -0
- package/dist/src/callback-outbox-settlement.js.map +1 -0
- package/dist/src/canonical-json.d.ts +1 -0
- package/dist/src/canonical-json.js +14 -0
- package/dist/src/canonical-json.js.map +1 -0
- package/dist/src/claude-lifecycle-compatibility.js +15 -0
- package/dist/src/claude-lifecycle-compatibility.js.map +1 -1
- package/dist/src/claude-local-transcript-provider.d.ts +82 -1
- package/dist/src/claude-local-transcript-provider.js +873 -23
- package/dist/src/claude-local-transcript-provider.js.map +1 -1
- package/dist/src/cli-command-runtime.d.ts +14 -0
- package/dist/src/cli-command-runtime.js +164 -0
- package/dist/src/cli-command-runtime.js.map +1 -0
- package/dist/src/cli-core.d.ts +4 -42
- package/dist/src/cli-core.js +958 -28684
- package/dist/src/cli-core.js.map +1 -1
- package/dist/src/cli-runtime-context.d.ts +57 -0
- package/dist/src/cli-runtime-context.js +118 -0
- package/dist/src/cli-runtime-context.js.map +1 -0
- package/dist/src/codex-lifecycle-compatibility.js +2 -1
- package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
- package/dist/src/codex-local-session-provider.d.ts +18 -0
- package/dist/src/codex-local-session-provider.js +67 -0
- package/dist/src/codex-local-session-provider.js.map +1 -1
- package/dist/src/codex-session-provider.js +1 -3
- package/dist/src/codex-session-provider.js.map +1 -1
- package/dist/src/conversation-trace.d.ts +30 -0
- package/dist/src/conversation-trace.js +185 -0
- package/dist/src/conversation-trace.js.map +1 -0
- package/dist/src/deferred-foreground-application-service.d.ts +84 -0
- package/dist/src/deferred-foreground-application-service.js +742 -0
- package/dist/src/deferred-foreground-application-service.js.map +1 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.d.ts +78 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.js +795 -0
- package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-boundary.d.ts +61 -0
- package/dist/src/deferred-foreground-boundary.js +2 -0
- package/dist/src/deferred-foreground-boundary.js.map +1 -0
- package/dist/src/deferred-foreground-capability.d.ts +4 -0
- package/dist/src/deferred-foreground-capability.js +184 -0
- package/dist/src/deferred-foreground-capability.js.map +1 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.d.ts +53 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.js +210 -0
- package/dist/src/deferred-foreground-preparation-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-preparation-service.d.ts +72 -0
- package/dist/src/deferred-foreground-preparation-service.js +179 -0
- package/dist/src/deferred-foreground-preparation-service.js.map +1 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +139 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.js +1125 -0
- package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -0
- package/dist/src/deferred-foreground-recovery-service.d.ts +48 -0
- package/dist/src/deferred-foreground-recovery-service.js +91 -0
- package/dist/src/deferred-foreground-recovery-service.js.map +1 -0
- package/dist/src/deferred-foreground-transfer-policy.d.ts +2 -0
- package/dist/src/deferred-foreground-transfer-policy.js +10 -0
- package/dist/src/deferred-foreground-transfer-policy.js.map +1 -0
- package/dist/src/deferred-foreground-transfer.js +46 -105
- package/dist/src/deferred-foreground-transfer.js.map +1 -1
- package/dist/src/doctor-capabilities.d.ts +9 -0
- package/dist/src/doctor-capabilities.js +22 -0
- package/dist/src/doctor-capabilities.js.map +1 -1
- package/dist/src/durable-json-file.d.ts +22 -0
- package/dist/src/durable-json-file.js +127 -0
- package/dist/src/durable-json-file.js.map +1 -0
- package/dist/src/file-lock-cli-adapter.d.ts +43 -0
- package/dist/src/file-lock-cli-adapter.js +150 -0
- package/dist/src/file-lock-cli-adapter.js.map +1 -0
- package/dist/src/herdr-terminal-control-provider.js +1 -3
- package/dist/src/herdr-terminal-control-provider.js.map +1 -1
- package/dist/src/install-doctor-command-adapter.d.ts +17 -0
- package/dist/src/install-doctor-command-adapter.js +225 -0
- package/dist/src/install-doctor-command-adapter.js.map +1 -0
- package/dist/src/live-lifecycle-evidence.js +76 -46
- package/dist/src/live-lifecycle-evidence.js.map +1 -1
- package/dist/src/live-lifecycle-smoke.js +74 -50
- package/dist/src/live-lifecycle-smoke.js.map +1 -1
- package/dist/src/managed-session.d.ts +2 -0
- package/dist/src/managed-session.js +24 -0
- package/dist/src/managed-session.js.map +1 -1
- package/dist/src/managed-turn-recovery-service.d.ts +86 -0
- package/dist/src/managed-turn-recovery-service.js +145 -0
- package/dist/src/managed-turn-recovery-service.js.map +1 -0
- package/dist/src/mutation-transaction.d.ts +48 -0
- package/dist/src/mutation-transaction.js +73 -0
- package/dist/src/mutation-transaction.js.map +1 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.d.ts +154 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.js +786 -0
- package/dist/src/native-thread-lifecycle-cli-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.d.ts +25 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js +50 -0
- package/dist/src/native-thread-lifecycle-ledger-cli-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-query-service.d.ts +75 -0
- package/dist/src/native-thread-lifecycle-query-service.js +297 -0
- package/dist/src/native-thread-lifecycle-query-service.js.map +1 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.d.ts +45 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.js +154 -0
- package/dist/src/native-thread-lifecycle-recovery-adapter.js.map +1 -0
- package/dist/src/native-thread-lifecycle-recovery-service.d.ts +211 -0
- package/dist/src/native-thread-lifecycle-recovery-service.js +1093 -0
- package/dist/src/native-thread-lifecycle-recovery-service.js.map +1 -0
- package/dist/src/native-thread-resume-snapshot-policy.d.ts +73 -0
- package/dist/src/native-thread-resume-snapshot-policy.js +218 -0
- package/dist/src/native-thread-resume-snapshot-policy.js.map +1 -0
- package/dist/src/native-thread-resume-snapshot.d.ts +3 -61
- package/dist/src/native-thread-resume-snapshot.js +2 -143
- package/dist/src/native-thread-resume-snapshot.js.map +1 -1
- package/dist/src/native-thread-transition-application.d.ts +147 -0
- package/dist/src/native-thread-transition-application.js +974 -0
- package/dist/src/native-thread-transition-application.js.map +1 -0
- package/dist/src/native-thread-transition-policy.d.ts +127 -0
- package/dist/src/native-thread-transition-policy.js +154 -0
- package/dist/src/native-thread-transition-policy.js.map +1 -0
- package/dist/src/native-thread-transition-resource-adapter.d.ts +12 -0
- package/dist/src/native-thread-transition-resource-adapter.js +44 -0
- package/dist/src/native-thread-transition-resource-adapter.js.map +1 -0
- package/dist/src/native-thread-transition-settlement-service.d.ts +94 -0
- package/dist/src/native-thread-transition-settlement-service.js +134 -0
- package/dist/src/native-thread-transition-settlement-service.js.map +1 -0
- package/dist/src/native-thread-transition-verification-adapter.d.ts +62 -0
- package/dist/src/native-thread-transition-verification-adapter.js +451 -0
- package/dist/src/native-thread-transition-verification-adapter.js.map +1 -0
- package/dist/src/openclaw-callback-transport.d.ts +69 -0
- package/dist/src/openclaw-callback-transport.js +583 -0
- package/dist/src/openclaw-callback-transport.js.map +1 -0
- package/dist/src/openclaw-plugin-callback-adapter.d.ts +1 -0
- package/dist/src/openclaw-plugin-callback-adapter.js +423 -0
- package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-command-adapter.d.ts +16 -0
- package/dist/src/openclaw-plugin-command-adapter.js +1868 -0
- package/dist/src/openclaw-plugin-command-adapter.js.map +1 -0
- package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
- package/dist/src/openclaw-plugin-helpers.js +356 -81
- package/dist/src/openclaw-plugin-helpers.js.map +1 -1
- package/dist/src/openclaw-plugin-schemas.d.ts +358 -0
- package/dist/src/openclaw-plugin-schemas.js +366 -0
- package/dist/src/openclaw-plugin-schemas.js.map +1 -0
- package/dist/src/openclaw-plugin-supervisor.d.ts +6 -0
- package/dist/src/openclaw-plugin-supervisor.js +123 -0
- package/dist/src/openclaw-plugin-supervisor.js.map +1 -0
- package/dist/src/openclaw-plugin.js +7 -2212
- package/dist/src/openclaw-plugin.js.map +1 -1
- package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
- package/dist/src/openclaw-private-authority-offers.js +177 -0
- package/dist/src/openclaw-private-authority-offers.js.map +1 -0
- package/dist/src/protocol.d.ts +5 -0
- package/dist/src/protocol.js +59 -3
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/session-store.d.ts +1 -0
- package/dist/src/session-store.js +84 -106
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/store.d.ts +1 -0
- package/dist/src/store.js +5 -2
- package/dist/src/store.js.map +1 -1
- package/dist/src/terminal-acceptance-application-service.d.ts +65 -0
- package/dist/src/terminal-acceptance-application-service.js +63 -0
- package/dist/src/terminal-acceptance-application-service.js.map +1 -0
- package/dist/src/terminal-acceptance-cli-adapter.d.ts +210 -0
- package/dist/src/terminal-acceptance-cli-adapter.js +957 -0
- package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -0
- package/dist/src/terminal-action-projection.d.ts +309 -0
- package/dist/src/terminal-action-projection.js +376 -0
- package/dist/src/terminal-action-projection.js.map +1 -0
- package/dist/src/terminal-agent-bridge.d.ts +3 -0
- package/dist/src/terminal-agent-bridge.js +29 -8
- package/dist/src/terminal-agent-bridge.js.map +1 -1
- package/dist/src/terminal-authority-policy.d.ts +141 -0
- package/dist/src/terminal-authority-policy.js +367 -0
- package/dist/src/terminal-authority-policy.js.map +1 -0
- package/dist/src/terminal-binding-authority.d.ts +118 -0
- package/dist/src/terminal-binding-authority.js +245 -0
- package/dist/src/terminal-binding-authority.js.map +1 -0
- package/dist/src/terminal-binding-reconciliation-service.d.ts +75 -0
- package/dist/src/terminal-binding-reconciliation-service.js +73 -0
- package/dist/src/terminal-binding-reconciliation-service.js.map +1 -0
- package/dist/src/terminal-command-cli-adapter.d.ts +380 -0
- package/dist/src/terminal-command-cli-adapter.js +3288 -0
- package/dist/src/terminal-command-cli-adapter.js.map +1 -0
- package/dist/src/terminal-control-ref.d.ts +4 -0
- package/dist/src/terminal-control-ref.js +11 -8
- package/dist/src/terminal-control-ref.js.map +1 -1
- package/dist/src/terminal-delegate-cli-adapter.d.ts +41 -0
- package/dist/src/terminal-delegate-cli-adapter.js +242 -0
- package/dist/src/terminal-delegate-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-abort.d.ts +22 -0
- package/dist/src/terminal-dispatch-abort.js +30 -0
- package/dist/src/terminal-dispatch-abort.js.map +1 -0
- package/dist/src/terminal-dispatch-application.d.ts +115 -0
- package/dist/src/terminal-dispatch-application.js +478 -0
- package/dist/src/terminal-dispatch-application.js.map +1 -0
- package/dist/src/terminal-dispatch-capability.d.ts +63 -0
- package/dist/src/terminal-dispatch-capability.js +133 -0
- package/dist/src/terminal-dispatch-capability.js.map +1 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.d.ts +22 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.js +96 -0
- package/dist/src/terminal-dispatch-completion-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-composition.d.ts +79 -0
- package/dist/src/terminal-dispatch-composition.js +2 -0
- package/dist/src/terminal-dispatch-composition.js.map +1 -0
- package/dist/src/terminal-dispatch-execution.d.ts +290 -0
- package/dist/src/terminal-dispatch-execution.js +916 -0
- package/dist/src/terminal-dispatch-execution.js.map +1 -0
- package/dist/src/terminal-dispatch-ledger-codec.d.ts +110 -0
- package/dist/src/terminal-dispatch-ledger-codec.js +399 -0
- package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -0
- package/dist/src/terminal-dispatch-policy.d.ts +93 -0
- package/dist/src/terminal-dispatch-policy.js +122 -0
- package/dist/src/terminal-dispatch-policy.js.map +1 -0
- package/dist/src/terminal-dispatch-presenter.d.ts +65 -0
- package/dist/src/terminal-dispatch-presenter.js +258 -0
- package/dist/src/terminal-dispatch-presenter.js.map +1 -0
- package/dist/src/terminal-dispatch-receipt.d.ts +79 -0
- package/dist/src/terminal-dispatch-receipt.js +314 -0
- package/dist/src/terminal-dispatch-receipt.js.map +1 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.d.ts +132 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js +1330 -0
- package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -0
- package/dist/src/terminal-dispatch-recovery-service.d.ts +271 -0
- package/dist/src/terminal-dispatch-recovery-service.js +374 -0
- package/dist/src/terminal-dispatch-recovery-service.js.map +1 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.d.ts +35 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.js +483 -0
- package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -0
- package/dist/src/terminal-handoff-application-service.d.ts +7 -0
- package/dist/src/terminal-handoff-application-service.js +18 -0
- package/dist/src/terminal-handoff-application-service.js.map +1 -0
- package/dist/src/terminal-handoff-cli-adapter.d.ts +229 -0
- package/dist/src/terminal-handoff-cli-adapter.js +1389 -0
- package/dist/src/terminal-handoff-cli-adapter.js.map +1 -0
- package/dist/src/terminal-handoff-facts.d.ts +12 -0
- package/dist/src/terminal-handoff-facts.js +16 -0
- package/dist/src/terminal-handoff-facts.js.map +1 -0
- package/dist/src/terminal-identity-authority-cli-adapter.d.ts +318 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js +1072 -0
- package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -0
- package/dist/src/terminal-identity-authority-service.d.ts +48 -0
- package/dist/src/terminal-identity-authority-service.js +45 -0
- package/dist/src/terminal-identity-authority-service.js.map +1 -0
- package/dist/src/terminal-list-cli-adapter.d.ts +274 -0
- package/dist/src/terminal-list-cli-adapter.js +2444 -0
- package/dist/src/terminal-list-cli-adapter.js.map +1 -0
- package/dist/src/terminal-list-renderer.d.ts +34 -0
- package/dist/src/terminal-list-renderer.js +643 -0
- package/dist/src/terminal-list-renderer.js.map +1 -0
- package/dist/src/terminal-maintenance-cli-adapter.d.ts +102 -0
- package/dist/src/terminal-maintenance-cli-adapter.js +1059 -0
- package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-application-service.d.ts +275 -0
- package/dist/src/terminal-monitor-application-service.js +982 -0
- package/dist/src/terminal-monitor-application-service.js.map +1 -0
- package/dist/src/terminal-monitor-cli-adapter.d.ts +130 -0
- package/dist/src/terminal-monitor-cli-adapter.js +442 -0
- package/dist/src/terminal-monitor-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-decision-policy.d.ts +98 -0
- package/dist/src/terminal-monitor-decision-policy.js +246 -0
- package/dist/src/terminal-monitor-decision-policy.js.map +1 -0
- package/dist/src/terminal-monitor-launch-plan.d.ts +45 -0
- package/dist/src/terminal-monitor-launch-plan.js +94 -0
- package/dist/src/terminal-monitor-launch-plan.js.map +1 -0
- package/dist/src/terminal-monitor-ownership-policy.d.ts +31 -0
- package/dist/src/terminal-monitor-ownership-policy.js +64 -0
- package/dist/src/terminal-monitor-ownership-policy.js.map +1 -0
- package/dist/src/terminal-monitor-poll-policy.d.ts +48 -0
- package/dist/src/terminal-monitor-poll-policy.js +67 -0
- package/dist/src/terminal-monitor-poll-policy.js.map +1 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +53 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.js +114 -0
- package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -0
- package/dist/src/terminal-monitor-state-cli-adapter.d.ts +106 -0
- package/dist/src/terminal-monitor-state-cli-adapter.js +1492 -0
- package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +87 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js +106 -0
- package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +122 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js +685 -0
- package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -0
- package/dist/src/terminal-process-facts.d.ts +2 -0
- package/dist/src/terminal-process-facts.js +19 -0
- package/dist/src/terminal-process-facts.js.map +1 -0
- package/dist/src/terminal-process-source.d.ts +6 -4
- package/dist/src/terminal-process-source.js +25 -20
- package/dist/src/terminal-process-source.js.map +1 -1
- package/dist/src/terminal-runtime-cli-adapter.d.ts +80 -0
- package/dist/src/terminal-runtime-cli-adapter.js +488 -0
- package/dist/src/terminal-runtime-cli-adapter.js.map +1 -0
- package/dist/src/terminal-scoped-approval-authority.d.ts +62 -0
- package/dist/src/terminal-scoped-approval-authority.js +281 -0
- package/dist/src/terminal-scoped-approval-authority.js.map +1 -0
- package/dist/src/terminal-selector-projection-service.d.ts +26 -0
- package/dist/src/terminal-selector-projection-service.js +81 -0
- package/dist/src/terminal-selector-projection-service.js.map +1 -0
- package/dist/src/terminal-status-cli-adapter.d.ts +126 -0
- package/dist/src/terminal-status-cli-adapter.js +577 -0
- package/dist/src/terminal-status-cli-adapter.js.map +1 -0
- package/dist/src/terminal-status-facts.d.ts +56 -0
- package/dist/src/terminal-status-facts.js +199 -0
- package/dist/src/terminal-status-facts.js.map +1 -0
- package/dist/src/terminal-submission-acceptance.d.ts +62 -145
- package/dist/src/terminal-submission-acceptance.js +555 -222
- package/dist/src/terminal-submission-acceptance.js.map +1 -1
- package/dist/src/terminal-submission-facts.d.ts +123 -0
- package/dist/src/terminal-submission-facts.js +205 -0
- package/dist/src/terminal-submission-facts.js.map +1 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.d.ts +14 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.js +90 -0
- package/dist/src/terminal-turn-binding-authority-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-callback-cli-adapter.d.ts +20 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js +55 -0
- package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
- package/dist/src/terminal-watch-cli-adapter.js +508 -0
- package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
- package/dist/src/terminal-watch-service.d.ts +97 -0
- package/dist/src/terminal-watch-service.js +714 -0
- package/dist/src/terminal-watch-service.js.map +1 -0
- package/dist/src/terminal-watch-store.d.ts +149 -0
- package/dist/src/terminal-watch-store.js +861 -0
- package/dist/src/terminal-watch-store.js.map +1 -0
- package/dist/src/value-guards.d.ts +4 -0
- package/dist/src/value-guards.js +12 -0
- package/dist/src/value-guards.js.map +1 -0
- package/dist/src/verified-dead-agent-policy.d.ts +192 -0
- package/dist/src/verified-dead-agent-policy.js +262 -0
- package/dist/src/verified-dead-agent-policy.js.map +1 -0
- package/docs/quickstart-herdr.md +8 -0
- package/docs/quickstart-tmux.md +33 -6
- package/openclaw.plugin.json +11 -3
- package/package.json +3 -1
- package/templates/openclaw-skills/agent-knock-knock/SKILL.md +58 -40
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { TERMINAL_WATCH_SCHEMA, TERMINAL_WATCH_VERSION, assertTerminalWatch, initialTerminalWatchObservationCheckpoint, terminalWatchIdentityFingerprint, terminalWatchNotificationId, terminalWatchNotificationIdempotencyKey, terminalWatchRevision } from "./terminal-watch-store.js";
|
|
3
|
+
const DEFAULT_NOTIFICATION_LEASE_MS = 30_000;
|
|
4
|
+
const DEFAULT_NOTIFICATION_RETRY_DELAY_MS = 5_000;
|
|
5
|
+
const DEFAULT_NOTIFICATION_MAX_RETRY_DELAY_MS = 60_000;
|
|
6
|
+
const DEFAULT_RECONCILIATION_DELIVERY_LIMIT = 1;
|
|
7
|
+
export class ActiveTerminalWatchConflictError extends Error {
|
|
8
|
+
code = "AKK_ACTIVE_TERMINAL_WATCH_CONFLICT";
|
|
9
|
+
existingWatchId;
|
|
10
|
+
constructor(existingWatchId) {
|
|
11
|
+
super(`exact terminal/native task already has active Watch ${existingWatchId}`);
|
|
12
|
+
this.name = "ActiveTerminalWatchConflictError";
|
|
13
|
+
this.existingWatchId = existingWatchId;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create the durable Watch application service. Terminal observation and
|
|
18
|
+
* callback transport are the only effects; the API intentionally has no port
|
|
19
|
+
* capable of writing to, approving, cancelling, or taking ownership of a TUI.
|
|
20
|
+
*/
|
|
21
|
+
export function createTerminalWatchService(dependencies) {
|
|
22
|
+
const { notificationLeaseMs, reconciliationDeliveryLimit, retryDelay } = terminalWatchNotificationDeliveryPolicy(dependencies);
|
|
23
|
+
function create(input) {
|
|
24
|
+
const createdAt = canonicalNow(dependencies.now());
|
|
25
|
+
const watchId = input.watch_id ??
|
|
26
|
+
`terminal-watch-${dependencies.randomUUID()}`;
|
|
27
|
+
const timeoutMs = positiveMilliseconds(input.timeout_ms, undefined, "terminal Watch timeout");
|
|
28
|
+
let candidate = {
|
|
29
|
+
schema: TERMINAL_WATCH_SCHEMA,
|
|
30
|
+
version: TERMINAL_WATCH_VERSION,
|
|
31
|
+
watch_id: watchId,
|
|
32
|
+
agent: input.agent,
|
|
33
|
+
terminal: input.terminal,
|
|
34
|
+
anchor: input.anchor,
|
|
35
|
+
observation_checkpoint: initialTerminalWatchObservationCheckpoint(input.anchor),
|
|
36
|
+
openclaw_session: input.openclaw_session,
|
|
37
|
+
openclaw_bin: input.openclaw_bin,
|
|
38
|
+
created_at: createdAt,
|
|
39
|
+
deadline_at: new Date(Date.parse(createdAt) + timeoutMs).toISOString(),
|
|
40
|
+
updated_at: createdAt,
|
|
41
|
+
status: "active",
|
|
42
|
+
last_activity_at: input.last_activity_at ?? createdAt,
|
|
43
|
+
notification_outbox: []
|
|
44
|
+
};
|
|
45
|
+
if (input.approval_fingerprint !== undefined) {
|
|
46
|
+
candidate = withApprovalNotification(candidate, {
|
|
47
|
+
evidenceFingerprint: input.approval_fingerprint,
|
|
48
|
+
reasonCode: input.approval_reason_code,
|
|
49
|
+
createdAt
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
assertTerminalWatch(candidate, watchId, { allowMissingRevision: true });
|
|
53
|
+
return dependencies.repository.withWatchLock(watchId, () => {
|
|
54
|
+
const identity = terminalWatchIdentityFingerprint(candidate);
|
|
55
|
+
const existing = dependencies.repository.list().find((watch) => watch.status === "active" &&
|
|
56
|
+
terminalWatchIdentityFingerprint(watch) === identity);
|
|
57
|
+
if (existing) {
|
|
58
|
+
throw new ActiveTerminalWatchConflictError(existing.watch_id);
|
|
59
|
+
}
|
|
60
|
+
return dependencies.repository.save(candidate, { expectedRevision: null });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function cancel(watchId) {
|
|
64
|
+
return dependencies.repository.withWatchLock(watchId, () => {
|
|
65
|
+
const current = dependencies.repository.load(watchId);
|
|
66
|
+
if (current.status !== "active") {
|
|
67
|
+
return current;
|
|
68
|
+
}
|
|
69
|
+
const now = canonicalNow(dependencies.now());
|
|
70
|
+
const fingerprint = deterministicEvidenceFingerprint({
|
|
71
|
+
version: 1,
|
|
72
|
+
watch_id: current.watch_id,
|
|
73
|
+
kind: "cancelled",
|
|
74
|
+
anchor_fingerprint: current.anchor.anchor_fingerprint
|
|
75
|
+
});
|
|
76
|
+
return saveSettlement(current, {
|
|
77
|
+
kind: "cancelled",
|
|
78
|
+
evidenceFingerprint: fingerprint,
|
|
79
|
+
observedAt: now,
|
|
80
|
+
reasonCode: "manual_cancel",
|
|
81
|
+
updatedAt: now
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async function reconcile(watchId) {
|
|
86
|
+
const listed = dependencies.repository.load(watchId);
|
|
87
|
+
if (listed.status !== "active") {
|
|
88
|
+
return listed;
|
|
89
|
+
}
|
|
90
|
+
if (isDeadlineElapsed(listed, dependencies.now())) {
|
|
91
|
+
return settleTimeout(watchId);
|
|
92
|
+
}
|
|
93
|
+
const observation = await dependencies.observe(listed);
|
|
94
|
+
return dependencies.repository.withWatchLock(watchId, () => {
|
|
95
|
+
const current = dependencies.repository.load(watchId);
|
|
96
|
+
if (current.status !== "active") {
|
|
97
|
+
return current;
|
|
98
|
+
}
|
|
99
|
+
const now = canonicalNow(dependencies.now());
|
|
100
|
+
if (Date.parse(now) >= Date.parse(current.deadline_at)) {
|
|
101
|
+
return saveTimeout(current, now);
|
|
102
|
+
}
|
|
103
|
+
assertObservationForWatch(observation, current, now);
|
|
104
|
+
return applyObservation(current, observation, now);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function settleTimeout(watchId) {
|
|
108
|
+
return dependencies.repository.withWatchLock(watchId, () => {
|
|
109
|
+
const current = dependencies.repository.load(watchId);
|
|
110
|
+
if (current.status !== "active") {
|
|
111
|
+
return current;
|
|
112
|
+
}
|
|
113
|
+
const now = canonicalNow(dependencies.now());
|
|
114
|
+
if (Date.parse(now) < Date.parse(current.deadline_at)) {
|
|
115
|
+
return current;
|
|
116
|
+
}
|
|
117
|
+
return saveTimeout(current, now);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function saveTimeout(current, now) {
|
|
121
|
+
const fingerprint = deterministicEvidenceFingerprint({
|
|
122
|
+
version: 1,
|
|
123
|
+
watch_id: current.watch_id,
|
|
124
|
+
kind: "timed_out",
|
|
125
|
+
deadline_at: current.deadline_at,
|
|
126
|
+
anchor_fingerprint: current.anchor.anchor_fingerprint
|
|
127
|
+
});
|
|
128
|
+
return saveSettlement(current, {
|
|
129
|
+
kind: "timed_out",
|
|
130
|
+
evidenceFingerprint: fingerprint,
|
|
131
|
+
observedAt: now,
|
|
132
|
+
reasonCode: "deadline_elapsed",
|
|
133
|
+
updatedAt: now
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function applyObservation(current, observation, now) {
|
|
137
|
+
const checkpoint = nextObservationCheckpoint(current, observation);
|
|
138
|
+
const checkpointAdvanced = checkpoint.safe_resume_offset_bytes >
|
|
139
|
+
current.observation_checkpoint.safe_resume_offset_bytes;
|
|
140
|
+
const activityAt = latestActivityAt(current.last_activity_at, checkpointAdvanced
|
|
141
|
+
? observation.last_activity_at ?? observation.observed_at
|
|
142
|
+
: observation.last_activity_at);
|
|
143
|
+
if (observation.kind === "unavailable") {
|
|
144
|
+
if (!checkpointAdvanced)
|
|
145
|
+
return current;
|
|
146
|
+
return dependencies.repository.save({
|
|
147
|
+
...current,
|
|
148
|
+
observation_checkpoint: checkpoint,
|
|
149
|
+
last_activity_at: activityAt,
|
|
150
|
+
updated_at: now
|
|
151
|
+
}, { expectedRevision: terminalWatchRevision(current) });
|
|
152
|
+
}
|
|
153
|
+
if (observation.kind === "pending") {
|
|
154
|
+
if (activityAt === current.last_activity_at &&
|
|
155
|
+
!checkpointAdvanced) {
|
|
156
|
+
return current;
|
|
157
|
+
}
|
|
158
|
+
return dependencies.repository.save({
|
|
159
|
+
...current,
|
|
160
|
+
observation_checkpoint: checkpoint,
|
|
161
|
+
last_activity_at: activityAt,
|
|
162
|
+
updated_at: now
|
|
163
|
+
}, { expectedRevision: terminalWatchRevision(current) });
|
|
164
|
+
}
|
|
165
|
+
if (observation.kind === "approval") {
|
|
166
|
+
const duplicate = current.notification_outbox.some((notification) => notification.kind === "approval" &&
|
|
167
|
+
notification.evidence_fingerprint === observation.evidence_fingerprint);
|
|
168
|
+
if (duplicate) {
|
|
169
|
+
if (activityAt === current.last_activity_at &&
|
|
170
|
+
!checkpointAdvanced) {
|
|
171
|
+
return current;
|
|
172
|
+
}
|
|
173
|
+
return dependencies.repository.save({
|
|
174
|
+
...current,
|
|
175
|
+
observation_checkpoint: checkpoint,
|
|
176
|
+
last_activity_at: activityAt,
|
|
177
|
+
updated_at: now
|
|
178
|
+
}, { expectedRevision: terminalWatchRevision(current) });
|
|
179
|
+
}
|
|
180
|
+
const next = withApprovalNotification({
|
|
181
|
+
...current,
|
|
182
|
+
observation_checkpoint: checkpoint,
|
|
183
|
+
last_activity_at: activityAt,
|
|
184
|
+
updated_at: now
|
|
185
|
+
}, {
|
|
186
|
+
evidenceFingerprint: observation.evidence_fingerprint,
|
|
187
|
+
reasonCode: observation.reason_code,
|
|
188
|
+
createdAt: now
|
|
189
|
+
});
|
|
190
|
+
return dependencies.repository.save(next, { expectedRevision: terminalWatchRevision(current) });
|
|
191
|
+
}
|
|
192
|
+
return saveSettlement(current, {
|
|
193
|
+
kind: observation.kind,
|
|
194
|
+
evidenceFingerprint: observation.evidence_fingerprint,
|
|
195
|
+
observedAt: observation.observed_at,
|
|
196
|
+
reasonCode: observation.reason_code,
|
|
197
|
+
updatedAt: now,
|
|
198
|
+
lastActivityAt: activityAt,
|
|
199
|
+
completionText: observation.kind === "completed" ||
|
|
200
|
+
observation.kind === "failed"
|
|
201
|
+
? observation.completion_text
|
|
202
|
+
: undefined,
|
|
203
|
+
completionId: observation.kind === "completed" ||
|
|
204
|
+
observation.kind === "failed"
|
|
205
|
+
? observation.completion_id
|
|
206
|
+
: undefined,
|
|
207
|
+
completionTimestamp: observation.kind === "completed" ||
|
|
208
|
+
observation.kind === "failed"
|
|
209
|
+
? observation.completion_timestamp
|
|
210
|
+
: undefined
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function saveSettlement(current, input) {
|
|
214
|
+
const notification = pendingNotification(current.watch_id, input.kind, input.evidenceFingerprint, input.updatedAt, input.reasonCode);
|
|
215
|
+
const candidate = {
|
|
216
|
+
...current,
|
|
217
|
+
status: input.kind,
|
|
218
|
+
updated_at: input.updatedAt,
|
|
219
|
+
last_activity_at: input.lastActivityAt ?? current.last_activity_at,
|
|
220
|
+
settlement: {
|
|
221
|
+
kind: input.kind,
|
|
222
|
+
evidence_fingerprint: input.evidenceFingerprint,
|
|
223
|
+
observed_at: input.observedAt,
|
|
224
|
+
reason_code: input.reasonCode,
|
|
225
|
+
completion_text: input.completionText,
|
|
226
|
+
completion_id: input.completionId,
|
|
227
|
+
completion_timestamp: input.completionTimestamp
|
|
228
|
+
},
|
|
229
|
+
notification_outbox: [
|
|
230
|
+
...supersedeUndeliveredApprovals(current.notification_outbox, input.updatedAt),
|
|
231
|
+
notification
|
|
232
|
+
]
|
|
233
|
+
};
|
|
234
|
+
return dependencies.repository.save(candidate, { expectedRevision: terminalWatchRevision(current) });
|
|
235
|
+
}
|
|
236
|
+
function claimNextNotification(watchId) {
|
|
237
|
+
return dependencies.repository.withWatchLock(watchId, () => {
|
|
238
|
+
let current = dependencies.repository.load(watchId);
|
|
239
|
+
const now = canonicalNow(dependencies.now());
|
|
240
|
+
let index = firstUnresolvedNotificationIndex(current);
|
|
241
|
+
const first = current.notification_outbox[index];
|
|
242
|
+
if (current.status !== "active" &&
|
|
243
|
+
first?.kind === "approval" &&
|
|
244
|
+
notificationIsClaimable(first, now)) {
|
|
245
|
+
current = dependencies.repository.save({
|
|
246
|
+
...current,
|
|
247
|
+
updated_at: now,
|
|
248
|
+
notification_outbox: replaceAt(current.notification_outbox, index, supersededApprovalNotification(first, now))
|
|
249
|
+
}, { expectedRevision: terminalWatchRevision(current) });
|
|
250
|
+
index = firstUnresolvedNotificationIndex(current);
|
|
251
|
+
}
|
|
252
|
+
if (index < 0 ||
|
|
253
|
+
!notificationIsClaimable(current.notification_outbox[index], now)) {
|
|
254
|
+
return { watch: current };
|
|
255
|
+
}
|
|
256
|
+
const selected = current.notification_outbox[index];
|
|
257
|
+
const attemptId = dependencies.randomUUID();
|
|
258
|
+
const claimed = withNotificationReceipt(selected, {
|
|
259
|
+
status: "delivering",
|
|
260
|
+
attempts: selected.attempts + 1,
|
|
261
|
+
last_attempt_at: now,
|
|
262
|
+
attempt_id: attemptId,
|
|
263
|
+
attempt_lease_expires_at: new Date(Date.parse(now) + notificationLeaseMs).toISOString()
|
|
264
|
+
});
|
|
265
|
+
const saved = dependencies.repository.save({
|
|
266
|
+
...current,
|
|
267
|
+
updated_at: now,
|
|
268
|
+
notification_outbox: replaceAt(current.notification_outbox, index, claimed)
|
|
269
|
+
}, { expectedRevision: terminalWatchRevision(current) });
|
|
270
|
+
return {
|
|
271
|
+
watch: saved,
|
|
272
|
+
notification: saved.notification_outbox[index],
|
|
273
|
+
attempt_id: attemptId
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function finishNotification(watchId, notificationId, attemptId, outcome) {
|
|
278
|
+
return dependencies.repository.withWatchLock(watchId, () => {
|
|
279
|
+
const current = dependencies.repository.load(watchId);
|
|
280
|
+
const index = current.notification_outbox.findIndex((notification) => notification.notification_id === notificationId);
|
|
281
|
+
if (index < 0) {
|
|
282
|
+
return { settled: false, watch: current };
|
|
283
|
+
}
|
|
284
|
+
const selected = current.notification_outbox[index];
|
|
285
|
+
if (selected.status !== "delivering" ||
|
|
286
|
+
selected.attempt_id !== attemptId) {
|
|
287
|
+
return { settled: false, watch: current };
|
|
288
|
+
}
|
|
289
|
+
const now = canonicalNow(dependencies.now());
|
|
290
|
+
const settled = outcome.delivered
|
|
291
|
+
? withNotificationReceipt(selected, {
|
|
292
|
+
status: "delivered",
|
|
293
|
+
attempts: selected.attempts,
|
|
294
|
+
last_attempt_at: selected.last_attempt_at,
|
|
295
|
+
delivered_at: now
|
|
296
|
+
})
|
|
297
|
+
: selected.kind === "approval" && current.status !== "active"
|
|
298
|
+
? supersededApprovalNotification(selected, now)
|
|
299
|
+
: withNotificationReceipt(selected, {
|
|
300
|
+
status: "failed",
|
|
301
|
+
attempts: selected.attempts,
|
|
302
|
+
last_attempt_at: selected.last_attempt_at,
|
|
303
|
+
failed_at: now,
|
|
304
|
+
next_attempt_at: new Date(Date.parse(now) + retryDelay(selected.attempts)).toISOString(),
|
|
305
|
+
last_error_code: safeReasonCode(outcome.error_code, "callback_delivery_failed")
|
|
306
|
+
});
|
|
307
|
+
const saved = dependencies.repository.save({
|
|
308
|
+
...current,
|
|
309
|
+
updated_at: now,
|
|
310
|
+
notification_outbox: replaceAt(current.notification_outbox, index, settled)
|
|
311
|
+
}, { expectedRevision: terminalWatchRevision(current) });
|
|
312
|
+
return { settled: true, watch: saved };
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async function deliverNextNotification(watchId) {
|
|
316
|
+
const claim = claimNextNotification(watchId);
|
|
317
|
+
if (!claim.notification || !claim.attempt_id) {
|
|
318
|
+
return { status: "none", watch: claim.watch };
|
|
319
|
+
}
|
|
320
|
+
let errorCode;
|
|
321
|
+
try {
|
|
322
|
+
await dependencies.deliver({
|
|
323
|
+
watch: claim.watch,
|
|
324
|
+
notification: claim.notification
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
errorCode = safeReasonCode(dependencies.classifyDeliveryError?.(error), "callback_delivery_failed");
|
|
329
|
+
}
|
|
330
|
+
const notificationId = claim.notification.notification_id;
|
|
331
|
+
const finished = finishNotification(watchId, notificationId, claim.attempt_id, errorCode === undefined
|
|
332
|
+
? { delivered: true }
|
|
333
|
+
: { delivered: false, error_code: errorCode });
|
|
334
|
+
if (!finished.settled) {
|
|
335
|
+
return {
|
|
336
|
+
status: "claim_changed",
|
|
337
|
+
watch: finished.watch,
|
|
338
|
+
notification_id: notificationId
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return errorCode === undefined
|
|
342
|
+
? { status: "delivered", watch: finished.watch, notification_id: notificationId }
|
|
343
|
+
: {
|
|
344
|
+
status: "failed",
|
|
345
|
+
watch: finished.watch,
|
|
346
|
+
notification_id: notificationId,
|
|
347
|
+
error_code: errorCode
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
async function reconcileAll() {
|
|
351
|
+
return reconcileAllTerminalWatches({
|
|
352
|
+
dependencies,
|
|
353
|
+
reconciliationDeliveryLimit,
|
|
354
|
+
reconcile,
|
|
355
|
+
deliverNextNotification
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return Object.freeze({
|
|
359
|
+
create,
|
|
360
|
+
cancel,
|
|
361
|
+
get: (watchId) => dependencies.repository.load(watchId),
|
|
362
|
+
list: () => dependencies.repository.list(),
|
|
363
|
+
reconcile,
|
|
364
|
+
reconcileAll
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
async function reconcileAllTerminalWatches(input) {
|
|
368
|
+
const { dependencies, reconciliationDeliveryLimit, reconcile, deliverNextNotification } = input;
|
|
369
|
+
const scan = dependencies.repository.scanForReconciliation();
|
|
370
|
+
const work = scan.watches
|
|
371
|
+
.filter((watch) => watch.status === "active" || hasUnresolvedNotification(watch))
|
|
372
|
+
.map((watch) => ({
|
|
373
|
+
beforeRevision: terminalWatchRevision(watch),
|
|
374
|
+
callbacksDelivered: 0,
|
|
375
|
+
current: watch,
|
|
376
|
+
errorCode: undefined
|
|
377
|
+
}));
|
|
378
|
+
// Observation has priority over callback transport. A slow or permanently
|
|
379
|
+
// failing OpenClaw callback must never prevent another active Watch from
|
|
380
|
+
// learning that its exact task completed or became invalid.
|
|
381
|
+
for (const item of work) {
|
|
382
|
+
if (item.current.status !== "active")
|
|
383
|
+
continue;
|
|
384
|
+
try {
|
|
385
|
+
item.current = await reconcile(item.current.watch_id);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
item.errorCode = safeReasonCode(dependencies.classifyDeliveryError?.(error), "terminal_watch_observation_failed");
|
|
389
|
+
item.current = safeLoadWatch(dependencies.repository, item.current.watch_id, item.current);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Delivery is a separate, bounded and fair phase. At most one notification
|
|
393
|
+
// per Watch can be attempted in this reconciliation, and oldest attempts
|
|
394
|
+
// win so a recently failed callback cannot remain permanently at the head.
|
|
395
|
+
const deliveryNow = canonicalNow(dependencies.now());
|
|
396
|
+
const deliveryCandidates = work
|
|
397
|
+
.flatMap((item) => {
|
|
398
|
+
const notification = firstClaimableNotification(item.current, deliveryNow);
|
|
399
|
+
return notification ? [{ item, notification }] : [];
|
|
400
|
+
})
|
|
401
|
+
.sort((left, right) => notificationFairnessTime(left.notification).localeCompare(notificationFairnessTime(right.notification)) || left.item.current.watch_id.localeCompare(right.item.current.watch_id))
|
|
402
|
+
.slice(0, reconciliationDeliveryLimit);
|
|
403
|
+
for (const candidate of deliveryCandidates) {
|
|
404
|
+
const item = candidate.item;
|
|
405
|
+
try {
|
|
406
|
+
const delivery = await deliverNextNotification(item.current.watch_id);
|
|
407
|
+
item.current = delivery.watch;
|
|
408
|
+
if (delivery.status === "delivered") {
|
|
409
|
+
item.callbacksDelivered += 1;
|
|
410
|
+
}
|
|
411
|
+
else if (delivery.status === "failed") {
|
|
412
|
+
item.errorCode = item.errorCode ?? delivery.error_code;
|
|
413
|
+
}
|
|
414
|
+
else if (delivery.status === "claim_changed") {
|
|
415
|
+
item.errorCode = item.errorCode ?? "callback_claim_changed";
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
item.errorCode = item.errorCode ?? safeReasonCode(dependencies.classifyDeliveryError?.(error), "terminal_watch_callback_recovery_failed");
|
|
420
|
+
item.current = safeLoadWatch(dependencies.repository, item.current.watch_id, item.current);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const summary = {
|
|
424
|
+
checked: work.length + scan.errors.length,
|
|
425
|
+
changed: 0,
|
|
426
|
+
callbacks_delivered: 0,
|
|
427
|
+
errors: 0,
|
|
428
|
+
items: []
|
|
429
|
+
};
|
|
430
|
+
for (const item of work) {
|
|
431
|
+
const changed = terminalWatchRevision(item.current) !== item.beforeRevision;
|
|
432
|
+
if (changed)
|
|
433
|
+
summary.changed += 1;
|
|
434
|
+
summary.callbacks_delivered += item.callbacksDelivered;
|
|
435
|
+
if (item.errorCode)
|
|
436
|
+
summary.errors += 1;
|
|
437
|
+
summary.items.push({
|
|
438
|
+
watch_id: item.current.watch_id,
|
|
439
|
+
status: item.current.status,
|
|
440
|
+
changed,
|
|
441
|
+
callbacks_delivered: item.callbacksDelivered,
|
|
442
|
+
error_code: item.errorCode
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
for (const error of scan.errors) {
|
|
446
|
+
summary.errors += 1;
|
|
447
|
+
summary.items.push({
|
|
448
|
+
watch_id: error.watch_id,
|
|
449
|
+
status: "error",
|
|
450
|
+
changed: false,
|
|
451
|
+
callbacks_delivered: 0,
|
|
452
|
+
error_code: error.error_code
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
return summary;
|
|
456
|
+
}
|
|
457
|
+
function firstUnresolvedNotificationIndex(watch) {
|
|
458
|
+
return watch.notification_outbox.findIndex((notification) => notification.status !== "delivered" &&
|
|
459
|
+
notification.status !== "superseded");
|
|
460
|
+
}
|
|
461
|
+
function hasUnresolvedNotification(watch) {
|
|
462
|
+
return firstUnresolvedNotificationIndex(watch) >= 0;
|
|
463
|
+
}
|
|
464
|
+
function firstClaimableNotification(watch, now) {
|
|
465
|
+
const index = firstUnresolvedNotificationIndex(watch);
|
|
466
|
+
const notification = watch.notification_outbox[index];
|
|
467
|
+
return notification && notificationIsClaimable(notification, now)
|
|
468
|
+
? notification
|
|
469
|
+
: undefined;
|
|
470
|
+
}
|
|
471
|
+
function notificationFairnessTime(notification) {
|
|
472
|
+
return notification.last_attempt_at ?? notification.created_at;
|
|
473
|
+
}
|
|
474
|
+
function safeLoadWatch(repository, watchId, fallback) {
|
|
475
|
+
try {
|
|
476
|
+
return repository.load(watchId);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return fallback;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function terminalWatchNotificationDeliveryPolicy(dependencies) {
|
|
483
|
+
const maximumRetryDelay = positiveMilliseconds(dependencies.notificationMaxRetryDelayMs, DEFAULT_NOTIFICATION_MAX_RETRY_DELAY_MS, "terminal Watch maximum notification retry delay");
|
|
484
|
+
const configuredRetryDelay = dependencies.notificationRetryDelayMs;
|
|
485
|
+
return {
|
|
486
|
+
notificationLeaseMs: positiveMilliseconds(dependencies.notificationLeaseMs, DEFAULT_NOTIFICATION_LEASE_MS, "terminal Watch notification lease"),
|
|
487
|
+
reconciliationDeliveryLimit: positiveMilliseconds(dependencies.reconciliationDeliveryLimit, DEFAULT_RECONCILIATION_DELIVERY_LIMIT, "terminal Watch reconciliation delivery limit"),
|
|
488
|
+
retryDelay: (attempt) => nonNegativeMilliseconds(Math.min(maximumRetryDelay, typeof configuredRetryDelay === "function"
|
|
489
|
+
? configuredRetryDelay(attempt)
|
|
490
|
+
: (configuredRetryDelay ?? DEFAULT_NOTIFICATION_RETRY_DELAY_MS) *
|
|
491
|
+
2 ** Math.max(0, attempt - 1)), DEFAULT_NOTIFICATION_RETRY_DELAY_MS, "terminal Watch notification retry delay")
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function nextObservationCheckpoint(watch, observation) {
|
|
495
|
+
const suppliedCheckpoint = observation.observation_checkpoint;
|
|
496
|
+
const suppliedOffset = observation.safe_resume_offset_bytes;
|
|
497
|
+
if (suppliedCheckpoint !== undefined &&
|
|
498
|
+
suppliedOffset !== undefined &&
|
|
499
|
+
suppliedCheckpoint.safe_resume_offset_bytes !== suppliedOffset) {
|
|
500
|
+
throw new Error("terminal Watch observation checkpoints disagree");
|
|
501
|
+
}
|
|
502
|
+
if (suppliedCheckpoint === undefined && suppliedOffset === undefined) {
|
|
503
|
+
return watch.observation_checkpoint;
|
|
504
|
+
}
|
|
505
|
+
const candidate = suppliedCheckpoint ?? {
|
|
506
|
+
safe_resume_offset_bytes: suppliedOffset
|
|
507
|
+
};
|
|
508
|
+
if (!Number.isSafeInteger(candidate.safe_resume_offset_bytes) ||
|
|
509
|
+
candidate.safe_resume_offset_bytes <
|
|
510
|
+
watch.observation_checkpoint.safe_resume_offset_bytes) {
|
|
511
|
+
throw new Error("terminal Watch observation checkpoint is invalid");
|
|
512
|
+
}
|
|
513
|
+
const candidateWatch = { ...watch, observation_checkpoint: candidate };
|
|
514
|
+
assertTerminalWatch(candidateWatch, watch.watch_id);
|
|
515
|
+
return candidate;
|
|
516
|
+
}
|
|
517
|
+
export function terminalWatchObservationFence(watch) {
|
|
518
|
+
return {
|
|
519
|
+
watch_id: watch.watch_id,
|
|
520
|
+
terminal_identity_fingerprint: terminalWatchIdentityFingerprint(watch),
|
|
521
|
+
anchor_fingerprint: watch.anchor.anchor_fingerprint
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function assertObservationForWatch(observation, watch, now) {
|
|
525
|
+
const expected = terminalWatchObservationFence(watch);
|
|
526
|
+
if (observation.watch_id !== expected.watch_id ||
|
|
527
|
+
observation.terminal_identity_fingerprint !==
|
|
528
|
+
expected.terminal_identity_fingerprint ||
|
|
529
|
+
observation.anchor_fingerprint !== expected.anchor_fingerprint) {
|
|
530
|
+
throw new Error("terminal Watch observation does not match its exact anchor");
|
|
531
|
+
}
|
|
532
|
+
assertTimestamp(observation.observed_at, "terminal Watch observation time");
|
|
533
|
+
if (Date.parse(observation.observed_at) > Date.parse(now)) {
|
|
534
|
+
throw new Error("terminal Watch observation cannot come from the future");
|
|
535
|
+
}
|
|
536
|
+
if (observation.last_activity_at !== undefined) {
|
|
537
|
+
assertTimestamp(observation.last_activity_at, "terminal Watch observation activity");
|
|
538
|
+
if (Date.parse(observation.last_activity_at) > Date.parse(now)) {
|
|
539
|
+
throw new Error("terminal Watch activity cannot come from the future");
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (observation.safe_resume_offset_bytes !== undefined) {
|
|
543
|
+
if (!Number.isSafeInteger(observation.safe_resume_offset_bytes) ||
|
|
544
|
+
observation.safe_resume_offset_bytes <
|
|
545
|
+
watch.observation_checkpoint.safe_resume_offset_bytes) {
|
|
546
|
+
throw new Error("terminal Watch observation checkpoint is invalid");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (observation.observation_checkpoint !== undefined &&
|
|
550
|
+
(observation.observation_checkpoint.safe_resume_offset_bytes <
|
|
551
|
+
watch.observation_checkpoint.safe_resume_offset_bytes ||
|
|
552
|
+
(observation.safe_resume_offset_bytes !== undefined &&
|
|
553
|
+
observation.safe_resume_offset_bytes !==
|
|
554
|
+
observation.observation_checkpoint.safe_resume_offset_bytes))) {
|
|
555
|
+
throw new Error("terminal Watch observation checkpoint is invalid");
|
|
556
|
+
}
|
|
557
|
+
if (observation.kind === "unavailable") {
|
|
558
|
+
if (observation.reason_code !== undefined) {
|
|
559
|
+
safeReasonCode(observation.reason_code, undefined);
|
|
560
|
+
}
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (observation.kind !== "pending") {
|
|
564
|
+
assertSha256(observation.evidence_fingerprint, "terminal Watch observation fingerprint");
|
|
565
|
+
if (observation.reason_code !== undefined) {
|
|
566
|
+
safeReasonCode(observation.reason_code, undefined);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (observation.kind === "completed" || observation.kind === "failed") {
|
|
570
|
+
if (observation.completion_text !== undefined &&
|
|
571
|
+
(typeof observation.completion_text !== "string" ||
|
|
572
|
+
observation.completion_text.length > 4000 ||
|
|
573
|
+
observation.completion_text.includes("\0"))) {
|
|
574
|
+
throw new Error("terminal Watch completion text exceeds its safe bound");
|
|
575
|
+
}
|
|
576
|
+
if (observation.completion_id !== undefined &&
|
|
577
|
+
(typeof observation.completion_id !== "string" ||
|
|
578
|
+
observation.completion_id.trim().length === 0 ||
|
|
579
|
+
observation.completion_id.includes("\0"))) {
|
|
580
|
+
throw new Error("terminal Watch completion id is invalid");
|
|
581
|
+
}
|
|
582
|
+
if (observation.completion_timestamp !== undefined) {
|
|
583
|
+
assertTimestamp(observation.completion_timestamp, "terminal Watch completion timestamp");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function withApprovalNotification(watch, input) {
|
|
588
|
+
const notification = pendingNotification(watch.watch_id, "approval", input.evidenceFingerprint, input.createdAt, input.reasonCode);
|
|
589
|
+
return {
|
|
590
|
+
...watch,
|
|
591
|
+
notification_outbox: [...watch.notification_outbox, notification]
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function pendingNotification(watchId, kind, evidenceFingerprint, createdAt, reasonCode) {
|
|
595
|
+
assertSha256(evidenceFingerprint, "terminal Watch notification fingerprint");
|
|
596
|
+
const notificationId = terminalWatchNotificationId(watchId, kind, evidenceFingerprint);
|
|
597
|
+
return {
|
|
598
|
+
notification_id: notificationId,
|
|
599
|
+
idempotency_key: terminalWatchNotificationIdempotencyKey(watchId, notificationId),
|
|
600
|
+
kind,
|
|
601
|
+
evidence_fingerprint: evidenceFingerprint,
|
|
602
|
+
reason_code: reasonCode === undefined
|
|
603
|
+
? undefined
|
|
604
|
+
: safeReasonCode(reasonCode, undefined),
|
|
605
|
+
status: "pending",
|
|
606
|
+
attempts: 0,
|
|
607
|
+
created_at: createdAt
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function supersedeUndeliveredApprovals(notifications, supersededAt) {
|
|
611
|
+
return notifications.map((notification) => {
|
|
612
|
+
if (notification.kind !== "approval" ||
|
|
613
|
+
notification.status === "delivered" ||
|
|
614
|
+
notification.status === "delivering" ||
|
|
615
|
+
notification.status === "superseded") {
|
|
616
|
+
return notification;
|
|
617
|
+
}
|
|
618
|
+
return supersededApprovalNotification(notification, supersededAt);
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
function supersededApprovalNotification(notification, supersededAt) {
|
|
622
|
+
return withNotificationReceipt(notification, {
|
|
623
|
+
status: "superseded",
|
|
624
|
+
attempts: notification.attempts,
|
|
625
|
+
superseded_at: supersededAt
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
/** Replace the mutable delivery receipt without leaking stale lease/error data. */
|
|
629
|
+
function withNotificationReceipt(notification, receipt) {
|
|
630
|
+
return {
|
|
631
|
+
notification_id: notification.notification_id,
|
|
632
|
+
idempotency_key: notification.idempotency_key,
|
|
633
|
+
kind: notification.kind,
|
|
634
|
+
evidence_fingerprint: notification.evidence_fingerprint,
|
|
635
|
+
reason_code: notification.reason_code,
|
|
636
|
+
created_at: notification.created_at,
|
|
637
|
+
...receipt
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function notificationIsClaimable(notification, now) {
|
|
641
|
+
if (notification.status === "pending") {
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
if (notification.status === "failed") {
|
|
645
|
+
return Date.parse(notification.next_attempt_at ?? "") <= Date.parse(now);
|
|
646
|
+
}
|
|
647
|
+
if (notification.status === "delivering") {
|
|
648
|
+
return Date.parse(notification.attempt_lease_expires_at ?? "") <=
|
|
649
|
+
Date.parse(now);
|
|
650
|
+
}
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
function replaceAt(values, index, value) {
|
|
654
|
+
return values.map((candidate, candidateIndex) => candidateIndex === index ? value : candidate);
|
|
655
|
+
}
|
|
656
|
+
function latestActivityAt(current, candidate) {
|
|
657
|
+
return candidate !== undefined && Date.parse(candidate) > Date.parse(current)
|
|
658
|
+
? candidate
|
|
659
|
+
: current;
|
|
660
|
+
}
|
|
661
|
+
function isDeadlineElapsed(watch, now) {
|
|
662
|
+
return now.getTime() >= Date.parse(watch.deadline_at);
|
|
663
|
+
}
|
|
664
|
+
function deterministicEvidenceFingerprint(value) {
|
|
665
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
666
|
+
}
|
|
667
|
+
function canonicalNow(value) {
|
|
668
|
+
const milliseconds = value.getTime();
|
|
669
|
+
if (!Number.isFinite(milliseconds)) {
|
|
670
|
+
throw new Error("terminal Watch clock returned an invalid Date");
|
|
671
|
+
}
|
|
672
|
+
return value.toISOString();
|
|
673
|
+
}
|
|
674
|
+
function positiveMilliseconds(value, fallback, label) {
|
|
675
|
+
const candidate = value === undefined ? fallback : Number(value);
|
|
676
|
+
if (candidate === undefined ||
|
|
677
|
+
!Number.isSafeInteger(candidate) ||
|
|
678
|
+
candidate <= 0) {
|
|
679
|
+
throw new Error(`${label} must be a positive safe integer`);
|
|
680
|
+
}
|
|
681
|
+
return candidate;
|
|
682
|
+
}
|
|
683
|
+
function nonNegativeMilliseconds(value, fallback, label) {
|
|
684
|
+
const candidate = value === undefined ? fallback : Number(value);
|
|
685
|
+
if (!Number.isSafeInteger(candidate) || candidate < 0) {
|
|
686
|
+
throw new Error(`${label} must be a non-negative safe integer`);
|
|
687
|
+
}
|
|
688
|
+
return candidate;
|
|
689
|
+
}
|
|
690
|
+
function safeReasonCode(value, fallback) {
|
|
691
|
+
if (typeof value === "string" &&
|
|
692
|
+
value.length >= 1 &&
|
|
693
|
+
value.length <= 128 &&
|
|
694
|
+
/^[a-z0-9][a-z0-9_.:-]*$/u.test(value)) {
|
|
695
|
+
return value;
|
|
696
|
+
}
|
|
697
|
+
if (fallback !== undefined) {
|
|
698
|
+
return fallback;
|
|
699
|
+
}
|
|
700
|
+
throw new Error("terminal Watch reason must be a privacy-safe reason code");
|
|
701
|
+
}
|
|
702
|
+
function assertTimestamp(value, label) {
|
|
703
|
+
if (typeof value !== "string" ||
|
|
704
|
+
!Number.isFinite(Date.parse(value)) ||
|
|
705
|
+
new Date(value).toISOString() !== value) {
|
|
706
|
+
throw new Error(`${label} must be a canonical ISO timestamp`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function assertSha256(value, label) {
|
|
710
|
+
if (typeof value !== "string" || !/^[0-9a-f]{64}$/u.test(value)) {
|
|
711
|
+
throw new Error(`${label} must be a lowercase SHA-256 digest`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
//# sourceMappingURL=terminal-watch-service.js.map
|