mercury-agent 0.16.0 → 0.16.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/container/Dockerfile +6 -1
- package/container/Dockerfile.base +6 -1
- package/package.json +1 -1
- package/src/adapters/whatsapp.ts +55 -12
- package/src/bridges/whatsapp.ts +6 -4
- package/src/core/handler.ts +38 -5
package/container/Dockerfile
CHANGED
|
@@ -95,7 +95,12 @@ RUN bun install --production
|
|
|
95
95
|
# but before the volatile /app source COPYs below — so this expensive `chown -R`
|
|
96
96
|
# (it walks the whole Chromium + .bun tree) lands in a stable cached layer.
|
|
97
97
|
# Editing source files no longer invalidates it or forces the huge layer re-export.
|
|
98
|
-
|
|
98
|
+
# `mkdir` here rather than leaving it to Docker: the host mounts the global dir
|
|
99
|
+
# into PI_CODING_AGENT_DIR entry by entry, so Docker would create this dir as
|
|
100
|
+
# the mount parent and own it as root — and pi 0.84's credential store writes
|
|
101
|
+
# an empty auth.json on its first *read*, which then fails EACCES and takes
|
|
102
|
+
# every model leg down with it. Folded into the chown so it shares the layer.
|
|
103
|
+
RUN mkdir -p /home/mercury/.pi/agent && chown -R mercury:mercury /home/mercury
|
|
99
104
|
|
|
100
105
|
COPY src/agent/container-entry.ts /app/src/agent/container-entry.ts
|
|
101
106
|
COPY src/agent/model-capabilities-core.ts /app/src/agent/model-capabilities-core.ts
|
|
@@ -94,7 +94,12 @@ RUN echo '#!/bin/sh\nbun run /app/src/cli/mrctl.ts "$@"' > /usr/local/bin/mrctl
|
|
|
94
94
|
chmod +x /usr/local/bin/mrctl
|
|
95
95
|
|
|
96
96
|
# Fix ownership of all mercury home dir artifacts before switching user
|
|
97
|
-
|
|
97
|
+
# `mkdir` here rather than leaving it to Docker: the host mounts the global dir
|
|
98
|
+
# into PI_CODING_AGENT_DIR entry by entry, so Docker would create this dir as
|
|
99
|
+
# the mount parent and own it as root — and pi 0.84's credential store writes
|
|
100
|
+
# an empty auth.json on its first *read*, which then fails EACCES and takes
|
|
101
|
+
# every model leg down with it. Folded into the chown so it shares the layer.
|
|
102
|
+
RUN mkdir -p /home/mercury/.pi/agent && chown -R mercury:mercury /home/mercury
|
|
98
103
|
|
|
99
104
|
USER mercury
|
|
100
105
|
|
package/package.json
CHANGED
package/src/adapters/whatsapp.ts
CHANGED
|
@@ -421,18 +421,28 @@ export class WhatsAppBaileysAdapter
|
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
if (!this.connected || !this.sock) {
|
|
424
|
-
this.
|
|
425
|
-
logger.warn("WhatsApp queued outbound", {
|
|
426
|
-
chatJid,
|
|
427
|
-
queueSize: this.outgoingQueue.length,
|
|
428
|
-
});
|
|
429
|
-
return { id: `queued-${Date.now()}`, threadId, raw: {} };
|
|
424
|
+
return this.queueOutbound(threadId, chatJid, text, "not connected");
|
|
430
425
|
}
|
|
431
426
|
|
|
432
427
|
logger.info("WhatsApp outbound", { chatJid, preview: text.slice(0, 120) });
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
428
|
+
let sent: WAMessage | undefined;
|
|
429
|
+
try {
|
|
430
|
+
sent = await this.sock.sendMessage(chatJid, {
|
|
431
|
+
text: applyRtlDirection(normalizeChatMarkdown(text)),
|
|
432
|
+
});
|
|
433
|
+
} catch (err) {
|
|
434
|
+
// The `connected` check above is a pre-flight guard, not a failure
|
|
435
|
+
// handler: `connection.update` fires asynchronously, so a socket that
|
|
436
|
+
// dies mid-send throws here with the flag still true. Queueing on the
|
|
437
|
+
// throw is what actually survives a drop — without it the reply is lost
|
|
438
|
+
// for good, and the caller's dangling ⏳ status message never clears.
|
|
439
|
+
return this.queueOutbound(
|
|
440
|
+
threadId,
|
|
441
|
+
chatJid,
|
|
442
|
+
text,
|
|
443
|
+
err instanceof Error ? err.message : String(err),
|
|
444
|
+
);
|
|
445
|
+
}
|
|
436
446
|
if (!sent) {
|
|
437
447
|
throw new Error("WhatsApp sendMessage returned no message");
|
|
438
448
|
}
|
|
@@ -443,6 +453,26 @@ export class WhatsAppBaileysAdapter
|
|
|
443
453
|
};
|
|
444
454
|
}
|
|
445
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Park an outbound text for `flushOutgoingQueue` to replay once the socket
|
|
458
|
+
* reopens. Returns a synthetic id: the send has not happened yet, so there
|
|
459
|
+
* is no platform message id to record against it.
|
|
460
|
+
*/
|
|
461
|
+
private queueOutbound(
|
|
462
|
+
threadId: string,
|
|
463
|
+
chatJid: string,
|
|
464
|
+
text: string,
|
|
465
|
+
reason: string,
|
|
466
|
+
): RawMessage<proto.IWebMessageInfo> {
|
|
467
|
+
this.outgoingQueue.push({ jid: chatJid, text });
|
|
468
|
+
logger.warn("WhatsApp queued outbound", {
|
|
469
|
+
chatJid,
|
|
470
|
+
reason,
|
|
471
|
+
queueSize: this.outgoingQueue.length,
|
|
472
|
+
});
|
|
473
|
+
return { id: `queued-${Date.now()}`, threadId, raw: {} };
|
|
474
|
+
}
|
|
475
|
+
|
|
446
476
|
async editMessage(
|
|
447
477
|
_threadId: string,
|
|
448
478
|
_messageId: string,
|
|
@@ -762,9 +792,22 @@ export class WhatsAppBaileysAdapter
|
|
|
762
792
|
while (this.outgoingQueue.length > 0) {
|
|
763
793
|
const item = this.outgoingQueue.shift();
|
|
764
794
|
if (!item) continue;
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
795
|
+
try {
|
|
796
|
+
await this.sock.sendMessage(item.jid, {
|
|
797
|
+
text: applyRtlDirection(normalizeChatMarkdown(item.text)),
|
|
798
|
+
});
|
|
799
|
+
} catch (err) {
|
|
800
|
+
// Per item, not per flush: an unsendable message (or a socket that
|
|
801
|
+
// dropped again mid-drain) must not discard everything still queued
|
|
802
|
+
// behind it. The item itself is already shifted off — dropping one
|
|
803
|
+
// is the cost of not blocking the rest, and it is logged.
|
|
804
|
+
logger.error("WhatsApp queued outbound failed, dropping", {
|
|
805
|
+
chatJid: item.jid,
|
|
806
|
+
remaining: this.outgoingQueue.length,
|
|
807
|
+
error: err instanceof Error ? err.message : String(err),
|
|
808
|
+
});
|
|
809
|
+
if (!this.connected || !this.sock) return;
|
|
810
|
+
}
|
|
768
811
|
}
|
|
769
812
|
} finally {
|
|
770
813
|
this.flushing = false;
|
package/src/bridges/whatsapp.ts
CHANGED
|
@@ -297,10 +297,12 @@ export class WhatsAppBridge implements PlatformBridge {
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
if (!textSent) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
// Through the adapter, not the raw socket: `postMessage` owns the
|
|
301
|
+
// queue-and-flush-on-reconnect path, and reaching around it is what let
|
|
302
|
+
// a mid-run disconnect destroy the whole reply — text included — while
|
|
303
|
+
// text-only replies in other spaces survived the same drop.
|
|
304
|
+
const sent = await this.adapter.postMessage(threadId, text);
|
|
305
|
+
if (sent.id) lastSentId = sent.id;
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
return lastSentId;
|
package/src/core/handler.ts
CHANGED
|
@@ -185,11 +185,44 @@ export function createMessageHandler(opts: MessageHandlerOptions) {
|
|
|
185
185
|
? `${reply}\n\n_(responded in ${elapsed}s)_`
|
|
186
186
|
: reply;
|
|
187
187
|
if (finalReply || files.length > 0) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
// Guarded, because this await used to be the one unprotected step
|
|
189
|
+
// between a finished run and the user: the try/catch above covers
|
|
190
|
+
// `handleRawInput` only, so a delivery throw escaped `processIngress`
|
|
191
|
+
// entirely and surfaced as a bare "Debounce flush error" — with the ⏳
|
|
192
|
+
// status message left dangling (its best-effort delete had already
|
|
193
|
+
// failed against the same dead socket). The run is done and the reply
|
|
194
|
+
// is already stored; losing the transport must not read as "still
|
|
195
|
+
// thinking".
|
|
196
|
+
let sentPlatformId: string | undefined;
|
|
197
|
+
try {
|
|
198
|
+
sentPlatformId = await bridge.sendReply(
|
|
199
|
+
threadId,
|
|
200
|
+
finalReply,
|
|
201
|
+
files.length > 0 ? files : undefined,
|
|
202
|
+
);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
logger.error("Failed to deliver reply", {
|
|
205
|
+
platform: bridge.platform,
|
|
206
|
+
threadId,
|
|
207
|
+
spaceId: ingress.spaceId,
|
|
208
|
+
fileCount: files.length,
|
|
209
|
+
error:
|
|
210
|
+
err instanceof Error ? (err.stack ?? err.message) : String(err),
|
|
211
|
+
});
|
|
212
|
+
// Retry the text alone. On WhatsApp this lands in the adapter's
|
|
213
|
+
// outbound queue and is replayed on reconnect, so the human gets the
|
|
214
|
+
// answer even when the attachments are gone; the files stay in
|
|
215
|
+
// `outbox/` for retrieval.
|
|
216
|
+
if (finalReply) {
|
|
217
|
+
await bridge.sendReply(threadId, finalReply).catch((e) =>
|
|
218
|
+
logger.error("Failed to deliver reply text after retry", {
|
|
219
|
+
threadId,
|
|
220
|
+
error: e instanceof Error ? e.message : String(e),
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
193
226
|
|
|
194
227
|
if (
|
|
195
228
|
sentPlatformId &&
|