baxian 2.0.2 → 2.0.4

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 (232) hide show
  1. package/README.md +2 -2
  2. package/README.zh-CN.md +3 -3
  3. package/dist/agent/bootstrap.js +4 -4
  4. package/dist/agent/branch.d.ts.map +1 -1
  5. package/dist/agent/branch.js +0 -7
  6. package/dist/agent/branch.js.map +1 -1
  7. package/dist/agent/detect/region.d.ts.map +1 -1
  8. package/dist/agent/detect/region.js +0 -3
  9. package/dist/agent/detect/region.js.map +1 -1
  10. package/dist/agent/dispatch-reconciler.d.ts +2 -0
  11. package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
  12. package/dist/agent/dispatch-reconciler.js +48 -55
  13. package/dist/agent/dispatch-reconciler.js.map +1 -1
  14. package/dist/agent/lineage.d.ts.map +1 -1
  15. package/dist/agent/lineage.js +0 -9
  16. package/dist/agent/lineage.js.map +1 -1
  17. package/dist/agent/manager.d.ts +42 -8
  18. package/dist/agent/manager.d.ts.map +1 -1
  19. package/dist/agent/manager.js +717 -375
  20. package/dist/agent/manager.js.map +1 -1
  21. package/dist/agent/net-exec.d.ts.map +1 -1
  22. package/dist/agent/net-exec.js +0 -5
  23. package/dist/agent/net-exec.js.map +1 -1
  24. package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
  25. package/dist/agent/pane-streamer-manager.js +0 -36
  26. package/dist/agent/pane-streamer-manager.js.map +1 -1
  27. package/dist/agent/pane-streamer.d.ts +6 -0
  28. package/dist/agent/pane-streamer.d.ts.map +1 -1
  29. package/dist/agent/pane-streamer.js +126 -83
  30. package/dist/agent/pane-streamer.js.map +1 -1
  31. package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
  32. package/dist/agent/phase-signal-watcher.js +2 -77
  33. package/dist/agent/phase-signal-watcher.js.map +1 -1
  34. package/dist/agent/phase-signal.d.ts +0 -1
  35. package/dist/agent/phase-signal.d.ts.map +1 -1
  36. package/dist/agent/phase-signal.js +0 -17
  37. package/dist/agent/phase-signal.js.map +1 -1
  38. package/dist/agent/preflight.d.ts.map +1 -1
  39. package/dist/agent/preflight.js +0 -18
  40. package/dist/agent/preflight.js.map +1 -1
  41. package/dist/agent/prompt.d.ts.map +1 -1
  42. package/dist/agent/prompt.js +0 -1
  43. package/dist/agent/prompt.js.map +1 -1
  44. package/dist/agent/repo-store.d.ts.map +1 -1
  45. package/dist/agent/repo-store.js +0 -38
  46. package/dist/agent/repo-store.js.map +1 -1
  47. package/dist/agent/runner.d.ts.map +1 -1
  48. package/dist/agent/runner.js +0 -2
  49. package/dist/agent/runner.js.map +1 -1
  50. package/dist/agent/tmux-install.js +0 -1
  51. package/dist/agent/tmux-install.js.map +1 -1
  52. package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
  53. package/dist/agent/tmux-probe-poller.js +4 -11
  54. package/dist/agent/tmux-probe-poller.js.map +1 -1
  55. package/dist/agent/tmux.d.ts.map +1 -1
  56. package/dist/agent/tmux.js +0 -86
  57. package/dist/agent/tmux.js.map +1 -1
  58. package/dist/agent/vt-visible-text.d.ts +46 -1
  59. package/dist/agent/vt-visible-text.d.ts.map +1 -1
  60. package/dist/agent/vt-visible-text.js +639 -63
  61. package/dist/agent/vt-visible-text.js.map +1 -1
  62. package/dist/api/agents.d.ts.map +1 -1
  63. package/dist/api/agents.js +3 -49
  64. package/dist/api/agents.js.map +1 -1
  65. package/dist/api/config.d.ts.map +1 -1
  66. package/dist/api/config.js +10 -59
  67. package/dist/api/config.js.map +1 -1
  68. package/dist/api/hosts.d.ts.map +1 -1
  69. package/dist/api/hosts.js +8 -63
  70. package/dist/api/hosts.js.map +1 -1
  71. package/dist/api/platform-guard.d.ts.map +1 -1
  72. package/dist/api/platform-guard.js +2 -3
  73. package/dist/api/platform-guard.js.map +1 -1
  74. package/dist/api/probe.d.ts.map +1 -1
  75. package/dist/api/probe.js +0 -1
  76. package/dist/api/probe.js.map +1 -1
  77. package/dist/api/projects.d.ts.map +1 -1
  78. package/dist/api/projects.js +15 -28
  79. package/dist/api/projects.js.map +1 -1
  80. package/dist/api/tasks.d.ts.map +1 -1
  81. package/dist/api/tasks.js +94 -58
  82. package/dist/api/tasks.js.map +1 -1
  83. package/dist/app.d.ts +2 -2
  84. package/dist/app.d.ts.map +1 -1
  85. package/dist/app.js +0 -6
  86. package/dist/app.js.map +1 -1
  87. package/dist/cli.d.ts.map +1 -1
  88. package/dist/cli.js +6 -31
  89. package/dist/cli.js.map +1 -1
  90. package/dist/config/hot-reload.d.ts.map +1 -1
  91. package/dist/config/hot-reload.js +0 -4
  92. package/dist/config/hot-reload.js.map +1 -1
  93. package/dist/config/loader.d.ts.map +1 -1
  94. package/dist/config/loader.js +6 -29
  95. package/dist/config/loader.js.map +1 -1
  96. package/dist/config/normalizer.d.ts.map +1 -1
  97. package/dist/config/normalizer.js +0 -19
  98. package/dist/config/normalizer.js.map +1 -1
  99. package/dist/config/validator.d.ts.map +1 -1
  100. package/dist/config/validator.js +31 -229
  101. package/dist/config/validator.js.map +1 -1
  102. package/dist/event/handlers.d.ts.map +1 -1
  103. package/dist/event/handlers.js +3 -11
  104. package/dist/event/handlers.js.map +1 -1
  105. package/dist/event/log.js +0 -2
  106. package/dist/event/log.js.map +1 -1
  107. package/dist/event/ws.d.ts.map +1 -1
  108. package/dist/event/ws.js +3 -3
  109. package/dist/event/ws.js.map +1 -1
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +5 -46
  112. package/dist/index.js.map +1 -1
  113. package/dist/platform/body-digest.d.ts.map +1 -1
  114. package/dist/platform/body-digest.js +0 -4
  115. package/dist/platform/body-digest.js.map +1 -1
  116. package/dist/platform/command-renderer.d.ts.map +1 -1
  117. package/dist/platform/command-renderer.js +0 -3
  118. package/dist/platform/command-renderer.js.map +1 -1
  119. package/dist/platform/comment-cursor.d.ts.map +1 -1
  120. package/dist/platform/comment-cursor.js +0 -21
  121. package/dist/platform/comment-cursor.js.map +1 -1
  122. package/dist/platform/driver-spec.d.ts.map +1 -1
  123. package/dist/platform/driver-spec.js +1 -42
  124. package/dist/platform/driver-spec.js.map +1 -1
  125. package/dist/platform/feedback.d.ts +5 -1
  126. package/dist/platform/feedback.d.ts.map +1 -1
  127. package/dist/platform/feedback.js +7 -1
  128. package/dist/platform/feedback.js.map +1 -1
  129. package/dist/platform/field-mapper.d.ts.map +1 -1
  130. package/dist/platform/field-mapper.js +3 -11
  131. package/dist/platform/field-mapper.js.map +1 -1
  132. package/dist/platform/git-driver.d.ts.map +1 -1
  133. package/dist/platform/git-driver.js +0 -20
  134. package/dist/platform/git-driver.js.map +1 -1
  135. package/dist/platform/manifest.d.ts.map +1 -1
  136. package/dist/platform/manifest.js +0 -3
  137. package/dist/platform/manifest.js.map +1 -1
  138. package/dist/platform/markers.d.ts.map +1 -1
  139. package/dist/platform/markers.js +0 -19
  140. package/dist/platform/markers.js.map +1 -1
  141. package/dist/platform/platform-poller.d.ts +5 -1
  142. package/dist/platform/platform-poller.d.ts.map +1 -1
  143. package/dist/platform/platform-poller.js +8 -49
  144. package/dist/platform/platform-poller.js.map +1 -1
  145. package/dist/platform/plugin-registry.d.ts.map +1 -1
  146. package/dist/platform/plugin-registry.js +1 -12
  147. package/dist/platform/plugin-registry.js.map +1 -1
  148. package/dist/platform/pr-conversation-cache.d.ts +12 -7
  149. package/dist/platform/pr-conversation-cache.d.ts.map +1 -1
  150. package/dist/platform/pr-conversation-cache.js +60 -40
  151. package/dist/platform/pr-conversation-cache.js.map +1 -1
  152. package/dist/platform/preflight-exec.d.ts.map +1 -1
  153. package/dist/platform/preflight-exec.js +0 -9
  154. package/dist/platform/preflight-exec.js.map +1 -1
  155. package/dist/platform/response-parser.d.ts.map +1 -1
  156. package/dist/platform/response-parser.js +0 -4
  157. package/dist/platform/response-parser.js.map +1 -1
  158. package/dist/platform/review-timeline.d.ts +17 -2
  159. package/dist/platform/review-timeline.d.ts.map +1 -1
  160. package/dist/platform/review-timeline.js +93 -106
  161. package/dist/platform/review-timeline.js.map +1 -1
  162. package/dist/platform/row-schema.d.ts.map +1 -1
  163. package/dist/platform/row-schema.js +0 -11
  164. package/dist/platform/row-schema.js.map +1 -1
  165. package/dist/platform/startup.d.ts.map +1 -1
  166. package/dist/platform/startup.js +1 -12
  167. package/dist/platform/startup.js.map +1 -1
  168. package/dist/platform/types.d.ts.map +1 -1
  169. package/dist/platform/types.js +0 -5
  170. package/dist/platform/types.js.map +1 -1
  171. package/dist/platform/verdict-engine.d.ts.map +1 -1
  172. package/dist/platform/verdict-engine.js +0 -15
  173. package/dist/platform/verdict-engine.js.map +1 -1
  174. package/dist/shared/concurrency.d.ts.map +1 -1
  175. package/dist/shared/concurrency.js +0 -2
  176. package/dist/shared/concurrency.js.map +1 -1
  177. package/dist/shared/constants.d.ts +0 -2
  178. package/dist/shared/constants.d.ts.map +1 -1
  179. package/dist/shared/constants.js +0 -3
  180. package/dist/shared/constants.js.map +1 -1
  181. package/dist/shared/git-url.d.ts.map +1 -1
  182. package/dist/shared/git-url.js +0 -9
  183. package/dist/shared/git-url.js.map +1 -1
  184. package/dist/shared/types.d.ts +31 -12
  185. package/dist/shared/types.d.ts.map +1 -1
  186. package/dist/shared/types.js +14 -0
  187. package/dist/shared/types.js.map +1 -1
  188. package/dist/skill/registry.d.ts.map +1 -1
  189. package/dist/skill/registry.js +0 -5
  190. package/dist/skill/registry.js.map +1 -1
  191. package/dist/skills/baxian-signals/SKILL.md +0 -6
  192. package/dist/state/agent-store.d.ts.map +1 -1
  193. package/dist/state/agent-store.js +0 -6
  194. package/dist/state/agent-store.js.map +1 -1
  195. package/dist/state/error-record-store.d.ts.map +1 -1
  196. package/dist/state/error-record-store.js +0 -5
  197. package/dist/state/error-record-store.js.map +1 -1
  198. package/dist/state/init.d.ts.map +1 -1
  199. package/dist/state/init.js +0 -1
  200. package/dist/state/init.js.map +1 -1
  201. package/dist/state/lock.d.ts.map +1 -1
  202. package/dist/state/lock.js +0 -1
  203. package/dist/state/lock.js.map +1 -1
  204. package/dist/state/snapshot.d.ts +0 -1
  205. package/dist/state/snapshot.d.ts.map +1 -1
  206. package/dist/state/snapshot.js +0 -13
  207. package/dist/state/snapshot.js.map +1 -1
  208. package/dist/state/task-store.d.ts.map +1 -1
  209. package/dist/state/task-store.js +96 -11
  210. package/dist/state/task-store.js.map +1 -1
  211. package/dist/terminal/attach.d.ts.map +1 -1
  212. package/dist/terminal/attach.js +0 -4
  213. package/dist/terminal/attach.js.map +1 -1
  214. package/dist/terminal/stream-ws.js +0 -5
  215. package/dist/terminal/stream-ws.js.map +1 -1
  216. package/dist/web/assets/index-CzVzThuT.js +15 -0
  217. package/dist/web/assets/{index-BS2adAfI.css → index-Rmp4wT_5.css} +1 -1
  218. package/dist/web/index.html +2 -2
  219. package/package.json +1 -1
  220. package/dist/agent/root-agent-runtime.d.ts +0 -80
  221. package/dist/agent/root-agent-runtime.d.ts.map +0 -1
  222. package/dist/agent/root-agent-runtime.js +0 -637
  223. package/dist/agent/root-agent-runtime.js.map +0 -1
  224. package/dist/agent/root-recovery-coordinator.d.ts +0 -124
  225. package/dist/agent/root-recovery-coordinator.d.ts.map +0 -1
  226. package/dist/agent/root-recovery-coordinator.js +0 -1557
  227. package/dist/agent/root-recovery-coordinator.js.map +0 -1
  228. package/dist/state/root-recovery-store.d.ts +0 -107
  229. package/dist/state/root-recovery-store.d.ts.map +0 -1
  230. package/dist/state/root-recovery-store.js +0 -400
  231. package/dist/state/root-recovery-store.js.map +0 -1
  232. package/dist/web/assets/index-BKiM0MSM.js +0 -15
@@ -1,1557 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { RECOVERABLE_QA_DISPATCH_HOLD_PHASES } from './manager.js';
3
- import { ROOT_OUTBOX_DIR, RootAgentTerminationError, RootAgentResponseInvalidError, RootPromptNotSubmittedError, } from './root-agent-runtime.js';
4
- import { ExecOutcomeUnknownError } from './net-exec.js';
5
- import { ROOT_RECOVERY_ACTION_SET, ROOT_RECOVERY_MAX_REASON_BYTES, } from '../state/root-recovery-store.js';
6
- import { isRecord, taskMatchesGeneration, TERMINAL_INTERVENTION_PHASE_SET, } from '../shared/index.js';
7
- import { PeriodicTaskRunner } from '../timing/periodic-task-runner.js';
8
- const RECOVERABLE_TASK_STATUSES = new Set([
9
- 'pending', 'in_progress', 'review', 'fixing', 'approved',
10
- ]);
11
- const STALL_REASONS = new Set(['STUCK_BUSY', 'PENDING_IDLE']);
12
- const MAX_CONTEXT_STRING_BYTES = 8 * 1024;
13
- const MAX_EVENT_DATA_BYTES = 2 * 1024;
14
- const MAX_PANE_BYTES = 12 * 1024;
15
- const MAX_CONTEXT_ERROR_BYTES = 512;
16
- const MAX_FIELD_NAME_CHARS = 256;
17
- const MAX_SECRET_SCAN_NODES = 4_096;
18
- const MIN_GLOBAL_SECRET_CHARS = 8;
19
- const RECENT_EVENT_LIMIT = 20;
20
- const SENSITIVE_FIELD_RE = /(?:token|password|secret|authorization|credential|nonce)/i;
21
- const KEY_CREDENTIAL_QUALIFIERS = new Set(['api', 'access', 'private', 'ssh']);
22
- const KEY_CREDENTIAL_FIELDS = new Set(['apikey', 'accesskey', 'privatekey', 'sshkey']);
23
- const AUTHORIZATION_FIELDS = new Set(['authorization', 'proxyauthorization']);
24
- const BARE_AUTH_CREDENTIAL_RE = /^(?:Basic|Bearer)[\t ]+([A-Za-z0-9._~+\/-]{8,}=*)[\t ]*$/i;
25
- const LABELED_FIELD_RE = /(^|[^A-Za-z0-9_.-])(["']?)([A-Za-z0-9_.-]+)\2([^\S\r\n]*[:=][^\S\r\n]*)/gim;
26
- const RECOVERY_RETENTION_MS = 30 * 24 * 60 * 60_000;
27
- const RECOVERY_PRUNE_INTERVAL_MS = 24 * 60 * 60_000;
28
- export class RootRuntimeStopIncompleteError extends Error {
29
- constructor(detail) {
30
- super(`Root recovery shutdown is incomplete; retry stopping root-agent: ${detail}`);
31
- this.name = 'RootRuntimeStopIncompleteError';
32
- }
33
- }
34
- export class RootRecoveryCoordinator {
35
- config;
36
- eventBus;
37
- manager;
38
- taskStore;
39
- agentStore;
40
- statusStore;
41
- runtime;
42
- store;
43
- capturePane;
44
- now;
45
- periodicRunner;
46
- runtimeRetryIntervalMs;
47
- processing = new Map();
48
- delivering = new Set();
49
- polling = new Set();
50
- reconciling = new Set();
51
- reconciliationFailures = [];
52
- runtimeRechecks = new Set();
53
- triggerRecoveryRechecks = new Map();
54
- intakeChain = Promise.resolve();
55
- kickChain = Promise.resolve();
56
- runtimeStart = null;
57
- runtimeStop = null;
58
- runtimeReady = false;
59
- lastRuntimeAttemptAt = 0;
60
- lastRuntimeError = '';
61
- runtimeAlerts = new Map();
62
- deliveryAlerts = new Set();
63
- lastPruneAt = 0;
64
- offTaskChange;
65
- offAgentChange;
66
- offStatusChange;
67
- started = false;
68
- runtimeControlStatus = 'active';
69
- runtimeStopWarning = '';
70
- runtimeStopHostRepairable = false;
71
- onIntervention = async (event) => {
72
- try {
73
- await this.handleIntervention(event);
74
- }
75
- catch (err) {
76
- console.warn('[root-agent] intervention intake failed:', err);
77
- }
78
- };
79
- constructor(deps) {
80
- this.config = deps.config;
81
- this.eventBus = deps.eventBus;
82
- this.manager = deps.manager;
83
- this.taskStore = deps.taskStore;
84
- this.agentStore = deps.agentStore;
85
- this.statusStore = deps.statusStore;
86
- this.runtime = deps.runtime;
87
- this.store = deps.store;
88
- this.capturePane = deps.capturePane;
89
- this.now = deps.now ?? (() => new Date());
90
- this.runtimeRetryIntervalMs = deps.runtimeRetryIntervalMs ?? 60_000;
91
- this.periodicRunner = new PeriodicTaskRunner({
92
- name: 'root-recovery',
93
- intervalMs: deps.pollIntervalMs ?? 2_000,
94
- run: () => this.pollOnce(),
95
- onError: err => console.warn('[root-agent] recovery poll failed:', err),
96
- });
97
- }
98
- async start() {
99
- if (this.started)
100
- return;
101
- this.started = true;
102
- this.runtimeControlStatus = 'active';
103
- this.eventBus.on('human.intervention', this.onIntervention);
104
- this.offTaskChange = this.taskStore.onChange((_kind, taskId) => {
105
- void this.finishIfStale(taskId).catch(err => {
106
- console.warn(`[root-agent] stale-request reconciliation failed for ${taskId}:`, err);
107
- });
108
- });
109
- this.offAgentChange = this.agentStore.onChange((_kind, agentId) => {
110
- void this.handleRuntimeObservation(agentId).catch(err => {
111
- console.warn(`[root-agent] agent-state reconciliation failed for ${agentId}:`, err);
112
- });
113
- });
114
- this.offStatusChange = this.statusStore.onChange((_kind, agentId) => {
115
- void this.handleRuntimeObservation(agentId).catch(err => {
116
- console.warn(`[root-agent] runtime-stall intake failed for ${agentId}:`, err);
117
- });
118
- });
119
- try {
120
- await this.recoverInterruptedExecutions();
121
- await this.recoverHeldAgents();
122
- this.periodicRunner.start({ runImmediately: true });
123
- }
124
- catch (err) {
125
- try {
126
- await this.deactivate();
127
- }
128
- catch (cleanupErr) {
129
- console.warn('[root-agent] startup rollback could not stop the runtime cleanly:', cleanupErr);
130
- }
131
- throw err;
132
- }
133
- }
134
- async stop() {
135
- if (!this.started)
136
- return;
137
- await this.deactivate();
138
- }
139
- async deactivate() {
140
- this.started = false;
141
- this.periodicRunner.stop();
142
- this.eventBus.off('human.intervention', this.onIntervention);
143
- this.offTaskChange?.();
144
- this.offAgentChange?.();
145
- this.offStatusChange?.();
146
- this.offTaskChange = undefined;
147
- this.offAgentChange = undefined;
148
- this.offStatusChange = undefined;
149
- this.runtimeRechecks.clear();
150
- let drainFailure;
151
- try {
152
- await this.waitForCoordinatorWork();
153
- }
154
- catch (err) {
155
- drainFailure = err;
156
- }
157
- this.runtimeAlerts.clear();
158
- this.deliveryAlerts.clear();
159
- this.runtimeReady = false;
160
- try {
161
- await this.runtime.stop();
162
- }
163
- catch (err) {
164
- if (drainFailure !== undefined) {
165
- throw new AggregateError([drainFailure, err], 'Root recovery coordinator shutdown failed');
166
- }
167
- throw err;
168
- }
169
- if (drainFailure !== undefined)
170
- throw drainFailure;
171
- }
172
- async isRuntimeLive() {
173
- return this.runtime.isLive();
174
- }
175
- isRuntimeExplicitlyStopped() {
176
- return this.getRuntimeControlStatus() === 'stopped-until-restart';
177
- }
178
- getRuntimeControlStatus() {
179
- if (this.started || this.runtimeControlStatus !== 'active')
180
- return this.runtimeControlStatus;
181
- return 'disabled';
182
- }
183
- getRuntimeStopWarning() {
184
- return this.runtimeStopWarning || undefined;
185
- }
186
- canRepairHostAfterIncompleteStop() {
187
- return this.runtimeControlStatus === 'stop-incomplete' && this.runtimeStopHostRepairable;
188
- }
189
- isRuntimeDispatchDisabled() {
190
- return !this.started || this.runtimeControlStatus !== 'active';
191
- }
192
- usesHost(hostId) {
193
- return this.config.mode === 'remote' && this.config.host === hostId;
194
- }
195
- async invalidateRuntimeStreamer() {
196
- this.runtimeReady = false;
197
- this.lastRuntimeAttemptAt = 0;
198
- await this.runtime.invalidateStreamer();
199
- }
200
- stopRuntime() {
201
- if (this.runtimeStop)
202
- return this.runtimeStop;
203
- this.runtimeControlStatus = 'stopping';
204
- this.runtimeStopHostRepairable = false;
205
- this.runtimeReady = false;
206
- const priorKicks = this.kickChain;
207
- const run = this.stopRuntimeNow(priorKicks);
208
- this.runtimeStop = run;
209
- this.kickChain = run.catch(() => undefined);
210
- void run.finally(() => {
211
- if (this.runtimeStop === run)
212
- this.runtimeStop = null;
213
- }).catch(() => undefined);
214
- return run;
215
- }
216
- async stopRuntimeNow(priorKicks) {
217
- const stopFailures = [];
218
- const ledgerFailures = [];
219
- this.runtimeStopWarning = '';
220
- try {
221
- await priorKicks;
222
- await this.waitForReconciliation();
223
- }
224
- catch (err) {
225
- stopFailures.push(err);
226
- }
227
- try {
228
- await this.runtime.terminate();
229
- }
230
- catch (err) {
231
- stopFailures.push(err);
232
- }
233
- try {
234
- await this.intakeChain;
235
- await this.waitForResponseProcessing();
236
- }
237
- catch (err) {
238
- stopFailures.push(err);
239
- }
240
- try {
241
- for (const record of await this.store.listActive()) {
242
- await this.finishRecoveryAfterRuntimeStop(record);
243
- }
244
- }
245
- catch (err) {
246
- ledgerFailures.push(err);
247
- }
248
- this.lastRuntimeAttemptAt = this.now().getTime();
249
- this.lastRuntimeError = '';
250
- this.runtimeAlerts.clear();
251
- this.deliveryAlerts.clear();
252
- if (stopFailures.length > 0) {
253
- this.runtimeControlStatus = 'stop-incomplete';
254
- this.runtimeStopHostRepairable = ledgerFailures.length === 0
255
- && stopFailures.length === 1
256
- && stopFailures[0] instanceof RootAgentTerminationError
257
- && stopFailures[0].hostConnectionUnknown;
258
- const detail = [...stopFailures, ...ledgerFailures]
259
- .map(err => err instanceof Error ? err.message : String(err))
260
- .join('; ');
261
- throw new RootRuntimeStopIncompleteError(detail);
262
- }
263
- this.runtimeControlStatus = 'stopped-until-restart';
264
- if (ledgerFailures.length > 0) {
265
- const detail = ledgerFailures
266
- .map(err => err instanceof Error ? err.message : String(err))
267
- .join('; ');
268
- this.runtimeStopWarning =
269
- `Root session termination was confirmed, but recovery ledger reconciliation failed: ${detail}`;
270
- console.warn(`[root-agent] ${this.runtimeStopWarning}`);
271
- }
272
- }
273
- async waitForResponseProcessing() {
274
- const results = await Promise.allSettled(this.processing.values());
275
- const failures = results
276
- .filter((result) => result.status === 'rejected')
277
- .map(result => result.reason);
278
- if (failures.length === 1)
279
- throw failures[0];
280
- if (failures.length > 1)
281
- throw new AggregateError(failures, 'Multiple root decisions failed while stopping');
282
- }
283
- async waitForCoordinatorWork() {
284
- while (true) {
285
- const intake = this.intakeChain;
286
- const kicks = this.kickChain;
287
- const polling = [...this.polling];
288
- const processing = [...this.processing.values()];
289
- const reconciling = [...this.reconciling];
290
- const results = await Promise.allSettled([
291
- intake,
292
- kicks,
293
- ...polling,
294
- ...processing,
295
- ...reconciling,
296
- ]);
297
- const failures = results
298
- .filter((result) => result.status === 'rejected')
299
- .map(result => result.reason)
300
- .concat(this.takeReconciliationFailures());
301
- if (failures.length === 1)
302
- throw failures[0];
303
- if (failures.length > 1)
304
- throw new AggregateError(failures, 'Root recovery work failed while stopping');
305
- if (intake === this.intakeChain
306
- && kicks === this.kickChain
307
- && this.polling.size === 0
308
- && this.processing.size === 0
309
- && this.reconciling.size === 0)
310
- return;
311
- }
312
- }
313
- async waitForReconciliation() {
314
- while (this.reconciling.size > 0) {
315
- const results = await Promise.allSettled(this.reconciling);
316
- const failures = results
317
- .filter((result) => result.status === 'rejected')
318
- .map(result => result.reason)
319
- .concat(this.takeReconciliationFailures());
320
- if (failures.length === 1)
321
- throw failures[0];
322
- if (failures.length > 1) {
323
- throw new AggregateError(failures, 'Multiple root recovery reconciliations failed while stopping');
324
- }
325
- }
326
- const failures = this.takeReconciliationFailures();
327
- if (failures.length === 1)
328
- throw failures[0];
329
- if (failures.length > 1) {
330
- throw new AggregateError(failures, 'Multiple root recovery reconciliations failed while stopping');
331
- }
332
- }
333
- pollOnce() {
334
- const run = this.pollOnceNow().finally(() => {
335
- this.polling.delete(run);
336
- });
337
- this.polling.add(run);
338
- return run;
339
- }
340
- async pollOnceNow() {
341
- if (this.runtimeControlStatus !== 'active')
342
- return;
343
- if (!this.started) {
344
- await this.pruneCompleted();
345
- return;
346
- }
347
- for (const agentId of [...this.runtimeRechecks]) {
348
- try {
349
- await this.handleRuntimeObservation(agentId);
350
- }
351
- catch (err) {
352
- this.runtimeRechecks.add(agentId);
353
- console.warn(`[root-agent] runtime recheck failed for ${agentId}; will retry:`, err);
354
- }
355
- }
356
- for (const [taskId, record] of [...this.triggerRecoveryRechecks]) {
357
- try {
358
- await this.recoverCurrentTaskTriggers(record);
359
- if (this.triggerRecoveryRechecks.get(taskId)?.id === record.id) {
360
- this.triggerRecoveryRechecks.delete(taskId);
361
- }
362
- }
363
- catch (err) {
364
- console.warn(`[root-agent] follow-up trigger recovery retry failed for ${taskId}:`, err);
365
- }
366
- }
367
- const active = await this.store.listActive();
368
- for (const record of active) {
369
- if (record.status !== 'inflight'
370
- || this.processing.has(record.id)
371
- || this.delivering.has(record.id))
372
- continue;
373
- if (record.decision) {
374
- if (await this.complete(record, 'unknown', 'Server restarted or lost execution state after persisting the root decision; action was not retried.')) {
375
- await this.emitRootIntervention(record, 'root-action-outcome-unknown', record.decision.reason);
376
- }
377
- continue;
378
- }
379
- await this.processResponse(record);
380
- const fresh = await this.store.get(record.id);
381
- if (fresh?.status === 'inflight')
382
- await this.finishTimedOutResponse(fresh);
383
- }
384
- await this.kick();
385
- await this.pruneCompleted();
386
- }
387
- async handleIntervention(event) {
388
- if (event.data.source === 'root-agent' || !event.taskId)
389
- return;
390
- const phase = typeof event.data.phase === 'string' ? event.data.phase : undefined;
391
- if (!phase
392
- || phase === 'resumed'
393
- || phase === 'git-review-dispatch-hold-cleared'
394
- || TERMINAL_INTERVENTION_PHASE_SET.has(phase))
395
- return;
396
- const hold = event.agentId
397
- ? await this.currentHold(event.agentId, event.taskId, phase)
398
- : undefined;
399
- await this.createRequest(event.taskId, {
400
- kind: 'intervention',
401
- observedAt: event.timestamp,
402
- eventId: event.id,
403
- ...(event.agentId ? { agentId: event.agentId } : {}),
404
- ...(phase ? { phase: truncateUtf8(phase, 256) } : {}),
405
- ...(typeof event.data.reason === 'string'
406
- ? { reason: truncateUtf8(event.data.reason, ROOT_RECOVERY_MAX_REASON_BYTES) }
407
- : {}),
408
- ...(typeof event.data.message === 'string'
409
- ? { message: truncateUtf8(event.data.message, ROOT_RECOVERY_MAX_REASON_BYTES) }
410
- : {}),
411
- ...hold,
412
- });
413
- }
414
- async handleRuntimeObservation(agentId) {
415
- await this.finishIfTriggerStale(agentId);
416
- const observation = this.statusStore.get(agentId);
417
- if (!observation.reason || !STALL_REASONS.has(observation.reason)) {
418
- this.runtimeRechecks.delete(agentId);
419
- return;
420
- }
421
- const binding = await this.agentStore.get(agentId);
422
- if (!binding?.taskId
423
- || binding.status === 'awaiting_human'
424
- || binding.needInput?.at !== undefined) {
425
- this.runtimeRechecks.delete(agentId);
426
- return;
427
- }
428
- const task = await this.taskStore.get(binding.taskId);
429
- const observedAt = observation.observedAt ? Date.parse(observation.observedAt) : Number.NaN;
430
- const latestContextWrite = Math.max(Date.parse(binding.updatedAt), task ? Date.parse(task.updatedAt) : Number.NaN);
431
- if (Number.isFinite(observedAt)
432
- && Number.isFinite(latestContextWrite)
433
- && observedAt < latestContextWrite) {
434
- this.runtimeRechecks.add(agentId);
435
- return;
436
- }
437
- this.runtimeRechecks.delete(agentId);
438
- const triggerObservedAt = observation.stateChangedAt
439
- ?? observation.observedAt
440
- ?? this.now().toISOString();
441
- await this.createRequest(binding.taskId, {
442
- kind: 'runtime-stall',
443
- observedAt: triggerObservedAt,
444
- agentId,
445
- reason: observation.reason,
446
- ...(observation.message ? { message: truncateUtf8(observation.message, ROOT_RECOVERY_MAX_REASON_BYTES) } : {}),
447
- });
448
- }
449
- createRequest(taskId, trigger) {
450
- if (this.isRuntimeDispatchDisabled())
451
- return Promise.resolve();
452
- const run = this.intakeChain.then(() => this.createRequestNow(taskId, trigger));
453
- this.intakeChain = run.catch(() => undefined);
454
- return run;
455
- }
456
- async createRequestNow(taskId, trigger) {
457
- if (this.isRuntimeDispatchDisabled())
458
- return;
459
- const task = await this.taskStore.get(taskId);
460
- if (this.isRuntimeDispatchDisabled())
461
- return;
462
- if (!task || !this.projectEnabled(task.projectId) || !RECOVERABLE_TASK_STATUSES.has(task.status))
463
- return;
464
- const guard = guardFor(task);
465
- const redispatchLimitReached = triggerCanRedispatch(trigger)
466
- && await this.redispatchLimitReached(task.id, guard);
467
- const result = await this.store.createIfIdle({
468
- taskId: task.id,
469
- projectId: task.projectId,
470
- trigger: redispatchLimitReached
471
- ? { ...trigger, eventId: redispatchLimitEventId(task.id, guard) }
472
- : trigger,
473
- guard,
474
- });
475
- if (result.created) {
476
- if (redispatchLimitReached) {
477
- const attempts = this.manager.getConfig().server.dispatchReconcileMaxAttempts;
478
- const detail = `Automatic root redispatch reached the configured limit (${attempts}) for this task phase; ` +
479
- 'the task remains stopped for human recovery.';
480
- const completed = await this.store.complete(result.record.id, {
481
- kind: 'escalated',
482
- detail,
483
- at: this.now().toISOString(),
484
- }, result.record);
485
- if (completed.completed) {
486
- console.log(`[root-agent] recovery ${result.record.id} finished: escalated`);
487
- await this.emitRootIntervention(result.record, 'root-redispatch-attempts-exhausted', detail);
488
- }
489
- else {
490
- console.warn(`[root-agent] redispatch-limit completion lost its state race for ${result.record.id}; ` +
491
- 'the current recovery record was preserved');
492
- }
493
- return;
494
- }
495
- console.log(`[root-agent] queued recovery ${result.record.id} for task ${task.id} (${trigger.kind})`);
496
- this.scheduleKick();
497
- }
498
- }
499
- async redispatchLimitReached(taskId, guard) {
500
- const limit = this.manager.getConfig().server.dispatchReconcileMaxAttempts;
501
- let attempts = 0;
502
- for (const record of await this.store.list()) {
503
- if (record.taskId === taskId
504
- && record.status === 'done'
505
- && record.decision?.action === 'redispatch-current-phase'
506
- && (record.outcome?.kind === 'executed' || record.outcome?.kind === 'unknown')
507
- && sameRedispatchLineage(record.guard, guard))
508
- attempts++;
509
- }
510
- return attempts >= limit;
511
- }
512
- scheduleKick() {
513
- void this.kick().catch(err => console.warn('[root-agent] recovery dispatch failed:', err));
514
- }
515
- kick() {
516
- const run = this.kickChain.then(() => this.dispatchNext());
517
- this.kickChain = run.catch(() => undefined);
518
- return run;
519
- }
520
- async dispatchNext() {
521
- if (!this.started || this.isRuntimeDispatchDisabled())
522
- return;
523
- const active = await this.store.listActive();
524
- if (active.some(record => record.status === 'inflight'))
525
- return;
526
- let record = active.find(item => item.status === 'pending');
527
- if (!record)
528
- return;
529
- if (!this.projectEnabled(record.projectId)) {
530
- await this.complete(record, 'stale', 'The project is no longer in the configured root-agent scope.');
531
- return;
532
- }
533
- const task = await this.taskStore.get(record.taskId);
534
- if (this.isRuntimeDispatchDisabled())
535
- return;
536
- if (!task
537
- || task.projectId !== record.projectId
538
- || !taskMatchesGeneration(task, record.guard)
539
- || !(await this.triggerStillActive(record))) {
540
- await this.complete(record, 'stale', 'Task generation changed before root dispatch.');
541
- return;
542
- }
543
- const mailboxMayExist = record.dispatchedAt !== undefined;
544
- const dispatchStarted = await this.store.markDispatchStarted(record.id);
545
- if (this.isRuntimeDispatchDisabled())
546
- return;
547
- if (!dispatchStarted || dispatchStarted.status !== 'pending')
548
- return;
549
- record = dispatchStarted;
550
- if (await this.finishTimedOutDelivery(record)) {
551
- this.scheduleKick();
552
- return;
553
- }
554
- if (await this.runtimeObservationLagsBinding(record))
555
- return;
556
- if (!(await this.ensureRuntime(record)) || this.isRuntimeDispatchDisabled())
557
- return;
558
- if (mailboxMayExist) {
559
- try {
560
- await this.runtime.cleanup(record);
561
- console.log(`[root-agent] removed residual pending mailbox for ${record.id}`);
562
- }
563
- catch (err) {
564
- console.warn(`[root-agent] residual pending mailbox cleanup failed for ${record.id}:`, err);
565
- return;
566
- }
567
- }
568
- let body;
569
- try {
570
- body = await this.buildRequest(record, task);
571
- if (this.isRuntimeDispatchDisabled())
572
- return;
573
- await this.runtime.writeRequest(record, body);
574
- this.deliveryAlerts.delete(record.id);
575
- }
576
- catch (err) {
577
- const detail = err instanceof Error ? err.message : String(err);
578
- if (err instanceof ExecOutcomeUnknownError) {
579
- this.runtimeReady = false;
580
- console.warn(`[root-agent] request delivery probe failed for ${record.id}; ` +
581
- `kept pending for deadline-bounded reconciliation: ${detail}`);
582
- if (!this.deliveryAlerts.has(record.id)) {
583
- this.deliveryAlerts.add(record.id);
584
- await this.emitRootIntervention(record, 'root-request-delivery-retrying', detail);
585
- }
586
- return;
587
- }
588
- if (await this.complete(record, 'failed', `Failed to deliver root request file: ${detail}`)) {
589
- await this.emitRootIntervention(record, 'root-request-delivery-failed', detail);
590
- }
591
- return;
592
- }
593
- const inflight = await this.store.markDispatched(record.id);
594
- if (!inflight || inflight.status !== 'inflight') {
595
- try {
596
- await this.runtime.cleanup(inflight ?? record);
597
- console.log(`[root-agent] removed stale request mailbox for ${record.id}`);
598
- }
599
- catch (err) {
600
- console.warn(`[root-agent] stale request mailbox cleanup failed for ${record.id}:`, err);
601
- }
602
- return;
603
- }
604
- if (this.isRuntimeDispatchDisabled()) {
605
- const retry = await this.store.requeueUndelivered(inflight.id, inflight.attemptToken);
606
- if (!retry.requeued) {
607
- console.warn(`[root-agent] stopped request ${record.id} could not be returned to pending; ` +
608
- 'its mailbox was preserved for reconciliation');
609
- return;
610
- }
611
- try {
612
- await this.runtime.cleanup(inflight);
613
- console.log(`[root-agent] removed stopped undelivered request mailbox for ${record.id}`);
614
- }
615
- catch (err) {
616
- console.warn(`[root-agent] stopped request mailbox cleanup failed for ${record.id}:`, err);
617
- }
618
- return;
619
- }
620
- this.delivering.add(inflight.id);
621
- try {
622
- await this.runtime.notify(inflight);
623
- await this.store.markDelivered(inflight.id, inflight.attemptToken);
624
- }
625
- catch (err) {
626
- const detail = err instanceof Error ? err.message : String(err);
627
- this.runtimeReady = false;
628
- if (err instanceof RootPromptNotSubmittedError) {
629
- const retry = await this.store.requeueUndelivered(inflight.id, inflight.attemptToken);
630
- if (retry.requeued) {
631
- console.warn(`[root-agent] prompt for ${inflight.id} was not submitted; returned to pending: ${detail}`);
632
- try {
633
- await this.runtime.cleanup(inflight);
634
- console.log(`[root-agent] removed unsubmitted request mailbox for ${inflight.id}`);
635
- }
636
- catch (cleanupError) {
637
- console.warn(`[root-agent] unsubmitted request mailbox cleanup failed for ${inflight.id}:`, cleanupError);
638
- }
639
- }
640
- return;
641
- }
642
- const completed = await this.complete(inflight, 'unknown', `Root prompt delivery outcome is unknown and was not retried: ${detail}`);
643
- if (completed)
644
- await this.emitRootIntervention(inflight, 'root-prompt-delivery-unknown', detail);
645
- }
646
- finally {
647
- this.delivering.delete(inflight.id);
648
- this.trackReconciliation(inflight);
649
- }
650
- }
651
- trackReconciliation(record) {
652
- const run = this.reconcileAfterDelivery(record)
653
- .catch(err => {
654
- console.warn(`[root-agent] post-delivery reconciliation failed for ${record.id}:`, err);
655
- if (!this.started || this.runtimeControlStatus !== 'active') {
656
- this.reconciliationFailures.push(err);
657
- }
658
- })
659
- .finally(() => {
660
- this.reconciling.delete(run);
661
- });
662
- this.reconciling.add(run);
663
- }
664
- takeReconciliationFailures() {
665
- return this.reconciliationFailures.splice(0);
666
- }
667
- async reconcileAfterDelivery(record) {
668
- await this.finishIfStale(record.taskId);
669
- if (record.trigger.agentId)
670
- await this.finishIfTriggerStale(record.trigger.agentId);
671
- }
672
- processResponse(record) {
673
- if (this.isRuntimeDispatchDisabled())
674
- return Promise.resolve();
675
- if (this.delivering.has(record.id))
676
- return Promise.resolve();
677
- const existing = this.processing.get(record.id);
678
- if (existing)
679
- return existing;
680
- const run = this.processResponseNow(record).finally(() => {
681
- if (this.processing.get(record.id) === run)
682
- this.processing.delete(record.id);
683
- this.scheduleKick();
684
- });
685
- this.processing.set(record.id, run);
686
- return run;
687
- }
688
- async processResponseNow(record) {
689
- if (!this.projectEnabled(record.projectId)) {
690
- await this.complete(record, 'stale', 'The project is no longer in the configured root-agent scope.');
691
- return;
692
- }
693
- if (await this.finishTimedOutDelivery(record))
694
- return;
695
- if (await this.finishTimedOutResponse(record))
696
- return;
697
- if (await this.runtimeObservationLagsBinding(record))
698
- return;
699
- let raw;
700
- try {
701
- raw = await this.runtime.readResponse(record);
702
- }
703
- catch (err) {
704
- if (err instanceof RootAgentResponseInvalidError) {
705
- const detail = err.message;
706
- if (await this.complete(record, 'failed', detail)) {
707
- await this.emitRootIntervention(record, 'root-response-invalid', detail);
708
- }
709
- return;
710
- }
711
- console.warn(`[root-agent] response read failed for ${record.id}; will reconcile again:`, err);
712
- return;
713
- }
714
- if (raw === null)
715
- return;
716
- let decision;
717
- try {
718
- decision = parseResponse(raw, record);
719
- }
720
- catch (err) {
721
- const detail = err instanceof Error ? err.message : String(err);
722
- if (await this.complete(record, 'failed', detail)) {
723
- await this.emitRootIntervention(record, 'root-response-invalid', detail);
724
- }
725
- return;
726
- }
727
- const taskBeforeClaim = await this.taskStore.get(record.taskId);
728
- if (await this.runtimeObservationLagsBinding(record))
729
- return;
730
- if (!taskBeforeClaim
731
- || !this.projectEnabled(record.projectId)
732
- || taskBeforeClaim.projectId !== record.projectId
733
- || !taskMatchesGeneration(taskBeforeClaim, record.guard)
734
- || !(await this.triggerStillActive(record))) {
735
- await this.complete(record, 'stale', 'The original task stall or hold no longer exists.');
736
- return;
737
- }
738
- if (await this.finishTimedOutResponse(record))
739
- return;
740
- const claim = await this.store.claimDecision(record.id, record.attemptToken, decision);
741
- if (!claim.claimed || !claim.record)
742
- return;
743
- const claimed = claim.record;
744
- const task = await this.taskStore.get(record.taskId);
745
- if (await this.runtimeObservationLagsBinding(claimed)) {
746
- await this.complete(claimed, 'stale', 'The agent binding advanced before the root decision could be executed.');
747
- return;
748
- }
749
- if (!task
750
- || !this.projectEnabled(record.projectId)
751
- || task.projectId !== record.projectId
752
- || !taskMatchesGeneration(task, record.guard)
753
- || !(await this.triggerStillActive(claimed))) {
754
- await this.complete(claimed, 'stale', 'Task generation changed before root decision execution.');
755
- return;
756
- }
757
- if (this.isRuntimeDispatchDisabled())
758
- return;
759
- if (decision.action === 'no-op') {
760
- await this.complete(claimed, 'ignored', decision.reason);
761
- return;
762
- }
763
- if (decision.action === 'escalate') {
764
- if (await this.complete(claimed, 'escalated', decision.reason)) {
765
- await this.emitRootIntervention(claimed, 'root-escalated', decision.reason);
766
- }
767
- return;
768
- }
769
- try {
770
- const result = await this.manager.redispatchCurrentTaskPhase(record.taskId, record.guard);
771
- if (result === 'dispatched') {
772
- await this.complete(claimed, 'executed', decision.reason);
773
- }
774
- else if (result === 'stale') {
775
- await this.complete(claimed, 'stale', 'Task generation changed while redispatching.');
776
- }
777
- else {
778
- if (await this.complete(claimed, 'failed', 'Current task phase has no safe redispatch path.')) {
779
- await this.emitRootIntervention(claimed, 'root-action-unsupported', `Root requested redispatch, but task status=${task.status} phase=${task.phase ?? 'none'} has no safe replay path.`);
780
- }
781
- }
782
- }
783
- catch (err) {
784
- const detail = err instanceof Error ? err.message : String(err);
785
- const completed = await this.complete(claimed, 'unknown', `Redispatch may have produced a side effect; it was not retried: ${detail}`);
786
- if (completed)
787
- await this.emitRootIntervention(claimed, 'root-action-outcome-unknown', detail);
788
- }
789
- }
790
- async finishIfStale(taskId) {
791
- while (true) {
792
- const active = (await this.store.listActive()).find(record => record.taskId === taskId);
793
- if (!active || this.processing.has(active.id) || this.delivering.has(active.id))
794
- return;
795
- const task = await this.taskStore.get(taskId);
796
- if (task
797
- && task.projectId === active.projectId
798
- && this.projectEnabled(active.projectId)
799
- && taskMatchesGeneration(task, active.guard))
800
- return;
801
- if (await this.complete(active, 'stale', 'Server advanced the task before the root decision was applied.')) {
802
- this.scheduleKick();
803
- return;
804
- }
805
- }
806
- }
807
- async finishIfTriggerStale(agentId) {
808
- while (true) {
809
- const active = (await this.store.listActive()).find(record => record.trigger.agentId === agentId);
810
- if (!active
811
- || this.processing.has(active.id)
812
- || this.delivering.has(active.id)
813
- || await this.triggerStillActive(active))
814
- return;
815
- if (await this.complete(active, 'stale', 'The original agent stall or hold no longer exists.')) {
816
- this.scheduleKick();
817
- return;
818
- }
819
- }
820
- }
821
- async triggerStillActive(record) {
822
- const { trigger } = record;
823
- if (!trigger.agentId)
824
- return true;
825
- if (trigger.kind === 'runtime-stall') {
826
- const observation = this.statusStore.get(trigger.agentId);
827
- const binding = await this.agentStore.get(trigger.agentId);
828
- if (binding?.taskId !== record.taskId
829
- || binding.status === 'awaiting_human'
830
- || binding.needInput?.at !== undefined
831
- || observation.reason !== trigger.reason)
832
- return false;
833
- const stateChangedAt = observation.stateChangedAt ?? observation.observedAt;
834
- return stateChangedAt === undefined || stateChangedAt === trigger.observedAt;
835
- }
836
- if (trigger.holdPhase === undefined)
837
- return true;
838
- const binding = await this.agentStore.get(trigger.agentId);
839
- return binding?.taskId === record.taskId
840
- && binding.status === 'awaiting_human'
841
- && (binding.awaitingPhase ?? '') === trigger.holdPhase
842
- && (binding.awaitingSince ?? '') === trigger.holdSince
843
- && (binding.awaitingNonce ?? '') === trigger.holdNonce;
844
- }
845
- async runtimeObservationLagsBinding(record) {
846
- const { trigger } = record;
847
- if (trigger.kind !== 'runtime-stall' || !trigger.agentId)
848
- return false;
849
- const binding = await this.agentStore.get(trigger.agentId);
850
- if (binding?.taskId !== record.taskId)
851
- return false;
852
- const observation = this.statusStore.get(trigger.agentId);
853
- const observedAt = observation.observedAt ? Date.parse(observation.observedAt) : Number.NaN;
854
- const bindingUpdatedAt = Date.parse(binding.updatedAt);
855
- if (!Number.isFinite(observedAt)
856
- || !Number.isFinite(bindingUpdatedAt)
857
- || observedAt >= bindingUpdatedAt)
858
- return false;
859
- this.runtimeRechecks.add(trigger.agentId);
860
- return true;
861
- }
862
- async currentHold(agentId, taskId, phase) {
863
- const binding = await this.agentStore.get(agentId);
864
- if (binding?.taskId !== taskId
865
- || binding.status !== 'awaiting_human'
866
- || binding.awaitingPhase !== phase)
867
- return undefined;
868
- return {
869
- eventId: holdEventId(binding),
870
- holdPhase: binding.awaitingPhase,
871
- holdSince: binding.awaitingSince ?? '',
872
- holdNonce: binding.awaitingNonce ?? '',
873
- };
874
- }
875
- async recoverInterruptedExecutions() {
876
- for (const record of await this.store.listActive()) {
877
- if (record.status !== 'inflight')
878
- continue;
879
- if (record.decision) {
880
- if (await this.complete(record, 'unknown', 'Server restarted after persisting the root decision; action was not blindly retried.')) {
881
- await this.emitRootIntervention(record, 'root-action-outcome-unknown', record.decision.reason);
882
- }
883
- }
884
- else if (!record.deliveredAt && await this.complete(record, 'unknown', 'Server restarted while root prompt delivery was in progress; delivery was not retried.')) {
885
- await this.emitRootIntervention(record, 'root-prompt-delivery-unknown', 'Server restarted before prompt delivery could be confirmed.');
886
- }
887
- }
888
- }
889
- async recoverHeldAgents(taskId) {
890
- for (const binding of await this.agentStore.list()) {
891
- if (binding.status !== 'awaiting_human'
892
- || !binding.taskId
893
- || (taskId !== undefined && binding.taskId !== taskId))
894
- continue;
895
- await this.recoverHeldAgent(binding);
896
- }
897
- }
898
- recoverHeldAgent(binding) {
899
- if (!binding.taskId)
900
- return Promise.resolve();
901
- return this.createRequest(binding.taskId, {
902
- kind: 'intervention',
903
- observedAt: binding.awaitingSince ?? binding.updatedAt,
904
- agentId: binding.id,
905
- eventId: holdEventId(binding),
906
- ...(binding.awaitingPhase ? { phase: truncateUtf8(binding.awaitingPhase, 256) } : {}),
907
- reason: 'agent-awaiting-human',
908
- ...(binding.awaitingReason
909
- ? { message: truncateUtf8(binding.awaitingReason, ROOT_RECOVERY_MAX_REASON_BYTES) }
910
- : {}),
911
- holdPhase: binding.awaitingPhase ?? '',
912
- holdSince: binding.awaitingSince ?? '',
913
- holdNonce: binding.awaitingNonce ?? '',
914
- });
915
- }
916
- async recoverCurrentTaskTriggers(completed) {
917
- const task = await this.taskStore.get(completed.taskId);
918
- const sameTaskGeneration = task !== null && taskMatchesGeneration(task, completed.guard);
919
- const bindings = (await this.agentStore.list()).filter(binding => binding.taskId === completed.taskId);
920
- for (const binding of bindings) {
921
- if (binding.status === 'awaiting_human') {
922
- const prior = completed.trigger;
923
- if (prior.kind === 'intervention'
924
- && prior.agentId === binding.id
925
- && prior.holdPhase === (binding.awaitingPhase ?? '')
926
- && prior.holdSince === (binding.awaitingSince ?? '')
927
- && prior.holdNonce === (binding.awaitingNonce ?? ''))
928
- continue;
929
- await this.recoverHeldAgent(binding);
930
- continue;
931
- }
932
- const observation = this.statusStore.get(binding.id);
933
- if (!observation.reason || !STALL_REASONS.has(observation.reason))
934
- continue;
935
- const prior = completed.trigger;
936
- const stateChangedAt = observation.stateChangedAt ?? observation.observedAt;
937
- if (prior.kind === 'runtime-stall'
938
- && prior.agentId === binding.id
939
- && prior.reason === observation.reason
940
- && sameTaskGeneration
941
- && !isLaterObservation(stateChangedAt, prior.observedAt))
942
- continue;
943
- await this.handleRuntimeObservation(binding.id);
944
- }
945
- }
946
- async pruneCompleted() {
947
- if (this.runtimeControlStatus !== 'active')
948
- return;
949
- const now = this.now().getTime();
950
- if (now - this.lastPruneAt < RECOVERY_PRUNE_INTERVAL_MS)
951
- return;
952
- this.lastPruneAt = now;
953
- const cutoff = new Date(now - RECOVERY_RETENTION_MS).toISOString();
954
- for (const record of await this.store.listDoneBefore(cutoff)) {
955
- try {
956
- if (this.runtimeControlStatus !== 'active')
957
- return;
958
- if (record.trigger.agentId !== undefined
959
- && record.trigger.holdPhase !== undefined
960
- && await this.triggerStillActive(record))
961
- continue;
962
- if (this.runtimeControlStatus !== 'active')
963
- return;
964
- await this.runtime.cleanup(record);
965
- if (await this.store.removeDone(record.id, record.updatedAt)) {
966
- console.log(`[root-agent] pruned retained recovery ${record.id}`);
967
- }
968
- }
969
- catch (err) {
970
- console.warn(`[root-agent] retained recovery cleanup failed for ${record.id}:`, err);
971
- }
972
- }
973
- }
974
- ensureRuntime(record) {
975
- if (this.isRuntimeDispatchDisabled())
976
- return Promise.resolve(false);
977
- if (this.runtimeReady)
978
- return Promise.resolve(true);
979
- if (this.runtimeStart)
980
- return this.runtimeStart;
981
- const now = this.now().getTime();
982
- if (now - this.lastRuntimeAttemptAt < this.runtimeRetryIntervalMs) {
983
- return Promise.resolve(false);
984
- }
985
- this.lastRuntimeAttemptAt = now;
986
- const pending = this.startRuntime(record);
987
- this.runtimeStart = pending;
988
- void pending.finally(() => {
989
- if (this.runtimeStart === pending)
990
- this.runtimeStart = null;
991
- }).catch(() => undefined);
992
- return pending;
993
- }
994
- async startRuntime(record) {
995
- try {
996
- await this.runtime.start(token => {
997
- void this.processSignal(token).catch(err => {
998
- console.warn(`[root-agent] signal processing failed for token ${token}:`, err);
999
- });
1000
- });
1001
- if (this.lastRuntimeError)
1002
- console.log('[root-agent] runtime recovered');
1003
- this.lastRuntimeError = '';
1004
- this.runtimeAlerts.clear();
1005
- this.runtimeReady = true;
1006
- return true;
1007
- }
1008
- catch (err) {
1009
- const detail = err instanceof Error ? err.message : String(err);
1010
- this.runtimeReady = false;
1011
- if (detail !== this.lastRuntimeError) {
1012
- this.lastRuntimeError = detail;
1013
- console.warn(`[root-agent] runtime unavailable: ${detail}`);
1014
- }
1015
- if (this.runtimeAlerts.get(record.id) !== detail) {
1016
- this.runtimeAlerts.set(record.id, detail);
1017
- await this.emitRootIntervention(record, 'root-runtime-unavailable', detail);
1018
- }
1019
- return false;
1020
- }
1021
- }
1022
- async processSignal(attemptToken) {
1023
- const record = (await this.store.listActive()).find(item => item.status === 'inflight' && item.attemptToken === attemptToken);
1024
- if (record)
1025
- await this.processResponse(record);
1026
- }
1027
- async buildRequest(record, task) {
1028
- const contextErrors = [];
1029
- const ids = new Set([
1030
- record.trigger.agentId,
1031
- task.agentId,
1032
- task.devAgentId,
1033
- task.qaAgentId,
1034
- ].filter((id) => typeof id === 'string' && id !== ''));
1035
- const secrets = collectSecretValues(this.manager.getConfig());
1036
- collectSecretValues(task, secrets);
1037
- collectSecretValues(record.trigger, secrets);
1038
- const agentContexts = [];
1039
- for (const id of ids) {
1040
- const config = this.manager.getAgentConfig(id);
1041
- if (!config)
1042
- continue;
1043
- const binding = await this.agentStore.get(id);
1044
- collectSecretValues(binding, secrets);
1045
- agentContexts.push({ id, config, binding });
1046
- }
1047
- const recentEvents = await this.recentEvents(task.id, contextErrors, secrets);
1048
- for (const event of recentEvents)
1049
- collectSecretValues(event.data, secrets);
1050
- const agents = [];
1051
- for (const { id, config, binding } of agentContexts) {
1052
- let pane;
1053
- if (this.capturePane) {
1054
- try {
1055
- const captured = await this.capturePane(id);
1056
- if (captured)
1057
- pane = truncateUtf8(redactContext(captured, secrets), MAX_PANE_BYTES);
1058
- }
1059
- catch (err) {
1060
- const detail = `pane ${id}: ${err instanceof Error ? err.message : String(err)}`;
1061
- const sanitizedDetail = sanitizeContextError(detail, secrets);
1062
- contextErrors.push(sanitizedDetail);
1063
- console.warn(`[root-agent] ${sanitizedDetail}`);
1064
- }
1065
- }
1066
- const observation = this.statusStore.get(id);
1067
- agents.push({
1068
- id,
1069
- role: config.role,
1070
- runtime: config.runtime,
1071
- binding: binding ? {
1072
- taskId: binding.taskId,
1073
- status: binding.status,
1074
- awaitingPhase: binding.awaitingPhase,
1075
- awaitingReason: binding.awaitingReason
1076
- ? redactContext(binding.awaitingReason, secrets)
1077
- : undefined,
1078
- awaitingSince: binding.awaitingSince,
1079
- updatedAt: binding.updatedAt,
1080
- } : null,
1081
- observation: {
1082
- tmuxSessionStatus: observation.tmuxSessionStatus,
1083
- runtimeStatusHint: observation.runtimeStatusHint,
1084
- reason: observation.reason,
1085
- message: observation.message ? redactContext(observation.message, secrets) : undefined,
1086
- observedAt: observation.observedAt,
1087
- },
1088
- ...(pane ? { pane } : {}),
1089
- });
1090
- }
1091
- const events = recentEvents.map(event => ({
1092
- id: event.id,
1093
- type: event.type,
1094
- timestamp: event.timestamp,
1095
- agentId: event.agentId,
1096
- data: sanitizeEventData(event.data, secrets),
1097
- }));
1098
- const request = {
1099
- version: 1,
1100
- requestId: record.id,
1101
- attemptToken: record.attemptToken,
1102
- createdAt: record.createdAt,
1103
- trigger: sanitizeValue(record.trigger, ROOT_RECOVERY_MAX_REASON_BYTES, 0, secrets),
1104
- task: {
1105
- id: task.id,
1106
- projectId: task.projectId,
1107
- title: truncateUtf8(redactContext(task.title, secrets), MAX_CONTEXT_STRING_BYTES),
1108
- description: truncateUtf8(redactContext(task.description, secrets), MAX_CONTEXT_STRING_BYTES),
1109
- status: task.status,
1110
- phase: task.phase,
1111
- agentId: task.agentId,
1112
- devAgentId: task.devAgentId,
1113
- qaAgentId: task.qaAgentId,
1114
- prNumber: task.prNumber,
1115
- prUrl: task.prUrl,
1116
- branch: task.branch,
1117
- reviewRound: task.reviewRound,
1118
- specReviewRound: task.specReviewRound,
1119
- createdAt: task.createdAt,
1120
- updatedAt: task.updatedAt,
1121
- },
1122
- agents,
1123
- recentEvents: events,
1124
- contextErrors: contextErrors.map(value => redactContext(value, secrets)),
1125
- allowedDecisions: [
1126
- ...(canRedispatch(record) ? [{
1127
- action: 'redispatch-current-phase',
1128
- meaning: 'Replay the current persisted phase through the existing Server recovery primitive.',
1129
- }] : []),
1130
- { action: 'escalate', meaning: 'Keep the task stopped and report why a human must decide.' },
1131
- { action: 'no-op', meaning: 'The task should remain untouched; explain why.' },
1132
- ],
1133
- response: {
1134
- relativePath: `${ROOT_OUTBOX_DIR}/${record.id}.json`,
1135
- signalKind: 'root-done',
1136
- },
1137
- };
1138
- return JSON.stringify(request, null, 2) + '\n';
1139
- }
1140
- async recentEvents(taskId, contextErrors, secrets) {
1141
- const to = this.now();
1142
- const from = new Date(to.getTime() - 24 * 60 * 60_000);
1143
- try {
1144
- const events = await this.eventBus.readRange(day(from), day(to));
1145
- return events
1146
- .filter(event => event.taskId === taskId)
1147
- .slice(-RECENT_EVENT_LIMIT);
1148
- }
1149
- catch (err) {
1150
- const detail = `recent events: ${err instanceof Error ? err.message : String(err)}`;
1151
- const sanitizedDetail = sanitizeContextError(detail, secrets);
1152
- contextErrors.push(sanitizedDetail);
1153
- console.warn(`[root-agent] ${sanitizedDetail}`);
1154
- return [];
1155
- }
1156
- }
1157
- projectEnabled(projectId) {
1158
- return this.config.projects === undefined || this.config.projects.includes(projectId);
1159
- }
1160
- isTimedOut(record) {
1161
- if (!record.deliveredAt)
1162
- return false;
1163
- return this.timeoutReached(record.deliveredAt);
1164
- }
1165
- async finishTimedOutResponse(record) {
1166
- if (!this.isTimedOut(record))
1167
- return false;
1168
- if (await this.complete(record, 'timeout', 'Root agent did not return a valid response before the deadline.')) {
1169
- await this.emitRootIntervention(record, 'root-response-timeout', 'Root agent response timed out.');
1170
- }
1171
- return true;
1172
- }
1173
- async finishTimedOutDelivery(record) {
1174
- if (!this.isDeliveryTimedOut(record))
1175
- return false;
1176
- const detail = 'Root prompt could not be submitted before the configured recovery deadline.';
1177
- if (await this.complete(record, 'timeout', detail)) {
1178
- await this.emitRootIntervention(record, 'root-prompt-delivery-timeout', detail);
1179
- }
1180
- return true;
1181
- }
1182
- isDeliveryTimedOut(record) {
1183
- return record.deliveredAt === undefined
1184
- && record.dispatchedAt !== undefined
1185
- && this.timeoutReached(record.dispatchedAt);
1186
- }
1187
- timeoutReached(since) {
1188
- const timestamp = Date.parse(since);
1189
- return Number.isFinite(timestamp)
1190
- && this.now().getTime() - timestamp >= this.config.responseTimeoutMinutes * 60_000;
1191
- }
1192
- async complete(record, kind, detail) {
1193
- const outcome = {
1194
- kind,
1195
- detail: truncateUtf8(detail, ROOT_RECOVERY_MAX_REASON_BYTES),
1196
- at: this.now().toISOString(),
1197
- };
1198
- const result = await this.store.complete(record.id, outcome, record);
1199
- if (!result.completed)
1200
- return false;
1201
- console.log(`[root-agent] recovery ${record.id} finished: ${kind}`);
1202
- this.runtimeAlerts.delete(record.id);
1203
- this.deliveryAlerts.delete(record.id);
1204
- try {
1205
- await this.runtime.cleanup(result.record);
1206
- }
1207
- catch (err) {
1208
- console.warn('[root-agent] mailbox cleanup failed for ' + record.id + ':', err);
1209
- }
1210
- if (!this.isRuntimeDispatchDisabled()) {
1211
- try {
1212
- await this.recoverCurrentTaskTriggers(result.record);
1213
- if (this.triggerRecoveryRechecks.get(record.taskId)?.id === result.record.id) {
1214
- this.triggerRecoveryRechecks.delete(record.taskId);
1215
- }
1216
- }
1217
- catch (err) {
1218
- this.triggerRecoveryRechecks.set(record.taskId, result.record);
1219
- console.warn(`[root-agent] follow-up trigger recovery failed for ${record.taskId}:`, err);
1220
- }
1221
- }
1222
- return true;
1223
- }
1224
- async finishRecoveryAfterRuntimeStop(initial) {
1225
- let record = initial;
1226
- while (record && record.status !== 'done') {
1227
- const inflight = record.status === 'inflight';
1228
- const detail = inflight
1229
- ? 'Root runtime was explicitly stopped after dispatch; the recovery outcome is unknown and was not retried.'
1230
- : 'Root runtime was explicitly stopped before delivery; the recovery was escalated to a human.';
1231
- if (await this.complete(record, inflight ? 'unknown' : 'escalated', detail)) {
1232
- await this.emitRootIntervention(record, inflight ? 'root-recovery-stopped-unknown' : 'root-recovery-stopped', detail);
1233
- return;
1234
- }
1235
- record = await this.store.get(initial.id);
1236
- }
1237
- }
1238
- async emitRootIntervention(record, phase, detail) {
1239
- await this.safeEmit({
1240
- id: '',
1241
- type: 'human.intervention',
1242
- timestamp: this.now().toISOString(),
1243
- projectId: record.projectId,
1244
- taskId: record.taskId,
1245
- ...(record.trigger.agentId ? { agentId: record.trigger.agentId } : {}),
1246
- data: {
1247
- source: 'root-agent',
1248
- phase,
1249
- rootRecoveryId: record.id,
1250
- note: truncateUtf8(detail, ROOT_RECOVERY_MAX_REASON_BYTES),
1251
- },
1252
- });
1253
- }
1254
- async safeEmit(event) {
1255
- try {
1256
- await this.eventBus.emit(event);
1257
- }
1258
- catch (err) {
1259
- console.warn(`[root-agent] failed to emit ${event.type}/${String(event.data.phase)}:`, err);
1260
- }
1261
- }
1262
- }
1263
- function guardFor(task) {
1264
- return {
1265
- status: task.status,
1266
- ...(task.phase !== undefined ? { phase: task.phase } : {}),
1267
- ...(task.signalToken !== undefined ? { signalToken: task.signalToken } : {}),
1268
- agentId: task.agentId,
1269
- reviewRound: task.reviewRound,
1270
- ...(task.specReviewRound !== undefined ? { specReviewRound: task.specReviewRound } : {}),
1271
- };
1272
- }
1273
- function parseResponse(raw, record) {
1274
- if (!isRecord(raw) || raw.version !== 1)
1275
- throw new Error('root response.version must be 1');
1276
- if (raw.requestId !== record.id)
1277
- throw new Error('root response requestId does not match');
1278
- if (raw.attemptToken !== record.attemptToken)
1279
- throw new Error('root response attemptToken does not match');
1280
- if (!isRecord(raw.decision))
1281
- throw new Error('root response decision must be an object');
1282
- if (!ROOT_RECOVERY_ACTION_SET.has(raw.decision.action)) {
1283
- throw new Error('root response decision.action is not allowed');
1284
- }
1285
- if (raw.decision.action === 'redispatch-current-phase' && !canRedispatch(record)) {
1286
- throw new Error('root response requested redispatch for a non-replayable intervention');
1287
- }
1288
- if (typeof raw.decision.reason !== 'string' || raw.decision.reason.trim() === '') {
1289
- throw new Error('root response decision.reason must be a non-empty string');
1290
- }
1291
- if (Buffer.byteLength(raw.decision.reason, 'utf8') > ROOT_RECOVERY_MAX_REASON_BYTES) {
1292
- throw new Error(`root response decision.reason exceeds ${ROOT_RECOVERY_MAX_REASON_BYTES} bytes`);
1293
- }
1294
- const topKeys = Object.keys(raw).sort().join(',');
1295
- const decisionKeys = Object.keys(raw.decision).sort().join(',');
1296
- if (topKeys !== 'attemptToken,decision,requestId,version' || decisionKeys !== 'action,reason') {
1297
- throw new Error('root response contains unsupported fields');
1298
- }
1299
- return {
1300
- action: raw.decision.action,
1301
- reason: raw.decision.reason,
1302
- };
1303
- }
1304
- function canRedispatch(record) {
1305
- return triggerCanRedispatch(record.trigger);
1306
- }
1307
- function triggerCanRedispatch(trigger) {
1308
- if (trigger.kind === 'runtime-stall')
1309
- return trigger.reason === 'PENDING_IDLE';
1310
- return trigger.phase !== undefined
1311
- && trigger.holdPhase === trigger.phase
1312
- && RECOVERABLE_QA_DISPATCH_HOLD_PHASES.has(trigger.phase);
1313
- }
1314
- function sameRedispatchLineage(left, right) {
1315
- return left.status === right.status
1316
- && left.phase === right.phase
1317
- && left.agentId === right.agentId
1318
- && left.reviewRound === right.reviewRound
1319
- && left.specReviewRound === right.specReviewRound;
1320
- }
1321
- function redispatchLimitEventId(taskId, guard) {
1322
- const digest = createHash('sha256')
1323
- .update(JSON.stringify([
1324
- taskId,
1325
- guard.status,
1326
- guard.phase,
1327
- guard.agentId,
1328
- guard.reviewRound,
1329
- guard.specReviewRound,
1330
- ]))
1331
- .digest('hex')
1332
- .slice(0, 24);
1333
- return `root-redispatch-limit-${digest}`;
1334
- }
1335
- function truncateUtf8(value, maxBytes) {
1336
- const bytes = Buffer.from(value, 'utf8');
1337
- if (bytes.byteLength <= maxBytes)
1338
- return value;
1339
- let end = maxBytes;
1340
- while (end > 0 && (bytes[end] & 0xc0) === 0x80)
1341
- end--;
1342
- return bytes.subarray(0, end).toString('utf8');
1343
- }
1344
- function sanitizeValue(value, maxBytes, depth = 0, secrets = new Set()) {
1345
- if (depth > 4)
1346
- return '[truncated]';
1347
- if (typeof value === 'string')
1348
- return truncateUtf8(redactContext(value, secrets), maxBytes);
1349
- if (typeof value !== 'object' || value === null)
1350
- return value;
1351
- if (Array.isArray(value)) {
1352
- return value.slice(0, 20).map(item => sanitizeValue(item, maxBytes, depth + 1, secrets));
1353
- }
1354
- const output = {};
1355
- for (const [key, item] of Object.entries(value).slice(0, 40)) {
1356
- if (isSensitiveField(key)) {
1357
- output[key] = '[redacted]';
1358
- }
1359
- else {
1360
- output[key] = sanitizeValue(item, maxBytes, depth + 1, secrets);
1361
- }
1362
- }
1363
- return output;
1364
- }
1365
- function sanitizeEventData(value, secrets) {
1366
- const sanitized = sanitizeValue(value, 512, 0, secrets);
1367
- const serialized = JSON.stringify(sanitized);
1368
- if (Buffer.byteLength(serialized, 'utf8') <= MAX_EVENT_DATA_BYTES)
1369
- return sanitized;
1370
- return {
1371
- truncated: true,
1372
- preview: truncateUtf8(serialized, MAX_EVENT_DATA_BYTES - 64),
1373
- };
1374
- }
1375
- function collectSecretValues(value, output = new Set()) {
1376
- const pending = [value];
1377
- const seen = new WeakSet();
1378
- let scanned = 0;
1379
- while (pending.length > 0 && scanned < MAX_SECRET_SCAN_NODES) {
1380
- const current = pending.pop();
1381
- if (typeof current !== 'object' || current === null || seen.has(current))
1382
- continue;
1383
- seen.add(current);
1384
- scanned++;
1385
- if (Array.isArray(current)) {
1386
- for (const item of current)
1387
- pending.push(item);
1388
- continue;
1389
- }
1390
- for (const [key, item] of Object.entries(current)) {
1391
- if (isSensitiveField(key)
1392
- && typeof item === 'string'
1393
- && item.length >= MIN_GLOBAL_SECRET_CHARS) {
1394
- output.add(item);
1395
- if (isBareCredentialField(key)) {
1396
- const credential = BARE_AUTH_CREDENTIAL_RE.exec(item)?.[1];
1397
- if (credential)
1398
- output.add(credential);
1399
- }
1400
- }
1401
- pending.push(item);
1402
- }
1403
- }
1404
- return output;
1405
- }
1406
- function fieldWords(field) {
1407
- if (field.length > MAX_FIELD_NAME_CHARS)
1408
- return [];
1409
- return field
1410
- .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
1411
- .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
1412
- .toLowerCase()
1413
- .split(/[^a-z0-9]+/)
1414
- .filter(Boolean);
1415
- }
1416
- function isSensitiveField(field) {
1417
- if (field.length > MAX_FIELD_NAME_CHARS)
1418
- return true;
1419
- if (SENSITIVE_FIELD_RE.test(field))
1420
- return true;
1421
- const normalized = field.replace(/[^a-z0-9]/gi, '').toLowerCase();
1422
- for (const credential of KEY_CREDENTIAL_FIELDS) {
1423
- if (normalized.endsWith(credential))
1424
- return true;
1425
- }
1426
- const words = fieldWords(field);
1427
- return words.at(-1) === 'key'
1428
- && KEY_CREDENTIAL_QUALIFIERS.has(words.at(-2) ?? '');
1429
- }
1430
- function isBareCredentialField(field) {
1431
- if (field.length > MAX_FIELD_NAME_CHARS)
1432
- return true;
1433
- const normalized = field.replace(/[^a-z0-9]/gi, '').toLowerCase();
1434
- if (AUTHORIZATION_FIELDS.has(normalized))
1435
- return true;
1436
- return /credentials?$/.test(normalized);
1437
- }
1438
- function sanitizeContextError(value, secrets) {
1439
- return truncateUtf8(redactContext(value, secrets), MAX_CONTEXT_ERROR_BYTES);
1440
- }
1441
- // 无状态、无长度上限:被吞掉的未闭合序列与漏匹配剩下的残渣,都会挤掉真正的故障证据。
1442
- const OSC_TERMINATOR_RE = /\x07|\x1b\\/g;
1443
- const COMPLETE_CSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
1444
- const RESIDUAL_CONTROL_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g;
1445
- function stripCompleteOsc(value) {
1446
- let out = '';
1447
- let cursor = 0;
1448
- for (;;) {
1449
- const start = value.indexOf('\x1b]', cursor);
1450
- if (start < 0)
1451
- break;
1452
- OSC_TERMINATOR_RE.lastIndex = start + 2;
1453
- const terminator = OSC_TERMINATOR_RE.exec(value);
1454
- // 这里找不到终结符,更靠后的起始符就更找不到——不能退回去逐个重扫,那是平方级。
1455
- if (!terminator)
1456
- break;
1457
- // 终结符配的是它前面最后一个起始符;更早那些没终结,得连同其后的证据一起留成可读文本。
1458
- let paired = start;
1459
- for (let probe = value.indexOf('\x1b]', paired + 2); probe >= 0 && probe < terminator.index;) {
1460
- paired = probe;
1461
- probe = value.indexOf('\x1b]', paired + 2);
1462
- }
1463
- out += value.slice(cursor, paired);
1464
- cursor = terminator.index + terminator[0].length;
1465
- }
1466
- return cursor === 0 ? value : out + value.slice(cursor);
1467
- }
1468
- function stripControlSequences(value) {
1469
- return stripCompleteOsc(value)
1470
- .replace(COMPLETE_CSI_RE, '')
1471
- .replace(RESIDUAL_CONTROL_RE, '');
1472
- }
1473
- function redactContext(value, secrets) {
1474
- // 凭据自带控制字节、回显又被转义序列打断时,原串与清洗串各自都命中不了,两侧同规则归一化才盖得全。
1475
- const ordered = [...new Set([...secrets].flatMap(secret => [secret, stripControlSequences(secret)]))]
1476
- .filter(candidate => candidate.length >= MIN_GLOBAL_SECRET_CHARS)
1477
- .sort((left, right) => right.length - left.length);
1478
- const replaceSecrets = (text) => ordered.reduce((carried, secret) => carried.split(secret).join('[redacted]'), text);
1479
- const redacted = stripControlSequences(replaceSecrets(value))
1480
- .replace(/\[bx:[^\]]{0,2048}\]/g, '[bx:redacted]');
1481
- return redactLabeledValues(replaceSecrets(redacted));
1482
- }
1483
- function redactLabeledValues(value) {
1484
- const matcher = new RegExp(LABELED_FIELD_RE.source, LABELED_FIELD_RE.flags);
1485
- let cursor = 0;
1486
- let output = '';
1487
- for (let match = matcher.exec(value); match; match = matcher.exec(value)) {
1488
- const valueStart = matcher.lastIndex;
1489
- if (!isSensitiveField(match[3])) {
1490
- matcher.lastIndex = Math.max(match.index + 1, valueStart - 1);
1491
- continue;
1492
- }
1493
- const valueEnd = labeledValueEnd(value, valueStart);
1494
- if (valueEnd === valueStart)
1495
- continue;
1496
- output += value.slice(cursor, valueStart) + '[redacted]';
1497
- cursor = valueEnd;
1498
- matcher.lastIndex = Math.max(match.index + 1, valueEnd - 1);
1499
- }
1500
- return cursor === 0 ? value : output + value.slice(cursor);
1501
- }
1502
- function labeledValueEnd(value, start) {
1503
- const quote = value[start];
1504
- if (quote === '"' || quote === "'") {
1505
- for (let index = start + 1; index < value.length; index++) {
1506
- if (value[index] === '\n' || value[index] === '\r') {
1507
- return index;
1508
- }
1509
- else if (value[index] === '\\') {
1510
- if (value[index + 1] === '\n' || value[index + 1] === '\r')
1511
- return index + 1;
1512
- index++;
1513
- }
1514
- else if (value[index] === quote) {
1515
- return index + 1;
1516
- }
1517
- }
1518
- return value.length;
1519
- }
1520
- const authorization = /^(?:Basic|Bearer)[\t ]+/i.exec(value.slice(start));
1521
- if (authorization) {
1522
- let end = start + authorization[0].length;
1523
- while (end < value.length && !/[\s,;]/.test(value[end]))
1524
- end++;
1525
- return end;
1526
- }
1527
- let end = start;
1528
- while (end < value.length && !/[\s,;]/.test(value[end]))
1529
- end++;
1530
- return end;
1531
- }
1532
- function isLaterObservation(current, previous) {
1533
- if (current === undefined || current === previous)
1534
- return false;
1535
- const currentAt = Date.parse(current);
1536
- const previousAt = Date.parse(previous);
1537
- if (!Number.isFinite(currentAt) || !Number.isFinite(previousAt))
1538
- return true;
1539
- return currentAt > previousAt;
1540
- }
1541
- function day(date) {
1542
- return date.toISOString().slice(0, 10);
1543
- }
1544
- function holdEventId(binding) {
1545
- const digest = createHash('sha256')
1546
- .update(JSON.stringify([
1547
- binding.id,
1548
- binding.taskId,
1549
- binding.awaitingPhase,
1550
- binding.awaitingSince,
1551
- binding.awaitingNonce,
1552
- ]))
1553
- .digest('hex')
1554
- .slice(0, 24);
1555
- return `root-hold-${digest}`;
1556
- }
1557
- //# sourceMappingURL=root-recovery-coordinator.js.map