baxian 2.0.1 → 2.0.3

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