@pellux/goodvibes-sdk 0.37.1 → 0.38.0

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 (165) hide show
  1. package/dist/contracts/artifacts/operator-contract.json +1 -1
  2. package/dist/events/communication.d.ts +21 -1
  3. package/dist/events/communication.d.ts.map +1 -1
  4. package/dist/platform/agents/message-bus-core.d.ts +7 -0
  5. package/dist/platform/agents/message-bus-core.d.ts.map +1 -1
  6. package/dist/platform/agents/message-bus-core.js +2 -1
  7. package/dist/platform/agents/orchestrator-runner.d.ts +37 -1
  8. package/dist/platform/agents/orchestrator-runner.d.ts.map +1 -1
  9. package/dist/platform/agents/orchestrator-runner.js +158 -7
  10. package/dist/platform/agents/orchestrator.d.ts +40 -0
  11. package/dist/platform/agents/orchestrator.d.ts.map +1 -1
  12. package/dist/platform/agents/orchestrator.js +29 -0
  13. package/dist/platform/agents/turn-knowledge-injection.d.ts +107 -0
  14. package/dist/platform/agents/turn-knowledge-injection.d.ts.map +1 -0
  15. package/dist/platform/agents/turn-knowledge-injection.js +152 -0
  16. package/dist/platform/agents/wrfc-controller.d.ts +23 -0
  17. package/dist/platform/agents/wrfc-controller.d.ts.map +1 -1
  18. package/dist/platform/agents/wrfc-controller.js +55 -0
  19. package/dist/platform/config/manager.d.ts +9 -0
  20. package/dist/platform/config/manager.d.ts.map +1 -1
  21. package/dist/platform/config/manager.js +15 -0
  22. package/dist/platform/control-plane/approval-broker.d.ts +1 -0
  23. package/dist/platform/control-plane/approval-broker.d.ts.map +1 -1
  24. package/dist/platform/control-plane/approval-broker.js +2 -0
  25. package/dist/platform/core/adaptive-planner.d.ts +50 -0
  26. package/dist/platform/core/adaptive-planner.d.ts.map +1 -1
  27. package/dist/platform/core/adaptive-planner.js +49 -0
  28. package/dist/platform/core/index.d.ts +1 -0
  29. package/dist/platform/core/index.d.ts.map +1 -1
  30. package/dist/platform/core/index.js +1 -0
  31. package/dist/platform/core/orchestrator-runtime.d.ts +11 -0
  32. package/dist/platform/core/orchestrator-runtime.d.ts.map +1 -1
  33. package/dist/platform/core/orchestrator-turn-loop.d.ts +28 -1
  34. package/dist/platform/core/orchestrator-turn-loop.d.ts.map +1 -1
  35. package/dist/platform/core/orchestrator-turn-loop.js +115 -3
  36. package/dist/platform/core/orchestrator.d.ts +61 -0
  37. package/dist/platform/core/orchestrator.d.ts.map +1 -1
  38. package/dist/platform/core/orchestrator.js +74 -1
  39. package/dist/platform/core/plan-proposal.d.ts +165 -0
  40. package/dist/platform/core/plan-proposal.d.ts.map +1 -0
  41. package/dist/platform/core/plan-proposal.js +296 -0
  42. package/dist/platform/knowledge/knowledge-api.d.ts +26 -0
  43. package/dist/platform/knowledge/knowledge-api.d.ts.map +1 -1
  44. package/dist/platform/knowledge/knowledge-api.js +11 -0
  45. package/dist/platform/orchestration/budget.d.ts +22 -0
  46. package/dist/platform/orchestration/budget.d.ts.map +1 -0
  47. package/dist/platform/orchestration/budget.js +35 -0
  48. package/dist/platform/orchestration/cancellation.d.ts +31 -0
  49. package/dist/platform/orchestration/cancellation.d.ts.map +1 -0
  50. package/dist/platform/orchestration/cancellation.js +25 -0
  51. package/dist/platform/orchestration/controller-compat.d.ts +32 -0
  52. package/dist/platform/orchestration/controller-compat.d.ts.map +1 -0
  53. package/dist/platform/orchestration/controller-compat.js +13 -0
  54. package/dist/platform/orchestration/dirty-guard.d.ts +32 -0
  55. package/dist/platform/orchestration/dirty-guard.d.ts.map +1 -0
  56. package/dist/platform/orchestration/dirty-guard.js +147 -0
  57. package/dist/platform/orchestration/engine.d.ts +57 -0
  58. package/dist/platform/orchestration/engine.d.ts.map +1 -0
  59. package/dist/platform/orchestration/engine.js +418 -0
  60. package/dist/platform/orchestration/index.d.ts +18 -0
  61. package/dist/platform/orchestration/index.d.ts.map +1 -0
  62. package/dist/platform/orchestration/index.js +10 -0
  63. package/dist/platform/orchestration/persistence.d.ts +24 -0
  64. package/dist/platform/orchestration/persistence.d.ts.map +1 -0
  65. package/dist/platform/orchestration/persistence.js +176 -0
  66. package/dist/platform/orchestration/phase-runner.d.ts +87 -0
  67. package/dist/platform/orchestration/phase-runner.d.ts.map +1 -0
  68. package/dist/platform/orchestration/phase-runner.js +274 -0
  69. package/dist/platform/orchestration/scheduler.d.ts +45 -0
  70. package/dist/platform/orchestration/scheduler.d.ts.map +1 -0
  71. package/dist/platform/orchestration/scheduler.js +57 -0
  72. package/dist/platform/orchestration/types.d.ts +250 -0
  73. package/dist/platform/orchestration/types.d.ts.map +1 -0
  74. package/dist/platform/orchestration/types.js +15 -0
  75. package/dist/platform/runtime/emitters/communication.d.ts +11 -0
  76. package/dist/platform/runtime/emitters/communication.d.ts.map +1 -1
  77. package/dist/platform/runtime/emitters/communication.js +9 -0
  78. package/dist/platform/runtime/feature-flags/flags.d.ts.map +1 -1
  79. package/dist/platform/runtime/feature-flags/flags.js +17 -0
  80. package/dist/platform/runtime/fleet/adapters/agent.d.ts +56 -0
  81. package/dist/platform/runtime/fleet/adapters/agent.d.ts.map +1 -0
  82. package/dist/platform/runtime/fleet/adapters/agent.js +143 -0
  83. package/dist/platform/runtime/fleet/adapters/automation.d.ts +30 -0
  84. package/dist/platform/runtime/fleet/adapters/automation.d.ts.map +1 -0
  85. package/dist/platform/runtime/fleet/adapters/automation.js +41 -0
  86. package/dist/platform/runtime/fleet/adapters/background-process.d.ts +12 -0
  87. package/dist/platform/runtime/fleet/adapters/background-process.d.ts.map +1 -0
  88. package/dist/platform/runtime/fleet/adapters/background-process.js +46 -0
  89. package/dist/platform/runtime/fleet/adapters/code-index.d.ts +21 -0
  90. package/dist/platform/runtime/fleet/adapters/code-index.d.ts.map +1 -0
  91. package/dist/platform/runtime/fleet/adapters/code-index.js +44 -0
  92. package/dist/platform/runtime/fleet/adapters/orchestration.d.ts +47 -0
  93. package/dist/platform/runtime/fleet/adapters/orchestration.d.ts.map +1 -0
  94. package/dist/platform/runtime/fleet/adapters/orchestration.js +225 -0
  95. package/dist/platform/runtime/fleet/adapters/schedule.d.ts +14 -0
  96. package/dist/platform/runtime/fleet/adapters/schedule.d.ts.map +1 -0
  97. package/dist/platform/runtime/fleet/adapters/schedule.js +28 -0
  98. package/dist/platform/runtime/fleet/adapters/trigger.d.ts +12 -0
  99. package/dist/platform/runtime/fleet/adapters/trigger.d.ts.map +1 -0
  100. package/dist/platform/runtime/fleet/adapters/trigger.js +22 -0
  101. package/dist/platform/runtime/fleet/adapters/watcher.d.ts +13 -0
  102. package/dist/platform/runtime/fleet/adapters/watcher.d.ts.map +1 -0
  103. package/dist/platform/runtime/fleet/adapters/watcher.js +47 -0
  104. package/dist/platform/runtime/fleet/adapters/workflow.d.ts +11 -0
  105. package/dist/platform/runtime/fleet/adapters/workflow.d.ts.map +1 -0
  106. package/dist/platform/runtime/fleet/adapters/workflow.js +34 -0
  107. package/dist/platform/runtime/fleet/adapters/wrfc.d.ts +22 -0
  108. package/dist/platform/runtime/fleet/adapters/wrfc.d.ts.map +1 -0
  109. package/dist/platform/runtime/fleet/adapters/wrfc.js +228 -0
  110. package/dist/platform/runtime/fleet/index.d.ts +10 -0
  111. package/dist/platform/runtime/fleet/index.d.ts.map +1 -0
  112. package/dist/platform/runtime/fleet/index.js +6 -0
  113. package/dist/platform/runtime/fleet/registry.d.ts +113 -0
  114. package/dist/platform/runtime/fleet/registry.d.ts.map +1 -0
  115. package/dist/platform/runtime/fleet/registry.js +630 -0
  116. package/dist/platform/runtime/fleet/types.d.ts +199 -0
  117. package/dist/platform/runtime/fleet/types.d.ts.map +1 -0
  118. package/dist/platform/runtime/fleet/types.js +2 -0
  119. package/dist/platform/runtime/runtime-knowledge-api.d.ts +1 -1
  120. package/dist/platform/runtime/runtime-knowledge-api.d.ts.map +1 -1
  121. package/dist/platform/runtime/runtime-knowledge-api.js +1 -0
  122. package/dist/platform/runtime/services.d.ts +46 -0
  123. package/dist/platform/runtime/services.d.ts.map +1 -1
  124. package/dist/platform/runtime/services.js +78 -0
  125. package/dist/platform/state/code-index-chunking.d.ts +49 -0
  126. package/dist/platform/state/code-index-chunking.d.ts.map +1 -0
  127. package/dist/platform/state/code-index-chunking.js +111 -0
  128. package/dist/platform/state/code-index-db.d.ts +53 -0
  129. package/dist/platform/state/code-index-db.d.ts.map +1 -0
  130. package/dist/platform/state/code-index-db.js +101 -0
  131. package/dist/platform/state/code-index-store.d.ts +175 -0
  132. package/dist/platform/state/code-index-store.d.ts.map +1 -0
  133. package/dist/platform/state/code-index-store.js +612 -0
  134. package/dist/platform/state/index.d.ts +4 -2
  135. package/dist/platform/state/index.d.ts.map +1 -1
  136. package/dist/platform/state/index.js +2 -1
  137. package/dist/platform/state/knowledge-injection.d.ts +23 -0
  138. package/dist/platform/state/knowledge-injection.d.ts.map +1 -1
  139. package/dist/platform/state/knowledge-injection.js +34 -19
  140. package/dist/platform/state/memory-vector-store.d.ts +1 -13
  141. package/dist/platform/state/memory-vector-store.d.ts.map +1 -1
  142. package/dist/platform/state/memory-vector-store.js +6 -38
  143. package/dist/platform/state/sqlite-vec-loader.d.ts +25 -0
  144. package/dist/platform/state/sqlite-vec-loader.d.ts.map +1 -0
  145. package/dist/platform/state/sqlite-vec-loader.js +43 -0
  146. package/dist/platform/tools/agent/manager.d.ts +128 -1
  147. package/dist/platform/tools/agent/manager.d.ts.map +1 -1
  148. package/dist/platform/tools/agent/manager.js +137 -1
  149. package/dist/platform/tools/exec/runtime.d.ts.map +1 -1
  150. package/dist/platform/tools/exec/runtime.js +139 -44
  151. package/dist/platform/tools/exec/schema.d.ts +2 -0
  152. package/dist/platform/tools/exec/schema.d.ts.map +1 -1
  153. package/dist/platform/tools/fetch/runtime.d.ts +7 -0
  154. package/dist/platform/tools/fetch/runtime.d.ts.map +1 -1
  155. package/dist/platform/tools/fetch/runtime.js +8 -6
  156. package/dist/platform/tools/registry.d.ts +9 -3
  157. package/dist/platform/tools/registry.d.ts.map +1 -1
  158. package/dist/platform/tools/registry.js +9 -3
  159. package/dist/platform/types/tools.d.ts +14 -1
  160. package/dist/platform/types/tools.d.ts.map +1 -1
  161. package/dist/platform/version.js +1 -1
  162. package/dist/platform/workspace/checkpoint/side-git.d.ts +20 -8
  163. package/dist/platform/workspace/checkpoint/side-git.d.ts.map +1 -1
  164. package/dist/platform/workspace/checkpoint/side-git.js +21 -11
  165. package/package.json +17 -9
@@ -0,0 +1,630 @@
1
+ /** SDK-owned platform module. This implementation is maintained in goodvibes-sdk. */
2
+ import { adaptAgent } from './adapters/agent.js';
3
+ import { activeSubtaskMemberAgentId, adaptChain, adaptSubtask } from './adapters/wrfc.js';
4
+ import { adaptWorkflow } from './adapters/workflow.js';
5
+ import { adaptTrigger } from './adapters/trigger.js';
6
+ import { adaptSchedule } from './adapters/schedule.js';
7
+ import { adaptWatcher } from './adapters/watcher.js';
8
+ import { adaptBackgroundProcess } from './adapters/background-process.js';
9
+ import { adaptAutomationJob, isAutomationJobRaw } from './adapters/automation.js';
10
+ import { activeWorkItemAgentId, adaptPhase, adaptWorkItem, adaptWorkstream, phaseNodeId, workItemNodeId, workstreamNodeId, } from './adapters/orchestration.js';
11
+ import { adaptCodeIndex } from './adapters/code-index.js';
12
+ import { logger } from '../../utils/logger.js';
13
+ import { summarizeError } from '../../utils/error-display.js';
14
+ /** Default stalled threshold: running agent with no bus activity for this long → 'stalled'. */
15
+ export const DEFAULT_STALLED_THRESHOLD_MS = 20_000;
16
+ /** Default coalesced tick interval for subscriber notification. */
17
+ export const DEFAULT_TICK_INTERVAL_MS = 750;
18
+ /**
19
+ * TTL for a steer message, deliberately much larger than
20
+ * AgentMessageBus's own DEFAULT_TTL_MS (5 min, message-bus-core.ts). A
21
+ * steer queued against an agent mid long-running tool call (build, test
22
+ * suite) must survive the wait for that tool to return rather than
23
+ * silently expiring before the agent reaches its next turn boundary.
24
+ */
25
+ export const STEER_TTL_MS = 30 * 60 * 1000;
26
+ const defaultTimers = {
27
+ setInterval: (callback, intervalMs) => {
28
+ const handle = setInterval(callback, intervalMs);
29
+ // Unref at the creation site so a dangling tick never keeps the process
30
+ // alive if a consumer forgets to dispose. The registry also unrefs the
31
+ // returned handle via unrefHandle() for injected timer impls; this inline
32
+ // unref keeps the raw setInterval site self-contained (invariant guard).
33
+ handle.unref?.();
34
+ return handle;
35
+ },
36
+ clearInterval: (handle) => clearInterval(handle),
37
+ };
38
+ function unrefHandle(handle) {
39
+ if (handle !== null && typeof handle === 'object' && 'unref' in handle) {
40
+ const unref = handle.unref;
41
+ if (typeof unref === 'function')
42
+ unref.call(handle);
43
+ }
44
+ }
45
+ /**
46
+ * Change signature over the fields that constitute a MATERIAL change.
47
+ * Deliberately excludes elapsedMs/capturedAt/activity.at — continuously
48
+ * varying derived values must not wake subscribers every tick.
49
+ * Joined with an escaped NUL so no printable field value can collide.
50
+ */
51
+ function snapshotSignature(nodes) {
52
+ const parts = [];
53
+ for (const node of nodes) {
54
+ parts.push(node.id, node.kind, node.parentId ?? '', node.state, node.label, String(node.completedAt ?? ''), node.usage
55
+ ? `${node.usage.inputTokens}:${node.usage.outputTokens}:${node.usage.toolCallCount}:${node.usage.turnCount}`
56
+ : '', node.currentActivity ? `${node.currentActivity.kind}=${node.currentActivity.text}` : '', String(node.costUsd ?? ''), node.costState, node.sessionRef?.sessionId ?? '');
57
+ }
58
+ return parts.join('\u0000');
59
+ }
60
+ /** Create the live process registry over the already-composed managers. */
61
+ export function createProcessRegistry(deps) {
62
+ const now = deps.now ?? (() => Date.now());
63
+ const stalledThresholdMs = deps.stalledThresholdMs ?? DEFAULT_STALLED_THRESHOLD_MS;
64
+ const tickIntervalMs = deps.tickIntervalMs ?? DEFAULT_TICK_INTERVAL_MS;
65
+ const timers = deps.timers ?? defaultTimers;
66
+ // ── Activity side-table (the only liveness mechanism, registry-owned) ─────
67
+ const activity = new Map();
68
+ const busUnsubscribers = [];
69
+ let disposed = false;
70
+ function recordActivity(agentId, entry) {
71
+ activity.set(agentId, { ...entry, at: now() });
72
+ }
73
+ if (deps.runtimeBus) {
74
+ busUnsubscribers.push(deps.runtimeBus.onDomain('agents', (envelope) => {
75
+ const event = envelope.payload;
76
+ const prior = activity.get(event.agentId);
77
+ switch (event.type) {
78
+ case 'AGENT_SPAWNING':
79
+ recordActivity(event.agentId, { state: 'queued' });
80
+ break;
81
+ case 'AGENT_RUNNING':
82
+ case 'AGENT_AWAITING_MESSAGE':
83
+ case 'AGENT_FINALIZING':
84
+ recordActivity(event.agentId, { state: 'thinking', activity: prior?.activity });
85
+ break;
86
+ case 'AGENT_AWAITING_TOOL':
87
+ recordActivity(event.agentId, {
88
+ state: 'executing-tool',
89
+ activity: { kind: 'tool', text: event.tool, toolName: event.tool, at: now() },
90
+ });
91
+ break;
92
+ case 'AGENT_STREAM_DELTA':
93
+ recordActivity(event.agentId, { state: 'streaming', activity: prior?.activity });
94
+ break;
95
+ case 'AGENT_PROGRESS':
96
+ // progress is already `Turn N · ToolName` (orchestrator-runner).
97
+ recordActivity(event.agentId, {
98
+ state: 'executing-tool',
99
+ activity: { kind: 'phase', text: event.progress, at: now() },
100
+ });
101
+ break;
102
+ case 'AGENT_COMPLETED':
103
+ recordActivity(event.agentId, { state: 'done', activity: prior?.activity });
104
+ break;
105
+ case 'AGENT_FAILED':
106
+ recordActivity(event.agentId, { state: 'failed', activity: prior?.activity });
107
+ break;
108
+ default:
109
+ break;
110
+ }
111
+ }));
112
+ busUnsubscribers.push(deps.runtimeBus.on('STREAM_RETRY', (envelope) => {
113
+ const agentId = envelope.agentId;
114
+ if (!agentId)
115
+ return;
116
+ const prior = activity.get(agentId);
117
+ recordActivity(agentId, { state: 'retrying', activity: prior?.activity });
118
+ }));
119
+ }
120
+ // ── Query assembly (aggregate-on-read, all O(n) in-memory scans) ──────────
121
+ function collectPendingApprovals() {
122
+ const agentIds = new Set();
123
+ const sessionIds = new Set();
124
+ if (!deps.approvalBroker)
125
+ return { agentIds, sessionIds };
126
+ try {
127
+ for (const approval of deps.approvalBroker.listApprovals(200)) {
128
+ if (approval.status !== 'pending')
129
+ continue;
130
+ const metaAgentId = approval.metadata['agentId'];
131
+ if (typeof metaAgentId === 'string' && metaAgentId.length > 0)
132
+ agentIds.add(metaAgentId);
133
+ if (approval.sessionId)
134
+ sessionIds.add(approval.sessionId);
135
+ }
136
+ }
137
+ catch (error) {
138
+ logger.warn('[fleet] approval cross-reference failed', { error: summarizeError(error) });
139
+ }
140
+ return { agentIds, sessionIds };
141
+ }
142
+ function collectSessionBindings() {
143
+ const byAgentId = new Map();
144
+ if (!deps.sessionBroker)
145
+ return byAgentId;
146
+ try {
147
+ for (const session of deps.sessionBroker.listSessions(500)) {
148
+ if (session.activeAgentId)
149
+ byAgentId.set(session.activeAgentId, session.id);
150
+ if (session.lastAgentId && !byAgentId.has(session.lastAgentId)) {
151
+ byAgentId.set(session.lastAgentId, session.id);
152
+ }
153
+ }
154
+ }
155
+ catch (error) {
156
+ logger.warn('[fleet] session binding scan failed', { error: summarizeError(error) });
157
+ }
158
+ return byAgentId;
159
+ }
160
+ function assemble() {
161
+ const capturedAt = now();
162
+ const agents = deps.agentManager.list();
163
+ const chains = deps.wrfcController.listChains();
164
+ const chainIds = new Set(chains.map((chain) => chain.id));
165
+ const subtaskIds = new Set();
166
+ for (const chain of chains) {
167
+ for (const subtask of chain.subtasks ?? [])
168
+ subtaskIds.add(subtask.id);
169
+ }
170
+ const workstreams = deps.orchestrationEngine?.listWorkstreams() ?? [];
171
+ const workItemIds = new Set();
172
+ for (const workstream of workstreams) {
173
+ for (const item of workstream.items)
174
+ workItemIds.add(item.id);
175
+ }
176
+ const agentIds = new Set(agents.map((record) => record.id));
177
+ const agentIdByOrchestrationNodeId = new Map();
178
+ for (const record of agents) {
179
+ if (record.orchestrationNodeId)
180
+ agentIdByOrchestrationNodeId.set(record.orchestrationNodeId, record.id);
181
+ }
182
+ const { agentIds: pendingApprovalAgentIds, sessionIds: pendingApprovalSessionIds } = collectPendingApprovals();
183
+ const sessionIdByAgentId = collectSessionBindings();
184
+ const agentCtx = {
185
+ now: capturedAt,
186
+ stalledThresholdMs,
187
+ activity,
188
+ liveness: deps.runtimeBus !== undefined,
189
+ pendingApprovalAgentIds,
190
+ pendingApprovalSessionIds,
191
+ sessionIdByAgentId,
192
+ chainIds,
193
+ subtaskIds,
194
+ workItemIds,
195
+ agentIdByOrchestrationNodeId,
196
+ agentIds,
197
+ priceUsage: deps.priceUsage,
198
+ messageBusPresent: deps.messageBus !== undefined,
199
+ };
200
+ const nodes = [];
201
+ const agentNodeById = new Map();
202
+ for (const record of agents) {
203
+ const node = adaptAgent(record, agentCtx);
204
+ agentNodeById.set(node.id, node);
205
+ nodes.push(node);
206
+ }
207
+ for (const chain of chains) {
208
+ // Members exclude the owner: its usage is populated FROM phase children
209
+ // at completion time, so including it would double-count (see wrfc.ts).
210
+ const memberNodes = [];
211
+ for (const agentId of chain.allAgentIds) {
212
+ if (agentId === chain.ownerAgentId)
213
+ continue;
214
+ const node = agentNodeById.get(agentId);
215
+ if (node)
216
+ memberNodes.push(node);
217
+ }
218
+ nodes.push(adaptChain(chain, memberNodes, capturedAt));
219
+ for (const subtask of chain.subtasks ?? []) {
220
+ // Steerable only when the subtask's currently-active member agent is
221
+ // both present in this snapshot and not terminal, AND a messageBus
222
+ // dep exists to actually deliver the steer.
223
+ const activeMemberId = activeSubtaskMemberAgentId(subtask);
224
+ const activeMemberNode = activeMemberId ? agentNodeById.get(activeMemberId) : undefined;
225
+ const memberLive = activeMemberNode !== undefined
226
+ && activeMemberNode.state !== 'done'
227
+ && activeMemberNode.state !== 'failed'
228
+ && activeMemberNode.state !== 'killed';
229
+ nodes.push(adaptSubtask(subtask, chain, { steerable: deps.messageBus !== undefined && memberLive }));
230
+ }
231
+ }
232
+ for (const workstream of workstreams) {
233
+ nodes.push(adaptWorkstream(workstream, capturedAt));
234
+ for (const phase of workstream.phases) {
235
+ nodes.push(adaptPhase(phase, workstream));
236
+ }
237
+ for (const item of workstream.items) {
238
+ const activeAgentId = activeWorkItemAgentId(item);
239
+ const activeAgentNode = activeAgentId ? agentNodeById.get(activeAgentId) : undefined;
240
+ const memberLive = activeAgentNode !== undefined
241
+ && activeAgentNode.state !== 'done'
242
+ && activeAgentNode.state !== 'failed'
243
+ && activeAgentNode.state !== 'killed';
244
+ const parentId = item.currentPhaseId
245
+ ? phaseNodeId(workstream.id, item.currentPhaseId)
246
+ : workstreamNodeId(workstream.id);
247
+ nodes.push(adaptWorkItem(item, workstream.id, parentId, { steerable: deps.messageBus !== undefined && memberLive }));
248
+ }
249
+ }
250
+ for (const instance of deps.workflow.workflowManager.list()) {
251
+ nodes.push(adaptWorkflow(instance, capturedAt));
252
+ }
253
+ for (const trigger of deps.workflow.triggerManager.list()) {
254
+ nodes.push(adaptTrigger(trigger));
255
+ }
256
+ for (const schedule of deps.workflow.scheduleManager.list()) {
257
+ nodes.push(adaptSchedule(schedule));
258
+ }
259
+ if (deps.automationManager) {
260
+ for (const job of deps.automationManager.listJobs()) {
261
+ nodes.push(adaptAutomationJob(job));
262
+ }
263
+ }
264
+ for (const watcher of deps.watcherRegistry.list()) {
265
+ nodes.push(adaptWatcher(watcher, capturedAt));
266
+ }
267
+ for (const summary of deps.processManager.list()) {
268
+ const record = deps.processManager.getStatus(summary.id);
269
+ if (record)
270
+ nodes.push(adaptBackgroundProcess(record, capturedAt));
271
+ }
272
+ if (deps.codeIndexService) {
273
+ nodes.push(adaptCodeIndex(deps.codeIndexService, capturedAt));
274
+ }
275
+ return { capturedAt, nodes };
276
+ }
277
+ function query(filter) {
278
+ const { capturedAt, nodes } = assemble();
279
+ let selected = nodes;
280
+ if (filter?.kinds && filter.kinds.length > 0) {
281
+ const kinds = new Set(filter.kinds);
282
+ selected = selected.filter((node) => kinds.has(node.kind));
283
+ }
284
+ if (filter?.states && filter.states.length > 0) {
285
+ const states = new Set(filter.states);
286
+ selected = selected.filter((node) => states.has(node.state));
287
+ }
288
+ return { capturedAt, nodes: selected };
289
+ }
290
+ function getNode(id) {
291
+ const { nodes } = assemble();
292
+ return nodes.find((node) => node.id === id) ?? null;
293
+ }
294
+ // ── Coalesced tick + subscription ──────────────────────────────────────────
295
+ const listeners = new Set();
296
+ let tickHandle = null;
297
+ let lastSignature = null;
298
+ function tick() {
299
+ if (listeners.size === 0)
300
+ return;
301
+ const snapshot = query();
302
+ const signature = snapshotSignature(snapshot.nodes);
303
+ if (signature === lastSignature)
304
+ return;
305
+ lastSignature = signature;
306
+ for (const listener of listeners) {
307
+ try {
308
+ listener(snapshot);
309
+ }
310
+ catch (error) {
311
+ logger.warn('[fleet] subscriber threw', { error: summarizeError(error) });
312
+ }
313
+ }
314
+ }
315
+ function startTicking() {
316
+ if (tickHandle !== null)
317
+ return;
318
+ tickHandle = timers.setInterval(tick, tickIntervalMs);
319
+ // Never pin the event loop: an idle runtime must be able to exit even if
320
+ // a consumer forgot to dispose (mirrors ScheduleManager.destroy hygiene).
321
+ unrefHandle(tickHandle);
322
+ }
323
+ function stopTicking() {
324
+ if (tickHandle === null)
325
+ return;
326
+ timers.clearInterval(tickHandle);
327
+ tickHandle = null;
328
+ lastSignature = null;
329
+ }
330
+ function subscribe(listener) {
331
+ if (disposed)
332
+ return () => undefined;
333
+ listeners.add(listener);
334
+ startTicking();
335
+ let active = true;
336
+ return () => {
337
+ if (!active)
338
+ return;
339
+ active = false;
340
+ listeners.delete(listener);
341
+ if (listeners.size === 0)
342
+ stopTicking();
343
+ };
344
+ }
345
+ // ── Control dispatch (existing manager paths only) ─────────────────────────
346
+ /** Cascade kill over member agents — always a hard kill, never an interrupt. */
347
+ function cancelAgents(agentIds) {
348
+ const affected = [];
349
+ for (const agentId of agentIds) {
350
+ if (agentId && deps.agentManager.cancel(agentId, 'kill'))
351
+ affected.push(agentId);
352
+ }
353
+ return affected;
354
+ }
355
+ /**
356
+ * Fire-and-forget an AutomationManager async control call (Wave 6, wo-F
357
+ * item d4): the registry's own kill/interrupt/resume verbs are
358
+ * synchronous, but AutomationManager.removeJob/setEnabled are Promises —
359
+ * dispatch without awaiting rather than making every registry verb async,
360
+ * but never let a rejection vanish silently. The next tick's assemble()
361
+ * reflects the real outcome once the promise settles (mirrors the
362
+ * existing TUI automation-control-panel's swallow-and-tick behavior).
363
+ */
364
+ function dispatchAutomationOp(op, jobId, promise) {
365
+ promise.catch((error) => {
366
+ logger.warn(`[fleet] automation job ${op} failed`, { jobId, error: summarizeError(error) });
367
+ });
368
+ }
369
+ /** Primitive per-kind kill. Returns the node ids actually acted on. */
370
+ function killNode(node) {
371
+ switch (node.kind) {
372
+ case 'agent':
373
+ return deps.agentManager.cancel(node.id, 'kill') ? [node.id] : [];
374
+ case 'background-process':
375
+ return deps.processManager.stop(node.id) ? [node.id] : [];
376
+ case 'watcher':
377
+ return deps.watcherRegistry.stopWatcher(node.id) !== null ? [node.id] : [];
378
+ case 'workflow':
379
+ return deps.workflow.workflowManager.cancel(node.id) ? [node.id] : [];
380
+ case 'trigger':
381
+ return deps.workflow.triggerManager.remove(node.id) ? [node.id] : [];
382
+ case 'schedule': {
383
+ if (isAutomationJobRaw(node.raw)) {
384
+ if (!deps.automationManager)
385
+ return [];
386
+ const jobId = node.raw.job.id;
387
+ dispatchAutomationOp('kill', jobId, deps.automationManager.removeJob(jobId));
388
+ return [node.id];
389
+ }
390
+ const entry = node.raw;
391
+ return deps.workflow.scheduleManager.remove(entry.name) ? [node.id] : [];
392
+ }
393
+ case 'wrfc-chain': {
394
+ // DERIVED, not native: WrfcController exposes no public cancel/abort,
395
+ // so chain kill cascades AgentManager.cancel over the member agents.
396
+ const chain = node.raw;
397
+ const affected = cancelAgents(chain.allAgentIds);
398
+ return affected.length > 0 ? [node.id, ...affected] : [];
399
+ }
400
+ case 'wrfc-subtask': {
401
+ const subtask = node.raw;
402
+ const affected = cancelAgents([subtask.engineerAgentId, subtask.reviewerAgentId, subtask.fixerAgentId]);
403
+ return affected.length > 0 ? [node.id, ...affected] : [];
404
+ }
405
+ case 'workstream': {
406
+ // DERIVED: no native single-call cancel, so kill cascades
407
+ // engine.kill(itemId) over every non-terminal item — routed through
408
+ // the engine (not a raw AgentManager.cancel cascade) so cooperative
409
+ // cancellation (AbortController -> exec/fetch signal) fires the same
410
+ // way it would for an engine-internal kill.
411
+ const workstream = node.raw;
412
+ if (!deps.orchestrationEngine)
413
+ return [];
414
+ const affected = [];
415
+ for (const item of workstream.items) {
416
+ if (deps.orchestrationEngine.kill(item.id))
417
+ affected.push(workItemNodeId(item.id));
418
+ }
419
+ return affected.length > 0 ? [node.id, ...affected] : [];
420
+ }
421
+ case 'work-item': {
422
+ const { item } = node.raw;
423
+ if (!deps.orchestrationEngine?.kill(item.id))
424
+ return [];
425
+ return [node.id];
426
+ }
427
+ case 'phase':
428
+ // Pure grouping node — not killable (see adaptPhase capabilities).
429
+ return [];
430
+ default:
431
+ return [];
432
+ }
433
+ }
434
+ function kill(id, opts) {
435
+ const { nodes } = assemble();
436
+ const target = nodes.find((node) => node.id === id);
437
+ if (!target)
438
+ return [];
439
+ const affected = new Set();
440
+ const targets = [target];
441
+ if (opts?.cascade) {
442
+ // Children first, depth-first, so leaves stop before their parents.
443
+ const byParent = new Map();
444
+ for (const node of nodes) {
445
+ if (!node.parentId)
446
+ continue;
447
+ const siblings = byParent.get(node.parentId) ?? [];
448
+ siblings.push(node);
449
+ byParent.set(node.parentId, siblings);
450
+ }
451
+ const stack = [target];
452
+ const descendants = [];
453
+ const seen = new Set([target.id]);
454
+ while (stack.length > 0) {
455
+ const current = stack.pop();
456
+ for (const child of byParent.get(current.id) ?? []) {
457
+ if (seen.has(child.id))
458
+ continue;
459
+ seen.add(child.id);
460
+ descendants.push(child);
461
+ stack.push(child);
462
+ }
463
+ }
464
+ targets.unshift(...descendants.reverse());
465
+ }
466
+ for (const node of targets) {
467
+ for (const affectedId of killNode(node))
468
+ affected.add(affectedId);
469
+ }
470
+ // Chain-kill consistency: under cascade, member agents are terminated by
471
+ // the descendant pass BEFORE the chain's own killNode runs, so its
472
+ // internal cancelAgents() call finds every member already cancelled
473
+ // (affected.length === 0 there) and omits 'chain:<id>' — even though the
474
+ // chain itself was the kill target and a termination did occur. Make the
475
+ // return value consistent across both cascade and non-cascade: include
476
+ // the chain id whenever the chain was targeted and either a termination
477
+ // occurred (any node above) or the chain was still live.
478
+ if (target.kind === 'wrfc-chain' && (affected.size > 0 || target.capabilities.killable)) {
479
+ affected.add(target.id);
480
+ }
481
+ return [...affected];
482
+ }
483
+ function interrupt(id) {
484
+ const { nodes } = assemble();
485
+ const target = nodes.find((node) => node.id === id);
486
+ if (!target)
487
+ return false;
488
+ switch (target.kind) {
489
+ case 'agent':
490
+ return deps.agentManager.cancel(target.id, 'interrupt');
491
+ case 'trigger':
492
+ return deps.workflow.triggerManager.disable(target.id);
493
+ case 'schedule': {
494
+ if (isAutomationJobRaw(target.raw)) {
495
+ if (!deps.automationManager)
496
+ return false;
497
+ const jobId = target.raw.job.id;
498
+ dispatchAutomationOp('pause', jobId, deps.automationManager.setEnabled(jobId, false));
499
+ return true;
500
+ }
501
+ const entry = target.raw;
502
+ return deps.workflow.scheduleManager.disable(entry.name);
503
+ }
504
+ case 'work-item': {
505
+ const { item } = target.raw;
506
+ const agentId = activeWorkItemAgentId(item);
507
+ return agentId ? deps.agentManager.cancel(agentId, 'interrupt') : false;
508
+ }
509
+ default:
510
+ return false;
511
+ }
512
+ }
513
+ /**
514
+ * Wave 6 (wo-F item d2): re-arm a `paused` trigger/schedule via the owning
515
+ * manager's `enable()` — the inverse of interrupt()'s disable. Re-assemble
516
+ * + dispatch-by-kind mirrors interrupt()/kill()'s own shape exactly (same
517
+ * synchronous, no-owned-state pattern as every other control verb here).
518
+ * Honest refusal (false, never throws) for anything not currently
519
+ * resumable: not found, already armed, terminal, or a kind with no
520
+ * enable-based pause/resume cycle at all (e.g. an agent).
521
+ */
522
+ function resume(id) {
523
+ const { nodes } = assemble();
524
+ const target = nodes.find((node) => node.id === id);
525
+ if (!target || !target.capabilities.resumable)
526
+ return false;
527
+ switch (target.kind) {
528
+ case 'trigger':
529
+ return deps.workflow.triggerManager.enable(target.id);
530
+ case 'schedule': {
531
+ if (isAutomationJobRaw(target.raw)) {
532
+ if (!deps.automationManager)
533
+ return false;
534
+ const jobId = target.raw.job.id;
535
+ dispatchAutomationOp('resume', jobId, deps.automationManager.setEnabled(jobId, true));
536
+ return true;
537
+ }
538
+ const entry = target.raw;
539
+ return deps.workflow.scheduleManager.enable(entry.name);
540
+ }
541
+ default:
542
+ return false;
543
+ }
544
+ }
545
+ /**
546
+ * Queue a human message onto a live in-process agent's inbox (or the
547
+ * current live member agent of a wrfc-subtask). Mirrors interrupt()/kill()
548
+ * dispatch shape: re-assemble, find the target, switch on kind.
549
+ *
550
+ * Honest refusal for anything that cannot take mid-run input: no
551
+ * messageBus dep, a terminal/non-conversational kind, or a wrfc-chain
552
+ * (coordinate FSM, no conversation loop of its own — steer its member
553
+ * subtask instead).
554
+ */
555
+ function steer(id, text) {
556
+ const { nodes } = assemble();
557
+ const target = nodes.find((node) => node.id === id);
558
+ if (!target)
559
+ return { queued: false, reason: 'no such process' };
560
+ if (!deps.messageBus) {
561
+ return { queued: false, reason: 'steering is unavailable: no message bus configured' };
562
+ }
563
+ switch (target.kind) {
564
+ case 'agent': {
565
+ if (!target.capabilities.steerable) {
566
+ return { queued: false, reason: 'agent is not active and cannot be steered' };
567
+ }
568
+ const messageId = crypto.randomUUID();
569
+ const sent = deps.messageBus.send('operator', target.id, text, {
570
+ kind: 'steer',
571
+ ttlMs: STEER_TTL_MS,
572
+ id: messageId,
573
+ });
574
+ return sent ? { queued: true, messageId } : { queued: false, reason: 'steering message was blocked' };
575
+ }
576
+ case 'wrfc-subtask': {
577
+ const subtask = target.raw;
578
+ const agentId = activeSubtaskMemberAgentId(subtask);
579
+ if (!agentId || !target.capabilities.steerable) {
580
+ return { queued: false, reason: 'no live member agent to steer for this subtask' };
581
+ }
582
+ const messageId = crypto.randomUUID();
583
+ const sent = deps.messageBus.send('operator', agentId, text, {
584
+ kind: 'steer',
585
+ ttlMs: STEER_TTL_MS,
586
+ id: messageId,
587
+ });
588
+ return sent ? { queued: true, messageId } : { queued: false, reason: 'steering message was blocked' };
589
+ }
590
+ case 'wrfc-chain':
591
+ return { queued: false, reason: 'steer a member agent, not the chain' };
592
+ case 'work-item': {
593
+ const { item } = target.raw;
594
+ const agentId = activeWorkItemAgentId(item);
595
+ if (!agentId || !target.capabilities.steerable) {
596
+ return { queued: false, reason: 'no live agent to steer for this work item' };
597
+ }
598
+ const messageId = crypto.randomUUID();
599
+ const sent = deps.messageBus.send('operator', agentId, text, {
600
+ kind: 'steer',
601
+ ttlMs: STEER_TTL_MS,
602
+ id: messageId,
603
+ });
604
+ return sent ? { queued: true, messageId } : { queued: false, reason: 'steering message was blocked' };
605
+ }
606
+ case 'workstream':
607
+ return { queued: false, reason: 'steer a work item, not the workstream' };
608
+ default:
609
+ return { queued: false, reason: `${target.kind} cannot take steering input` };
610
+ }
611
+ }
612
+ function dispose() {
613
+ if (disposed)
614
+ return;
615
+ disposed = true;
616
+ stopTicking();
617
+ for (const unsubscribe of busUnsubscribers) {
618
+ try {
619
+ unsubscribe();
620
+ }
621
+ catch (error) {
622
+ logger.warn('[fleet] bus unsubscribe failed on dispose', { error: summarizeError(error) });
623
+ }
624
+ }
625
+ busUnsubscribers.length = 0;
626
+ listeners.clear();
627
+ activity.clear();
628
+ }
629
+ return { query, getNode, subscribe, interrupt, kill, resume, steer, dispose };
630
+ }