@scotthuang/agent-knock-knock 0.12.12 → 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.
Files changed (76) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +39 -26
  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/claude-local-transcript-provider.d.ts +81 -0
  7. package/dist/src/claude-local-transcript-provider.js +854 -18
  8. package/dist/src/claude-local-transcript-provider.js.map +1 -1
  9. package/dist/src/cli-core.js +85 -0
  10. package/dist/src/cli-core.js.map +1 -1
  11. package/dist/src/deferred-foreground-authority-cli-adapter.js +90 -19
  12. package/dist/src/deferred-foreground-authority-cli-adapter.js.map +1 -1
  13. package/dist/src/deferred-foreground-transfer.js +3 -4
  14. package/dist/src/deferred-foreground-transfer.js.map +1 -1
  15. package/dist/src/openclaw-callback-transport.d.ts +5 -0
  16. package/dist/src/openclaw-callback-transport.js +1 -1
  17. package/dist/src/openclaw-callback-transport.js.map +1 -1
  18. package/dist/src/openclaw-plugin-callback-adapter.js +96 -14
  19. package/dist/src/openclaw-plugin-callback-adapter.js.map +1 -1
  20. package/dist/src/openclaw-plugin-command-adapter.js +548 -107
  21. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  22. package/dist/src/openclaw-plugin-helpers.d.ts +27 -4
  23. package/dist/src/openclaw-plugin-helpers.js +309 -44
  24. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  25. package/dist/src/openclaw-plugin-schemas.d.ts +44 -68
  26. package/dist/src/openclaw-plugin-schemas.js +63 -91
  27. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  28. package/dist/src/openclaw-plugin-supervisor.js +48 -10
  29. package/dist/src/openclaw-plugin-supervisor.js.map +1 -1
  30. package/dist/src/openclaw-private-authority-offers.d.ts +22 -0
  31. package/dist/src/openclaw-private-authority-offers.js +177 -0
  32. package/dist/src/openclaw-private-authority-offers.js.map +1 -0
  33. package/dist/src/store.d.ts +1 -0
  34. package/dist/src/store.js +3 -1
  35. package/dist/src/store.js.map +1 -1
  36. package/dist/src/terminal-action-projection.d.ts +19 -1
  37. package/dist/src/terminal-action-projection.js +23 -0
  38. package/dist/src/terminal-action-projection.js.map +1 -1
  39. package/dist/src/terminal-command-cli-adapter.d.ts +5 -0
  40. package/dist/src/terminal-command-cli-adapter.js +249 -89
  41. package/dist/src/terminal-command-cli-adapter.js.map +1 -1
  42. package/dist/src/terminal-dispatch-repository-cli-adapter.js +305 -79
  43. package/dist/src/terminal-dispatch-repository-cli-adapter.js.map +1 -1
  44. package/dist/src/terminal-list-cli-adapter.d.ts +31 -7
  45. package/dist/src/terminal-list-cli-adapter.js +285 -67
  46. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  47. package/dist/src/terminal-list-renderer.d.ts +2 -0
  48. package/dist/src/terminal-list-renderer.js +116 -41
  49. package/dist/src/terminal-list-renderer.js.map +1 -1
  50. package/dist/src/terminal-monitor-cli-adapter.js +1 -1
  51. package/dist/src/terminal-monitor-cli-adapter.js.map +1 -1
  52. package/dist/src/terminal-monitor-state-cli-adapter.js +6 -15
  53. package/dist/src/terminal-monitor-state-cli-adapter.js.map +1 -1
  54. package/dist/src/terminal-status-cli-adapter.d.ts +10 -0
  55. package/dist/src/terminal-status-cli-adapter.js +89 -7
  56. package/dist/src/terminal-status-cli-adapter.js.map +1 -1
  57. package/dist/src/terminal-submission-acceptance.d.ts +60 -1
  58. package/dist/src/terminal-submission-acceptance.js +560 -0
  59. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  60. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +20 -0
  61. package/dist/src/terminal-watch-callback-cli-adapter.js +55 -0
  62. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -0
  63. package/dist/src/terminal-watch-cli-adapter.d.ts +64 -0
  64. package/dist/src/terminal-watch-cli-adapter.js +508 -0
  65. package/dist/src/terminal-watch-cli-adapter.js.map +1 -0
  66. package/dist/src/terminal-watch-service.d.ts +97 -0
  67. package/dist/src/terminal-watch-service.js +714 -0
  68. package/dist/src/terminal-watch-service.js.map +1 -0
  69. package/dist/src/terminal-watch-store.d.ts +149 -0
  70. package/dist/src/terminal-watch-store.js +861 -0
  71. package/dist/src/terminal-watch-store.js.map +1 -0
  72. package/docs/quickstart-herdr.md +8 -0
  73. package/docs/quickstart-tmux.md +33 -6
  74. package/openclaw.plugin.json +11 -3
  75. package/package.json +1 -1
  76. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +57 -39
@@ -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