@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
@@ -11,6 +11,7 @@ import { projectSessionSelectorCandidate } from "./terminal-selector-projection-
11
11
  import { listManagedSessions, loadNativeThreadTransition, nativeThreadTransitionsDir, tryLoadManagedSession } from "./session-store.js";
12
12
  import { defaultStoreDir, ensureStoreWritable, inspectStoreCompatibility, listConversations, STORE_SESSION_AUTHORITY_PROTOCOL } from "./store.js";
13
13
  import { exactCodexReadyStyledComposerCapture } from "./terminal-agent-bridge.js";
14
+ import { captureCodexHumanStartedActiveTaskAnchor } from "./terminal-submission-acceptance.js";
14
15
  import { applySessionAuthorityToDispatch, authoritativeTerminalIdentity, compareManagedConversationRecency, decideManagedTerminalAssociation, decideLocalTerminalDispatchOwnership, decideTerminalSendAuthority, decideTerminalSessionAuthorityConflict, managedTurnNeedsAttention as terminalManagedTurnNeedsAttention, nonOwnerTerminalActions, projectBlockingTurn, projectHandoffDecision, projectHandoffPresentation, projectPublicManagementConflict, projectReconcileBindingAction, projectTerminalDispatchConflict, projectTerminalManagement, selectManagedTerminalHistory, selectTerminalAvailableActions } from "./terminal-action-projection.js";
15
16
  import { childProcessIdsForRoot, decideManagedBindingConflict, deferredCodexForegroundBindingToken, exactBoundCodexSendSource, isCompleteNativeRollout, nativeAgentIdentityMatchesTurn, processIncarnationRelationship, selectRootTerminalProcesses, terminalControlAliasMatches, terminalControlsShareIncarnation, verifiedEmptyCodexHandoffToken } from "./terminal-authority-policy.js";
16
17
  import { decideTerminalBindingMatch, terminalObservationFromListEntry } from "./terminal-binding-authority.js";
@@ -46,6 +47,7 @@ export function createTerminalListCliFacade(dependencies) {
46
47
  return {
47
48
  runList: (options) => call(() => runList(options)),
48
49
  buildTerminalListGroup: (request) => call(() => buildTerminalListGroup(request)),
50
+ observeExactTerminal: (request) => call(() => observeExactTerminal(request)),
49
51
  provisionalManagedBindingTurnCount: (storeDir, session) => call(() => provisionalManagedBindingTurnCount(storeDir, session)),
50
52
  managedSessionHasUnresolvedNativeTransition: (storeDir, session) => call(() => managedSessionHasUnresolvedNativeTransition(storeDir, session)),
51
53
  managedSessionHasAnyNativeTransition: (storeDir, session) => call(() => managedSessionHasAnyNativeTransition(storeDir, session)),
@@ -70,56 +72,149 @@ async function runList(options) {
70
72
  status: "disabled",
71
73
  reason: "standalone list is read-only unless --reconcile is supplied"
72
74
  };
73
- const includeAll = Boolean(options.all);
74
75
  const agentFilter = options.agent ? resolveExecutor({ kind: options.agent }).kind : undefined;
75
76
  const statusFilter = options.status;
76
- const allStoredConversations = listConversations(storeDir);
77
- const allManagedConversations = allStoredConversations
77
+ const terminalScan = await buildTerminalListGroup({ options, agentFilter, statusFilter });
78
+ const projected = projectTerminalListScan({
79
+ options,
80
+ storeDir,
81
+ store,
82
+ terminalScan,
83
+ agentFilter,
84
+ statusFilter
85
+ });
86
+ printJson({
87
+ store_dir: storeDir,
88
+ store,
89
+ reconciliation,
90
+ action_contracts: listActionContracts(),
91
+ terminals: projected.terminals,
92
+ terminal_watches: projected.terminalWatches,
93
+ unavailable_managed_turns: projected.unavailableManagedTurns,
94
+ terminal_scan: {
95
+ ...terminalScan.summary,
96
+ terminal_count: projected.terminals.length
97
+ }
98
+ });
99
+ runtimeLog("info", "terminals_listed", {
100
+ store_dir: storeDir,
101
+ terminal_count: projected.terminals.length,
102
+ unavailable_managed_turn_count: projected.unavailableManagedTurns.length,
103
+ terminal_scan_error: terminalScan.summary.error,
104
+ include_all: projected.includeAll,
105
+ agent_filter: agentFilter,
106
+ status_filter: statusFilter,
107
+ reconciliation
108
+ });
109
+ }
110
+ function projectTerminalListScan(input) {
111
+ const { options, storeDir, store, terminalScan, agentFilter, statusFilter, tolerateInvalidWatchRecords } = input;
112
+ const includeAll = Boolean(options.all);
113
+ const allManagedConversations = listConversations(storeDir)
78
114
  .filter(terminalListRuntime().isDiscoverableTmuxConversation);
79
115
  const managedSessions = store.readable
80
116
  ? listManagedSessions(storeDir)
81
117
  : [];
82
- const storedConversations = allManagedConversations
118
+ const displayedConversations = allManagedConversations
83
119
  .filter((conversation) => includeAll || terminalListRuntime().isActiveStatus(conversation.status))
84
120
  .filter((conversation) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, conversation.workspace))
85
121
  .filter((conversation) => !agentFilter || executorForConversation(conversation).kind === agentFilter)
86
122
  .filter((conversation) => !statusFilter || conversation.status === statusFilter);
87
- const terminalScan = await buildTerminalListGroup({ options, agentFilter, statusFilter });
123
+ const workspaceConversations = allManagedConversations.filter((conversation) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, conversation.workspace));
88
124
  const physicalTerminals = terminalScan.terminalControlled.filter((entry) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, entry.workspace ?? entry.cwd));
89
125
  const projection = terminalFirstListProjection({
90
126
  storeDir,
91
127
  terminals: physicalTerminals,
92
128
  managedSessions,
93
129
  sessionAuthorityRequired: Number(store.writer_protocol) >= STORE_SESSION_AUTHORITY_PROTOCOL,
94
- allConversations: allManagedConversations.filter((conversation) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, conversation.workspace)),
95
- displayedConversations: storedConversations,
130
+ allConversations: workspaceConversations,
131
+ displayedConversations,
96
132
  includeAll,
97
133
  managedOnly: options.managedOnly === true,
98
134
  statusFilter,
99
135
  mutationsAllowed: store.writable === true
100
136
  });
101
- printJson({
102
- store_dir: storeDir,
137
+ const exactWatchScan = terminalListRuntime()
138
+ .scanTerminalWatchesForExactObservation;
139
+ const watchObservation = tolerateInvalidWatchRecords && exactWatchScan
140
+ ? exactWatchScan(storeDir, { includeAll })
141
+ : {
142
+ watches: terminalListRuntime().listTerminalWatches?.(storeDir, { includeAll }) ?? [],
143
+ activeOverlayTrusted: true
144
+ };
145
+ const observedTerminalWatches = watchObservation.watches;
146
+ const terminalWatches = options.managedOnly
147
+ ? []
148
+ : observedTerminalWatches
149
+ .filter((watch) => !agentFilter || watch.agent === agentFilter)
150
+ .filter((watch) => terminalListRuntime().matchesConfiguredWorkspace(options.workspace, watch.workspace))
151
+ .filter((watch) => !statusFilter || watch.status === statusFilter);
152
+ const activeWatchedTerminals = new Set(observedTerminalWatches
153
+ .filter((watch) => watch.status === "active")
154
+ .map((watch) => stringValue(watch.terminal_id))
155
+ .filter((terminalId) => terminalId !== undefined));
156
+ const terminals = projection.terminals.map((terminal) => !watchObservation.activeOverlayTrusted ||
157
+ activeWatchedTerminals.has(stringValue(terminal.id) ?? "")
158
+ ? withoutTerminalWatchAuthority(terminal)
159
+ : terminal);
160
+ return {
161
+ includeAll,
162
+ terminals,
163
+ terminalWatches,
164
+ unavailableManagedTurns: projection.unavailableManagedTurns
165
+ };
166
+ }
167
+ async function observeExactTerminal(request) {
168
+ const { options, terminalId } = request;
169
+ const storeDir = expandHome(options.storeDir ?? options.logDir ?? defaultStoreDir(cliCwd()));
170
+ const store = inspectStoreCompatibility(storeDir);
171
+ const scan = await buildTerminalListGroup({ options, terminalId });
172
+ const matches = scan.terminalControlled.filter((terminal) => stringValue(terminal.id) === terminalId);
173
+ if (matches.length !== 1) {
174
+ return scan.summary.error
175
+ ? {
176
+ state: "unavailable",
177
+ reason: scan.summary.error,
178
+ summary: scan.summary
179
+ }
180
+ : { state: "absent", summary: scan.summary };
181
+ }
182
+ const projected = projectTerminalListScan({
183
+ options,
184
+ storeDir,
103
185
  store,
104
- reconciliation,
105
- action_contracts: listActionContracts(),
106
- terminals: projection.terminals,
107
- unavailable_managed_turns: projection.unavailableManagedTurns,
108
- terminal_scan: {
109
- ...terminalScan.summary,
110
- terminal_count: projection.terminals.length
111
- }
112
- });
113
- runtimeLog("info", "terminals_listed", {
114
- store_dir: storeDir,
115
- terminal_count: projection.terminals.length,
116
- unavailable_managed_turn_count: projection.unavailableManagedTurns.length,
117
- terminal_scan_error: terminalScan.summary.error,
118
- include_all: includeAll,
119
- agent_filter: agentFilter,
120
- status_filter: statusFilter,
121
- reconciliation
186
+ terminalScan: scan,
187
+ tolerateInvalidWatchRecords: true
122
188
  });
189
+ const projectedMatches = projected.terminals.filter((terminal) => stringValue(terminal.id) === terminalId);
190
+ if (projectedMatches.length !== 1) {
191
+ return {
192
+ state: "unavailable",
193
+ reason: "the exact terminal could not be projected authoritatively",
194
+ summary: scan.summary
195
+ };
196
+ }
197
+ return {
198
+ state: "available",
199
+ rawTerminal: matches[0],
200
+ terminal: projectedMatches[0],
201
+ summary: scan.summary
202
+ };
203
+ }
204
+ function withoutAvailableAction(terminal, action) {
205
+ if (!isRecord(terminal.available_actions))
206
+ return terminal;
207
+ const actions = { ...terminal.available_actions };
208
+ delete actions[action];
209
+ return { ...terminal, available_actions: actions };
210
+ }
211
+ function withoutTerminalWatchAuthority(terminal) {
212
+ const projected = withoutAvailableAction(terminal, "watch");
213
+ if (!Object.hasOwn(projected, "terminal_watch_hint"))
214
+ return projected;
215
+ const safe = { ...projected };
216
+ delete safe.terminal_watch_hint;
217
+ return safe;
123
218
  }
124
219
  async function reconcileStoreForList(storeDir, options) {
125
220
  try {
@@ -157,7 +252,7 @@ async function reconcileStoreForList(storeDir, options) {
157
252
  idle_timeout_minutes: idle.idle_timeout_minutes
158
253
  };
159
254
  }
160
- async function buildTerminalListGroup({ options, agentFilter, statusFilter }) {
255
+ async function buildTerminalListGroup({ options, agentFilter, statusFilter, terminalId }) {
161
256
  const empty = {
162
257
  terminalControlled: [],
163
258
  summary: {
@@ -198,8 +293,17 @@ async function buildTerminalListGroup({ options, agentFilter, statusFilter }) {
198
293
  const rootSessions = selectRootTerminalProcesses(activeSessions);
199
294
  const controlledSessions = rootSessions.filter((session) => session.terminalControl !== undefined);
200
295
  activeCount = controlledSessions.length;
201
- for (const session of controlledSessions) {
202
- terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options, bridge));
296
+ const selectedSessions = terminalId
297
+ ? controlledSessions.filter((session) => bridge.terminalConversationId(session) === terminalId)
298
+ : controlledSessions;
299
+ for (const session of selectedSessions) {
300
+ try {
301
+ terminalControlled.push(await terminalControlledListEntry(session, activeSessions, options, bridge));
302
+ }
303
+ catch (error) {
304
+ errors.push(`terminal process ${session.pid}: ` +
305
+ (error instanceof Error ? error.message : String(error)));
306
+ }
203
307
  }
204
308
  }
205
309
  catch (error) {
@@ -272,17 +376,28 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
272
376
  terminalTarget: terminalControl.target
273
377
  });
274
378
  const { orphanedDispatch, nativeIdentityObservation, nativeAgentIdentity, codexOpenRootRolloutInventory, nativeProcessUuid, nativeProcessBirth, nativeProcessEvidence } = await observeTerminalNativeListIdentity(session, terminalControl, options, bridge);
379
+ const effectiveTerminalState = effectiveTerminalListState({
380
+ session,
381
+ terminalState,
382
+ nativeIdentityObservation,
383
+ nativeAgentIdentity,
384
+ nativeProcessUuid,
385
+ nativeProcessBirth
386
+ });
387
+ const terminalStatusSnapshot = effectiveTerminalState._terminal_status_snapshot
388
+ ? terminalBridgeStatusWithActivity(effectiveTerminalState._terminal_status_snapshot, effectiveTerminalState.activity_state, effectiveTerminalState.activity_reason)
389
+ : undefined;
275
390
  const statusCardObservation = session.agent === "codex" &&
276
- typeof terminalState.screen_excerpt === "string"
391
+ typeof effectiveTerminalState.screen_excerpt === "string"
277
392
  ? bridge.registry.require("codex").observeThreadLifecycle?.({
278
393
  operation: { kind: "new_thread" },
279
394
  phase: "before",
280
- screen: terminalState.screen_excerpt
395
+ screen: effectiveTerminalState.screen_excerpt
281
396
  })
282
397
  : undefined;
283
398
  const statusCardNativeThreadId = statusCardObservation?.status === "observed" &&
284
- terminalState.activity_state === "idle" &&
285
- terminalState.approval_state.blocked !== true &&
399
+ effectiveTerminalState.activity_state === "idle" &&
400
+ effectiveTerminalState.approval_state.blocked !== true &&
286
401
  isExactNativeThreadId(statusCardObservation.nativeThreadId)
287
402
  ? statusCardObservation.nativeThreadId
288
403
  : undefined;
@@ -304,7 +419,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
304
419
  };
305
420
  const codexLatentClearResumeObservationValue = session.agent === "codex"
306
421
  ? terminalListRuntime().codexLatentClearResumeObservation({
307
- screen: terminalState.screen_excerpt,
422
+ screen: effectiveTerminalState.screen_excerpt,
308
423
  agentVersion
309
424
  })
310
425
  : undefined;
@@ -325,7 +440,7 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
325
440
  const automatedInputComposerReady = await observeAutomatedInputComposerReady({
326
441
  session,
327
442
  terminalControl,
328
- terminalState,
443
+ terminalState: effectiveTerminalState,
329
444
  nativeIdentityObservation,
330
445
  codexOpenRootRolloutInventory,
331
446
  options
@@ -363,9 +478,15 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
363
478
  confidence: session.confidence,
364
479
  reason: session.reason,
365
480
  terminal_control: terminalControl,
366
- approval_state: terminalState.approval_state,
367
- activity_state: terminalState.activity_state,
368
- activity_reason: terminalState.activity_reason,
481
+ approval_state: effectiveTerminalState.approval_state,
482
+ activity_state: effectiveTerminalState.activity_state,
483
+ activity_reason: effectiveTerminalState.activity_reason,
484
+ // Internal exact-observation evidence. The public projection strips this
485
+ // object; raw terminal status reuses it so screen, approval, and activity
486
+ // all describe the same capture.
487
+ ...(terminalStatusSnapshot
488
+ ? { _terminal_status_snapshot: terminalStatusSnapshot }
489
+ : {}),
369
490
  // Internal action-projection evidence. terminalFirstListProjection strips
370
491
  // this field after gating every automated-input action that can follow a
371
492
  // human native-thread switch.
@@ -398,33 +519,20 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
398
519
  }
399
520
  }
400
521
  : {}),
401
- commands: {
402
- send: !terminalHasBlockingTurn,
403
- approve: terminalControl.capabilities.includes("terminal_approval") &&
404
- terminalState.approval_state.approvable === true,
405
- status: true,
406
- cancel: terminalControl.capabilities.includes("terminal_cancel"),
407
- close: orphanedDispatch !== undefined,
408
- new_thread: lifecycleCapability.status === "supported" &&
409
- lifecycleCapability.newThread === true &&
410
- codexLifecycleIncarnationAvailable &&
411
- !terminalHasBlockingTurn,
412
- list_resumable_threads: lifecycleCapability.status === "supported" &&
413
- lifecycleCapability.resumeExact === true &&
414
- codexLifecycleIncarnationAvailable,
415
- native_inspect: nativeInspectionCapability.status === "supported" &&
416
- nativeInspectionCapability.statusInspection === true &&
417
- terminalState.activity_state === "idle" &&
418
- automatedInputComposerReady &&
419
- terminalControl.capabilities.includes("send_keys") &&
420
- terminalControl.capabilities.includes("screen_status") &&
421
- (session.agent === "codex"
422
- ? codexLifecycleIncarnationAvailable
423
- : Boolean(nativeAgentIdentity?.sessionId &&
424
- nativeAgentIdentity.processUuid)) &&
425
- orphanedDispatch === undefined &&
426
- !terminalHasBlockingTurn
427
- }
522
+ commands: terminalListCommands({
523
+ agent: session.agent,
524
+ terminalControl,
525
+ terminalState: effectiveTerminalState,
526
+ lifecycleCapability,
527
+ nativeInspectionCapability,
528
+ nativeAgentIdentity,
529
+ nativeProcessUuid,
530
+ nativeProcessBirth,
531
+ codexLifecycleIncarnationAvailable,
532
+ automatedInputComposerReady,
533
+ hasOrphanedDispatch: orphanedDispatch !== undefined,
534
+ terminalHasBlockingTurn
535
+ })
428
536
  };
429
537
  const availableActions = renderAvailableListActions(entry);
430
538
  const { commands: _commands, ...publicEntry } = entry;
@@ -433,6 +541,115 @@ async function terminalControlledListEntry(session, activeSessions, options, bri
433
541
  available_actions: availableActions
434
542
  };
435
543
  }
544
+ function terminalListCommands(input) {
545
+ const { agent, terminalControl, terminalState, lifecycleCapability, nativeInspectionCapability, nativeAgentIdentity, nativeProcessUuid, nativeProcessBirth, codexLifecycleIncarnationAvailable, automatedInputComposerReady, hasOrphanedDispatch, terminalHasBlockingTurn } = input;
546
+ return {
547
+ send: !terminalHasBlockingTurn,
548
+ approve: terminalControl.capabilities.includes("terminal_approval") &&
549
+ terminalState.approval_state.approvable === true,
550
+ status: true,
551
+ cancel: terminalControl.capabilities.includes("terminal_cancel"),
552
+ close: hasOrphanedDispatch,
553
+ new_thread: lifecycleCapability.status === "supported" &&
554
+ lifecycleCapability.newThread === true &&
555
+ codexLifecycleIncarnationAvailable &&
556
+ !terminalHasBlockingTurn,
557
+ list_resumable_threads: lifecycleCapability.status === "supported" &&
558
+ lifecycleCapability.resumeExact === true &&
559
+ codexLifecycleIncarnationAvailable,
560
+ native_inspect: nativeInspectionCapability.status === "supported" &&
561
+ nativeInspectionCapability.statusInspection === true &&
562
+ terminalState.activity_state === "idle" &&
563
+ automatedInputComposerReady &&
564
+ terminalControl.capabilities.includes("send_keys") &&
565
+ terminalControl.capabilities.includes("screen_status") &&
566
+ (agent === "codex"
567
+ ? codexLifecycleIncarnationAvailable
568
+ : Boolean(nativeAgentIdentity?.sessionId &&
569
+ nativeAgentIdentity.processUuid)) &&
570
+ !hasOrphanedDispatch &&
571
+ !terminalHasBlockingTurn,
572
+ watch: lifecycleCapability.status === "supported" &&
573
+ (terminalState.activity_state === "working" ||
574
+ terminalState.activity_state === "awaiting_approval") &&
575
+ Boolean(nativeAgentIdentity?.sessionId) &&
576
+ (agent !== "codex" || Boolean(nativeAgentIdentity?.rollout)) &&
577
+ Boolean(nativeProcessUuid) &&
578
+ Boolean(nativeProcessBirth) &&
579
+ !hasOrphanedDispatch &&
580
+ !terminalHasBlockingTurn
581
+ };
582
+ }
583
+ function effectiveTerminalListState(input) {
584
+ const { session, terminalState, nativeIdentityObservation, nativeAgentIdentity, nativeProcessUuid, nativeProcessBirth } = input;
585
+ if (session.agent !== "codex" ||
586
+ terminalState.activity_state === "working" ||
587
+ terminalState.activity_state === "awaiting_approval" ||
588
+ terminalState.approval_state.blocked === true) {
589
+ return terminalState;
590
+ }
591
+ if (nativeIdentityObservation.status === "verified_absent") {
592
+ return terminalState;
593
+ }
594
+ if (nativeIdentityObservation.status !== "resolved" ||
595
+ !nativeAgentIdentity?.rollout ||
596
+ !nativeAgentIdentity.sessionId ||
597
+ !nativeProcessUuid ||
598
+ !nativeProcessBirth) {
599
+ return terminalListStateWithUnavailableDurableActivity(terminalState, nativeIdentityObservation.status === "unavailable"
600
+ ? nativeIdentityObservation.reason ??
601
+ "exact Codex identity observation failed"
602
+ : "exact Codex rollout/process identity is incomplete");
603
+ }
604
+ const currentIdentity = {
605
+ sessionId: nativeAgentIdentity.sessionId,
606
+ processUuid: nativeProcessUuid,
607
+ processBirth: nativeProcessBirth,
608
+ rollout: nativeAgentIdentity.rollout
609
+ };
610
+ try {
611
+ if (!captureCodexHumanStartedActiveTaskAnchor({ currentIdentity })) {
612
+ return terminalState;
613
+ }
614
+ return {
615
+ ...terminalState,
616
+ activity_state: "working",
617
+ activity_reason: "Codex rollout contains an exact unfinished human-started task"
618
+ };
619
+ }
620
+ catch (error) {
621
+ const reason = error instanceof Error ? error.message : String(error);
622
+ runtimeLog("warn", "terminal_durable_activity_unavailable", {
623
+ agent: session.agent,
624
+ pid: session.pid,
625
+ reason
626
+ });
627
+ return terminalListStateWithUnavailableDurableActivity(terminalState, reason);
628
+ }
629
+ }
630
+ function terminalListStateWithUnavailableDurableActivity(terminalState, reason) {
631
+ return {
632
+ ...terminalState,
633
+ activity_state: "unknown",
634
+ activity_reason: `durable Codex activity evidence is unavailable: ${reason}`
635
+ };
636
+ }
637
+ function terminalBridgeStatusWithActivity(status, activityState, activityReason) {
638
+ const descriptors = Object.getOwnPropertyDescriptors(status);
639
+ descriptors.activity_state = {
640
+ configurable: true,
641
+ enumerable: true,
642
+ value: activityState,
643
+ writable: true
644
+ };
645
+ descriptors.activity_reason = {
646
+ configurable: true,
647
+ enumerable: true,
648
+ value: activityReason,
649
+ writable: true
650
+ };
651
+ return Object.create(Object.getPrototypeOf(status), descriptors);
652
+ }
436
653
  async function observeTerminalNativeListIdentity(session, terminalControl, options, bridge) {
437
654
  const orphanedDispatch = terminalListRuntime().orphanedTerminalDispatchForRecovery(terminalControl);
438
655
  let codexIdentityContext;
@@ -562,7 +779,7 @@ async function observeAutomatedInputComposerReady({ session, terminalControl, te
562
779
  }
563
780
  function observeTerminalListBindingAuthority(terminal, context) {
564
781
  const { storeDir, managedSessions, allConversations, displayedConversations, mutationsAllowed, nonterminalDeferredTransfers } = context;
565
- const { _automated_input_composer_ready: automatedInputComposerReady, _codex_open_root_rollout_inventory: codexOpenRootRolloutInventoryValue, _codex_latent_clear_resume: codexLatentClearResumeValue, ...publicTerminal } = terminal;
782
+ const { _automated_input_composer_ready: automatedInputComposerReady, _codex_open_root_rollout_inventory: codexOpenRootRolloutInventoryValue, _codex_latent_clear_resume: codexLatentClearResumeValue, _terminal_status_snapshot: _terminalStatusSnapshot, ...publicTerminal } = terminal;
566
783
  const codexOpenRootRolloutInventory = isRecord(codexOpenRootRolloutInventoryValue)
567
784
  ? codexOpenRootRolloutInventoryValue
568
785
  : undefined;
@@ -1995,6 +2212,7 @@ async function listStateForTerminal(agent, terminalControl, options, bridge = te
1995
2212
  activity_state: status.activity_state,
1996
2213
  activity_reason: status.activity_reason,
1997
2214
  capability_limitation: status.capability_limitation,
2215
+ _terminal_status_snapshot: status,
1998
2216
  // Internal projection evidence; terminalControlledListEntry selects all
1999
2217
  // public fields explicitly and never exposes the pane excerpt itself.
2000
2218
  screen_excerpt: status.screen.excerpt