@scotthuang/agent-knock-knock 0.12.19 → 0.12.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +9 -9
  3. package/dist/src/codex-lifecycle-compatibility.js +2 -1
  4. package/dist/src/codex-lifecycle-compatibility.js.map +1 -1
  5. package/dist/src/openclaw-plugin-command-adapter.js +5 -5
  6. package/dist/src/openclaw-plugin-command-adapter.js.map +1 -1
  7. package/dist/src/openclaw-plugin-helpers.js +19 -4
  8. package/dist/src/openclaw-plugin-helpers.js.map +1 -1
  9. package/dist/src/openclaw-plugin-schemas.js +3 -3
  10. package/dist/src/openclaw-plugin-schemas.js.map +1 -1
  11. package/dist/src/terminal-action-projection.d.ts +0 -18
  12. package/dist/src/terminal-action-projection.js +0 -23
  13. package/dist/src/terminal-action-projection.js.map +1 -1
  14. package/dist/src/terminal-agent-bridge.js +6 -1
  15. package/dist/src/terminal-agent-bridge.js.map +1 -1
  16. package/dist/src/terminal-list-cli-adapter.js +4 -33
  17. package/dist/src/terminal-list-cli-adapter.js.map +1 -1
  18. package/dist/src/terminal-list-renderer.js +17 -18
  19. package/dist/src/terminal-list-renderer.js.map +1 -1
  20. package/dist/src/terminal-submission-acceptance.js +48 -12
  21. package/dist/src/terminal-submission-acceptance.js.map +1 -1
  22. package/dist/src/terminal-watch-callback-cli-adapter.d.ts +2 -1
  23. package/dist/src/terminal-watch-callback-cli-adapter.js.map +1 -1
  24. package/dist/src/terminal-watch-cli-adapter.d.ts +1 -1
  25. package/dist/src/terminal-watch-cli-adapter.js +379 -110
  26. package/dist/src/terminal-watch-cli-adapter.js.map +1 -1
  27. package/dist/src/terminal-watch-service.d.ts +1 -5
  28. package/dist/src/terminal-watch-service.js +10 -21
  29. package/dist/src/terminal-watch-service.js.map +1 -1
  30. package/dist/src/terminal-watch-store.d.ts +47 -4
  31. package/dist/src/terminal-watch-store.js +161 -12
  32. package/dist/src/terminal-watch-store.js.map +1 -1
  33. package/docs/quickstart-herdr.md +3 -3
  34. package/docs/quickstart-tmux.md +7 -7
  35. package/package.json +1 -1
  36. package/templates/openclaw-skills/agent-knock-knock/SKILL.md +17 -13
@@ -1,18 +1,15 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import path from "node:path";
3
- import { callbackRouteForConversation } from "./callback-route-authority.js";
3
+ import { callbackRouteFingerprint, callbackRouteForConversation } from "./callback-route-authority.js";
4
4
  import { parseCallbackRoute } from "./callback-transport.js";
5
5
  import { captureClaudeTranscriptAnchor, captureClaudeHumanStartedActiveTaskAnchor, observeClaudeUserExplicitFallbackTranscript, observeClaudeHumanStartedActiveTask } from "./claude-local-transcript-provider.js";
6
6
  import { claudeRuntimeCompatibilityWarning } from "./claude-lifecycle-compatibility.js";
7
7
  import { codexRuntimeCompatibilityProfile } from "./codex-lifecycle-compatibility.js";
8
- import { turnIdForConversation } from "./protocol.js";
9
- import { decideTerminalWatchExternalTaskAuthority } from "./terminal-action-projection.js";
10
- import { exactTerminalWatchAction } from "./terminal-list-renderer.js";
11
8
  import { captureCodexCandidateSetRolloutAcceptanceAnchor, captureCodexRolloutAcceptanceAnchor, detectCodexBoundRolloutCompletion, detectCodexCandidateSetRolloutAcceptance, detectCodexRolloutAcceptance, captureCodexHumanStartedActiveTaskAnchor, observeCodexHumanStartedActiveTask } from "./terminal-submission-acceptance.js";
12
9
  import { sameTerminalControlEvidenceIncarnation, terminalControlEvidence } from "./terminal-control-ref.js";
13
10
  import { createTerminalWatchCallbackCliAdapter, resolveTerminalWatchOpenClawCallback, resolveTerminalWatchOpenClawCallbackContext } from "./terminal-watch-callback-cli-adapter.js";
14
11
  import { createTerminalWatchService, terminalWatchObservationFence } from "./terminal-watch-service.js";
15
- import { createClaudeUserExplicitFallbackWatchAnchor, createCodexUserExplicitFallbackWatchAnchor, createTerminalWatchStore, isUserExplicitFallbackWatch, terminalUserExplicitFallbackWatchId, terminalWatchIdentityFingerprint } from "./terminal-watch-store.js";
12
+ import { createClaudeUserExplicitFallbackWatchAnchor, createCodexUserExplicitFallbackWatchAnchor, createTerminalActivityWatchAnchor, createTerminalWatchStore, isTerminalActivityWatch, isUserExplicitFallbackWatch, terminalUserExplicitFallbackWatchId } from "./terminal-watch-store.js";
16
13
  import { isRecord, nonBlankString as stringValue } from "./value-guards.js";
17
14
  const DEFAULT_TERMINAL_WATCH_HARD_TIMEOUT_MINUTES = 720;
18
15
  export function createTerminalWatchCliAdapter(dependencies) {
@@ -67,6 +64,11 @@ export function createTerminalWatchCliAdapter(dependencies) {
67
64
  "string"
68
65
  ? input.context.legacyOptions.openclawBin
69
66
  : undefined,
67
+ origin: metadata.watch_origin === "user_selected_terminal" ||
68
+ metadata.watch_origin === "terminal_activity_fallback" ||
69
+ metadata.watch_origin === "terminal_user_explicit_fallback"
70
+ ? metadata.watch_origin
71
+ : undefined,
70
72
  detail: typeof metadata.reason_code === "string"
71
73
  ? metadata.reason_code
72
74
  : undefined,
@@ -245,57 +247,96 @@ export function createTerminalWatchCliAdapter(dependencies) {
245
247
  : undefined;
246
248
  const openclawSession = callbackRoute?.controller_session_id ??
247
249
  requiredString(options.openclawSession, "--openclaw-session");
248
- const storeDir = dependencies.storeDirFromOptions(options);
249
- const initiallyObserved = await exactTerminalForWatch(terminalId, options, dependencies);
250
- const initiallyObservedBindingToken = bindingTokenForWatch(initiallyObserved.rawTerminal);
251
- const initialTerminalControl = terminalControlForWatch(initiallyObserved.rawTerminal);
252
- const releaseTerminal = dependencies.acquireTerminalLock(storeDir, initialTerminalControl);
253
- let watch;
250
+ const observed = await exactTerminalForWatch(terminalId, options, dependencies);
251
+ const rawTerminal = observed.rawTerminal;
252
+ const projectedTerminal = observed.terminal;
253
+ // Endpoint evidence is the only indispensable observation authority. A
254
+ // manual Watch is read-only and therefore does not acquire terminal input
255
+ // authority or depend on managed-Turn ownership.
256
+ const terminalControl = terminalControlForWatch(rawTerminal);
257
+ const agent = terminalAgent(rawTerminal);
258
+ const warnings = [];
259
+ let anchor;
254
260
  try {
255
- const observed = await exactTerminalForWatch(terminalId, options, dependencies);
256
- const rawTerminal = observed.rawTerminal;
257
- const projectedTerminal = observed.terminal;
258
- const expectedBindingToken = bindingTokenForWatch(rawTerminal);
259
- const terminalControl = terminalControlForWatch(rawTerminal);
260
- if (!sameTerminalControlEvidenceIncarnation(initialTerminalControl, terminalControl) || expectedBindingToken !== initiallyObservedBindingToken) {
261
- throw new Error("terminal binding changed while AKK acquired authority; refresh " +
262
- "AKK list and retry");
263
- }
264
- assertExternalTerminalWatchAuthority(storeDir, terminalControl, dependencies);
265
- const watchService = serviceFor(options);
266
- const existing = watchService.list().find((candidate) => candidate.status === "active" &&
267
- candidate.terminal.terminal_id === terminalId);
268
- if (existing) {
269
- throw new Error(`terminal ${terminalId} is already monitored by Terminal Watch ` +
270
- `${existing.watch_id}; use watch-status instead of creating another`);
261
+ anchor = captureTerminalWatchAnchor(agent, rawTerminal, options, dependencies);
262
+ }
263
+ catch (error) {
264
+ warnings.push(`exact_task_anchor_unavailable: ${safeDiagnostic(error)}`);
265
+ }
266
+ if (anchor) {
267
+ warnings.push(...watchAnchorVersionWarnings(anchor, rawTerminal));
268
+ }
269
+ else {
270
+ if (!terminalControl.capabilities.includes("screen_status")) {
271
+ throw new Error("the exact terminal has neither a durable task anchor nor a " +
272
+ "read-only screen-status observation path; no effective Watch " +
273
+ "can be created");
271
274
  }
272
- assertExactPublicWatchAction(projectedTerminal, terminalId);
273
- const agent = terminalAgent(rawTerminal);
274
- const anchor = captureTerminalWatchAnchor(agent, rawTerminal, options, dependencies);
275
- if (!anchor) {
276
- throw new Error("the exact terminal has no unique supported human-started active task");
275
+ if (warnings.length === 0) {
276
+ warnings.push("exact_task_anchor_unavailable: no unique supported task anchor was present");
277
277
  }
278
- assertWatchAnchorVersion(anchor, rawTerminal);
279
- watch = watchService.create({
280
- agent,
281
- terminal: terminalWatchIdentity(rawTerminal, expectedBindingToken),
282
- anchor,
283
- ...(callbackRoute === undefined
284
- ? {}
285
- : { callback_route: callbackRoute }),
286
- openclaw_session: openclawSession,
287
- openclaw_bin: stringValue(options.openclawBin) ?? "openclaw",
288
- timeout_ms: positiveMinutes(options.hardTimeoutMinutes, DEFAULT_TERMINAL_WATCH_HARD_TIMEOUT_MINUTES) * 60_000,
289
- approval_fingerprint: approvalFingerprint(projectedTerminal),
290
- approval_reason_code: approvalFingerprint(projectedTerminal)
291
- ? "terminal_waiting_for_approval"
292
- : undefined
278
+ const initialActivityState = terminalActivityState(projectedTerminal);
279
+ anchor = createTerminalActivityWatchAnchor({
280
+ capturedAt: dependencies.now(),
281
+ terminalId,
282
+ pid: positiveInteger(rawTerminal.pid, "terminal PID"),
283
+ initialActivityState,
284
+ nativeProcessUuid: stringValue(rawTerminal.native_agent_process_uuid),
285
+ nativeProcessBirth: stringValue(rawTerminal.native_agent_process_birth),
286
+ agentVersion: stringValue(rawTerminal.agent_version)
293
287
  });
288
+ warnings.push("terminal_activity_fallback: observing the exact terminal/process activity epoch instead of claiming exact task completion");
289
+ if (initialActivityState === "idle" || initialActivityState === "unknown") {
290
+ warnings.push("terminal_activity_armed_for_next_activity: no active epoch is visible yet; Watch will wait to observe working or approval activity before stable idle can settle");
291
+ }
294
292
  }
295
- finally {
296
- releaseTerminal();
293
+ const binding = bestEffortBindingTokenForWatch(rawTerminal);
294
+ if (binding.warning)
295
+ warnings.push(binding.warning);
296
+ const terminalIdentity = terminalWatchIdentity(rawTerminal, binding.token);
297
+ const repository = createTerminalWatchStore(dependencies.storeDirFromOptions(options), { acquire: dependencies.acquireFileLock });
298
+ let existingCandidates = [];
299
+ let discoveryWarnings = [];
300
+ try {
301
+ const scan = repository.scanForReconciliation();
302
+ existingCandidates = scan.watches;
303
+ if (scan.errors.length > 0) {
304
+ discoveryWarnings = [
305
+ `terminal_watch_store_entries_skipped: ignored ${scan.errors.length} ` +
306
+ "invalid sibling Watch record(s) during idempotent discovery"
307
+ ];
308
+ }
297
309
  }
298
- dependencies.printJson({ watch: publicTerminalWatch(watch) });
310
+ catch (error) {
311
+ discoveryWarnings = [
312
+ `terminal_watch_store_discovery_unavailable: ${safeDiagnostic(error)}; ` +
313
+ "attempting the user-requested read-only Watch anyway"
314
+ ];
315
+ }
316
+ warnings.push(...discoveryWarnings);
317
+ const watchService = serviceFor(options);
318
+ const openclawBin = stringValue(options.openclawBin) ?? "openclaw";
319
+ const existing = existingCandidates.find((candidate) => sameManualWatchTarget(candidate, agent, terminalIdentity, anchor) &&
320
+ sameManualWatchCallbackAuthority(candidate, callbackRoute, openclawSession, openclawBin));
321
+ const watch = existing ?? watchService.create({
322
+ agent,
323
+ terminal: terminalIdentity,
324
+ anchor,
325
+ warnings,
326
+ ...(callbackRoute === undefined
327
+ ? {}
328
+ : { callback_route: callbackRoute }),
329
+ openclaw_session: openclawSession,
330
+ openclaw_bin: openclawBin,
331
+ timeout_ms: positiveMinutes(options.hardTimeoutMinutes, DEFAULT_TERMINAL_WATCH_HARD_TIMEOUT_MINUTES) * 60_000,
332
+ approval_fingerprint: approvalFingerprint(projectedTerminal),
333
+ approval_reason_code: approvalFingerprint(projectedTerminal)
334
+ ? "terminal_waiting_for_approval"
335
+ : undefined
336
+ });
337
+ dependencies.printJson({
338
+ watch: publicTerminalWatch(watch, existing ? discoveryWarnings : [])
339
+ });
299
340
  }
300
341
  async function runUnwatch(options) {
301
342
  const service = serviceFor(options);
@@ -326,7 +367,7 @@ export function createTerminalWatchCliAdapter(dependencies) {
326
367
  function publicTerminalWatches(watches, options) {
327
368
  return watches
328
369
  .filter((watch) => options.includeAll || watch.status === "active")
329
- .map(publicTerminalWatch);
370
+ .map((watch) => publicTerminalWatch(watch));
330
371
  }
331
372
  return Object.freeze({
332
373
  prepareUserExplicitFallbackWatch,
@@ -340,8 +381,13 @@ export function createTerminalWatchCliAdapter(dependencies) {
340
381
  scanPublicWatchesForExactObservation
341
382
  });
342
383
  }
343
- function assertWatchAnchorVersion(anchor, terminal) {
344
- const runningVersion = requiredString(terminal.agent_version, "running coding-agent version");
384
+ function watchAnchorVersionWarnings(anchor, terminal) {
385
+ const runningVersion = stringValue(terminal.agent_version);
386
+ if (!runningVersion) {
387
+ return [
388
+ "running_agent_version_unavailable: exact task observation remains enabled"
389
+ ];
390
+ }
345
391
  const artifactVersion = anchor.schema ===
346
392
  "agent-knock-knock/codex-human-started-active-task-anchor"
347
393
  ? anchor.codex_version
@@ -349,13 +395,16 @@ function assertWatchAnchorVersion(anchor, terminal) {
349
395
  "agent-knock-knock/claude-human-started-active-task-anchor"
350
396
  ? anchor.claude_version
351
397
  : undefined;
352
- if (!artifactVersion) {
353
- throw new Error("manual Terminal Watch requires a human-started task anchor");
354
- }
398
+ if (!artifactVersion)
399
+ return [];
355
400
  if (artifactVersion !== runningVersion) {
356
- throw new Error(`the active task artifact reports ${artifactVersion}, not the running ` +
357
- `coding-agent version ${runningVersion}; no Terminal Watch was created`);
401
+ return [
402
+ `the active task artifact reports ${artifactVersion}, not the running ` +
403
+ `coding-agent version ${runningVersion}; Watch remains enabled because ` +
404
+ "version evidence is advisory"
405
+ ];
358
406
  }
407
+ return [];
359
408
  }
360
409
  /**
361
410
  * A shared native-Host lifecycle owns no single controller session. Bind its
@@ -416,11 +465,89 @@ async function exactTerminalForWatch(terminalId, options, dependencies) {
416
465
  function bindingTokenForWatch(terminal) {
417
466
  return requiredSha256(terminal.lifecycle_binding_token, "current terminal binding token");
418
467
  }
419
- function assertExactPublicWatchAction(terminal, terminalId) {
420
- if (!exactTerminalWatchAction(terminal, terminalId)) {
421
- throw new Error("the exact terminal is not currently watchable; refresh AKK list and " +
422
- "use only its current available_actions.watch");
468
+ function bestEffortBindingTokenForWatch(terminal) {
469
+ try {
470
+ return { token: bindingTokenForWatch(terminal) };
471
+ }
472
+ catch {
473
+ const terminalControl = terminalControlForWatch(terminal);
474
+ return {
475
+ token: sha256({
476
+ schema: "agent-knock-knock/terminal-watch-observation-binding",
477
+ version: 1,
478
+ terminal_id: requiredString(terminal.id, "terminal id"),
479
+ agent: terminalAgent(terminal),
480
+ pid: positiveInteger(terminal.pid, "terminal PID"),
481
+ endpoint: terminalControlEvidence(terminalControl),
482
+ process_uuid: stringValue(terminal.native_agent_process_uuid) ?? null,
483
+ process_birth: stringValue(terminal.native_agent_process_birth) ?? null
484
+ }),
485
+ warning: "lifecycle_binding_token_unavailable: Watch used read-only terminal/process observation identity"
486
+ };
487
+ }
488
+ }
489
+ function terminalActivityState(terminal) {
490
+ const state = stringValue(terminal.activity_state);
491
+ return state === "awaiting_approval" || state === "working" ||
492
+ state === "idle"
493
+ ? state
494
+ : "unknown";
495
+ }
496
+ function sameManualWatchTarget(watch, agent, terminal, anchor) {
497
+ return watch.status === "active" &&
498
+ watch.agent === agent &&
499
+ watch.terminal.terminal_id === terminal.terminal_id &&
500
+ watch.terminal.workspace === terminal.workspace &&
501
+ sameTerminalControlEvidenceIncarnation(watch.terminal.terminal_endpoint, terminal.terminal_endpoint) &&
502
+ sameManualWatchAnchorTarget(watch.anchor, anchor);
503
+ }
504
+ function sameManualWatchAnchorTarget(left, right) {
505
+ if (left.schema !== right.schema)
506
+ return false;
507
+ if (left.schema ===
508
+ "agent-knock-knock/codex-human-started-active-task-anchor" &&
509
+ right.schema === left.schema) {
510
+ return left.turn_id === right.turn_id &&
511
+ left.process_uuid === right.process_uuid &&
512
+ left.process_birth === right.process_birth &&
513
+ left.rollout.device === right.rollout.device &&
514
+ left.rollout.inode === right.rollout.inode;
515
+ }
516
+ if (left.schema ===
517
+ "agent-knock-knock/claude-human-started-active-task-anchor" &&
518
+ right.schema === left.schema) {
519
+ return left.prompt_uuid === right.prompt_uuid &&
520
+ left.pid === right.pid &&
521
+ left.agent_started_at_ms === right.agent_started_at_ms &&
522
+ left.device === right.device &&
523
+ left.inode === right.inode;
524
+ }
525
+ if (left.schema === "agent-knock-knock/terminal-activity-watch-anchor" &&
526
+ right.schema === left.schema) {
527
+ return left.pid === right.pid &&
528
+ optionalIdentityCompatible(left.native_process_uuid, right.native_process_uuid) &&
529
+ optionalIdentityCompatible(left.native_process_birth, right.native_process_birth);
423
530
  }
531
+ return left.anchor_fingerprint === right.anchor_fingerprint;
532
+ }
533
+ function optionalIdentityCompatible(left, right) {
534
+ return left === undefined || right === undefined || left === right;
535
+ }
536
+ function sameManualWatchCallbackAuthority(watch, callbackRoute, openclawSession, openclawBin) {
537
+ if (watch.openclaw_session !== openclawSession)
538
+ return false;
539
+ if (callbackRoute === undefined) {
540
+ return watch.callback_route === undefined &&
541
+ watch.openclaw_bin === openclawBin;
542
+ }
543
+ return watch.callback_route !== undefined &&
544
+ callbackRouteFingerprint(watch.callback_route) ===
545
+ callbackRouteFingerprint(callbackRoute);
546
+ }
547
+ function safeDiagnostic(error) {
548
+ const message = error instanceof Error ? error.message : String(error);
549
+ return message.replace(/[\r\n\0]+/gu, " ").trim().slice(0, 500) ||
550
+ "unknown provider observation error";
424
551
  }
425
552
  function terminalControlForWatch(terminal) {
426
553
  if (!isRecord(terminal.terminal_control)) {
@@ -428,28 +555,6 @@ function terminalControlForWatch(terminal) {
428
555
  }
429
556
  return terminal.terminal_control;
430
557
  }
431
- function assertExternalTerminalWatchAuthority(storeDir, terminalControl, dependencies) {
432
- const blockingTurn = dependencies.terminalIncarnationBlockingTurns(storeDir, terminalControl)[0];
433
- const authority = decideTerminalWatchExternalTaskAuthority({
434
- blockingTurn,
435
- dispatchOwnership: blockingTurn
436
- ? { state: "none" }
437
- : dependencies.terminalDispatchOwnership(terminalControl)
438
- });
439
- if (authority.state === "external_task") {
440
- return;
441
- }
442
- if (authority.state === "managed_turn") {
443
- const turn = authority.conversation;
444
- throw new Error(`Terminal Watch is unavailable: this task already belongs to AKK Turn ` +
445
- `${turnIdForConversation(turn)} (${turn.status}) and is covered by the ` +
446
- "managed Turn monitor/callback path. Use AKK status for that Turn.");
447
- }
448
- const reason = stringValue(authority.conflict.reason) ??
449
- "terminal dispatch ownership is conflicted";
450
- throw new Error(`Terminal Watch is unavailable because AKK cannot prove this is external ` +
451
- `work: ${reason}. Refresh AKK list and resolve the ownership conflict.`);
452
- }
453
558
  function captureTerminalWatchAnchor(agent, terminal, options, dependencies) {
454
559
  if (agent === "codex") {
455
560
  return captureCodexHumanStartedActiveTaskAnchor({
@@ -476,10 +581,15 @@ async function observeTerminalWatch(watch, options, dependencies) {
476
581
  const projectedTerminal = exactTerminal.state === "available"
477
582
  ? exactTerminal.terminal
478
583
  : undefined;
584
+ const activityTerminalIdentity = rawTerminal && isTerminalActivityWatch(watch)
585
+ ? terminalActivityWatchIdentityMatch(rawTerminal, watch)
586
+ : undefined;
479
587
  const terminalMatches = rawTerminal
480
588
  ? isUserExplicitFallbackWatch(watch)
481
589
  ? terminalMatchesUserExplicitFallbackWatch(rawTerminal, watch)
482
- : terminalMatchesWatch(rawTerminal, watch)
590
+ : isTerminalActivityWatch(watch)
591
+ ? activityTerminalIdentity === "match"
592
+ : terminalMatchesWatch(rawTerminal, watch)
483
593
  : false;
484
594
  if (isUserExplicitFallbackWatch(watch)) {
485
595
  return observeUserExplicitFallbackTerminalWatch({
@@ -493,6 +603,15 @@ async function observeTerminalWatch(watch, options, dependencies) {
493
603
  dependencies
494
604
  });
495
605
  }
606
+ if (isTerminalActivityWatch(watch)) {
607
+ return observeTerminalActivityWatch({
608
+ watch,
609
+ exactTerminal,
610
+ projectedTerminal,
611
+ terminalIdentityMatch: activityTerminalIdentity ?? "mismatch",
612
+ observedAt
613
+ });
614
+ }
496
615
  const observation = watch.anchor.schema ===
497
616
  "agent-knock-knock/codex-human-started-active-task-anchor"
498
617
  ? observeCodexHumanStartedActiveTask({
@@ -591,6 +710,99 @@ async function observeTerminalWatch(watch, options, dependencies) {
591
710
  observation_checkpoint: observationCheckpoint
592
711
  };
593
712
  }
713
+ function observeTerminalActivityWatch(input) {
714
+ const { watch, exactTerminal, projectedTerminal, terminalIdentityMatch, observedAt } = input;
715
+ const fence = terminalWatchObservationFence(watch);
716
+ if (exactTerminal.state === "unavailable") {
717
+ return {
718
+ ...fence,
719
+ kind: "unavailable",
720
+ observed_at: observedAt,
721
+ reason_code: "terminal_observation_unavailable"
722
+ };
723
+ }
724
+ if (exactTerminal.state === "absent") {
725
+ return invalidatedObservation(watch, observedAt, "terminal_process_unavailable");
726
+ }
727
+ if (terminalIdentityMatch === "unavailable") {
728
+ return {
729
+ ...fence,
730
+ kind: "unavailable",
731
+ observed_at: observedAt,
732
+ reason_code: "terminal_identity_observation_incomplete"
733
+ };
734
+ }
735
+ if (terminalIdentityMatch === "mismatch") {
736
+ return invalidatedObservation(watch, observedAt, "terminal_identity_changed");
737
+ }
738
+ if (!projectedTerminal) {
739
+ return {
740
+ ...fence,
741
+ kind: "unavailable",
742
+ observed_at: observedAt,
743
+ reason_code: "terminal_activity_unavailable"
744
+ };
745
+ }
746
+ const state = terminalActivityState(projectedTerminal);
747
+ const current = terminalActivityObservationCheckpoint(watch);
748
+ const active = state === "working" || state === "awaiting_approval";
749
+ const hasSeenActivity = current.has_seen_activity || active;
750
+ const consecutiveIdle = state === "idle" && hasSeenActivity
751
+ ? current.last_activity_state === "idle"
752
+ ? current.consecutive_idle_observations + 1
753
+ : 1
754
+ : 0;
755
+ const checkpoint = {
756
+ ...current,
757
+ has_seen_activity: hasSeenActivity,
758
+ consecutive_idle_observations: consecutiveIdle,
759
+ last_activity_state: state
760
+ };
761
+ if (state === "unknown") {
762
+ return {
763
+ ...fence,
764
+ kind: "unavailable",
765
+ observed_at: observedAt,
766
+ observation_checkpoint: checkpoint,
767
+ reason_code: "terminal_activity_unknown"
768
+ };
769
+ }
770
+ if (state === "awaiting_approval") {
771
+ const approval = approvalFingerprint(projectedTerminal);
772
+ if (approval) {
773
+ return {
774
+ ...fence,
775
+ kind: "approval",
776
+ observed_at: observedAt,
777
+ last_activity_at: observedAt,
778
+ observation_checkpoint: checkpoint,
779
+ evidence_fingerprint: approval,
780
+ reason_code: "terminal_waiting_for_approval"
781
+ };
782
+ }
783
+ }
784
+ if (state === "idle" && hasSeenActivity && consecutiveIdle >= 2) {
785
+ return {
786
+ ...fence,
787
+ kind: "completed",
788
+ observed_at: observedAt,
789
+ observation_checkpoint: checkpoint,
790
+ evidence_fingerprint: sha256({
791
+ watch_id: watch.watch_id,
792
+ reason_code: "terminal_activity_became_stably_idle",
793
+ anchor_fingerprint: watch.anchor.anchor_fingerprint
794
+ }),
795
+ reason_code: "terminal_activity_became_stably_idle"
796
+ };
797
+ }
798
+ return {
799
+ ...fence,
800
+ kind: "pending",
801
+ observed_at: observedAt,
802
+ ...(active ? { last_activity_at: observedAt } : {}),
803
+ observation_checkpoint: checkpoint
804
+ };
805
+ }
594
806
  async function observeUserExplicitFallbackTerminalWatch(input) {
595
807
  const { watch, exactTerminal, rawTerminal, terminalMatches, observedAt, options, dependencies } = input;
596
808
  const fence = terminalWatchObservationFence(watch);
@@ -944,20 +1156,49 @@ async function currentTerminalForWatch(watch, options, dependencies) {
944
1156
  }
945
1157
  function terminalMatchesWatch(terminal, watch) {
946
1158
  try {
947
- const agent = terminalAgent(terminal);
948
- const current = terminalWatchIdentity(terminal, bindingTokenForWatch(terminal));
949
- return agent === watch.agent &&
950
- terminalWatchIdentityFingerprint({
951
- agent,
952
- terminal: current,
953
- anchor: watch.anchor
954
- }) === terminalWatchIdentityFingerprint(watch) &&
955
- sameTerminalControlEvidenceIncarnation(current.terminal_endpoint, watch.terminal.terminal_endpoint);
1159
+ return terminalAgent(terminal) === watch.agent &&
1160
+ requiredString(terminal.id, "terminal id") ===
1161
+ watch.terminal.terminal_id &&
1162
+ terminalWorkspace(terminal) === watch.terminal.workspace &&
1163
+ isRecord(terminal.terminal_control) &&
1164
+ sameTerminalControlEvidenceIncarnation(terminal.terminal_control, watch.terminal.terminal_endpoint);
956
1165
  }
957
1166
  catch {
958
1167
  return false;
959
1168
  }
960
1169
  }
1170
+ function terminalActivityWatchIdentityMatch(terminal, watch) {
1171
+ try {
1172
+ if (!isTerminalActivityWatch(watch) ||
1173
+ terminalAgent(terminal) !== watch.agent ||
1174
+ requiredString(terminal.id, "terminal id") !==
1175
+ watch.terminal.terminal_id ||
1176
+ terminalWorkspace(terminal) !== watch.terminal.workspace ||
1177
+ positiveInteger(terminal.pid, "terminal PID") !== watch.anchor.pid) {
1178
+ return "mismatch";
1179
+ }
1180
+ if (!isRecord(terminal.terminal_control)) {
1181
+ return "unavailable";
1182
+ }
1183
+ if (!sameTerminalControlEvidenceIncarnation(terminal.terminal_control, watch.terminal.terminal_endpoint)) {
1184
+ return "mismatch";
1185
+ }
1186
+ const processUuid = stringValue(terminal.native_agent_process_uuid);
1187
+ const processBirth = stringValue(terminal.native_agent_process_birth);
1188
+ if ((watch.anchor.native_process_uuid !== undefined && !processUuid) ||
1189
+ (watch.anchor.native_process_birth !== undefined && !processBirth)) {
1190
+ return "unavailable";
1191
+ }
1192
+ return (watch.anchor.native_process_uuid !== undefined &&
1193
+ processUuid !== watch.anchor.native_process_uuid) || (watch.anchor.native_process_birth !== undefined &&
1194
+ processBirth !== watch.anchor.native_process_birth)
1195
+ ? "mismatch"
1196
+ : "match";
1197
+ }
1198
+ catch {
1199
+ return "unavailable";
1200
+ }
1201
+ }
961
1202
  function terminalMatchesUserExplicitFallbackWatch(terminal, watch) {
962
1203
  try {
963
1204
  if (!isUserExplicitFallbackWatch(watch) ||
@@ -983,6 +1224,15 @@ function terminalMatchesUserExplicitFallbackWatch(terminal, watch) {
983
1224
  return false;
984
1225
  }
985
1226
  }
1227
+ function terminalActivityObservationCheckpoint(watch) {
1228
+ const checkpoint = watch.observation_checkpoint;
1229
+ if (!("schema" in checkpoint) ||
1230
+ checkpoint.schema !==
1231
+ "agent-knock-knock/terminal-activity-watch-checkpoint") {
1232
+ throw new Error("terminal activity Watch has no activity checkpoint");
1233
+ }
1234
+ return checkpoint;
1235
+ }
986
1236
  function terminalWatchIdentity(terminal, bindingToken) {
987
1237
  const terminalControl = terminal.terminal_control;
988
1238
  return {
@@ -1021,28 +1271,30 @@ function codexIdentityForWatch(watch) {
1021
1271
  rollout: watch.anchor.rollout
1022
1272
  };
1023
1273
  }
1024
- function publicTerminalWatch(watch) {
1274
+ function publicTerminalWatch(watch, additionalWarnings = []) {
1025
1275
  const pending = watch.notification_outbox.filter(({ status }) => status === "pending" || status === "delivering" || status === "failed").length;
1026
- const compatibilityWarning = watch.agent === "codex" &&
1027
- (watch.anchor.schema ===
1028
- "agent-knock-knock/codex-human-started-active-task-anchor" ||
1029
- watch.anchor.schema ===
1030
- "agent-knock-knock/codex-user-explicit-fallback-watch-anchor")
1031
- ? codexRuntimeCompatibilityProfile(watch.anchor.codex_version)
1032
- ?.compatibilityWarning
1033
- : watch.agent === "claude" &&
1034
- (watch.anchor.schema ===
1035
- "agent-knock-knock/claude-human-started-active-task-anchor" ||
1036
- watch.anchor.schema ===
1037
- "agent-knock-knock/claude-user-explicit-fallback-watch-anchor")
1038
- ? claudeRuntimeCompatibilityWarning(watch.anchor.claude_version)
1039
- : undefined;
1276
+ const capturedAgentVersion = terminalWatchCapturedAgentVersion(watch);
1277
+ const compatibilityWarning = capturedAgentVersion === undefined
1278
+ ? undefined
1279
+ : watch.agent === "codex"
1280
+ ? codexRuntimeCompatibilityProfile(capturedAgentVersion)
1281
+ ?.compatibilityWarning
1282
+ : claudeRuntimeCompatibilityWarning(capturedAgentVersion);
1040
1283
  const userExplicitFallback = isUserExplicitFallbackWatch(watch);
1284
+ const terminalActivityFallback = isTerminalActivityWatch(watch);
1285
+ const warnings = [...new Set([
1286
+ ...(watch.warnings ?? []),
1287
+ ...additionalWarnings
1288
+ ])];
1041
1289
  return {
1042
1290
  watch_id: watch.watch_id,
1043
1291
  source: userExplicitFallback
1044
1292
  ? "terminal_user_explicit_fallback_watch"
1045
- : "human_started_terminal_watch",
1293
+ : terminalActivityFallback
1294
+ ? "terminal_activity_watch"
1295
+ : "user_selected_terminal_watch",
1296
+ watch_mode: terminalActivityFallback ? "terminal_activity" : "exact_task",
1297
+ confidence: terminalActivityFallback ? "best_effort" : "exact",
1046
1298
  agent: watch.agent,
1047
1299
  terminal_id: watch.terminal.terminal_id,
1048
1300
  native_thread_id: terminalWatchNativeThreadId(watch),
@@ -1056,6 +1308,9 @@ function publicTerminalWatch(watch) {
1056
1308
  ...(compatibilityWarning
1057
1309
  ? { compatibility_warning: compatibilityWarning }
1058
1310
  : {}),
1311
+ ...(warnings.length > 0
1312
+ ? { warnings }
1313
+ : {}),
1059
1314
  callback: {
1060
1315
  pending,
1061
1316
  delivered: watch.notification_outbox.filter(({ status }) => status === "delivered").length,
@@ -1091,6 +1346,18 @@ function publicTerminalWatch(watch) {
1091
1346
  }
1092
1347
  };
1093
1348
  }
1349
+ function terminalWatchCapturedAgentVersion(watch) {
1350
+ switch (watch.anchor.schema) {
1351
+ case "agent-knock-knock/codex-human-started-active-task-anchor":
1352
+ case "agent-knock-knock/codex-user-explicit-fallback-watch-anchor":
1353
+ return watch.anchor.codex_version;
1354
+ case "agent-knock-knock/claude-human-started-active-task-anchor":
1355
+ case "agent-knock-knock/claude-user-explicit-fallback-watch-anchor":
1356
+ return watch.anchor.claude_version;
1357
+ case "agent-knock-knock/terminal-activity-watch-anchor":
1358
+ return watch.anchor.agent_version;
1359
+ }
1360
+ }
1094
1361
  function terminalWatchNativeThreadId(watch) {
1095
1362
  switch (watch.anchor.schema) {
1096
1363
  case "agent-knock-knock/codex-human-started-active-task-anchor":
@@ -1103,6 +1370,8 @@ function terminalWatchNativeThreadId(watch) {
1103
1370
  : undefined;
1104
1371
  case "agent-knock-knock/claude-user-explicit-fallback-watch-anchor":
1105
1372
  return watch.anchor.transcript_anchor.session_id;
1373
+ case "agent-knock-knock/terminal-activity-watch-anchor":
1374
+ return undefined;
1106
1375
  }
1107
1376
  }
1108
1377
  function approvalFingerprint(terminal) {