@scotthuang/agent-knock-knock 0.12.12 → 0.12.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +40 -27
  3. package/dist/src/approval-policy.d.ts +11 -2
  4. package/dist/src/approval-policy.js +33 -7
  5. package/dist/src/approval-policy.js.map +1 -1
  6. package/dist/src/callback-cli-adapter.d.ts +9 -6
  7. package/dist/src/callback-cli-adapter.js +119 -30
  8. package/dist/src/callback-cli-adapter.js.map +1 -1
  9. package/dist/src/callback-outbox-policy.d.ts +39 -0
  10. package/dist/src/callback-outbox-policy.js +163 -6
  11. package/dist/src/callback-outbox-policy.js.map +1 -1
  12. package/dist/src/callback-outbox-service.d.ts +28 -1
  13. package/dist/src/callback-outbox-service.js +413 -82
  14. package/dist/src/callback-outbox-service.js.map +1 -1
  15. package/dist/src/callback-outbox-settlement.d.ts +14 -1
  16. package/dist/src/callback-outbox-settlement.js +151 -30
  17. package/dist/src/callback-outbox-settlement.js.map +1 -1
  18. package/dist/src/callback-route-authority.d.ts +59 -0
  19. package/dist/src/callback-route-authority.js +123 -0
  20. package/dist/src/callback-route-authority.js.map +1 -0
  21. package/dist/src/callback-transport.d.ts +134 -0
  22. package/dist/src/callback-transport.js +256 -0
  23. package/dist/src/callback-transport.js.map +1 -0
  24. package/dist/src/claude-local-transcript-provider.d.ts +81 -0
  25. package/dist/src/claude-local-transcript-provider.js +854 -18
  26. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  27. package/dist/src/cli-core.js +102 -4
  28. package/dist/src/cli-core.js.map +1 -1
  29. package/dist/src/codex-lifecycle-compatibility.js +2 -1
  30. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  31. package/dist/src/deferred-foreground-application-service.d.ts +18 -0
  32. package/dist/src/deferred-foreground-application-service.js +72 -0
  33. package/dist/src/deferred-foreground-application-service.js.map +1 -1
  34. package/dist/src/deferred-foreground-authority-cli-adapter.js +92 -20
  35. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  36. package/dist/src/deferred-foreground-recovery-cli-adapter.d.ts +1 -1
  37. package/dist/src/deferred-foreground-recovery-cli-adapter.js +108 -37
  38. package/dist/src/deferred-foreground-recovery-cli-adapter.js.map +1 -1
  39. package/dist/src/deferred-foreground-recovery-service.d.ts +1 -1
  40. package/dist/src/deferred-foreground-recovery-service.js +10 -3
  41. package/dist/src/deferred-foreground-recovery-service.js.map +1 -1
  42. package/dist/src/deferred-foreground-transfer-policy.d.ts +1 -1
  43. package/dist/src/deferred-foreground-transfer-policy.js +3 -2
  44. package/dist/src/deferred-foreground-transfer-policy.js.map +1 -1
  45. package/dist/src/deferred-foreground-transfer.d.ts +17 -1
  46. package/dist/src/deferred-foreground-transfer.js +247 -39
  47. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  48. package/dist/src/deferred-foreground-user-close.d.ts +18 -0
  49. package/dist/src/deferred-foreground-user-close.js +180 -0
  50. package/dist/src/deferred-foreground-user-close.js.map +1 -0
  51. package/dist/src/openclaw-callback-transport.d.ts +9 -1
  52. package/dist/src/openclaw-callback-transport.js +404 -11
  53. package/dist/src/openclaw-callback-transport.js.map +1 -1
  54. package/dist/src/openclaw-managed-callback-cli-adapter.d.ts +23 -0
  55. package/dist/src/openclaw-managed-callback-cli-adapter.js +77 -0
  56. package/dist/src/openclaw-managed-callback-cli-adapter.js.map +1 -0
  57. package/dist/src/openclaw-plugin-callback-adapter.js +96 -14
  58. package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -1
  59. package/dist/src/openclaw-plugin-command-adapter.js +547 -109
  60. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  61. package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
  62. package/dist/src/openclaw-plugin-helpers.js +317 -44
  63. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  64. package/dist/src/openclaw-plugin-schemas.d.ts +61 -66
  65. package/dist/src/openclaw-plugin-schemas.js +87 -91
  66. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  67. package/dist/src/openclaw-plugin-supervisor.js +48 -10
  68. package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
  69. package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
  70. package/dist/src/openclaw-private-authority-offers.js +177 -0
  71. package/dist/src/openclaw-private-authority-offers.js.map +1 -0
  72. package/dist/src/protocol.d.ts +3 -0
  73. package/dist/src/protocol.js +4 -0
  74. package/dist/src/protocol.js.map +1 -1
  75. package/dist/src/store.d.ts +10 -0
  76. package/dist/src/store.js +82 -2
  77. package/dist/src/store.js.map +1 -1
  78. package/dist/src/terminal-acceptance-cli-adapter.d.ts +2 -0
  79. package/dist/src/terminal-acceptance-cli-adapter.js +62 -12
  80. package/dist/src/terminal-acceptance-cli-adapter.js.map +1 -1
  81. package/dist/src/terminal-action-projection.d.ts +19 -2
  82. package/dist/src/terminal-action-projection.js +27 -5
  83. package/dist/src/terminal-action-projection.js.map +1 -1
  84. package/dist/src/terminal-agent-bridge.d.ts +82 -0
  85. package/dist/src/terminal-agent-bridge.js +539 -111
  86. package/dist/src/terminal-agent-bridge.js.map +1 -1
  87. package/dist/src/terminal-command-cli-adapter.d.ts +14 -0
  88. package/dist/src/terminal-command-cli-adapter.js +2435 -150
  89. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  90. package/dist/src/terminal-dispatch-application.d.ts +4 -1
  91. package/dist/src/terminal-dispatch-application.js +11 -2
  92. package/dist/src/terminal-dispatch-application.js.map +1 -1
  93. package/dist/src/terminal-dispatch-execution.js +3 -1
  94. package/dist/src/terminal-dispatch-execution.js.map +1 -1
  95. package/dist/src/terminal-dispatch-ledger-codec.d.ts +3 -0
  96. package/dist/src/terminal-dispatch-ledger-codec.js +46 -19
  97. package/dist/src/terminal-dispatch-ledger-codec.js.map +1 -1
  98. package/dist/src/terminal-dispatch-presenter.js +5 -3
  99. package/dist/src/terminal-dispatch-presenter.js.map +1 -1
  100. package/dist/src/terminal-dispatch-receipt.d.ts +3 -0
  101. package/dist/src/terminal-dispatch-receipt.js +15 -1
  102. package/dist/src/terminal-dispatch-receipt.js.map +1 -1
  103. package/dist/src/terminal-dispatch-recovery-cli-adapter.js +68 -7
  104. package/dist/src/terminal-dispatch-recovery-cli-adapter.js.map +1 -1
  105. package/dist/src/terminal-dispatch-recovery-service.d.ts +2 -0
  106. package/dist/src/terminal-dispatch-recovery-service.js +9 -0
  107. package/dist/src/terminal-dispatch-recovery-service.js.map +1 -1
  108. package/dist/src/terminal-dispatch-repository-cli-adapter.js +305 -79
  109. package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -1
  110. package/dist/src/terminal-identity-authority-cli-adapter.d.ts +2 -0
  111. package/dist/src/terminal-identity-authority-cli-adapter.js +30 -27
  112. package/dist/src/terminal-identity-authority-cli-adapter.js.map +1 -1
  113. package/dist/src/terminal-list-cli-adapter.d.ts +32 -7
  114. package/dist/src/terminal-list-cli-adapter.js +335 -98
  115. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  116. package/dist/src/terminal-list-renderer.d.ts +11 -1
  117. package/dist/src/terminal-list-renderer.js +328 -132
  118. package/dist/src/terminal-list-renderer.js.map +1 -1
  119. package/dist/src/terminal-maintenance-cli-adapter.js +253 -326
  120. package/dist/src/terminal-maintenance-cli-adapter.js.map +1 -1
  121. package/dist/src/terminal-monitor-application-service.d.ts +5 -0
  122. package/dist/src/terminal-monitor-application-service.js +5 -0
  123. package/dist/src/terminal-monitor-application-service.js.map +1 -1
  124. package/dist/src/terminal-monitor-cli-adapter.d.ts +25 -1
  125. package/dist/src/terminal-monitor-cli-adapter.js +171 -28
  126. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  127. package/dist/src/terminal-monitor-reconciliation-eligibility.d.ts +1 -1
  128. package/dist/src/terminal-monitor-reconciliation-eligibility.js +220 -32
  129. package/dist/src/terminal-monitor-reconciliation-eligibility.js.map +1 -1
  130. package/dist/src/terminal-monitor-state-cli-adapter.d.ts +1 -1
  131. package/dist/src/terminal-monitor-state-cli-adapter.js +814 -162
  132. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  133. package/dist/src/terminal-monitor-state-reconciliation-service.d.ts +2 -1
  134. package/dist/src/terminal-monitor-state-reconciliation-service.js +12 -0
  135. package/dist/src/terminal-monitor-state-reconciliation-service.js.map +1 -1
  136. package/dist/src/terminal-monitor-supervision-cli-adapter.d.ts +2 -0
  137. package/dist/src/terminal-monitor-supervision-cli-adapter.js +31 -11
  138. package/dist/src/terminal-monitor-supervision-cli-adapter.js.map +1 -1
  139. package/dist/src/terminal-status-cli-adapter.d.ts +11 -0
  140. package/dist/src/terminal-status-cli-adapter.js +119 -35
  141. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  142. package/dist/src/terminal-submission-acceptance.d.ts +60 -1
  143. package/dist/src/terminal-submission-acceptance.js +613 -0
  144. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  145. package/dist/src/terminal-submission-retry-service.d.ts +114 -0
  146. package/dist/src/terminal-submission-retry-service.js +589 -0
  147. package/dist/src/terminal-submission-retry-service.js.map +1 -0
  148. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +28 -0
  149. package/dist/src/terminal-watch-callback-cli-adapter.js +94 -0
  150. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
  151. package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
  152. package/dist/src/terminal-watch-cli-adapter.js +532 -0
  153. package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
  154. package/dist/src/terminal-watch-service.d.ts +102 -0
  155. package/dist/src/terminal-watch-service.js +883 -0
  156. package/dist/src/terminal-watch-service.js.map +1 -0
  157. package/dist/src/terminal-watch-store.d.ts +172 -0
  158. package/dist/src/terminal-watch-store.js +1019 -0
  159. package/dist/src/terminal-watch-store.js.map +1 -0
  160. package/docs/quickstart-herdr.md +8 -0
  161. package/docs/quickstart-tmux.md +33 -6
  162. package/openclaw.plugin.json +11 -3
  163. package/package.json +1 -1
  164. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +61 -40
@@ -0,0 +1,883 @@
1
+ import { createHash } from "node:crypto";
2
+ import { parseCallbackAttemptOutcome, parseCallbackRoute } from "./callback-transport.js";
3
+ import { TERMINAL_WATCH_SCHEMA, TERMINAL_WATCH_VERSION, assertTerminalWatch, initialTerminalWatchObservationCheckpoint, terminalWatchIdentityFingerprint, terminalWatchCallbackEnvelope, terminalWatchNotificationCallbackSnapshot, terminalWatchNotificationId, terminalWatchNotificationIdempotencyKey, terminalWatchRevision } from "./terminal-watch-store.js";
4
+ export { terminalWatchCallbackEnvelope, terminalWatchCallbackMessage } from "./terminal-watch-store.js";
5
+ const DEFAULT_NOTIFICATION_LEASE_MS = 30_000;
6
+ const DEFAULT_NOTIFICATION_RETRY_DELAY_MS = 5_000;
7
+ const DEFAULT_NOTIFICATION_MAX_RETRY_DELAY_MS = 60_000;
8
+ const DEFAULT_RECONCILIATION_DELIVERY_LIMIT = 1;
9
+ export class ActiveTerminalWatchConflictError extends Error {
10
+ code = "AKK_ACTIVE_TERMINAL_WATCH_CONFLICT";
11
+ existingWatchId;
12
+ constructor(existingWatchId) {
13
+ super(`exact terminal/native task already has active Watch ${existingWatchId}`);
14
+ this.name = "ActiveTerminalWatchConflictError";
15
+ this.existingWatchId = existingWatchId;
16
+ }
17
+ }
18
+ /**
19
+ * Create the durable Watch application service. Terminal observation and
20
+ * callback transport are the only effects; the API intentionally has no port
21
+ * capable of writing to, approving, cancelling, or taking ownership of a TUI.
22
+ */
23
+ export function createTerminalWatchService(dependencies) {
24
+ const { notificationLeaseMs, reconciliationDeliveryLimit, retryDelay } = terminalWatchNotificationDeliveryPolicy(dependencies);
25
+ const deliverNextNotification = createTerminalWatchNotificationDelivery({
26
+ dependencies,
27
+ notificationLeaseMs,
28
+ retryDelay
29
+ });
30
+ function create(input) {
31
+ const createdAt = canonicalNow(dependencies.now());
32
+ const watchId = input.watch_id ??
33
+ `terminal-watch-${dependencies.randomUUID()}`;
34
+ const timeoutMs = positiveMilliseconds(input.timeout_ms, undefined, "terminal Watch timeout");
35
+ let candidate = {
36
+ schema: TERMINAL_WATCH_SCHEMA,
37
+ version: TERMINAL_WATCH_VERSION,
38
+ watch_id: watchId,
39
+ agent: input.agent,
40
+ terminal: input.terminal,
41
+ anchor: input.anchor,
42
+ observation_checkpoint: initialTerminalWatchObservationCheckpoint(input.anchor),
43
+ openclaw_session: input.openclaw_session,
44
+ openclaw_bin: input.openclaw_bin,
45
+ created_at: createdAt,
46
+ deadline_at: new Date(Date.parse(createdAt) + timeoutMs).toISOString(),
47
+ updated_at: createdAt,
48
+ status: "active",
49
+ last_activity_at: input.last_activity_at ?? createdAt,
50
+ notification_outbox: []
51
+ };
52
+ if (input.approval_fingerprint !== undefined) {
53
+ candidate = withApprovalNotification(candidate, {
54
+ evidenceFingerprint: input.approval_fingerprint,
55
+ reasonCode: input.approval_reason_code,
56
+ createdAt
57
+ });
58
+ }
59
+ assertTerminalWatch(candidate, watchId, { allowMissingRevision: true });
60
+ return dependencies.repository.withWatchLock(watchId, () => {
61
+ const identity = terminalWatchIdentityFingerprint(candidate);
62
+ const existing = dependencies.repository.list().find((watch) => watch.status === "active" &&
63
+ terminalWatchIdentityFingerprint(watch) === identity);
64
+ if (existing) {
65
+ throw new ActiveTerminalWatchConflictError(existing.watch_id);
66
+ }
67
+ return dependencies.repository.save(candidate, { expectedRevision: null });
68
+ });
69
+ }
70
+ function cancel(watchId) {
71
+ return dependencies.repository.withWatchLock(watchId, () => {
72
+ const current = dependencies.repository.load(watchId);
73
+ if (current.status !== "active") {
74
+ return current;
75
+ }
76
+ const now = canonicalNow(dependencies.now());
77
+ const fingerprint = deterministicEvidenceFingerprint({
78
+ version: 1,
79
+ watch_id: current.watch_id,
80
+ kind: "cancelled",
81
+ anchor_fingerprint: current.anchor.anchor_fingerprint
82
+ });
83
+ return saveSettlement(current, {
84
+ kind: "cancelled",
85
+ evidenceFingerprint: fingerprint,
86
+ observedAt: now,
87
+ reasonCode: "manual_cancel",
88
+ updatedAt: now
89
+ });
90
+ });
91
+ }
92
+ async function reconcile(watchId) {
93
+ const listed = dependencies.repository.load(watchId);
94
+ if (listed.status !== "active") {
95
+ return listed;
96
+ }
97
+ if (isDeadlineElapsed(listed, dependencies.now())) {
98
+ return settleTimeout(watchId);
99
+ }
100
+ const observation = await dependencies.observe(listed);
101
+ return dependencies.repository.withWatchLock(watchId, () => {
102
+ const current = dependencies.repository.load(watchId);
103
+ if (current.status !== "active") {
104
+ return current;
105
+ }
106
+ const now = canonicalNow(dependencies.now());
107
+ if (Date.parse(now) >= Date.parse(current.deadline_at)) {
108
+ return saveTimeout(current, now);
109
+ }
110
+ assertObservationForWatch(observation, current, now);
111
+ return applyObservation(current, observation, now);
112
+ });
113
+ }
114
+ function settleTimeout(watchId) {
115
+ return dependencies.repository.withWatchLock(watchId, () => {
116
+ const current = dependencies.repository.load(watchId);
117
+ if (current.status !== "active") {
118
+ return current;
119
+ }
120
+ const now = canonicalNow(dependencies.now());
121
+ if (Date.parse(now) < Date.parse(current.deadline_at)) {
122
+ return current;
123
+ }
124
+ return saveTimeout(current, now);
125
+ });
126
+ }
127
+ function saveTimeout(current, now) {
128
+ const fingerprint = deterministicEvidenceFingerprint({
129
+ version: 1,
130
+ watch_id: current.watch_id,
131
+ kind: "timed_out",
132
+ deadline_at: current.deadline_at,
133
+ anchor_fingerprint: current.anchor.anchor_fingerprint
134
+ });
135
+ return saveSettlement(current, {
136
+ kind: "timed_out",
137
+ evidenceFingerprint: fingerprint,
138
+ observedAt: now,
139
+ reasonCode: "deadline_elapsed",
140
+ updatedAt: now
141
+ });
142
+ }
143
+ function applyObservation(current, observation, now) {
144
+ const checkpoint = nextObservationCheckpoint(current, observation);
145
+ const checkpointAdvanced = checkpoint.safe_resume_offset_bytes >
146
+ current.observation_checkpoint.safe_resume_offset_bytes;
147
+ const activityAt = latestActivityAt(current.last_activity_at, checkpointAdvanced
148
+ ? observation.last_activity_at ?? observation.observed_at
149
+ : observation.last_activity_at);
150
+ if (observation.kind === "unavailable") {
151
+ if (!checkpointAdvanced)
152
+ return current;
153
+ return dependencies.repository.save({
154
+ ...current,
155
+ observation_checkpoint: checkpoint,
156
+ last_activity_at: activityAt,
157
+ updated_at: now
158
+ }, { expectedRevision: terminalWatchRevision(current) });
159
+ }
160
+ if (observation.kind === "pending") {
161
+ if (activityAt === current.last_activity_at &&
162
+ !checkpointAdvanced) {
163
+ return current;
164
+ }
165
+ return dependencies.repository.save({
166
+ ...current,
167
+ observation_checkpoint: checkpoint,
168
+ last_activity_at: activityAt,
169
+ updated_at: now
170
+ }, { expectedRevision: terminalWatchRevision(current) });
171
+ }
172
+ if (observation.kind === "approval") {
173
+ const duplicate = current.notification_outbox.some((notification) => notification.kind === "approval" &&
174
+ notification.evidence_fingerprint === observation.evidence_fingerprint);
175
+ if (duplicate) {
176
+ if (activityAt === current.last_activity_at &&
177
+ !checkpointAdvanced) {
178
+ return current;
179
+ }
180
+ return dependencies.repository.save({
181
+ ...current,
182
+ observation_checkpoint: checkpoint,
183
+ last_activity_at: activityAt,
184
+ updated_at: now
185
+ }, { expectedRevision: terminalWatchRevision(current) });
186
+ }
187
+ const next = withApprovalNotification({
188
+ ...current,
189
+ observation_checkpoint: checkpoint,
190
+ last_activity_at: activityAt,
191
+ updated_at: now
192
+ }, {
193
+ evidenceFingerprint: observation.evidence_fingerprint,
194
+ reasonCode: observation.reason_code,
195
+ createdAt: now
196
+ });
197
+ return dependencies.repository.save(next, { expectedRevision: terminalWatchRevision(current) });
198
+ }
199
+ return saveSettlement(current, {
200
+ kind: observation.kind,
201
+ evidenceFingerprint: observation.evidence_fingerprint,
202
+ observedAt: observation.observed_at,
203
+ reasonCode: observation.reason_code,
204
+ updatedAt: now,
205
+ lastActivityAt: activityAt,
206
+ completionText: observation.kind === "completed" ||
207
+ observation.kind === "failed"
208
+ ? observation.completion_text
209
+ : undefined,
210
+ completionId: observation.kind === "completed" ||
211
+ observation.kind === "failed"
212
+ ? observation.completion_id
213
+ : undefined,
214
+ completionTimestamp: observation.kind === "completed" ||
215
+ observation.kind === "failed"
216
+ ? observation.completion_timestamp
217
+ : undefined
218
+ });
219
+ }
220
+ function saveSettlement(current, input) {
221
+ const notification = pendingNotification(current.watch_id, input.kind, input.evidenceFingerprint, input.updatedAt, input.reasonCode);
222
+ const candidate = {
223
+ ...current,
224
+ status: input.kind,
225
+ updated_at: input.updatedAt,
226
+ last_activity_at: input.lastActivityAt ?? current.last_activity_at,
227
+ settlement: {
228
+ kind: input.kind,
229
+ evidence_fingerprint: input.evidenceFingerprint,
230
+ observed_at: input.observedAt,
231
+ reason_code: input.reasonCode,
232
+ completion_text: input.completionText,
233
+ completion_id: input.completionId,
234
+ completion_timestamp: input.completionTimestamp
235
+ },
236
+ notification_outbox: [
237
+ ...supersedeUndeliveredApprovals(current.notification_outbox, input.updatedAt),
238
+ notification
239
+ ]
240
+ };
241
+ return dependencies.repository.save(candidate, { expectedRevision: terminalWatchRevision(current) });
242
+ }
243
+ async function reconcileAll() {
244
+ return reconcileAllTerminalWatches({
245
+ dependencies,
246
+ reconciliationDeliveryLimit,
247
+ reconcile,
248
+ deliverNextNotification
249
+ });
250
+ }
251
+ return Object.freeze({
252
+ create,
253
+ cancel,
254
+ get: (watchId) => dependencies.repository.load(watchId),
255
+ list: () => dependencies.repository.list(),
256
+ reconcile,
257
+ reconcileAll
258
+ });
259
+ }
260
+ function createTerminalWatchNotificationDelivery(input) {
261
+ const { dependencies, notificationLeaseMs, retryDelay } = input;
262
+ function claimNextNotification(watchId) {
263
+ return dependencies.repository.withWatchLock(watchId, () => {
264
+ let current = dependencies.repository.load(watchId);
265
+ const now = canonicalNow(dependencies.now());
266
+ let index = firstUnresolvedNotificationIndex(current);
267
+ const first = current.notification_outbox[index];
268
+ if (current.status !== "active" &&
269
+ first?.kind === "approval" &&
270
+ notificationIsClaimable(first, now)) {
271
+ current = dependencies.repository.save({
272
+ ...current,
273
+ updated_at: now,
274
+ notification_outbox: replaceAt(current.notification_outbox, index, supersededApprovalNotification(first, now))
275
+ }, { expectedRevision: terminalWatchRevision(current) });
276
+ index = firstUnresolvedNotificationIndex(current);
277
+ }
278
+ if (index < 0 ||
279
+ !notificationIsClaimable(current.notification_outbox[index], now)) {
280
+ return { watch: current };
281
+ }
282
+ const selected = current.notification_outbox[index];
283
+ let snapshotted = selected;
284
+ let snapshotErrorCode;
285
+ if (!terminalWatchNotificationCallbackSnapshot(current, selected)) {
286
+ let callback;
287
+ try {
288
+ callback = dependencies.resolveCallback(current);
289
+ }
290
+ catch {
291
+ snapshotErrorCode = "callback_route_resolution_failed";
292
+ }
293
+ if (callback) {
294
+ try {
295
+ const route = parseCallbackRoute(callback.route);
296
+ snapshotted = {
297
+ ...selected,
298
+ callback_route: route,
299
+ callback_envelope: terminalWatchCallbackEnvelope(current, selected, route)
300
+ };
301
+ }
302
+ catch {
303
+ snapshotErrorCode = "callback_request_construction_failed";
304
+ }
305
+ }
306
+ }
307
+ const attemptId = dependencies.randomUUID();
308
+ const claimed = withNotificationReceipt(snapshotted, {
309
+ status: "delivering",
310
+ attempts: selected.attempts + 1,
311
+ last_attempt_at: now,
312
+ attempt_id: attemptId,
313
+ attempt_lease_expires_at: new Date(Date.parse(now) + notificationLeaseMs).toISOString()
314
+ });
315
+ const saved = dependencies.repository.save({
316
+ ...current,
317
+ updated_at: now,
318
+ notification_outbox: replaceAt(current.notification_outbox, index, claimed)
319
+ }, { expectedRevision: terminalWatchRevision(current) });
320
+ return {
321
+ watch: saved,
322
+ notification: saved.notification_outbox[index],
323
+ attempt_id: attemptId,
324
+ snapshot_error_code: snapshotErrorCode
325
+ };
326
+ });
327
+ }
328
+ function finishNotification(watchId, notificationId, attemptId, outcome) {
329
+ return dependencies.repository.withWatchLock(watchId, () => {
330
+ const current = dependencies.repository.load(watchId);
331
+ const index = current.notification_outbox.findIndex((notification) => notification.notification_id === notificationId);
332
+ if (index < 0) {
333
+ return { settled: false, watch: current };
334
+ }
335
+ const selected = current.notification_outbox[index];
336
+ if (selected.status !== "delivering" ||
337
+ selected.attempt_id !== attemptId) {
338
+ return { settled: false, watch: current };
339
+ }
340
+ const now = canonicalNow(dependencies.now());
341
+ const settled = outcome.disposition === "accepted"
342
+ ? withNotificationReceipt(selected, {
343
+ status: "delivered",
344
+ attempts: selected.attempts,
345
+ last_attempt_at: selected.last_attempt_at,
346
+ delivered_at: now
347
+ })
348
+ : selected.kind === "approval" && current.status !== "active"
349
+ ? supersededApprovalNotification(selected, now)
350
+ : withNotificationReceipt(selected, {
351
+ status: "failed",
352
+ attempts: selected.attempts,
353
+ last_attempt_at: selected.last_attempt_at,
354
+ failed_at: now,
355
+ next_attempt_at: new Date(Date.parse(now) + retryDelay(selected.attempts)).toISOString(),
356
+ last_error_code: callbackOutcomeErrorCode(outcome)
357
+ });
358
+ const saved = dependencies.repository.save({
359
+ ...current,
360
+ updated_at: now,
361
+ notification_outbox: replaceAt(current.notification_outbox, index, settled)
362
+ }, { expectedRevision: terminalWatchRevision(current) });
363
+ return { settled: true, watch: saved };
364
+ });
365
+ }
366
+ async function deliverNextNotification(watchId) {
367
+ const claim = claimNextNotification(watchId);
368
+ if (!claim.notification || !claim.attempt_id) {
369
+ return { status: "none", watch: claim.watch };
370
+ }
371
+ const notificationId = claim.notification.notification_id;
372
+ let outcome = {
373
+ disposition: "permanent_failure",
374
+ error_code: claim.snapshot_error_code ??
375
+ "callback_request_construction_failed"
376
+ };
377
+ const callbackSnapshot = terminalWatchNotificationCallbackSnapshot(claim.watch, claim.notification);
378
+ if (callbackSnapshot) {
379
+ let acceptedCheckpoint;
380
+ let acceptedSettlement;
381
+ let invalidCheckpoint = false;
382
+ const reportCheckpoint = (value) => {
383
+ try {
384
+ const checkpoint = parseCallbackAttemptOutcome(value);
385
+ if (checkpoint.disposition === "accepted") {
386
+ acceptedCheckpoint = checkpoint;
387
+ acceptedSettlement ??= finishNotification(watchId, notificationId, claim.attempt_id, { disposition: "accepted" });
388
+ }
389
+ }
390
+ catch {
391
+ invalidCheckpoint = true;
392
+ }
393
+ };
394
+ let prepared;
395
+ try {
396
+ prepared = prepareTerminalWatchCallbackDelivery({
397
+ ...callbackSnapshot,
398
+ context: dependencies.resolveCallbackContext?.(claim.watch, callbackSnapshot.route),
399
+ notification: claim.notification,
400
+ attemptId: claim.attempt_id,
401
+ reportCheckpoint
402
+ });
403
+ }
404
+ catch {
405
+ outcome = {
406
+ disposition: "permanent_failure",
407
+ error_code: "callback_request_construction_failed"
408
+ };
409
+ }
410
+ let delivered;
411
+ let deliveryReturned = false;
412
+ if (prepared) {
413
+ try {
414
+ delivered = await dependencies.deliver(prepared);
415
+ deliveryReturned = true;
416
+ }
417
+ catch {
418
+ outcome = acceptedCheckpoint ?? terminalWatchUncertainOutcome(dependencies, "callback_transport_threw");
419
+ }
420
+ }
421
+ if (deliveryReturned) {
422
+ try {
423
+ outcome = parseCallbackAttemptOutcome(delivered);
424
+ }
425
+ catch {
426
+ outcome = acceptedCheckpoint ?? terminalWatchUncertainOutcome(dependencies, "callback_transport_contract_violation");
427
+ }
428
+ if (acceptedCheckpoint && outcome.disposition !== "accepted") {
429
+ outcome = acceptedCheckpoint;
430
+ }
431
+ else if (invalidCheckpoint &&
432
+ outcome.disposition !== "accepted") {
433
+ outcome = terminalWatchUncertainOutcome(dependencies, "callback_transport_contract_violation");
434
+ }
435
+ }
436
+ if (acceptedSettlement?.settled) {
437
+ return {
438
+ status: "delivered",
439
+ watch: acceptedSettlement.watch,
440
+ notification_id: notificationId
441
+ };
442
+ }
443
+ }
444
+ const finished = finishNotification(watchId, notificationId, claim.attempt_id, outcome.disposition === "accepted"
445
+ ? { disposition: "accepted" }
446
+ : {
447
+ disposition: outcome.disposition,
448
+ error_code: outcome.error_code
449
+ });
450
+ if (!finished.settled) {
451
+ return {
452
+ status: "claim_changed",
453
+ watch: finished.watch,
454
+ notification_id: notificationId
455
+ };
456
+ }
457
+ if (outcome.disposition === "accepted") {
458
+ return {
459
+ status: "delivered",
460
+ watch: finished.watch,
461
+ notification_id: notificationId
462
+ };
463
+ }
464
+ return {
465
+ status: "failed",
466
+ watch: finished.watch,
467
+ notification_id: notificationId,
468
+ error_code: callbackOutcomeErrorCode(outcome)
469
+ };
470
+ }
471
+ return deliverNextNotification;
472
+ }
473
+ function prepareTerminalWatchCallbackDelivery(input) {
474
+ const route = parseCallbackRoute(input.route);
475
+ const context = snapshotTerminalWatchCallbackContext(input.context);
476
+ const attemptNumber = input.notification.attempts;
477
+ const attemptId = input.attemptId;
478
+ if (!Number.isSafeInteger(attemptNumber) ||
479
+ attemptNumber < 1 ||
480
+ attemptId.trim().length === 0) {
481
+ throw new Error("Terminal Watch callback attempt identity is invalid");
482
+ }
483
+ return {
484
+ route,
485
+ envelope: input.envelope,
486
+ attempt: { number: attemptNumber, id: attemptId },
487
+ context,
488
+ reportCheckpoint: input.reportCheckpoint
489
+ };
490
+ }
491
+ function snapshotTerminalWatchCallbackContext(value) {
492
+ if (!value)
493
+ return undefined;
494
+ const { legacyOptions, ...context } = value;
495
+ return {
496
+ ...context,
497
+ ...(legacyOptions
498
+ ? { legacyOptions: { ...legacyOptions } }
499
+ : {})
500
+ };
501
+ }
502
+ function terminalWatchUncertainOutcome(dependencies, errorCode) {
503
+ return {
504
+ disposition: "uncertain",
505
+ error_code: errorCode,
506
+ observed_at: canonicalNow(dependencies.now())
507
+ };
508
+ }
509
+ async function reconcileAllTerminalWatches(input) {
510
+ const { dependencies, reconciliationDeliveryLimit, reconcile, deliverNextNotification } = input;
511
+ const scan = dependencies.repository.scanForReconciliation();
512
+ const work = scan.watches
513
+ .filter((watch) => watch.status === "active" || hasUnresolvedNotification(watch))
514
+ .map((watch) => ({
515
+ beforeRevision: terminalWatchRevision(watch),
516
+ callbacksDelivered: 0,
517
+ current: watch,
518
+ errorCode: undefined
519
+ }));
520
+ // Observation has priority over callback transport. A slow or permanently
521
+ // failing callback transport must never prevent another active Watch from
522
+ // learning that its exact task completed or became invalid.
523
+ for (const item of work) {
524
+ if (item.current.status !== "active")
525
+ continue;
526
+ try {
527
+ item.current = await reconcile(item.current.watch_id);
528
+ }
529
+ catch (error) {
530
+ item.errorCode = safeReasonCode(dependencies.classifyDeliveryError?.(error), "terminal_watch_observation_failed");
531
+ item.current = safeLoadWatch(dependencies.repository, item.current.watch_id, item.current);
532
+ }
533
+ }
534
+ // Delivery is a separate, bounded and fair phase. At most one notification
535
+ // per Watch can be attempted in this reconciliation, and oldest attempts
536
+ // win so a recently failed callback cannot remain permanently at the head.
537
+ const deliveryNow = canonicalNow(dependencies.now());
538
+ const deliveryCandidates = work
539
+ .flatMap((item) => {
540
+ const notification = firstClaimableNotification(item.current, deliveryNow);
541
+ return notification ? [{ item, notification }] : [];
542
+ })
543
+ .sort((left, right) => notificationFairnessTime(left.notification).localeCompare(notificationFairnessTime(right.notification)) || left.item.current.watch_id.localeCompare(right.item.current.watch_id))
544
+ .slice(0, reconciliationDeliveryLimit);
545
+ for (const candidate of deliveryCandidates) {
546
+ const item = candidate.item;
547
+ try {
548
+ const delivery = await deliverNextNotification(item.current.watch_id);
549
+ item.current = delivery.watch;
550
+ if (delivery.status === "delivered") {
551
+ item.callbacksDelivered += 1;
552
+ }
553
+ else if (delivery.status === "failed") {
554
+ item.errorCode = item.errorCode ?? delivery.error_code;
555
+ }
556
+ else if (delivery.status === "claim_changed") {
557
+ item.errorCode = item.errorCode ?? "callback_claim_changed";
558
+ }
559
+ }
560
+ catch (error) {
561
+ item.errorCode = item.errorCode ?? safeReasonCode(dependencies.classifyDeliveryError?.(error), "terminal_watch_callback_recovery_failed");
562
+ item.current = safeLoadWatch(dependencies.repository, item.current.watch_id, item.current);
563
+ }
564
+ }
565
+ const summary = {
566
+ checked: work.length + scan.errors.length,
567
+ changed: 0,
568
+ callbacks_delivered: 0,
569
+ errors: 0,
570
+ items: []
571
+ };
572
+ for (const item of work) {
573
+ const changed = terminalWatchRevision(item.current) !== item.beforeRevision;
574
+ if (changed)
575
+ summary.changed += 1;
576
+ summary.callbacks_delivered += item.callbacksDelivered;
577
+ if (item.errorCode)
578
+ summary.errors += 1;
579
+ summary.items.push({
580
+ watch_id: item.current.watch_id,
581
+ status: item.current.status,
582
+ changed,
583
+ callbacks_delivered: item.callbacksDelivered,
584
+ error_code: item.errorCode
585
+ });
586
+ }
587
+ for (const error of scan.errors) {
588
+ summary.errors += 1;
589
+ summary.items.push({
590
+ watch_id: error.watch_id,
591
+ status: "error",
592
+ changed: false,
593
+ callbacks_delivered: 0,
594
+ error_code: error.error_code
595
+ });
596
+ }
597
+ return summary;
598
+ }
599
+ function firstUnresolvedNotificationIndex(watch) {
600
+ return watch.notification_outbox.findIndex((notification) => notification.status !== "delivered" &&
601
+ notification.status !== "superseded" &&
602
+ // Permanent and uncertain receipts are terminal for this exact
603
+ // notification. Keep their evidence, but do not let them become a queue
604
+ // head that starves a later, distinct approval observation.
605
+ !(notification.status === "failed" &&
606
+ isNonRetryableCallbackError(notification.last_error_code)));
607
+ }
608
+ function hasUnresolvedNotification(watch) {
609
+ return firstUnresolvedNotificationIndex(watch) >= 0;
610
+ }
611
+ function firstClaimableNotification(watch, now) {
612
+ const index = firstUnresolvedNotificationIndex(watch);
613
+ const notification = watch.notification_outbox[index];
614
+ return notification && notificationIsClaimable(notification, now)
615
+ ? notification
616
+ : undefined;
617
+ }
618
+ function notificationFairnessTime(notification) {
619
+ return notification.last_attempt_at ?? notification.created_at;
620
+ }
621
+ function safeLoadWatch(repository, watchId, fallback) {
622
+ try {
623
+ return repository.load(watchId);
624
+ }
625
+ catch {
626
+ return fallback;
627
+ }
628
+ }
629
+ function terminalWatchNotificationDeliveryPolicy(dependencies) {
630
+ const maximumRetryDelay = positiveMilliseconds(dependencies.notificationMaxRetryDelayMs, DEFAULT_NOTIFICATION_MAX_RETRY_DELAY_MS, "terminal Watch maximum notification retry delay");
631
+ const configuredRetryDelay = dependencies.notificationRetryDelayMs;
632
+ return {
633
+ notificationLeaseMs: positiveMilliseconds(dependencies.notificationLeaseMs, DEFAULT_NOTIFICATION_LEASE_MS, "terminal Watch notification lease"),
634
+ reconciliationDeliveryLimit: positiveMilliseconds(dependencies.reconciliationDeliveryLimit, DEFAULT_RECONCILIATION_DELIVERY_LIMIT, "terminal Watch reconciliation delivery limit"),
635
+ retryDelay: (attempt) => nonNegativeMilliseconds(Math.min(maximumRetryDelay, typeof configuredRetryDelay === "function"
636
+ ? configuredRetryDelay(attempt)
637
+ : (configuredRetryDelay ?? DEFAULT_NOTIFICATION_RETRY_DELAY_MS) *
638
+ 2 ** Math.max(0, attempt - 1)), DEFAULT_NOTIFICATION_RETRY_DELAY_MS, "terminal Watch notification retry delay")
639
+ };
640
+ }
641
+ function nextObservationCheckpoint(watch, observation) {
642
+ const suppliedCheckpoint = observation.observation_checkpoint;
643
+ const suppliedOffset = observation.safe_resume_offset_bytes;
644
+ if (suppliedCheckpoint !== undefined &&
645
+ suppliedOffset !== undefined &&
646
+ suppliedCheckpoint.safe_resume_offset_bytes !== suppliedOffset) {
647
+ throw new Error("terminal Watch observation checkpoints disagree");
648
+ }
649
+ if (suppliedCheckpoint === undefined && suppliedOffset === undefined) {
650
+ return watch.observation_checkpoint;
651
+ }
652
+ const candidate = suppliedCheckpoint ?? {
653
+ safe_resume_offset_bytes: suppliedOffset
654
+ };
655
+ if (!Number.isSafeInteger(candidate.safe_resume_offset_bytes) ||
656
+ candidate.safe_resume_offset_bytes <
657
+ watch.observation_checkpoint.safe_resume_offset_bytes) {
658
+ throw new Error("terminal Watch observation checkpoint is invalid");
659
+ }
660
+ const candidateWatch = { ...watch, observation_checkpoint: candidate };
661
+ assertTerminalWatch(candidateWatch, watch.watch_id);
662
+ return candidate;
663
+ }
664
+ export function terminalWatchObservationFence(watch) {
665
+ return {
666
+ watch_id: watch.watch_id,
667
+ terminal_identity_fingerprint: terminalWatchIdentityFingerprint(watch),
668
+ anchor_fingerprint: watch.anchor.anchor_fingerprint
669
+ };
670
+ }
671
+ function assertObservationForWatch(observation, watch, now) {
672
+ const expected = terminalWatchObservationFence(watch);
673
+ if (observation.watch_id !== expected.watch_id ||
674
+ observation.terminal_identity_fingerprint !==
675
+ expected.terminal_identity_fingerprint ||
676
+ observation.anchor_fingerprint !== expected.anchor_fingerprint) {
677
+ throw new Error("terminal Watch observation does not match its exact anchor");
678
+ }
679
+ assertTimestamp(observation.observed_at, "terminal Watch observation time");
680
+ if (Date.parse(observation.observed_at) > Date.parse(now)) {
681
+ throw new Error("terminal Watch observation cannot come from the future");
682
+ }
683
+ if (observation.last_activity_at !== undefined) {
684
+ assertTimestamp(observation.last_activity_at, "terminal Watch observation activity");
685
+ if (Date.parse(observation.last_activity_at) > Date.parse(now)) {
686
+ throw new Error("terminal Watch activity cannot come from the future");
687
+ }
688
+ }
689
+ if (observation.safe_resume_offset_bytes !== undefined) {
690
+ if (!Number.isSafeInteger(observation.safe_resume_offset_bytes) ||
691
+ observation.safe_resume_offset_bytes <
692
+ watch.observation_checkpoint.safe_resume_offset_bytes) {
693
+ throw new Error("terminal Watch observation checkpoint is invalid");
694
+ }
695
+ }
696
+ if (observation.observation_checkpoint !== undefined &&
697
+ (observation.observation_checkpoint.safe_resume_offset_bytes <
698
+ watch.observation_checkpoint.safe_resume_offset_bytes ||
699
+ (observation.safe_resume_offset_bytes !== undefined &&
700
+ observation.safe_resume_offset_bytes !==
701
+ observation.observation_checkpoint.safe_resume_offset_bytes))) {
702
+ throw new Error("terminal Watch observation checkpoint is invalid");
703
+ }
704
+ if (observation.kind === "unavailable") {
705
+ if (observation.reason_code !== undefined) {
706
+ safeReasonCode(observation.reason_code, undefined);
707
+ }
708
+ return;
709
+ }
710
+ if (observation.kind !== "pending") {
711
+ assertSha256(observation.evidence_fingerprint, "terminal Watch observation fingerprint");
712
+ if (observation.reason_code !== undefined) {
713
+ safeReasonCode(observation.reason_code, undefined);
714
+ }
715
+ }
716
+ if (observation.kind === "completed" || observation.kind === "failed") {
717
+ if (observation.completion_text !== undefined &&
718
+ (typeof observation.completion_text !== "string" ||
719
+ observation.completion_text.length > 4000 ||
720
+ observation.completion_text.includes("\0"))) {
721
+ throw new Error("terminal Watch completion text exceeds its safe bound");
722
+ }
723
+ if (observation.completion_id !== undefined &&
724
+ (typeof observation.completion_id !== "string" ||
725
+ observation.completion_id.trim().length === 0 ||
726
+ observation.completion_id.includes("\0"))) {
727
+ throw new Error("terminal Watch completion id is invalid");
728
+ }
729
+ if (observation.completion_timestamp !== undefined) {
730
+ assertTimestamp(observation.completion_timestamp, "terminal Watch completion timestamp");
731
+ }
732
+ }
733
+ }
734
+ function withApprovalNotification(watch, input) {
735
+ const notification = pendingNotification(watch.watch_id, "approval", input.evidenceFingerprint, input.createdAt, input.reasonCode);
736
+ return {
737
+ ...watch,
738
+ notification_outbox: [...watch.notification_outbox, notification]
739
+ };
740
+ }
741
+ function pendingNotification(watchId, kind, evidenceFingerprint, createdAt, reasonCode) {
742
+ assertSha256(evidenceFingerprint, "terminal Watch notification fingerprint");
743
+ const notificationId = terminalWatchNotificationId(watchId, kind, evidenceFingerprint);
744
+ return {
745
+ notification_id: notificationId,
746
+ idempotency_key: terminalWatchNotificationIdempotencyKey(watchId, notificationId),
747
+ kind,
748
+ evidence_fingerprint: evidenceFingerprint,
749
+ reason_code: reasonCode === undefined
750
+ ? undefined
751
+ : safeReasonCode(reasonCode, undefined),
752
+ status: "pending",
753
+ attempts: 0,
754
+ created_at: createdAt
755
+ };
756
+ }
757
+ function supersedeUndeliveredApprovals(notifications, supersededAt) {
758
+ return notifications.map((notification) => {
759
+ if (notification.kind !== "approval" ||
760
+ notification.status === "delivered" ||
761
+ notification.status === "delivering" ||
762
+ notification.status === "superseded") {
763
+ return notification;
764
+ }
765
+ return supersededApprovalNotification(notification, supersededAt);
766
+ });
767
+ }
768
+ function supersededApprovalNotification(notification, supersededAt) {
769
+ return withNotificationReceipt(notification, {
770
+ status: "superseded",
771
+ attempts: notification.attempts,
772
+ superseded_at: supersededAt
773
+ });
774
+ }
775
+ /** Replace the mutable delivery receipt without leaking stale lease/error data. */
776
+ function withNotificationReceipt(notification, receipt) {
777
+ return {
778
+ notification_id: notification.notification_id,
779
+ idempotency_key: notification.idempotency_key,
780
+ kind: notification.kind,
781
+ evidence_fingerprint: notification.evidence_fingerprint,
782
+ reason_code: notification.reason_code,
783
+ ...(Object.hasOwn(notification, "callback_route")
784
+ ? { callback_route: notification.callback_route }
785
+ : {}),
786
+ ...(Object.hasOwn(notification, "callback_envelope")
787
+ ? { callback_envelope: notification.callback_envelope }
788
+ : {}),
789
+ created_at: notification.created_at,
790
+ ...receipt
791
+ };
792
+ }
793
+ function notificationIsClaimable(notification, now) {
794
+ if (notification.status === "pending") {
795
+ return true;
796
+ }
797
+ if (notification.status === "failed") {
798
+ if (isNonRetryableCallbackError(notification.last_error_code)) {
799
+ return false;
800
+ }
801
+ return Date.parse(notification.next_attempt_at ?? "") <= Date.parse(now);
802
+ }
803
+ if (notification.status === "delivering") {
804
+ return Date.parse(notification.attempt_lease_expires_at ?? "") <=
805
+ Date.parse(now);
806
+ }
807
+ return false;
808
+ }
809
+ function callbackOutcomeErrorCode(outcome) {
810
+ const errorCode = safeReasonCode(outcome.error_code, "callback_delivery_failed");
811
+ const prefix = outcome.disposition === "permanent_failure"
812
+ ? "callback_permanent_"
813
+ : outcome.disposition === "uncertain"
814
+ ? "callback_uncertain_"
815
+ : "";
816
+ return safeReasonCode(`${prefix}${errorCode}`, `${prefix}callback_delivery_failed`);
817
+ }
818
+ function isNonRetryableCallbackError(errorCode) {
819
+ return errorCode?.startsWith("callback_permanent_") === true ||
820
+ errorCode?.startsWith("callback_uncertain_") === true;
821
+ }
822
+ function replaceAt(values, index, value) {
823
+ return values.map((candidate, candidateIndex) => candidateIndex === index ? value : candidate);
824
+ }
825
+ function latestActivityAt(current, candidate) {
826
+ return candidate !== undefined && Date.parse(candidate) > Date.parse(current)
827
+ ? candidate
828
+ : current;
829
+ }
830
+ function isDeadlineElapsed(watch, now) {
831
+ return now.getTime() >= Date.parse(watch.deadline_at);
832
+ }
833
+ function deterministicEvidenceFingerprint(value) {
834
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
835
+ }
836
+ function canonicalNow(value) {
837
+ const milliseconds = value.getTime();
838
+ if (!Number.isFinite(milliseconds)) {
839
+ throw new Error("terminal Watch clock returned an invalid Date");
840
+ }
841
+ return value.toISOString();
842
+ }
843
+ function positiveMilliseconds(value, fallback, label) {
844
+ const candidate = value === undefined ? fallback : Number(value);
845
+ if (candidate === undefined ||
846
+ !Number.isSafeInteger(candidate) ||
847
+ candidate <= 0) {
848
+ throw new Error(`${label} must be a positive safe integer`);
849
+ }
850
+ return candidate;
851
+ }
852
+ function nonNegativeMilliseconds(value, fallback, label) {
853
+ const candidate = value === undefined ? fallback : Number(value);
854
+ if (!Number.isSafeInteger(candidate) || candidate < 0) {
855
+ throw new Error(`${label} must be a non-negative safe integer`);
856
+ }
857
+ return candidate;
858
+ }
859
+ function safeReasonCode(value, fallback) {
860
+ if (typeof value === "string" &&
861
+ value.length >= 1 &&
862
+ value.length <= 128 &&
863
+ /^[a-z0-9][a-z0-9_.:-]*$/u.test(value)) {
864
+ return value;
865
+ }
866
+ if (fallback !== undefined) {
867
+ return fallback;
868
+ }
869
+ throw new Error("terminal Watch reason must be a privacy-safe reason code");
870
+ }
871
+ function assertTimestamp(value, label) {
872
+ if (typeof value !== "string" ||
873
+ !Number.isFinite(Date.parse(value)) ||
874
+ new Date(value).toISOString() !== value) {
875
+ throw new Error(`${label} must be a canonical ISO timestamp`);
876
+ }
877
+ }
878
+ function assertSha256(value, label) {
879
+ if (typeof value !== "string" || !/^[0-9a-f]{64}$/u.test(value)) {
880
+ throw new Error(`${label} must be a lowercase SHA-256 digest`);
881
+ }
882
+ }
883
+ //# sourceMappingURL=terminal-watch-service.js.map