@aion0/forge 0.4.16 → 0.5.1
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.
- package/README.md +27 -2
- package/RELEASE_NOTES.md +21 -14
- package/app/api/agents/route.ts +17 -0
- package/app/api/delivery/[id]/route.ts +62 -0
- package/app/api/delivery/route.ts +40 -0
- package/app/api/mobile-chat/route.ts +13 -7
- package/app/api/monitor/route.ts +10 -6
- package/app/api/pipelines/[id]/route.ts +16 -3
- package/app/api/tasks/route.ts +2 -1
- package/app/api/workspace/[id]/agents/route.ts +35 -0
- package/app/api/workspace/[id]/memory/route.ts +23 -0
- package/app/api/workspace/[id]/smith/route.ts +22 -0
- package/app/api/workspace/[id]/stream/route.ts +28 -0
- package/app/api/workspace/route.ts +100 -0
- package/app/global-error.tsx +10 -4
- package/app/icon.ico +0 -0
- package/app/layout.tsx +2 -2
- package/app/login/LoginForm.tsx +96 -0
- package/app/login/page.tsx +7 -98
- package/app/page.tsx +2 -2
- package/bin/forge-server.mjs +13 -1
- package/check-forge-status.sh +9 -0
- package/components/ConversationEditor.tsx +411 -0
- package/components/ConversationGraphView.tsx +347 -0
- package/components/ConversationTerminalView.tsx +303 -0
- package/components/Dashboard.tsx +36 -39
- package/components/DashboardWrapper.tsx +9 -0
- package/components/DeliveryFlowEditor.tsx +491 -0
- package/components/DeliveryList.tsx +230 -0
- package/components/DeliveryWorkspace.tsx +589 -0
- package/components/DocTerminal.tsx +10 -2
- package/components/DocsViewer.tsx +10 -2
- package/components/HelpTerminal.tsx +11 -6
- package/components/InlinePipelineView.tsx +111 -0
- package/components/MobileView.tsx +20 -0
- package/components/MonitorPanel.tsx +9 -4
- package/components/NewTaskModal.tsx +32 -0
- package/components/PipelineEditor.tsx +49 -6
- package/components/PipelineView.tsx +482 -64
- package/components/ProjectDetail.tsx +314 -56
- package/components/ProjectManager.tsx +49 -4
- package/components/SessionView.tsx +27 -13
- package/components/SettingsModal.tsx +790 -124
- package/components/SkillsPanel.tsx +31 -8
- package/components/TaskBoard.tsx +3 -0
- package/components/WebTerminal.tsx +257 -43
- package/components/WorkspaceTree.tsx +221 -0
- package/components/WorkspaceView.tsx +2245 -0
- package/install.sh +2 -2
- package/lib/agents/claude-adapter.ts +104 -0
- package/lib/agents/generic-adapter.ts +64 -0
- package/lib/agents/index.ts +242 -0
- package/lib/agents/types.ts +70 -0
- package/lib/artifacts.ts +106 -0
- package/lib/delivery.ts +787 -0
- package/lib/forge-skills/forge-inbox.md +37 -0
- package/lib/forge-skills/forge-send.md +40 -0
- package/lib/forge-skills/forge-status.md +32 -0
- package/lib/forge-skills/forge-workspace-sync.md +37 -0
- package/lib/help-docs/00-overview.md +7 -1
- package/lib/help-docs/01-settings.md +159 -2
- package/lib/help-docs/05-pipelines.md +89 -0
- package/lib/help-docs/07-projects.md +35 -1
- package/lib/help-docs/11-workspace.md +254 -0
- package/lib/help-docs/CLAUDE.md +7 -2
- package/lib/init.ts +60 -10
- package/lib/pipeline.ts +537 -1
- package/lib/settings.ts +115 -22
- package/lib/skills.ts +249 -372
- package/lib/task-manager.ts +113 -33
- package/lib/telegram-bot.ts +33 -1
- package/lib/workspace/__tests__/state-machine.test.ts +388 -0
- package/lib/workspace/__tests__/workspace.test.ts +311 -0
- package/lib/workspace/agent-bus.ts +416 -0
- package/lib/workspace/agent-worker.ts +667 -0
- package/lib/workspace/backends/api-backend.ts +262 -0
- package/lib/workspace/backends/cli-backend.ts +479 -0
- package/lib/workspace/index.ts +82 -0
- package/lib/workspace/manager.ts +136 -0
- package/lib/workspace/orchestrator.ts +1914 -0
- package/lib/workspace/persistence.ts +310 -0
- package/lib/workspace/presets.ts +170 -0
- package/lib/workspace/skill-installer.ts +188 -0
- package/lib/workspace/smith-memory.ts +498 -0
- package/lib/workspace/types.ts +231 -0
- package/lib/workspace/watch-manager.ts +288 -0
- package/lib/workspace-standalone.ts +814 -0
- package/middleware.ts +1 -0
- package/next-env.d.ts +1 -1
- package/package.json +4 -1
- package/src/config/index.ts +12 -1
- package/src/core/db/database.ts +1 -0
- package/start.sh +7 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Bus — reliable one-to-one message delivery for workspace agents.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - One-to-one delivery (no broadcast)
|
|
6
|
+
* - ACK confirmation from receiver
|
|
7
|
+
* - 30-second retry on no ACK (max 3 retries)
|
|
8
|
+
* - Message dedup by ID
|
|
9
|
+
* - Outbox for messages to down/unavailable agents
|
|
10
|
+
* - Inbox persistence per agent
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { EventEmitter } from 'node:events';
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import type { BusMessage, AgentLiveness, MessageCategory } from './types';
|
|
16
|
+
|
|
17
|
+
const ACK_TIMEOUT_MS = 30_000;
|
|
18
|
+
const MAX_RETRIES = 3;
|
|
19
|
+
|
|
20
|
+
export class AgentBus extends EventEmitter {
|
|
21
|
+
private log: BusMessage[] = [];
|
|
22
|
+
private seen = new Set<string>(); // dedup: message IDs already processed
|
|
23
|
+
private outbox = new Map<string, BusMessage[]>(); // agentId → undelivered messages
|
|
24
|
+
private pendingAcks = new Map<string, { timer: NodeJS.Timeout; msg: BusMessage; retries: number }>();
|
|
25
|
+
private pendingRequests = new Map<string, { resolve: (msg: BusMessage) => void; timer: NodeJS.Timeout }>();
|
|
26
|
+
private agentStatus = new Map<string, AgentLiveness>();
|
|
27
|
+
|
|
28
|
+
// ─── Agent status tracking ─────────────────────────────
|
|
29
|
+
|
|
30
|
+
setAgentStatus(agentId: string, status: AgentLiveness): void {
|
|
31
|
+
const prev = this.agentStatus.get(agentId);
|
|
32
|
+
this.agentStatus.set(agentId, status);
|
|
33
|
+
|
|
34
|
+
// If agent came back alive, flush its outbox
|
|
35
|
+
if (status === 'alive' && prev === 'down') {
|
|
36
|
+
this.flushOutbox(agentId);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getAgentStatus(agentId: string): AgentLiveness {
|
|
41
|
+
return this.agentStatus.get(agentId) || 'down';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─── Send (one-to-one, reliable) ──────────────────────
|
|
45
|
+
|
|
46
|
+
send(from: string, to: string, type: BusMessage['type'], payload: BusMessage['payload'], options?: {
|
|
47
|
+
category?: MessageCategory;
|
|
48
|
+
causedBy?: BusMessage['causedBy'];
|
|
49
|
+
ticketStatus?: BusMessage['ticketStatus'];
|
|
50
|
+
maxRetries?: number;
|
|
51
|
+
}): BusMessage {
|
|
52
|
+
const msg: BusMessage = {
|
|
53
|
+
id: randomUUID(),
|
|
54
|
+
from, to, type, payload,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
status: 'pending',
|
|
57
|
+
retries: 0,
|
|
58
|
+
category: options?.category || 'notification',
|
|
59
|
+
causedBy: options?.causedBy,
|
|
60
|
+
ticketStatus: options?.ticketStatus,
|
|
61
|
+
ticketRetries: 0,
|
|
62
|
+
maxRetries: options?.maxRetries ?? 3,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.log.push(msg);
|
|
66
|
+
this.emit('message', msg);
|
|
67
|
+
|
|
68
|
+
// ACK messages don't need delivery tracking
|
|
69
|
+
if (type === 'ack') {
|
|
70
|
+
this.handleAck(msg);
|
|
71
|
+
return msg;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if target is available
|
|
75
|
+
const targetStatus = this.getAgentStatus(to);
|
|
76
|
+
if (targetStatus === 'down') {
|
|
77
|
+
// Store in outbox, deliver when agent comes back
|
|
78
|
+
this.addToOutbox(to, msg);
|
|
79
|
+
return msg;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// No ACK timer — in same-process architecture, messages are handled synchronously
|
|
83
|
+
// Status is managed directly by orchestrator (pending → acked/failed)
|
|
84
|
+
|
|
85
|
+
// Check if this resolves a pending request
|
|
86
|
+
if (type === 'response' && payload.replyTo) {
|
|
87
|
+
const pending = this.pendingRequests.get(payload.replyTo);
|
|
88
|
+
if (pending) {
|
|
89
|
+
clearTimeout(pending.timer);
|
|
90
|
+
this.pendingRequests.delete(payload.replyTo);
|
|
91
|
+
pending.resolve(msg);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return msg;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Convenience: send ACK back to original sender */
|
|
99
|
+
ack(receiverId: string, senderId: string, messageId: string): void {
|
|
100
|
+
this.send(receiverId, senderId, 'ack', { action: 'ack', replyTo: messageId });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Request-Response ──────────────────────────────────
|
|
104
|
+
|
|
105
|
+
request(from: string, to: string, payload: BusMessage['payload'], timeoutMs = 300_000): Promise<BusMessage> {
|
|
106
|
+
const msg = this.send(from, to, 'request', payload);
|
|
107
|
+
|
|
108
|
+
return new Promise<BusMessage>((resolve, reject) => {
|
|
109
|
+
const timer = setTimeout(() => {
|
|
110
|
+
this.pendingRequests.delete(msg.id);
|
|
111
|
+
reject(new Error(`Bus request timed out: ${payload.action}`));
|
|
112
|
+
}, timeoutMs);
|
|
113
|
+
|
|
114
|
+
this.pendingRequests.set(msg.id, { resolve, timer });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Convenience methods ───────────────────────────────
|
|
119
|
+
|
|
120
|
+
notifyTaskComplete(agentId: string, files: string[], summary?: string): void {
|
|
121
|
+
// Only notify agents that depend on this one — caller handles routing
|
|
122
|
+
this.log.push({
|
|
123
|
+
id: randomUUID(),
|
|
124
|
+
from: agentId, to: '_system', type: 'notify',
|
|
125
|
+
payload: { action: 'task_complete', content: summary, files },
|
|
126
|
+
timestamp: Date.now(),
|
|
127
|
+
status: 'done',
|
|
128
|
+
});
|
|
129
|
+
this.emit('message', this.log[this.log.length - 1]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
notifyStepComplete(agentId: string, stepLabel: string, files?: string[]): void {
|
|
133
|
+
this.log.push({
|
|
134
|
+
id: randomUUID(),
|
|
135
|
+
from: agentId, to: '_system', type: 'notify',
|
|
136
|
+
payload: { action: 'step_complete', content: `Step "${stepLabel}" completed`, files },
|
|
137
|
+
timestamp: Date.now(),
|
|
138
|
+
status: 'done',
|
|
139
|
+
});
|
|
140
|
+
this.emit('message', this.log[this.log.length - 1]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
notifyError(agentId: string, error: string): void {
|
|
144
|
+
this.log.push({
|
|
145
|
+
id: randomUUID(),
|
|
146
|
+
from: agentId, to: '_system', type: 'notify',
|
|
147
|
+
payload: { action: 'error', content: error },
|
|
148
|
+
timestamp: Date.now(),
|
|
149
|
+
status: 'done',
|
|
150
|
+
});
|
|
151
|
+
this.emit('message', this.log[this.log.length - 1]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── ACK handling ──────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
private handleAck(ackMsg: BusMessage): void {
|
|
157
|
+
const originalId = ackMsg.payload.replyTo;
|
|
158
|
+
if (!originalId) return;
|
|
159
|
+
|
|
160
|
+
const pending = this.pendingAcks.get(originalId);
|
|
161
|
+
if (pending) {
|
|
162
|
+
clearTimeout(pending.timer);
|
|
163
|
+
this.pendingAcks.delete(originalId);
|
|
164
|
+
pending.msg.status = 'done';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private startAckTimer(msg: BusMessage): void {
|
|
169
|
+
const timer = setTimeout(() => {
|
|
170
|
+
this.retryAckTimeout(msg);
|
|
171
|
+
}, ACK_TIMEOUT_MS);
|
|
172
|
+
|
|
173
|
+
this.pendingAcks.set(msg.id, { timer, msg, retries: 0 });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private retryAckTimeout(msg: BusMessage): void {
|
|
177
|
+
const pending = this.pendingAcks.get(msg.id);
|
|
178
|
+
if (!pending) return;
|
|
179
|
+
|
|
180
|
+
pending.retries++;
|
|
181
|
+
msg.retries = pending.retries;
|
|
182
|
+
|
|
183
|
+
if (pending.retries >= MAX_RETRIES) {
|
|
184
|
+
// Give up — mark as failed
|
|
185
|
+
this.pendingAcks.delete(msg.id);
|
|
186
|
+
msg.status = 'failed';
|
|
187
|
+
console.log(`[bus] Message to ${msg.to} failed after ${MAX_RETRIES} retries: ${msg.payload.action}`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(`[bus] Retrying message to ${msg.to} (attempt ${pending.retries + 1}): ${msg.payload.action}`);
|
|
192
|
+
|
|
193
|
+
// Check if target is still available
|
|
194
|
+
if (this.getAgentStatus(msg.to) === 'down') {
|
|
195
|
+
this.pendingAcks.delete(msg.id);
|
|
196
|
+
this.addToOutbox(msg.to, msg);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Re-emit for delivery
|
|
201
|
+
this.emit('message', msg);
|
|
202
|
+
|
|
203
|
+
// Reset timer
|
|
204
|
+
pending.timer = setTimeout(() => {
|
|
205
|
+
this.retryAckTimeout(msg);
|
|
206
|
+
}, ACK_TIMEOUT_MS);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── Outbox (for down agents) ──────────────────────────
|
|
210
|
+
|
|
211
|
+
private addToOutbox(agentId: string, msg: BusMessage): void {
|
|
212
|
+
if (!this.outbox.has(agentId)) this.outbox.set(agentId, []);
|
|
213
|
+
this.outbox.get(agentId)!.push(msg);
|
|
214
|
+
msg.status = 'pending';
|
|
215
|
+
console.log(`[bus] Agent ${agentId} is down, queued message: ${msg.payload.action}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private flushOutbox(agentId: string): void {
|
|
219
|
+
const queued = this.outbox.get(agentId);
|
|
220
|
+
if (!queued || queued.length === 0) return;
|
|
221
|
+
|
|
222
|
+
console.log(`[bus] Agent ${agentId} is back, flushing ${queued.length} queued messages`);
|
|
223
|
+
this.outbox.delete(agentId);
|
|
224
|
+
|
|
225
|
+
for (const msg of queued) {
|
|
226
|
+
// Remove from seen set so handleBusMessage won't dedup it
|
|
227
|
+
this.unsee(msg.id);
|
|
228
|
+
this.emit('message', msg);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Dedup ─────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/** Check if a message was already processed (for receiver side) */
|
|
235
|
+
isDuplicate(messageId: string): boolean {
|
|
236
|
+
if (this.seen.has(messageId)) return true;
|
|
237
|
+
this.seen.add(messageId);
|
|
238
|
+
// Keep seen set bounded
|
|
239
|
+
if (this.seen.size > 1000) {
|
|
240
|
+
const arr = Array.from(this.seen);
|
|
241
|
+
this.seen = new Set(arr.slice(-500));
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Remove a message ID from the seen set (allow re-processing, e.g. after outbox flush) */
|
|
247
|
+
unsee(messageId: string): void {
|
|
248
|
+
this.seen.delete(messageId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Mark a message as delivered by ID */
|
|
252
|
+
markDelivered(messageId: string): void {
|
|
253
|
+
const msg = this.log.find(m => m.id === messageId);
|
|
254
|
+
if (msg && msg.status === 'pending') {
|
|
255
|
+
msg.status = 'done';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Get undelivered messages for an agent (pending status only, excludes ACKs) */
|
|
260
|
+
getPendingMessagesFor(agentId: string): BusMessage[] {
|
|
261
|
+
return this.log.filter(m => m.to === agentId && m.status === 'pending' && m.type !== 'ack');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Retry a failed message by ID — mark as pending and re-emit for delivery */
|
|
265
|
+
/** Retry/re-run a message — set back to pending and re-deliver */
|
|
266
|
+
retryMessage(messageId: string): BusMessage | null {
|
|
267
|
+
const msg = this.log.find(m => m.id === messageId);
|
|
268
|
+
if (!msg || msg.status === 'pending' || msg.status === 'pending_approval' || msg.status === 'running') return null;
|
|
269
|
+
msg.status = 'pending';
|
|
270
|
+
msg.retries = 0;
|
|
271
|
+
this.unsee(messageId);
|
|
272
|
+
console.log(`[bus] Retrying message ${messageId.slice(0, 8)} (${msg.payload.action})`);
|
|
273
|
+
return msg;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Create a ticket (1-to-1, ignores DAG direction) */
|
|
277
|
+
createTicket(from: string, to: string, action: string, content: string, files?: string[], causedBy?: BusMessage['causedBy']): BusMessage {
|
|
278
|
+
return this.send(from, to, 'request', { action, content, files }, {
|
|
279
|
+
category: 'ticket',
|
|
280
|
+
causedBy,
|
|
281
|
+
ticketStatus: 'open',
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Update ticket status */
|
|
286
|
+
updateTicketStatus(messageId: string, ticketStatus: BusMessage['ticketStatus']): void {
|
|
287
|
+
const msg = this.log.find(m => m.id === messageId && m.category === 'ticket');
|
|
288
|
+
if (msg) {
|
|
289
|
+
msg.ticketStatus = ticketStatus;
|
|
290
|
+
this.emit('message', msg);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Find outbox messages sent by an agent */
|
|
295
|
+
getOutboxFor(agentId: string): BusMessage[] {
|
|
296
|
+
return this.log.filter(m => m.from === agentId && m.type !== 'ack' && m.to !== '_system');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Find a message in agent's outbox by causedBy.messageId */
|
|
300
|
+
findInOutbox(agentId: string, causedByMessageId: string): BusMessage | null {
|
|
301
|
+
return this.log.find(m => m.from === agentId && m.causedBy?.messageId === causedByMessageId) || null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Delete a message from the log (only done/failed) */
|
|
305
|
+
deleteMessage(messageId: string): void {
|
|
306
|
+
const idx = this.log.findIndex(m => m.id === messageId);
|
|
307
|
+
if (idx === -1) {
|
|
308
|
+
console.log(`[bus] deleteMessage: ${messageId.slice(0, 8)} not found`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const msg = this.log[idx];
|
|
312
|
+
if (msg.status === 'done' || msg.status === 'failed') {
|
|
313
|
+
this.log.splice(idx, 1);
|
|
314
|
+
console.log(`[bus] deleteMessage: ${messageId.slice(0, 8)} deleted (was ${msg.status})`);
|
|
315
|
+
} else {
|
|
316
|
+
console.log(`[bus] deleteMessage: ${messageId.slice(0, 8)} skipped (status=${msg.status})`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Abort a pending message — mark as failed */
|
|
321
|
+
abortMessage(messageId: string): BusMessage | null {
|
|
322
|
+
const msg = this.log.find(m => m.id === messageId);
|
|
323
|
+
if (!msg || msg.status !== 'pending') return null;
|
|
324
|
+
msg.status = 'failed';
|
|
325
|
+
console.log(`[bus] Aborted message ${msg.payload.action} from ${msg.from} to ${msg.to}`);
|
|
326
|
+
return msg;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Mark all running messages as failed — called on stopDaemon/crash */
|
|
330
|
+
markAllRunningAsFailed(): void {
|
|
331
|
+
let count = 0;
|
|
332
|
+
for (const msg of this.log) {
|
|
333
|
+
if (msg.status === 'running' && msg.type !== 'ack') {
|
|
334
|
+
msg.status = 'failed';
|
|
335
|
+
count++;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (count > 0) console.log(`[bus] Marked ${count} running messages as failed (shutdown)`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Mark all pending (non-ack) messages as failed — called on restart/reload */
|
|
342
|
+
markAllPendingAsFailed(): void {
|
|
343
|
+
let count = 0;
|
|
344
|
+
for (const msg of this.log) {
|
|
345
|
+
if (msg.status === 'pending' && msg.type !== 'ack') {
|
|
346
|
+
msg.status = 'failed';
|
|
347
|
+
count++;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (count > 0) console.log(`[bus] Marked ${count} pending messages as failed (restart cleanup)`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ─── Query ─────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
getMessagesFor(agentId: string): BusMessage[] {
|
|
356
|
+
return this.log.filter(m => m.to === agentId);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
getMessagesFrom(agentId: string): BusMessage[] {
|
|
360
|
+
return this.log.filter(m => m.from === agentId);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
getConversation(a: string, b: string): BusMessage[] {
|
|
364
|
+
return this.log.filter(m =>
|
|
365
|
+
(m.from === a && m.to === b) || (m.from === b && m.to === a)
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
getOutbox(agentId: string): BusMessage[] {
|
|
370
|
+
return this.outbox.get(agentId) || [];
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
getLog(): readonly BusMessage[] {
|
|
374
|
+
return this.log;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Get all outbox queues (for persistence) */
|
|
378
|
+
getAllOutbox(): Record<string, BusMessage[]> {
|
|
379
|
+
const result: Record<string, BusMessage[]> = {};
|
|
380
|
+
for (const [id, msgs] of this.outbox) {
|
|
381
|
+
if (msgs.length > 0) result[id] = [...msgs];
|
|
382
|
+
}
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
loadLog(messages: BusMessage[]): void {
|
|
387
|
+
this.log = [...messages];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Restore outbox from persisted state */
|
|
391
|
+
loadOutbox(outbox: Record<string, BusMessage[]>): void {
|
|
392
|
+
this.outbox.clear();
|
|
393
|
+
for (const [id, msgs] of Object.entries(outbox)) {
|
|
394
|
+
this.outbox.set(id, [...msgs]);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
clear(): void {
|
|
399
|
+
// Reject all pending requests
|
|
400
|
+
for (const [, pending] of this.pendingRequests) {
|
|
401
|
+
clearTimeout(pending.timer);
|
|
402
|
+
try { pending.resolve({ id: '', from: '', to: '', type: 'response', payload: { action: 'cancelled' }, timestamp: Date.now() }); } catch {}
|
|
403
|
+
}
|
|
404
|
+
this.pendingRequests.clear();
|
|
405
|
+
|
|
406
|
+
// Clear all pending ACK timers
|
|
407
|
+
for (const [, pending] of this.pendingAcks) {
|
|
408
|
+
clearTimeout(pending.timer);
|
|
409
|
+
}
|
|
410
|
+
this.pendingAcks.clear();
|
|
411
|
+
|
|
412
|
+
this.log = [];
|
|
413
|
+
this.outbox.clear();
|
|
414
|
+
this.seen.clear();
|
|
415
|
+
}
|
|
416
|
+
}
|