openmausbot 0.1.74 → 0.1.76
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/assets/index-BtT047dk.css +1 -0
- package/dist/assets/{index-CwoQdM0z.js → index-C6blGSq5.js} +1 -1
- package/dist/assets/index-fKXFpB_J.js +305 -0
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +6 -2
- package/dist-server/drivers/agents-proxy.js +133 -11
- package/dist-server/index.js +3379 -1431
- package/dist-server/mcp-gate.js +28 -18
- package/dist-server/openmausbot.js +557 -325
- package/dist-server/pair-cli.js +557 -325
- package/dist-server/permission-proxy.js +119 -14
- package/dist-server/server/agent-tool-policy.js +1 -0
- package/dist-server/server/atomic.js +37 -1
- package/dist-server/server/auto-approve.js +15 -0
- package/dist-server/server/bot-overview.js +5 -1
- package/dist-server/server/chief-of-staff.js +17 -12
- package/dist-server/server/delegations.js +177 -63
- package/dist-server/server/drivers/acp/core.js +2 -1
- package/dist-server/server/drivers/acp/grok.js +12 -1
- package/dist-server/server/drivers/acp/hermes.js +21 -3
- package/dist-server/server/drivers/agents-proxy.js +113 -10
- package/dist-server/server/drivers/claude.js +91 -17
- package/dist-server/server/drivers/codex.js +15 -4
- package/dist-server/server/drivers/openai-chat.js +90 -67
- package/dist-server/server/drivers/openai-compat.js +9 -1
- package/dist-server/server/drivers/pi.js +18 -3
- package/dist-server/server/drivers/retry.js +4 -0
- package/dist-server/server/env-path.js +18 -17
- package/dist-server/server/index.js +1172 -302
- package/dist-server/server/message-db.js +21 -0
- package/dist-server/server/peer-roster.js +48 -9
- package/dist-server/server/permission-proxy.js +90 -15
- package/dist-server/server/procs.js +19 -3
- package/dist-server/server/room-handoffs.js +237 -0
- package/dist-server/server/section-context.js +64 -14
- package/dist-server/server/setup-mode.js +6 -7
- package/dist-server/server/store.js +266 -8
- package/dist-server/server/surface.js +16 -5
- package/dist-server/server/team-backup.js +17 -1
- package/dist-server/server/team-setup-requests.js +252 -0
- package/dist-server/server/thread-events.js +10 -1
- package/dist-server/server/turn-dispatch-guard.js +31 -3
- package/dist-server/server/workspace.js +13 -0
- package/dist-server/shared/approval-mode.js +7 -0
- package/dist-server/shared/ask-question.js +172 -0
- package/dist-server/shared/provider-safety.js +6 -0
- package/dist-server/shared/team-backup.js +1 -0
- package/dist-server/shared/team-setup.js +1 -0
- package/package.json +1 -1
- package/dist/assets/index-BRLHsQ5g.js +0 -298
- package/dist/assets/index-ByLQ7a3F.css +0 -1
|
@@ -17,8 +17,7 @@ import { getOrCreateChannel, mirrorExchange } from "./comms-visibility.js";
|
|
|
17
17
|
import { DATA_DIR } from "./config.js";
|
|
18
18
|
import { newId } from "./contracts.js";
|
|
19
19
|
import { requestPeerApproval } from "./peer-approval.js";
|
|
20
|
-
import { peerAllowed } from "./peer-roster.js";
|
|
21
|
-
import { sectionKey } from "./store.js";
|
|
20
|
+
import { canAccessTeam, peerAllowed } from "./peer-roster.js";
|
|
22
21
|
/** Per source-thread queue. Persisted to delegations.json on every change
|
|
23
22
|
* and reloaded at boot: a handoff queued right before a restart runs after
|
|
24
23
|
* it. (Provider PERMISSIONS still die with the process — nobody can answer
|
|
@@ -35,7 +34,12 @@ const RECEIPTS_FILE = join(DATA_DIR, "delegation-receipts.json");
|
|
|
35
34
|
const MAX_RECEIPTS = 100;
|
|
36
35
|
const RECEIPT_MAX_AGE_MS = 48 * 60 * 60 * 1000;
|
|
37
36
|
const RESULT_MAX_CHARS = 4_000;
|
|
38
|
-
|
|
37
|
+
/** A busy handoff's delivery window. An available target may still pick up
|
|
38
|
+
* an overdue item. At restart, elapsed windows are renewed before any boot
|
|
39
|
+
* dispatch, so the first recovered job cannot cause the remaining backlog
|
|
40
|
+
* to expire. This is not an uptime clock: sleep within a running process
|
|
41
|
+
* still counts, and a non-expired restored window keeps its deadline. */
|
|
42
|
+
export const DELEGATION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
39
43
|
let receipts = [];
|
|
40
44
|
function saveReceipts() {
|
|
41
45
|
try {
|
|
@@ -72,7 +76,7 @@ export function pendingDelegationInfo(id) {
|
|
|
72
76
|
for (const [sourceThreadId, items] of pendingDelegations) {
|
|
73
77
|
const item = items.find((candidate) => candidate.id === id);
|
|
74
78
|
if (item)
|
|
75
|
-
return { sourceThreadId, toBotId: item.toBotId,
|
|
79
|
+
return { sourceThreadId, toBotId: item.toBotId, queuedAt: item.queuedAt, waiting: item.waitingOnBusy === true };
|
|
76
80
|
}
|
|
77
81
|
return null;
|
|
78
82
|
}
|
|
@@ -85,8 +89,10 @@ export function threadsWaitingOn(toBotId) {
|
|
|
85
89
|
.map(([threadId]) => threadId);
|
|
86
90
|
}
|
|
87
91
|
/** Mark a target's observed busy period as finished and return the source
|
|
88
|
-
* threads that should be retried.
|
|
89
|
-
*
|
|
92
|
+
* threads that should be retried. A handoff waits until the target is free,
|
|
93
|
+
* bounded only by the 24-hour expiry — this just clears the "parked on a
|
|
94
|
+
* busy period" marker so the next drain re-evaluates it, rather than
|
|
95
|
+
* counting or limiting retries.
|
|
90
96
|
* `only` narrows the release: a bot that is still busy in one thread has
|
|
91
97
|
* nevertheless freed a slot for the fresh-thread handoffs waiting on it,
|
|
92
98
|
* while its active-thread handoffs go on waiting for it to go idle. */
|
|
@@ -118,8 +124,10 @@ function savePending() {
|
|
|
118
124
|
/** Load what a previous process left queued. Missing or corrupt → empty. */
|
|
119
125
|
export function _loadPending() {
|
|
120
126
|
pendingDelegations.clear();
|
|
127
|
+
let backfilled = false;
|
|
121
128
|
try {
|
|
122
129
|
const raw = JSON.parse(readFileSync(DELEGATIONS_FILE, "utf8"));
|
|
130
|
+
const now = Date.now();
|
|
123
131
|
for (const [threadId, list] of Object.entries(raw)) {
|
|
124
132
|
if (!Array.isArray(list))
|
|
125
133
|
continue;
|
|
@@ -131,6 +139,20 @@ export function _loadPending() {
|
|
|
131
139
|
typeof item.message !== "string" ||
|
|
132
140
|
!Number.isFinite(item.depth))
|
|
133
141
|
return [];
|
|
142
|
+
// Restore every elapsed window before boot dispatch. Merely checking
|
|
143
|
+
// whether the target is idle loses the second old job as soon as the
|
|
144
|
+
// first recovered job occupies it. Invalid/legacy timestamps get the
|
|
145
|
+
// same fresh window; valid, unexpired windows keep their deadline.
|
|
146
|
+
const hasUsableQueuedAt = Number.isFinite(item.queuedAt) && item.queuedAt <= now &&
|
|
147
|
+
now - item.queuedAt < DELEGATION_TTL_MS;
|
|
148
|
+
if (!hasUsableQueuedAt)
|
|
149
|
+
backfilled = true;
|
|
150
|
+
// A legacy item saved with attempts >= 1 already posted its old
|
|
151
|
+
// "retry n/3" chip under the since-removed bounded-retry scheme; load
|
|
152
|
+
// it as already announced so it doesn't post a second waiting chip
|
|
153
|
+
// the first time this queue drains.
|
|
154
|
+
const legacyAttempts = value.attempts;
|
|
155
|
+
const legacyAlreadyAnnounced = typeof legacyAttempts === "number" && Number.isFinite(legacyAttempts) && legacyAttempts > 0;
|
|
134
156
|
const loaded = {
|
|
135
157
|
id: typeof item.id === "string" && item.id ? item.id : newId(),
|
|
136
158
|
sourceBotId: typeof item.sourceBotId === "string" && item.sourceBotId ? item.sourceBotId : "",
|
|
@@ -138,12 +160,14 @@ export function _loadPending() {
|
|
|
138
160
|
message: item.message,
|
|
139
161
|
...(typeof item.reason === "string" ? { reason: item.reason } : {}),
|
|
140
162
|
depth: Math.max(0, Math.trunc(item.depth)),
|
|
141
|
-
|
|
163
|
+
queuedAt: hasUsableQueuedAt ? Math.min(item.queuedAt, now) : now,
|
|
142
164
|
};
|
|
143
165
|
if (item.approvalAlreadyGranted === true)
|
|
144
166
|
loaded.approvalAlreadyGranted = true;
|
|
145
167
|
if (item.waitingOnBusy === true)
|
|
146
168
|
loaded.waitingOnBusy = true;
|
|
169
|
+
if (item.waitAnnounced === true || legacyAlreadyAnnounced)
|
|
170
|
+
loaded.waitAnnounced = true;
|
|
147
171
|
if (typeof item.originatingGroupId === "string" && item.originatingGroupId) {
|
|
148
172
|
loaded.originatingGroupId = item.originatingGroupId;
|
|
149
173
|
}
|
|
@@ -159,6 +183,10 @@ export function _loadPending() {
|
|
|
159
183
|
catch {
|
|
160
184
|
/* fresh install, or unreadable — start empty */
|
|
161
185
|
}
|
|
186
|
+
// Persist repaired/renewed windows before dispatch. A quick restart loop
|
|
187
|
+
// must retain that still-valid deadline, not renew it on every load.
|
|
188
|
+
if (backfilled)
|
|
189
|
+
savePending();
|
|
162
190
|
receipts = [];
|
|
163
191
|
try {
|
|
164
192
|
const rawReceipts = JSON.parse(readFileSync(RECEIPTS_FILE, "utf8"));
|
|
@@ -239,7 +267,7 @@ export function queueDelegation(bus, from, item, maxDepth, sourceThreadId = from
|
|
|
239
267
|
? originatingGroup.id
|
|
240
268
|
: undefined;
|
|
241
269
|
const id = newId();
|
|
242
|
-
list.push({ ...item, id, sourceBotId: from.id,
|
|
270
|
+
list.push({ ...item, id, sourceBotId: from.id, queuedAt: Date.now(), ...(groupId ? { originatingGroupId: groupId } : {}) });
|
|
243
271
|
pendingDelegations.set(sourceThreadId, list);
|
|
244
272
|
savePending();
|
|
245
273
|
const sourceGroup = sourceThreadId ? bus.store.groupByThread(sourceThreadId) : undefined;
|
|
@@ -332,8 +360,9 @@ onSettled) {
|
|
|
332
360
|
}
|
|
333
361
|
}
|
|
334
362
|
finally {
|
|
335
|
-
// A requeued item (busy
|
|
336
|
-
// that the target's own settling
|
|
363
|
+
// A requeued item (target still busy, waiting until it's free or the
|
|
364
|
+
// 24-hour expiry) stays for the drain that the target's own settling
|
|
365
|
+
// turn will trigger.
|
|
337
366
|
const stillQueued = pendingDelegations.get(threadId)?.some((candidate) => candidate.id === item.id);
|
|
338
367
|
if (outcome !== "requeued")
|
|
339
368
|
acknowledgeDelegation(threadId, item.id);
|
|
@@ -353,8 +382,8 @@ onSettled) {
|
|
|
353
382
|
drainingThreads.delete(threadId);
|
|
354
383
|
// A later turn may have queued and settled while this thread was
|
|
355
384
|
// waiting for approval. Only items OUTSIDE our snapshot warrant a fresh
|
|
356
|
-
// drain — re-draining a just-requeued item would
|
|
357
|
-
//
|
|
385
|
+
// drain — re-draining a just-requeued item would spin it in a tight
|
|
386
|
+
// loop instead of once per target settle.
|
|
358
387
|
const redrainRequested = queuedRedrains.delete(threadId);
|
|
359
388
|
const snapshotIds = new Set(snapshot.map((item) => item.id));
|
|
360
389
|
const hasNewItems = pendingDelegations.get(threadId)?.some((item) => !snapshotIds.has(item.id)) ?? false;
|
|
@@ -375,6 +404,85 @@ function acknowledgeDelegation(threadId, itemId) {
|
|
|
375
404
|
pendingDelegations.delete(threadId);
|
|
376
405
|
savePending();
|
|
377
406
|
}
|
|
407
|
+
const isExpired = (item, now) => now - item.queuedAt >= DELEGATION_TTL_MS;
|
|
408
|
+
/** Record an expired handoff. The chip goes into the source thread only
|
|
409
|
+
* while it still belongs to the bot that owns the handoff — a deleted
|
|
410
|
+
* conversation gets the receipt and nothing else. */
|
|
411
|
+
function expireDelegation(bus, sourceThreadId, item, ownerId) {
|
|
412
|
+
const name = bus.store.bot(item.toBotId)?.name ?? item.toBotId;
|
|
413
|
+
recordDelegationReceipt({
|
|
414
|
+
id: item.id,
|
|
415
|
+
sourceThreadId,
|
|
416
|
+
toBotId: item.toBotId,
|
|
417
|
+
toBotName: name,
|
|
418
|
+
status: "expired",
|
|
419
|
+
result: `@${name} was not free to take this for 24 hours`,
|
|
420
|
+
});
|
|
421
|
+
if (!sourceThreadBelongsToBot(bus.store, ownerId, sourceThreadId))
|
|
422
|
+
return;
|
|
423
|
+
bus.store.appendMessage(sourceThreadId, {
|
|
424
|
+
role: "bot",
|
|
425
|
+
kind: "activity",
|
|
426
|
+
tool: { name: `Delegation to @${name} expired — not picked up within 24 hours`, ok: false },
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
/** Past its 24 hours AND unable to be delivered right now — the same rule
|
|
430
|
+
* `processOne` applies, so the hourly sweep and a live drain never disagree
|
|
431
|
+
* about which items are actually stuck. A target that was deleted counts as
|
|
432
|
+
* "cannot take the turn": there is nothing to wait on, so such items still
|
|
433
|
+
* expire even though there is no bot left to test busy/free against. */
|
|
434
|
+
function isDueForExpiry(bus, item, now) {
|
|
435
|
+
if (!isExpired(item, now))
|
|
436
|
+
return false;
|
|
437
|
+
const target = bus.store.bot(item.toBotId);
|
|
438
|
+
if (!target)
|
|
439
|
+
return true;
|
|
440
|
+
return !targetCanTakeTurn(bus, target, item);
|
|
441
|
+
}
|
|
442
|
+
/** Expire every queued handoff past DELEGATION_TTL_MS that still cannot be
|
|
443
|
+
* delivered, wherever it waits. A drain already expires what it touches;
|
|
444
|
+
* this covers the handoff nothing drains — a target that never settles
|
|
445
|
+
* while its source sits idle. A thread mid-drain is skipped: that drain
|
|
446
|
+
* owns its items and expires them itself. An item whose target could take
|
|
447
|
+
* the turn right now is left queued instead — the next drain on its source
|
|
448
|
+
* thread (or the next `retryDelegationsWaitingOn`) delivers it; the sweep
|
|
449
|
+
* only cleans up what is genuinely stuck. Each expiry is reported through
|
|
450
|
+
* `onSettled`, the same hook a drain uses to wake the delegating bot.
|
|
451
|
+
* Returns how many expired. */
|
|
452
|
+
export function expireStaleDelegations(bus, now, onSettled) {
|
|
453
|
+
const expired = [];
|
|
454
|
+
for (const [threadId, items] of pendingDelegations) {
|
|
455
|
+
if (drainingThreads.has(threadId))
|
|
456
|
+
continue;
|
|
457
|
+
const due = items.filter((item) => isDueForExpiry(bus, item, now));
|
|
458
|
+
if (!due.length)
|
|
459
|
+
continue;
|
|
460
|
+
const remaining = items.filter((item) => !isDueForExpiry(bus, item, now));
|
|
461
|
+
if (remaining.length)
|
|
462
|
+
pendingDelegations.set(threadId, remaining);
|
|
463
|
+
else
|
|
464
|
+
pendingDelegations.delete(threadId);
|
|
465
|
+
const ownerId = bus.store.botByThread(threadId)?.id;
|
|
466
|
+
for (const item of due) {
|
|
467
|
+
expireDelegation(bus, threadId, item, ownerId ?? item.sourceBotId);
|
|
468
|
+
const receipt = findDelegationReceipt(item.id);
|
|
469
|
+
if (receipt)
|
|
470
|
+
expired.push(receipt);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (!expired.length)
|
|
474
|
+
return 0;
|
|
475
|
+
savePending();
|
|
476
|
+
for (const receipt of expired) {
|
|
477
|
+
try {
|
|
478
|
+
onSettled?.(receipt);
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
console.error("delegation expired but its source could not be resumed", error);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return expired.length;
|
|
485
|
+
}
|
|
378
486
|
/** Drop a thread's queued handoffs without running them, telling the user
|
|
379
487
|
* they were dropped. Used when the queueing turn failed or was interrupted. */
|
|
380
488
|
export function discardDelegations(bus, threadId) {
|
|
@@ -441,7 +549,17 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
441
549
|
if (dropIfThreadGone(bus, target, sourceThreadId, item)) {
|
|
442
550
|
return "settled";
|
|
443
551
|
}
|
|
444
|
-
|
|
552
|
+
// Past its 24 hours AND the target still cannot take the turn: this is the
|
|
553
|
+
// bound on a busy wait. Use the same free/busy test as holdWhileTargetBusy;
|
|
554
|
+
// an available target gets even an overdue item. Restart recovery renews
|
|
555
|
+
// elapsed windows in _loadPending before any target becomes busy. Decide
|
|
556
|
+
// before announcing a wait so an item cannot post both chips in one pass.
|
|
557
|
+
const canTakeTurn = targetCanTakeTurn(bus, target, item);
|
|
558
|
+
if (!canTakeTurn && isExpired(item, Date.now())) {
|
|
559
|
+
expireDelegation(bus, sourceThreadId, item, sender.id);
|
|
560
|
+
return "settled";
|
|
561
|
+
}
|
|
562
|
+
const held = holdWhileTargetBusy(bus, target, sourceThreadId, item, canTakeTurn);
|
|
445
563
|
if (held)
|
|
446
564
|
return held;
|
|
447
565
|
if (item.waitingOnBusy) {
|
|
@@ -494,7 +612,15 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
494
612
|
if (dropIfThreadGone(bus, current, sourceThreadId, item)) {
|
|
495
613
|
return "settled";
|
|
496
614
|
}
|
|
497
|
-
|
|
615
|
+
// Approval may have waited for minutes (or, with approvalAlreadyGranted,
|
|
616
|
+
// up to 24h since the original ask_bot approval) — recheck the same
|
|
617
|
+
// free/busy-gated expiry as the pre-approval path before dispatching.
|
|
618
|
+
const canTakeTurnAfterApproval = targetCanTakeTurn(bus, current, item);
|
|
619
|
+
if (!canTakeTurnAfterApproval && isExpired(item, Date.now())) {
|
|
620
|
+
expireDelegation(bus, sourceThreadId, item, currentSender.id);
|
|
621
|
+
return "settled";
|
|
622
|
+
}
|
|
623
|
+
const heldAfterApproval = holdWhileTargetBusy(bus, current, sourceThreadId, item, canTakeTurnAfterApproval);
|
|
498
624
|
if (heldAfterApproval)
|
|
499
625
|
return heldAfterApproval;
|
|
500
626
|
sender = currentSender;
|
|
@@ -516,62 +642,50 @@ async function processOne(bus, approvalBus, from, sourceThreadId, item, runTarge
|
|
|
516
642
|
await runTarget(item.toBotId, prefixed, item.depth + 1, sourceThreadId, channel, item.id, sender.id, item.targetThreadId);
|
|
517
643
|
return "dispatched";
|
|
518
644
|
}
|
|
519
|
-
/**
|
|
520
|
-
* turn will run: a classic delegation lands in the
|
|
521
|
-
* so it
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
role: "bot",
|
|
540
|
-
kind: "activity",
|
|
541
|
-
tool: { name: `Thread #${title} on @${target.name} waiting for a free slot` },
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
return "requeued";
|
|
545
|
-
}
|
|
546
|
-
if (!target.busy)
|
|
645
|
+
/** "Is the target free to take this handoff right now?" What "busy" means
|
|
646
|
+
* depends on where the turn will run: a classic delegation lands in the
|
|
647
|
+
* target's active thread, so it needs the bot to be idle; a fresh-thread
|
|
648
|
+
* handoff needs only a free slot. This is the single free/busy test shared
|
|
649
|
+
* by the expiry decision in `processOne` and the hold decision below, so
|
|
650
|
+
* the two can never disagree about whether a handoff could have been
|
|
651
|
+
* delivered right now. */
|
|
652
|
+
function targetCanTakeTurn(bus, target, item) {
|
|
653
|
+
return item.targetThreadId
|
|
654
|
+
? (bus.threadSlotFree ? bus.threadSlotFree(target.id) : !target.busy)
|
|
655
|
+
: !target.busy;
|
|
656
|
+
}
|
|
657
|
+
/** A busy target holds the handoff. Neither counts busy periods — the only
|
|
658
|
+
* bound is DELEGATION_TTL_MS, checked in processOne before this runs (using
|
|
659
|
+
* the same `targetCanTakeTurn` test, passed in as `canTakeTurn` when the
|
|
660
|
+
* caller already computed it so the two checks can't disagree). One waiting
|
|
661
|
+
* chip per handoff, worded for what the target is actually doing. Returns
|
|
662
|
+
* null when the target can take the turn now. */
|
|
663
|
+
function holdWhileTargetBusy(bus, target, sourceThreadId, item, canTakeTurn = targetCanTakeTurn(bus, target, item)) {
|
|
664
|
+
if (canTakeTurn)
|
|
547
665
|
return null;
|
|
548
666
|
if (item.waitingOnBusy)
|
|
549
667
|
return "requeued";
|
|
550
|
-
item.attempts += 1;
|
|
551
668
|
item.waitingOnBusy = true;
|
|
552
|
-
if (item.
|
|
553
|
-
|
|
669
|
+
if (!item.waitAnnounced) {
|
|
670
|
+
item.waitAnnounced = true;
|
|
554
671
|
bus.store.appendMessage(sourceThreadId, {
|
|
555
672
|
role: "bot",
|
|
556
673
|
kind: "activity",
|
|
557
|
-
tool: { name:
|
|
674
|
+
tool: { name: waitingChipText(bus.store, target, item) },
|
|
558
675
|
});
|
|
559
|
-
return "requeued";
|
|
560
676
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
});
|
|
574
|
-
return "settled";
|
|
677
|
+
savePending();
|
|
678
|
+
return "requeued";
|
|
679
|
+
}
|
|
680
|
+
function waitingChipText(store, target, item) {
|
|
681
|
+
if (item.targetThreadId) {
|
|
682
|
+
const title = store.taskByThread(target.id, item.targetThreadId)?.title ?? "thread";
|
|
683
|
+
return `Thread #${title} on @${target.name} waiting for a free slot`;
|
|
684
|
+
}
|
|
685
|
+
if (target.activity === "waiting-on-you") {
|
|
686
|
+
return `Waiting for @${target.name}, who's waiting on you — it'll go through after you answer`;
|
|
687
|
+
}
|
|
688
|
+
return `Delegation to @${target.name} waiting — they're busy; it'll go through when they're free`;
|
|
575
689
|
}
|
|
576
690
|
/** The thread a fresh-thread handoff was opened in may be deleted while the
|
|
577
691
|
* handoff waits. There is nowhere for the turn to run then, and running it
|
|
@@ -609,8 +723,8 @@ function sourceThreadBelongsToBot(store, botId, threadId) {
|
|
|
609
723
|
* was queued must be checked again at the final dispatch edge — the user may
|
|
610
724
|
* have moved either bot, or narrowed the sender's peers, in between. */
|
|
611
725
|
function dropIfUnreachable(bus, sender, target, sourceThreadId, item) {
|
|
612
|
-
const sectionsDiffer =
|
|
613
|
-
if (!sectionsDiffer && peerAllowed(sender, target.id))
|
|
726
|
+
const sectionsDiffer = !canAccessTeam(sender, target.section);
|
|
727
|
+
if (!sectionsDiffer && !target.hidden && peerAllowed(sender, target.id))
|
|
614
728
|
return false;
|
|
615
729
|
const reason = sectionsDiffer
|
|
616
730
|
? "bots now belong to different sections"
|
|
@@ -745,7 +745,8 @@ export function createAcpDriver(support) {
|
|
|
745
745
|
}
|
|
746
746
|
}
|
|
747
747
|
const images = turn.images ?? [];
|
|
748
|
-
const runtimeAcceptsImages = init?.agentCapabilities?.promptCapabilities?.image === true
|
|
748
|
+
const runtimeAcceptsImages = init?.agentCapabilities?.promptCapabilities?.image === true ||
|
|
749
|
+
support.acceptsUnadvertisedImages?.(init) === true;
|
|
749
750
|
if (images.length && support.images === true && !runtimeAcceptsImages) {
|
|
750
751
|
throw new Error(`${support.displayName} is configured for image attachments, but this installed runtime does not advertise ACP image input. Update the ${support.displayName} CLI or send the message without an image.`);
|
|
751
752
|
}
|
|
@@ -175,10 +175,21 @@ export function ensureGrokInjectSlug(modelId, env = process.env) {
|
|
|
175
175
|
writeFileSync(path, next);
|
|
176
176
|
return slug;
|
|
177
177
|
}
|
|
178
|
+
/** Grok 1.0.25 consumes native image blocks but advertises image:false.
|
|
179
|
+
* Verified with the real CLI and a loopback model: scripts/verify-grok-images.ts.
|
|
180
|
+
* Keep unknown/older runtimes on the normal capability negotiation path. */
|
|
181
|
+
export function grokAcceptsUnadvertisedImages(init) {
|
|
182
|
+
const meta = init?._meta;
|
|
183
|
+
if (meta?.grokShell !== true || typeof meta.agentVersion !== "string")
|
|
184
|
+
return false;
|
|
185
|
+
const version = /^1\.0\.(\d+)$/.exec(meta.agentVersion);
|
|
186
|
+
return Boolean(version && Number(version[1]) >= 25);
|
|
187
|
+
}
|
|
178
188
|
const support = {
|
|
179
189
|
driverKind: "grokAgent",
|
|
180
190
|
displayName: "Grok",
|
|
181
|
-
images:
|
|
191
|
+
images: true,
|
|
192
|
+
acceptsUnadvertisedImages: grokAcceptsUnadvertisedImages,
|
|
182
193
|
models: STATIC_GROK_MODELS,
|
|
183
194
|
resolveModels: (env) => mergeLocalInject(readGrokModelCatalog(env), env),
|
|
184
195
|
// Grok's accepted levels vary by model and the CLI validates lazily — a
|
|
@@ -9,6 +9,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { parse as parseYaml } from "yaml";
|
|
12
|
+
import { resolveCli } from "../../procs.js";
|
|
12
13
|
import { decodeInjectId, hostApiKey, INJECT_SEP, localHost, mergeLocalInject } from "../local-inject.js";
|
|
13
14
|
import { createAcpDriver } from "./core.js";
|
|
14
15
|
const EMPTY = { default: "", options: [] };
|
|
@@ -249,11 +250,28 @@ export function hermesConfiguredModel(env = process.env) {
|
|
|
249
250
|
* Failure is non-fatal and returns [] — a catalog probe must never be the
|
|
250
251
|
* reason an agent becomes unselectable.
|
|
251
252
|
*/
|
|
252
|
-
|
|
253
|
+
export const HERMES_ACP_MODELS_TIMEOUT_ENV = "HERMES_ACP_MODELS_TIMEOUT_MS";
|
|
254
|
+
/** Overall deadline for the initialize + session/new catalog probe. A Hermes
|
|
255
|
+
* install with several authenticated providers answers `initialize` in about
|
|
256
|
+
* a second but needs ~6-7s for `session/new`, whose result carries the model
|
|
257
|
+
* list — the old 5s cap killed the probe every time on exactly the installs
|
|
258
|
+
* that have a catalog worth showing. */
|
|
259
|
+
export const HERMES_ACP_MODELS_DEFAULT_TIMEOUT_MS = 15_000;
|
|
260
|
+
function hermesAcpModelsTimeoutMs(env) {
|
|
261
|
+
const raw = env[HERMES_ACP_MODELS_TIMEOUT_ENV];
|
|
262
|
+
if (raw === undefined)
|
|
263
|
+
return HERMES_ACP_MODELS_DEFAULT_TIMEOUT_MS;
|
|
264
|
+
const parsed = Number(raw);
|
|
265
|
+
return Number.isInteger(parsed) && parsed > 0 && parsed <= 2_147_483_647
|
|
266
|
+
? parsed
|
|
267
|
+
: HERMES_ACP_MODELS_DEFAULT_TIMEOUT_MS;
|
|
268
|
+
}
|
|
269
|
+
export async function fetchHermesAcpModels(cli, env) {
|
|
253
270
|
return await new Promise((resolve) => {
|
|
254
271
|
let child;
|
|
255
272
|
try {
|
|
256
|
-
|
|
273
|
+
const resolved = resolveCli(cli, ["acp"], env);
|
|
274
|
+
child = spawn(resolved.command, resolved.args, { stdio: ["pipe", "pipe", "ignore"], env: env, windowsHide: true });
|
|
257
275
|
}
|
|
258
276
|
catch {
|
|
259
277
|
return resolve([]);
|
|
@@ -285,7 +303,7 @@ async function fetchHermesAcpModels(cli, env) {
|
|
|
285
303
|
}
|
|
286
304
|
resolve(out);
|
|
287
305
|
};
|
|
288
|
-
timer = setTimeout(() => done([]),
|
|
306
|
+
timer = setTimeout(() => done([]), hermesAcpModelsTimeoutMs(env));
|
|
289
307
|
child.once("error", () => done([]));
|
|
290
308
|
child.once("close", () => {
|
|
291
309
|
if (hardKillTimer)
|