foundr-companion 0.1.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.
- package/dist/bridge/adapters.d.mts +33 -0
- package/dist/bridge/adapters.d.mts.map +1 -0
- package/dist/bridge/adapters.mjs +159 -0
- package/dist/bridge/adapters.mjs.map +1 -0
- package/dist/bridge/bridge-auth.d.mts +5 -0
- package/dist/bridge/bridge-auth.d.mts.map +1 -0
- package/dist/bridge/bridge-auth.mjs +24 -0
- package/dist/bridge/bridge-auth.mjs.map +1 -0
- package/dist/bridge/codex-app-server-adapter.d.mts +37 -0
- package/dist/bridge/codex-app-server-adapter.d.mts.map +1 -0
- package/dist/bridge/codex-app-server-adapter.mjs +401 -0
- package/dist/bridge/codex-app-server-adapter.mjs.map +1 -0
- package/dist/bridge/daemon.mjs +190 -0
- package/dist/bridge/foundr-codex-reply.d.mts +96 -0
- package/dist/bridge/foundr-codex-reply.d.mts.map +1 -0
- package/dist/bridge/foundr-codex-reply.mjs +542 -0
- package/dist/bridge/foundr-codex-reply.mjs.map +1 -0
- package/dist/bridge/listener-flags.d.mts +5 -0
- package/dist/bridge/listener-flags.d.mts.map +1 -0
- package/dist/bridge/listener-flags.mjs +18 -0
- package/dist/bridge/listener-flags.mjs.map +1 -0
- package/dist/bridge/listener-loop.d.mts +58 -0
- package/dist/bridge/listener-loop.d.mts.map +1 -0
- package/dist/bridge/listener-loop.mjs +424 -0
- package/dist/bridge/listener-loop.mjs.map +1 -0
- package/dist/bridge/logout.d.mts +8 -0
- package/dist/bridge/logout.d.mts.map +1 -0
- package/dist/bridge/logout.mjs +35 -0
- package/dist/bridge/logout.mjs.map +1 -0
- package/dist/bridge/mcp-client.d.mts +97 -0
- package/dist/bridge/mcp-client.d.mts.map +1 -0
- package/dist/bridge/mcp-client.mjs +290 -0
- package/dist/bridge/mcp-client.mjs.map +1 -0
- package/dist/bridge/oauth-provider.d.mts +32 -0
- package/dist/bridge/oauth-provider.d.mts.map +1 -0
- package/dist/bridge/oauth-provider.mjs +94 -0
- package/dist/bridge/oauth-provider.mjs.map +1 -0
- package/dist/bridge/public-room-mention-listener-loop.d.mts +120 -0
- package/dist/bridge/public-room-mention-listener-loop.d.mts.map +1 -0
- package/dist/bridge/public-room-mention-listener-loop.mjs +225 -0
- package/dist/bridge/public-room-mention-listener-loop.mjs.map +1 -0
- package/dist/bridge/realtime-wake.d.mts +11 -0
- package/dist/bridge/realtime-wake.d.mts.map +1 -0
- package/dist/bridge/realtime-wake.mjs +134 -0
- package/dist/bridge/realtime-wake.mjs.map +1 -0
- package/dist/bridge/work-mission-listener-loop.d.mts +100 -0
- package/dist/bridge/work-mission-listener-loop.d.mts.map +1 -0
- package/dist/bridge/work-mission-listener-loop.mjs +737 -0
- package/dist/bridge/work-mission-listener-loop.mjs.map +1 -0
- package/dist/cli-parse.d.ts +27 -0
- package/dist/cli-parse.d.ts.map +1 -0
- package/dist/cli-parse.js +47 -0
- package/dist/cli-parse.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +232 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-preflight.d.ts +44 -0
- package/dist/codex-preflight.d.ts.map +1 -0
- package/dist/codex-preflight.js +74 -0
- package/dist/codex-preflight.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/launchd.d.ts +78 -0
- package/dist/launchd.d.ts.map +1 -0
- package/dist/launchd.js +118 -0
- package/dist/launchd.js.map +1 -0
- package/dist/paths.d.ts +30 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +26 -0
- package/dist/paths.js.map +1 -0
- package/install.sh +117 -0
- package/package.json +29 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { createPrivateChatWakeSubscription } from './realtime-wake.mjs'
|
|
2
|
+
import {
|
|
3
|
+
isPublicRoomMentionWakePayload,
|
|
4
|
+
runOnePublicRoomMentionIteration,
|
|
5
|
+
runOnePublicRoomMentionWakeIteration,
|
|
6
|
+
} from './public-room-mention-listener-loop.mjs'
|
|
7
|
+
|
|
8
|
+
const CLIENT_NAME = 'foundr-local-bridge'
|
|
9
|
+
const CLIENT_VERSION = '0.1.0'
|
|
10
|
+
const HEARTBEAT_INTERVAL_MS = 20_000
|
|
11
|
+
const POLL_INTERVAL_MS = 1_000
|
|
12
|
+
const PUBLIC_ROOM_MENTION_POLL_INTERVAL_MS = 1_000
|
|
13
|
+
const WAKE_FALLBACK_POLL_INTERVAL_MS = 15_000
|
|
14
|
+
const PROGRESS_FLUSH_INTERVAL_MS = 120
|
|
15
|
+
const PROGRESS_MAX_BUFFER_CHARS = 180
|
|
16
|
+
const FINAL_PROGRESS_FLUSH_TIMEOUT_MS = 150
|
|
17
|
+
|
|
18
|
+
function nowMs() {
|
|
19
|
+
return performance.now()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function timingEnabled() {
|
|
23
|
+
return process.env.FOUNDR_WORK_MISSION_TIMING === '1' || process.env.FOUNDR_PRIVATE_CHAT_TIMING === '1'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function timingLog(startedAt, message) {
|
|
27
|
+
if (!timingEnabled()) return
|
|
28
|
+
console.error(`[foundr:work-missions:timing +${Math.round(nowMs() - startedAt)}ms] ${message}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function boundedError(error) {
|
|
32
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
33
|
+
return message.replace(/\s+/g, ' ').trim().slice(0, 500)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function delay(ms) {
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const timer = setTimeout(resolve, ms)
|
|
39
|
+
timer.unref?.()
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function timestampMs(value) {
|
|
44
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
45
|
+
const time = date.getTime()
|
|
46
|
+
return Number.isNaN(time) ? 0 : time
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function replyClientMsgId(messageId) {
|
|
50
|
+
return `work-mission-reply:${messageId}`.slice(0, 128)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sortMessages(messages = []) {
|
|
54
|
+
return messages.slice().sort((a, b) => timestampMs(a.created_at) - timestampMs(b.created_at))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function pendingWorkMissionFounderMessages(snapshot, {
|
|
58
|
+
ignoredMessageIds = new Set(),
|
|
59
|
+
} = {}) {
|
|
60
|
+
const messages = sortMessages(snapshot?.messages ?? [])
|
|
61
|
+
const replyClientIds = new Set(
|
|
62
|
+
messages
|
|
63
|
+
.filter(message => message.sender_kind === 'agent' && message.client_msg_id)
|
|
64
|
+
.map(message => message.client_msg_id),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return messages.filter((message) => (
|
|
68
|
+
message.sender_kind === 'founder' &&
|
|
69
|
+
!ignoredMessageIds.has(message.id) &&
|
|
70
|
+
!replyClientIds.has(replyClientMsgId(message.id))
|
|
71
|
+
))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function workMissionEnvelope(snapshot, message, agent, delivery = null) {
|
|
75
|
+
return {
|
|
76
|
+
kind: 'work_mission',
|
|
77
|
+
mission: snapshot.mission,
|
|
78
|
+
thread: {
|
|
79
|
+
id: `mission:${snapshot.mission.id}`,
|
|
80
|
+
kind: 'work_mission',
|
|
81
|
+
},
|
|
82
|
+
message,
|
|
83
|
+
delivery: {
|
|
84
|
+
id: delivery?.id ?? message.id,
|
|
85
|
+
agent_key_id: delivery?.agent_key_id ?? agent?.id ?? '',
|
|
86
|
+
requested_model: delivery?.requested_model ?? null,
|
|
87
|
+
},
|
|
88
|
+
agent: agent ?? null,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isWorkMissionWakePayload(payload) {
|
|
93
|
+
return payload?.type === 'work_mission_delivery_ready' &&
|
|
94
|
+
typeof payload.delivery_id === 'string' &&
|
|
95
|
+
typeof payload.mission_id === 'string' &&
|
|
96
|
+
typeof payload.message_id === 'string' &&
|
|
97
|
+
typeof payload.body === 'string'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function workMissionWakeEnvelope(payload, agent) {
|
|
101
|
+
const message = {
|
|
102
|
+
id: payload.message_id,
|
|
103
|
+
mission_id: payload.mission_id,
|
|
104
|
+
sender_kind: 'founder',
|
|
105
|
+
sender_founder_id: null,
|
|
106
|
+
sender_agent_key_id: null,
|
|
107
|
+
body: payload.body,
|
|
108
|
+
client_msg_id: null,
|
|
109
|
+
created_at: payload.sent_at ?? new Date().toISOString(),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return workMissionEnvelope({
|
|
113
|
+
mission: {
|
|
114
|
+
id: payload.mission_id,
|
|
115
|
+
title: payload.mission_title ?? 'Mission',
|
|
116
|
+
status: 'running',
|
|
117
|
+
priority: 'medium',
|
|
118
|
+
},
|
|
119
|
+
tasks: [],
|
|
120
|
+
decisions: [],
|
|
121
|
+
artifacts: [],
|
|
122
|
+
messages: [message],
|
|
123
|
+
deliveries: [],
|
|
124
|
+
agent_status: { status: 'listening', assigned_count: 1 },
|
|
125
|
+
}, message, agent, {
|
|
126
|
+
id: payload.delivery_id,
|
|
127
|
+
agent_key_id: agent?.id ?? '',
|
|
128
|
+
requested_model: payload.requested_model ?? null,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function replyText(reply) {
|
|
133
|
+
return String(reply?.text ?? '').trim()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isUnknownToolError(error) {
|
|
137
|
+
const message = boundedError(error).toLowerCase()
|
|
138
|
+
return message.includes('unknown tool') ||
|
|
139
|
+
message.includes('method not found') ||
|
|
140
|
+
message.includes('tool not found')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function completeWorkMissionReply({
|
|
144
|
+
client,
|
|
145
|
+
deliveryId,
|
|
146
|
+
sessionId,
|
|
147
|
+
missionId,
|
|
148
|
+
messageId,
|
|
149
|
+
text,
|
|
150
|
+
}) {
|
|
151
|
+
const clientMsgId = replyClientMsgId(messageId)
|
|
152
|
+
try {
|
|
153
|
+
return await client.callTool('complete_work_mission_reply', {
|
|
154
|
+
delivery_id: deliveryId,
|
|
155
|
+
listener_session_id: sessionId,
|
|
156
|
+
body: text,
|
|
157
|
+
client_msg_id: clientMsgId,
|
|
158
|
+
})
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (!isUnknownToolError(error)) throw error
|
|
161
|
+
const sent = await client.callTool('append_work_mission_message', {
|
|
162
|
+
mission_id: missionId,
|
|
163
|
+
body: text,
|
|
164
|
+
client_msg_id: clientMsgId,
|
|
165
|
+
})
|
|
166
|
+
await client.callTool('ack_work_mission_message', {
|
|
167
|
+
delivery_id: deliveryId,
|
|
168
|
+
listener_session_id: sessionId,
|
|
169
|
+
status: 'replied',
|
|
170
|
+
response_message_id: sent.message.id,
|
|
171
|
+
})
|
|
172
|
+
return { message: sent.message }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function progressNumberFromEnv(name, fallback) {
|
|
177
|
+
const value = Number(process.env[name])
|
|
178
|
+
return Number.isFinite(value) && value > 0 ? value : fallback
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function wakeFallbackPollMs(pollMs) {
|
|
182
|
+
const configured = Number(process.env.FOUNDR_WORK_MISSION_WAKE_FALLBACK_POLL_MS)
|
|
183
|
+
if (Number.isFinite(configured) && configured > 0) return configured
|
|
184
|
+
return Math.max(pollMs, WAKE_FALLBACK_POLL_INTERVAL_MS)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function createProgressReporter({ client, deliveryId, sessionId, iterationStartedAt }) {
|
|
188
|
+
const flushIntervalMs = progressNumberFromEnv('FOUNDR_WORK_MISSION_PROGRESS_FLUSH_MS', PROGRESS_FLUSH_INTERVAL_MS)
|
|
189
|
+
const maxBufferChars = progressNumberFromEnv('FOUNDR_WORK_MISSION_PROGRESS_MAX_CHARS', PROGRESS_MAX_BUFFER_CHARS)
|
|
190
|
+
const finalFlushTimeoutMs = progressNumberFromEnv(
|
|
191
|
+
'FOUNDR_WORK_MISSION_FINAL_PROGRESS_TIMEOUT_MS',
|
|
192
|
+
FINAL_PROGRESS_FLUSH_TIMEOUT_MS,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
let bufferedText = ''
|
|
196
|
+
let pendingStatus = null
|
|
197
|
+
let pendingError = null
|
|
198
|
+
let flushTimer = null
|
|
199
|
+
let sawFirstDelta = false
|
|
200
|
+
let progressChain = Promise.resolve()
|
|
201
|
+
|
|
202
|
+
const clearFlushTimer = () => {
|
|
203
|
+
if (!flushTimer) return
|
|
204
|
+
clearTimeout(flushTimer)
|
|
205
|
+
flushTimer = null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const callProgressTool = (args) => {
|
|
209
|
+
progressChain = progressChain
|
|
210
|
+
.then(() => client.callTool('stream_work_mission_reply', {
|
|
211
|
+
delivery_id: deliveryId,
|
|
212
|
+
listener_session_id: sessionId,
|
|
213
|
+
...args,
|
|
214
|
+
}))
|
|
215
|
+
.catch(() => {})
|
|
216
|
+
return progressChain
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const flush = () => {
|
|
220
|
+
clearFlushTimer()
|
|
221
|
+
const textDelta = bufferedText
|
|
222
|
+
const status = pendingStatus
|
|
223
|
+
const error = pendingError
|
|
224
|
+
bufferedText = ''
|
|
225
|
+
pendingStatus = null
|
|
226
|
+
pendingError = null
|
|
227
|
+
|
|
228
|
+
if (!textDelta && !status && !error) return progressChain
|
|
229
|
+
const nextStatus = textDelta && status !== 'failed' ? 'writing' : status
|
|
230
|
+
|
|
231
|
+
return callProgressTool({
|
|
232
|
+
...(textDelta ? { text_delta: textDelta } : {}),
|
|
233
|
+
...(nextStatus ? { status: nextStatus } : {}),
|
|
234
|
+
...(error ? { error } : {}),
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const scheduleFlush = () => {
|
|
239
|
+
if (flushTimer) return
|
|
240
|
+
flushTimer = setTimeout(() => {
|
|
241
|
+
flushTimer = null
|
|
242
|
+
void flush()
|
|
243
|
+
}, flushIntervalMs)
|
|
244
|
+
flushTimer.unref?.()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
push(event) {
|
|
249
|
+
if (event.type === 'status') {
|
|
250
|
+
if (event.status === 'thinking') return
|
|
251
|
+
pendingStatus = event.status
|
|
252
|
+
if (event.status === 'failed') {
|
|
253
|
+
void flush()
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
scheduleFlush()
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (event.type !== 'delta' || !event.text) return
|
|
261
|
+
if (!sawFirstDelta) {
|
|
262
|
+
sawFirstDelta = true
|
|
263
|
+
timingLog(iterationStartedAt, `first delta delivery=${deliveryId}`)
|
|
264
|
+
}
|
|
265
|
+
bufferedText += event.text
|
|
266
|
+
pendingStatus = 'writing'
|
|
267
|
+
if (bufferedText.length >= maxBufferChars) {
|
|
268
|
+
void flush()
|
|
269
|
+
} else {
|
|
270
|
+
scheduleFlush()
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
async flushBeforeFinal() {
|
|
275
|
+
const flushed = flush()
|
|
276
|
+
await Promise.race([flushed, delay(finalFlushTimeoutMs)])
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
async fail(error) {
|
|
280
|
+
pendingStatus = 'failed'
|
|
281
|
+
pendingError = boundedError(error)
|
|
282
|
+
await flush()
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
cancel() {
|
|
286
|
+
clearFlushTimer()
|
|
287
|
+
bufferedText = ''
|
|
288
|
+
pendingStatus = null
|
|
289
|
+
pendingError = null
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export async function runOneWorkMissionIteration({
|
|
295
|
+
client,
|
|
296
|
+
adapter,
|
|
297
|
+
agent,
|
|
298
|
+
sessionId,
|
|
299
|
+
}) {
|
|
300
|
+
const iterationStartedAt = nowMs()
|
|
301
|
+
const inbound = await client.callTool('claim_work_mission_message', {
|
|
302
|
+
listener_session_id: sessionId,
|
|
303
|
+
include_snapshot: true,
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
if (inbound.timed_out) {
|
|
307
|
+
return { timed_out: true, replied: 0, skipped: 0, failed: 0 }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const deliveryId = inbound.delivery.id
|
|
311
|
+
timingLog(iterationStartedAt, `claimed delivery=${deliveryId}`)
|
|
312
|
+
|
|
313
|
+
const snapshotResult = inbound.snapshot
|
|
314
|
+
? { snapshot: inbound.snapshot }
|
|
315
|
+
: await client.callTool('get_work_mission', {
|
|
316
|
+
mission_id: inbound.mission.id,
|
|
317
|
+
})
|
|
318
|
+
const snapshot = snapshotResult.snapshot ?? {
|
|
319
|
+
mission: inbound.mission,
|
|
320
|
+
tasks: [],
|
|
321
|
+
decisions: [],
|
|
322
|
+
artifacts: [],
|
|
323
|
+
messages: [inbound.message],
|
|
324
|
+
deliveries: [inbound.delivery],
|
|
325
|
+
agent_status: { status: 'listening', assigned_count: 1 },
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const progress = createProgressReporter({
|
|
329
|
+
client,
|
|
330
|
+
deliveryId,
|
|
331
|
+
sessionId,
|
|
332
|
+
iterationStartedAt,
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
let text = ''
|
|
336
|
+
try {
|
|
337
|
+
text = replyText(await adapter.reply(
|
|
338
|
+
workMissionEnvelope(snapshot, inbound.message, agent, inbound.delivery),
|
|
339
|
+
{
|
|
340
|
+
onProgress: (event) => {
|
|
341
|
+
progress.push(event)
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
))
|
|
345
|
+
await progress.flushBeforeFinal()
|
|
346
|
+
timingLog(iterationStartedAt, `adapter completed delivery=${deliveryId}`)
|
|
347
|
+
} catch (error) {
|
|
348
|
+
const errorText = boundedError(error)
|
|
349
|
+
await progress.fail(errorText).catch(() => {})
|
|
350
|
+
await client.callTool('ack_work_mission_message', {
|
|
351
|
+
delivery_id: deliveryId,
|
|
352
|
+
listener_session_id: sessionId,
|
|
353
|
+
status: 'failed',
|
|
354
|
+
error: errorText,
|
|
355
|
+
}).catch(() => {})
|
|
356
|
+
console.error(`[foundr:work-missions] reply failed mission=${inbound.mission.id} message=${inbound.message.id}: ${errorText}`)
|
|
357
|
+
return { replied: 0, skipped: 0, failed: 1 }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!text) {
|
|
361
|
+
await client.callTool('ack_work_mission_message', {
|
|
362
|
+
delivery_id: deliveryId,
|
|
363
|
+
listener_session_id: sessionId,
|
|
364
|
+
status: 'skipped',
|
|
365
|
+
})
|
|
366
|
+
return { replied: 0, skipped: 1, failed: 0 }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const sent = await completeWorkMissionReply({
|
|
371
|
+
client,
|
|
372
|
+
deliveryId,
|
|
373
|
+
sessionId,
|
|
374
|
+
missionId: snapshot.mission.id,
|
|
375
|
+
messageId: inbound.message.id,
|
|
376
|
+
text,
|
|
377
|
+
})
|
|
378
|
+
const responseMessageId = sent.message.id
|
|
379
|
+
timingLog(iterationStartedAt, `reply sent delivery=${deliveryId}`)
|
|
380
|
+
return { replied: 1, skipped: 0, failed: 0, message_id: responseMessageId }
|
|
381
|
+
} catch (error) {
|
|
382
|
+
progress.cancel()
|
|
383
|
+
const errorText = boundedError(error)
|
|
384
|
+
await client.callTool('ack_work_mission_message', {
|
|
385
|
+
delivery_id: deliveryId,
|
|
386
|
+
listener_session_id: sessionId,
|
|
387
|
+
status: 'failed',
|
|
388
|
+
error: errorText,
|
|
389
|
+
}).catch(() => {})
|
|
390
|
+
return { replied: 0, skipped: 0, failed: 1, error: errorText }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function runOneWorkMissionWakeIteration({
|
|
395
|
+
client,
|
|
396
|
+
adapter,
|
|
397
|
+
agent,
|
|
398
|
+
sessionId,
|
|
399
|
+
wake,
|
|
400
|
+
}) {
|
|
401
|
+
if (!isWorkMissionWakePayload(wake)) {
|
|
402
|
+
return runOneWorkMissionIteration({ client, adapter, agent, sessionId })
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const iterationStartedAt = nowMs()
|
|
406
|
+
const deliveryId = wake.delivery_id
|
|
407
|
+
const messageId = wake.message_id
|
|
408
|
+
timingLog(iterationStartedAt, `wake delivery=${deliveryId}`)
|
|
409
|
+
|
|
410
|
+
let progress = null
|
|
411
|
+
const bufferedProgress = []
|
|
412
|
+
let replyError = null
|
|
413
|
+
const replyPromise = adapter.reply(workMissionWakeEnvelope(wake, agent), {
|
|
414
|
+
onProgress: (event) => {
|
|
415
|
+
if (progress) {
|
|
416
|
+
progress.push(event)
|
|
417
|
+
} else {
|
|
418
|
+
bufferedProgress.push(event)
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
}).catch((error) => {
|
|
422
|
+
replyError = error
|
|
423
|
+
return { text: '' }
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
const inbound = await client.callTool('claim_work_mission_message', {
|
|
427
|
+
listener_session_id: sessionId,
|
|
428
|
+
delivery_id: deliveryId,
|
|
429
|
+
include_snapshot: false,
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
if (inbound.timed_out) {
|
|
433
|
+
await replyPromise.catch(() => {})
|
|
434
|
+
return { timed_out: true, replied: 0, skipped: 0, failed: 0 }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
timingLog(iterationStartedAt, `claimed wake delivery=${deliveryId}`)
|
|
438
|
+
|
|
439
|
+
progress = createProgressReporter({
|
|
440
|
+
client,
|
|
441
|
+
deliveryId,
|
|
442
|
+
sessionId,
|
|
443
|
+
iterationStartedAt,
|
|
444
|
+
})
|
|
445
|
+
for (const event of bufferedProgress) progress.push(event)
|
|
446
|
+
|
|
447
|
+
let text = ''
|
|
448
|
+
try {
|
|
449
|
+
const reply = await replyPromise
|
|
450
|
+
if (replyError) throw replyError
|
|
451
|
+
text = replyText(reply)
|
|
452
|
+
await progress.flushBeforeFinal()
|
|
453
|
+
timingLog(iterationStartedAt, `wake adapter completed delivery=${deliveryId}`)
|
|
454
|
+
} catch (error) {
|
|
455
|
+
const errorText = boundedError(error)
|
|
456
|
+
await progress.fail(errorText).catch(() => {})
|
|
457
|
+
await client.callTool('ack_work_mission_message', {
|
|
458
|
+
delivery_id: deliveryId,
|
|
459
|
+
listener_session_id: sessionId,
|
|
460
|
+
status: 'failed',
|
|
461
|
+
error: errorText,
|
|
462
|
+
}).catch(() => {})
|
|
463
|
+
console.error(`[foundr:work-missions] wake reply failed mission=${wake.mission_id} message=${messageId}: ${errorText}`)
|
|
464
|
+
return { replied: 0, skipped: 0, failed: 1 }
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!text) {
|
|
468
|
+
await client.callTool('ack_work_mission_message', {
|
|
469
|
+
delivery_id: deliveryId,
|
|
470
|
+
listener_session_id: sessionId,
|
|
471
|
+
status: 'skipped',
|
|
472
|
+
})
|
|
473
|
+
return { replied: 0, skipped: 1, failed: 0 }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
const sent = await completeWorkMissionReply({
|
|
478
|
+
client,
|
|
479
|
+
deliveryId,
|
|
480
|
+
sessionId,
|
|
481
|
+
missionId: wake.mission_id,
|
|
482
|
+
messageId,
|
|
483
|
+
text,
|
|
484
|
+
})
|
|
485
|
+
const responseMessageId = sent.message.id
|
|
486
|
+
timingLog(iterationStartedAt, `wake reply sent delivery=${deliveryId}`)
|
|
487
|
+
return { replied: 1, skipped: 0, failed: 0, message_id: responseMessageId }
|
|
488
|
+
} catch (error) {
|
|
489
|
+
progress.cancel()
|
|
490
|
+
const errorText = boundedError(error)
|
|
491
|
+
await client.callTool('ack_work_mission_message', {
|
|
492
|
+
delivery_id: deliveryId,
|
|
493
|
+
listener_session_id: sessionId,
|
|
494
|
+
status: 'failed',
|
|
495
|
+
error: errorText,
|
|
496
|
+
}).catch(() => {})
|
|
497
|
+
return { replied: 0, skipped: 0, failed: 1, error: errorText }
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export async function runWorkMissionListener({
|
|
502
|
+
client,
|
|
503
|
+
adapter,
|
|
504
|
+
clientName = CLIENT_NAME,
|
|
505
|
+
clientVersion = CLIENT_VERSION,
|
|
506
|
+
expectedAgentId,
|
|
507
|
+
pollMs = POLL_INTERVAL_MS,
|
|
508
|
+
publicMentionPollMs = PUBLIC_ROOM_MENTION_POLL_INTERVAL_MS,
|
|
509
|
+
transport = 'wake',
|
|
510
|
+
wakeSubscriptionFactory = createPrivateChatWakeSubscription,
|
|
511
|
+
}) {
|
|
512
|
+
const adapterName = adapter.name ?? 'unknown'
|
|
513
|
+
const heartbeat = await client.callTool('heartbeat_private_chat_listener', {
|
|
514
|
+
client_name: clientName,
|
|
515
|
+
client_version: clientVersion,
|
|
516
|
+
status: 'listening',
|
|
517
|
+
metadata: { adapter: adapterName, channels: ['work_missions', 'public_room_mentions'] },
|
|
518
|
+
})
|
|
519
|
+
if (expectedAgentId && heartbeat.agent?.id !== expectedAgentId) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
`Authenticated MCP token resolved to ${heartbeat.agent?.id ?? 'unknown'}; expected ${expectedAgentId}`,
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const sessionId = heartbeat.session_id
|
|
526
|
+
const agent = heartbeat.agent ?? null
|
|
527
|
+
let stopping = false
|
|
528
|
+
let fatalError = null
|
|
529
|
+
|
|
530
|
+
const sendHeartbeat = (status) => client.callTool('heartbeat_private_chat_listener', {
|
|
531
|
+
session_id: sessionId,
|
|
532
|
+
client_name: clientName,
|
|
533
|
+
client_version: clientVersion,
|
|
534
|
+
status,
|
|
535
|
+
metadata: { adapter: adapterName, channels: ['work_missions', 'public_room_mentions'] },
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
const heartbeatTimer = setInterval(() => {
|
|
539
|
+
if (!stopping) void sendHeartbeat('listening').catch(() => {})
|
|
540
|
+
}, HEARTBEAT_INTERVAL_MS)
|
|
541
|
+
|
|
542
|
+
const stop = async () => {
|
|
543
|
+
if (stopping) return
|
|
544
|
+
stopping = true
|
|
545
|
+
clearInterval(heartbeatTimer)
|
|
546
|
+
adapter.close?.()
|
|
547
|
+
await sendHeartbeat('stopped').catch(() => {})
|
|
548
|
+
await client.close?.().catch(() => {})
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let stopPromise = null
|
|
552
|
+
let resolveStopSignal
|
|
553
|
+
const stopSignal = new Promise((resolve) => {
|
|
554
|
+
resolveStopSignal = resolve
|
|
555
|
+
})
|
|
556
|
+
const requestStop = () => {
|
|
557
|
+
stopPromise ??= stop()
|
|
558
|
+
return stopPromise
|
|
559
|
+
}
|
|
560
|
+
const stopAndResolve = () => {
|
|
561
|
+
void requestStop().finally(() => {
|
|
562
|
+
resolveStopSignal()
|
|
563
|
+
})
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const wakeQueue = []
|
|
567
|
+
const publicMentionWakeQueue = []
|
|
568
|
+
let wakeHealthy = false
|
|
569
|
+
|
|
570
|
+
const pollOnce = async ({
|
|
571
|
+
allowFallback = true,
|
|
572
|
+
allowPublicMentions = true,
|
|
573
|
+
publicMentionsOnly = false,
|
|
574
|
+
} = {}) => {
|
|
575
|
+
if (pollOnce.polling) {
|
|
576
|
+
pollOnce.needsPoll = pollOnce.needsPoll || !publicMentionsOnly
|
|
577
|
+
pollOnce.allowFallback = pollOnce.allowFallback || allowFallback
|
|
578
|
+
pollOnce.needsPublicMentions = pollOnce.needsPublicMentions || allowPublicMentions
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
pollOnce.polling = true
|
|
582
|
+
pollOnce.allowFallback = allowFallback
|
|
583
|
+
pollOnce.allowPublicMentions = allowPublicMentions
|
|
584
|
+
pollOnce.publicMentionsOnly = publicMentionsOnly
|
|
585
|
+
try {
|
|
586
|
+
do {
|
|
587
|
+
const shouldRunFallback = pollOnce.allowFallback
|
|
588
|
+
const shouldRunPublicMentions = pollOnce.allowPublicMentions || pollOnce.needsPublicMentions
|
|
589
|
+
const shouldRunWorkMissions = !pollOnce.publicMentionsOnly
|
|
590
|
+
pollOnce.needsPoll = false
|
|
591
|
+
pollOnce.allowFallback = false
|
|
592
|
+
pollOnce.allowPublicMentions = false
|
|
593
|
+
pollOnce.publicMentionsOnly = false
|
|
594
|
+
pollOnce.needsPublicMentions = false
|
|
595
|
+
if (shouldRunWorkMissions) {
|
|
596
|
+
while (!stopping && wakeQueue.length > 0) {
|
|
597
|
+
await runOneWorkMissionWakeIteration({
|
|
598
|
+
client,
|
|
599
|
+
adapter,
|
|
600
|
+
agent,
|
|
601
|
+
sessionId,
|
|
602
|
+
wake: wakeQueue.shift(),
|
|
603
|
+
})
|
|
604
|
+
}
|
|
605
|
+
if (shouldRunFallback) {
|
|
606
|
+
while (!stopping) {
|
|
607
|
+
const result = await runOneWorkMissionIteration({
|
|
608
|
+
client,
|
|
609
|
+
adapter,
|
|
610
|
+
agent,
|
|
611
|
+
sessionId,
|
|
612
|
+
})
|
|
613
|
+
if (wakeQueue.length > 0) break
|
|
614
|
+
if (result.timed_out && !pollOnce.needsPoll) break
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (shouldRunPublicMentions && !stopping) {
|
|
619
|
+
while (!stopping && publicMentionWakeQueue.length > 0) {
|
|
620
|
+
await runOnePublicRoomMentionWakeIteration({
|
|
621
|
+
client,
|
|
622
|
+
adapter,
|
|
623
|
+
agent,
|
|
624
|
+
wake: publicMentionWakeQueue.shift(),
|
|
625
|
+
})
|
|
626
|
+
}
|
|
627
|
+
const result = await runOnePublicRoomMentionIteration({
|
|
628
|
+
client,
|
|
629
|
+
adapter,
|
|
630
|
+
agent,
|
|
631
|
+
})
|
|
632
|
+
if (!result.timed_out) {
|
|
633
|
+
pollOnce.needsPublicMentions = true
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} while (!stopping && (
|
|
637
|
+
pollOnce.needsPoll ||
|
|
638
|
+
pollOnce.allowFallback ||
|
|
639
|
+
pollOnce.needsPublicMentions ||
|
|
640
|
+
wakeQueue.length > 0 ||
|
|
641
|
+
publicMentionWakeQueue.length > 0
|
|
642
|
+
))
|
|
643
|
+
} finally {
|
|
644
|
+
pollOnce.polling = false
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
pollOnce.polling = false
|
|
648
|
+
pollOnce.needsPoll = false
|
|
649
|
+
pollOnce.allowFallback = false
|
|
650
|
+
pollOnce.allowPublicMentions = false
|
|
651
|
+
pollOnce.needsPublicMentions = false
|
|
652
|
+
pollOnce.publicMentionsOnly = false
|
|
653
|
+
|
|
654
|
+
process.once('SIGINT', stopAndResolve)
|
|
655
|
+
process.once('SIGTERM', stopAndResolve)
|
|
656
|
+
|
|
657
|
+
const launchPoll = (options) => {
|
|
658
|
+
void pollOnce(options).catch((error) => {
|
|
659
|
+
if (stopping) return
|
|
660
|
+
fatalError = error
|
|
661
|
+
void requestStop().finally(resolveStopSignal)
|
|
662
|
+
})
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const launchWake = (payload) => {
|
|
666
|
+
if (isWorkMissionWakePayload(payload)) wakeQueue.push(payload)
|
|
667
|
+
if (isPublicRoomMentionWakePayload(payload)) publicMentionWakeQueue.push(payload)
|
|
668
|
+
launchPoll({
|
|
669
|
+
allowFallback: false,
|
|
670
|
+
allowPublicMentions: isPublicRoomMentionWakePayload(payload),
|
|
671
|
+
publicMentionsOnly: isPublicRoomMentionWakePayload(payload),
|
|
672
|
+
})
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
let wakeSubscription = null
|
|
676
|
+
if (transport === 'wake' && heartbeat.wake) {
|
|
677
|
+
wakeSubscription = await wakeSubscriptionFactory({
|
|
678
|
+
wake: heartbeat.wake,
|
|
679
|
+
onWake: launchWake,
|
|
680
|
+
onStatus: (status) => {
|
|
681
|
+
if (status === 'SUBSCRIBED') {
|
|
682
|
+
wakeHealthy = true
|
|
683
|
+
}
|
|
684
|
+
if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT' || status === 'CLOSED') {
|
|
685
|
+
wakeHealthy = false
|
|
686
|
+
console.warn(`[foundr:work-missions] wake channel ${status}; fallback polling remains active`)
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
}).catch((error) => {
|
|
690
|
+
console.warn(`[foundr:work-missions] wake subscription unavailable: ${boundedError(error)}`)
|
|
691
|
+
return null
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
void adapter.start?.().catch((error) => {
|
|
696
|
+
if (!stopping) {
|
|
697
|
+
console.warn(`[foundr:work-missions] adapter warmup failed: ${boundedError(error)}`)
|
|
698
|
+
}
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
const fallbackPollMs = wakeSubscription ? wakeFallbackPollMs(pollMs) : pollMs
|
|
702
|
+
const pollTimer = setInterval(() => {
|
|
703
|
+
if (!stopping && (!wakeSubscription || !wakeHealthy)) launchPoll()
|
|
704
|
+
}, fallbackPollMs)
|
|
705
|
+
const publicMentionFallbackMs = wakeSubscription ? wakeFallbackPollMs(publicMentionPollMs) : publicMentionPollMs
|
|
706
|
+
const publicMentionTimer = setInterval(() => {
|
|
707
|
+
if (!stopping) {
|
|
708
|
+
launchPoll({
|
|
709
|
+
allowFallback: false,
|
|
710
|
+
allowPublicMentions: true,
|
|
711
|
+
publicMentionsOnly: true,
|
|
712
|
+
})
|
|
713
|
+
}
|
|
714
|
+
}, publicMentionFallbackMs)
|
|
715
|
+
publicMentionTimer.unref?.()
|
|
716
|
+
if (wakeSubscription) {
|
|
717
|
+
pollTimer.unref?.()
|
|
718
|
+
} else {
|
|
719
|
+
launchPoll()
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
try {
|
|
723
|
+
await stopSignal
|
|
724
|
+
if (fatalError) throw fatalError
|
|
725
|
+
} finally {
|
|
726
|
+
clearInterval(pollTimer)
|
|
727
|
+
clearInterval(publicMentionTimer)
|
|
728
|
+
await Promise.resolve(wakeSubscription?.close?.()).catch(() => {})
|
|
729
|
+
process.off('SIGINT', stopAndResolve)
|
|
730
|
+
process.off('SIGTERM', stopAndResolve)
|
|
731
|
+
if (stopPromise) {
|
|
732
|
+
await stopPromise
|
|
733
|
+
} else if (!stopping) {
|
|
734
|
+
await requestStop()
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|