@kenkaiiii/gg-boss 4.3.152 → 4.3.154
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.
|
@@ -91403,6 +91403,81 @@ var bossStore = {
|
|
|
91403
91403
|
};
|
|
91404
91404
|
|
|
91405
91405
|
// src/worker.ts
|
|
91406
|
+
var CONTEXT_OVERFLOW_PATTERNS = [
|
|
91407
|
+
/context_length_exceeded/i,
|
|
91408
|
+
/context length exceeded/i,
|
|
91409
|
+
/context window/i,
|
|
91410
|
+
// OpenAI Codex / Responses
|
|
91411
|
+
/maximum context length/i,
|
|
91412
|
+
// OpenAI / OpenRouter / Mistral
|
|
91413
|
+
/prompt is too long/i,
|
|
91414
|
+
// Anthropic
|
|
91415
|
+
/request_too_large/i,
|
|
91416
|
+
// Anthropic HTTP 413
|
|
91417
|
+
/input is too long/i,
|
|
91418
|
+
// Bedrock
|
|
91419
|
+
/input token count.*exceeds the maximum/i,
|
|
91420
|
+
// Gemini
|
|
91421
|
+
/maximum prompt length/i,
|
|
91422
|
+
// xAI / Grok
|
|
91423
|
+
/reduce the length of the messages/i,
|
|
91424
|
+
// Groq
|
|
91425
|
+
/too large for model/i,
|
|
91426
|
+
// Mistral
|
|
91427
|
+
/token limit/i
|
|
91428
|
+
// generic
|
|
91429
|
+
];
|
|
91430
|
+
var RATE_LIMIT_PATTERNS2 = [
|
|
91431
|
+
/rate[ _-]?limit/i,
|
|
91432
|
+
/\b429\b/,
|
|
91433
|
+
/too many requests/i,
|
|
91434
|
+
/tokens per minute/i,
|
|
91435
|
+
/requests per minute/i
|
|
91436
|
+
];
|
|
91437
|
+
var BILLING_PATTERNS = [
|
|
91438
|
+
/insufficient balance/i,
|
|
91439
|
+
/insufficient[ _]quota/i,
|
|
91440
|
+
/quota exceeded/i,
|
|
91441
|
+
/quota_exceeded/i,
|
|
91442
|
+
/credit balance/i,
|
|
91443
|
+
/please recharge/i,
|
|
91444
|
+
/payment required/i,
|
|
91445
|
+
/\b402\b/
|
|
91446
|
+
];
|
|
91447
|
+
var AUTH_PATTERNS = [
|
|
91448
|
+
/invalid[ _]api[ _]key/i,
|
|
91449
|
+
/unauthorized/i,
|
|
91450
|
+
/\b401\b/,
|
|
91451
|
+
/authentication[ _]failed/i,
|
|
91452
|
+
/please run \/login/i
|
|
91453
|
+
// Anthropic Claude Code-style hint
|
|
91454
|
+
];
|
|
91455
|
+
function matchesAny(message, patterns) {
|
|
91456
|
+
return patterns.some((p) => p.test(message));
|
|
91457
|
+
}
|
|
91458
|
+
function classifyWorkerError(message) {
|
|
91459
|
+
if (matchesAny(message, CONTEXT_OVERFLOW_PATTERNS)) {
|
|
91460
|
+
return `[context_overflow] Worker context window exceeded \u2014 the conversation is too large to continue. Recovery: call reset_worker(project) to wipe history, then re-prompt with the task. Re-prompting WITHOUT reset will fail the same way.
|
|
91461
|
+
|
|
91462
|
+
Original: ${message}`;
|
|
91463
|
+
}
|
|
91464
|
+
if (matchesAny(message, BILLING_PATTERNS)) {
|
|
91465
|
+
return `[billing] Provider billing/quota issue. Recovery: surface to the user \u2014 they need to top up or switch providers. Do NOT retry.
|
|
91466
|
+
|
|
91467
|
+
Original: ${message}`;
|
|
91468
|
+
}
|
|
91469
|
+
if (matchesAny(message, AUTH_PATTERNS)) {
|
|
91470
|
+
return `[auth] Provider authentication failed. Recovery: surface to the user \u2014 they need to re-login. Do NOT retry.
|
|
91471
|
+
|
|
91472
|
+
Original: ${message}`;
|
|
91473
|
+
}
|
|
91474
|
+
if (matchesAny(message, RATE_LIMIT_PATTERNS2)) {
|
|
91475
|
+
return `[rate_limited] Provider rate limit hit. Recovery: wait ~30s, then re-prompt the same worker (no reset needed).
|
|
91476
|
+
|
|
91477
|
+
Original: ${message}`;
|
|
91478
|
+
}
|
|
91479
|
+
return message;
|
|
91480
|
+
}
|
|
91406
91481
|
function safeBusHandler(workerName, handlerName, fn, onError) {
|
|
91407
91482
|
return (event) => {
|
|
91408
91483
|
try {
|
|
@@ -91427,10 +91502,19 @@ var Worker = class {
|
|
|
91427
91502
|
currentText = "";
|
|
91428
91503
|
currentTools = [];
|
|
91429
91504
|
activeTools = /* @__PURE__ */ new Map();
|
|
91505
|
+
/** Parent (orchestrator-wide) signal — fires only on full shutdown. */
|
|
91506
|
+
parentSignal;
|
|
91507
|
+
/** Per-turn AbortController so the boss can cancel one worker mid-flight without taking down the whole pool. */
|
|
91508
|
+
turnAc = null;
|
|
91509
|
+
/** Set true when cancel() fired so the silent-death guard reports "Cancelled by boss" instead of a generic abort error. */
|
|
91510
|
+
wasCancelled = false;
|
|
91511
|
+
startedAt = null;
|
|
91512
|
+
lastEventAt = null;
|
|
91430
91513
|
constructor(opts) {
|
|
91431
91514
|
this.name = opts.name;
|
|
91432
91515
|
this.cwd = opts.cwd;
|
|
91433
91516
|
this.queue = opts.queue;
|
|
91517
|
+
this.parentSignal = opts.signal;
|
|
91434
91518
|
this.session = new AgentSession({
|
|
91435
91519
|
provider: opts.provider,
|
|
91436
91520
|
model: opts.model,
|
|
@@ -91454,10 +91538,44 @@ var Worker = class {
|
|
|
91454
91538
|
bossStore.setWorkerStatus(this.name, "working");
|
|
91455
91539
|
this.currentText = "";
|
|
91456
91540
|
this.currentTools = [];
|
|
91457
|
-
|
|
91458
|
-
|
|
91541
|
+
this.activeTools.clear();
|
|
91542
|
+
this.wasCancelled = false;
|
|
91543
|
+
this.startedAt = Date.now();
|
|
91544
|
+
this.lastEventAt = null;
|
|
91545
|
+
const turnAc = new AbortController();
|
|
91546
|
+
this.turnAc = turnAc;
|
|
91547
|
+
const onParentAbort = () => turnAc.abort();
|
|
91548
|
+
if (this.parentSignal.aborted) turnAc.abort();
|
|
91549
|
+
else this.parentSignal.addEventListener("abort", onParentAbort, { once: true });
|
|
91550
|
+
this.session.setSignal(turnAc.signal);
|
|
91551
|
+
void this.session.prompt(text).then(() => {
|
|
91552
|
+
if (this.status === "working") {
|
|
91553
|
+
const message = this.wasCancelled ? "Cancelled by boss." : "Session ended without agent_done \u2014 likely a silently swallowed abort or stream interruption.";
|
|
91554
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
91555
|
+
this.status = "error";
|
|
91556
|
+
this.startedAt = null;
|
|
91557
|
+
log2(
|
|
91558
|
+
this.wasCancelled ? "INFO" : "ERROR",
|
|
91559
|
+
"worker",
|
|
91560
|
+
this.wasCancelled ? "cancelled" : "silent session end",
|
|
91561
|
+
{ worker: this.name }
|
|
91562
|
+
);
|
|
91563
|
+
this.queue.removeStuckFor(this.name);
|
|
91564
|
+
bossStore.appendWorkerError(this.name, message, ts);
|
|
91565
|
+
this.queue.push({
|
|
91566
|
+
kind: "worker_error",
|
|
91567
|
+
project: this.name,
|
|
91568
|
+
message,
|
|
91569
|
+
timestamp: ts
|
|
91570
|
+
});
|
|
91571
|
+
}
|
|
91572
|
+
}).catch((err) => {
|
|
91573
|
+
const rawMessage = this.wasCancelled ? "Cancelled by boss." : err instanceof Error ? err.message : String(err);
|
|
91574
|
+
const message = this.wasCancelled ? rawMessage : classifyWorkerError(rawMessage);
|
|
91459
91575
|
this.status = "error";
|
|
91576
|
+
this.startedAt = null;
|
|
91460
91577
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
91578
|
+
this.queue.removeStuckFor(this.name);
|
|
91461
91579
|
bossStore.appendWorkerError(this.name, message, ts);
|
|
91462
91580
|
this.queue.push({
|
|
91463
91581
|
kind: "worker_error",
|
|
@@ -91465,8 +91583,64 @@ var Worker = class {
|
|
|
91465
91583
|
message,
|
|
91466
91584
|
timestamp: ts
|
|
91467
91585
|
});
|
|
91586
|
+
}).finally(() => {
|
|
91587
|
+
this.parentSignal.removeEventListener("abort", onParentAbort);
|
|
91588
|
+
if (this.turnAc === turnAc) this.turnAc = null;
|
|
91468
91589
|
});
|
|
91469
91590
|
}
|
|
91591
|
+
/**
|
|
91592
|
+
* Cancel the current turn. Aborts only this worker's per-turn controller —
|
|
91593
|
+
* other workers keep running. The aborted turn surfaces as a `worker_error`
|
|
91594
|
+
* event with message "Cancelled by boss." so the orchestrator clears its
|
|
91595
|
+
* in-flight task entry and the boss is notified.
|
|
91596
|
+
*
|
|
91597
|
+
* Returns true if a turn was actually cancelled.
|
|
91598
|
+
*/
|
|
91599
|
+
cancel() {
|
|
91600
|
+
if (this.status !== "working" || !this.turnAc) return false;
|
|
91601
|
+
this.wasCancelled = true;
|
|
91602
|
+
this.turnAc.abort();
|
|
91603
|
+
return true;
|
|
91604
|
+
}
|
|
91605
|
+
/**
|
|
91606
|
+
* Snapshot of the worker's current activity. Cheap to call; safe while the
|
|
91607
|
+
* worker is mid-turn. Used by the boss's get_worker_activity tool to peek
|
|
91608
|
+
* inside a long-running turn without waiting for completion.
|
|
91609
|
+
*/
|
|
91610
|
+
getActivity() {
|
|
91611
|
+
const now2 = Date.now();
|
|
91612
|
+
const TEXT_TAIL = 400;
|
|
91613
|
+
const tail = this.currentText.length > TEXT_TAIL ? "\u2026" + this.currentText.slice(-TEXT_TAIL) : this.currentText;
|
|
91614
|
+
return {
|
|
91615
|
+
status: this.status,
|
|
91616
|
+
startedAt: this.startedAt ? new Date(this.startedAt).toISOString() : null,
|
|
91617
|
+
lastEventAt: this.lastEventAt ? new Date(this.lastEventAt).toISOString() : null,
|
|
91618
|
+
workingSeconds: this.startedAt ? Math.floor((now2 - this.startedAt) / 1e3) : 0,
|
|
91619
|
+
silentSeconds: this.lastEventAt ? Math.floor((now2 - this.lastEventAt) / 1e3) : 0,
|
|
91620
|
+
activeTools: [...this.activeTools.values()],
|
|
91621
|
+
completedTools: [...this.currentTools],
|
|
91622
|
+
textTail: tail,
|
|
91623
|
+
lastEventAtMs: this.lastEventAt
|
|
91624
|
+
};
|
|
91625
|
+
}
|
|
91626
|
+
/**
|
|
91627
|
+
* Hard reset: cancel any in-flight turn, wipe conversation history, force
|
|
91628
|
+
* status back to idle. Use when a worker is wedged in `error` or stuck on a
|
|
91629
|
+
* bad context that re-prompting can't recover from.
|
|
91630
|
+
*/
|
|
91631
|
+
async reset() {
|
|
91632
|
+
this.cancel();
|
|
91633
|
+
await this.session.newSession();
|
|
91634
|
+
this.turnCount = 0;
|
|
91635
|
+
this.currentText = "";
|
|
91636
|
+
this.currentTools = [];
|
|
91637
|
+
this.activeTools.clear();
|
|
91638
|
+
this.startedAt = null;
|
|
91639
|
+
this.lastEventAt = null;
|
|
91640
|
+
this.wasCancelled = false;
|
|
91641
|
+
this.status = "idle";
|
|
91642
|
+
bossStore.setWorkerStatus(this.name, "idle");
|
|
91643
|
+
}
|
|
91470
91644
|
async dispose() {
|
|
91471
91645
|
await this.session.dispose();
|
|
91472
91646
|
}
|
|
@@ -91485,9 +91659,11 @@ var Worker = class {
|
|
|
91485
91659
|
}
|
|
91486
91660
|
wireEvents() {
|
|
91487
91661
|
const bus = this.session.eventBus;
|
|
91488
|
-
const reportError2 = (
|
|
91662
|
+
const reportError2 = (rawMessage) => {
|
|
91489
91663
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
91664
|
+
const message = classifyWorkerError(rawMessage);
|
|
91490
91665
|
this.status = "error";
|
|
91666
|
+
this.queue.removeStuckFor(this.name);
|
|
91491
91667
|
bossStore.appendWorkerError(this.name, message, ts);
|
|
91492
91668
|
this.queue.push({
|
|
91493
91669
|
kind: "worker_error",
|
|
@@ -91503,6 +91679,7 @@ var Worker = class {
|
|
|
91503
91679
|
"text_delta",
|
|
91504
91680
|
({ text }) => {
|
|
91505
91681
|
this.currentText += text;
|
|
91682
|
+
this.lastEventAt = Date.now();
|
|
91506
91683
|
},
|
|
91507
91684
|
reportError2
|
|
91508
91685
|
)
|
|
@@ -91514,6 +91691,7 @@ var Worker = class {
|
|
|
91514
91691
|
"tool_call_start",
|
|
91515
91692
|
({ toolCallId, name }) => {
|
|
91516
91693
|
this.activeTools.set(toolCallId, name);
|
|
91694
|
+
this.lastEventAt = Date.now();
|
|
91517
91695
|
},
|
|
91518
91696
|
reportError2
|
|
91519
91697
|
)
|
|
@@ -91527,6 +91705,7 @@ var Worker = class {
|
|
|
91527
91705
|
const name = this.activeTools.get(toolCallId);
|
|
91528
91706
|
this.activeTools.delete(toolCallId);
|
|
91529
91707
|
if (name) this.currentTools.push({ name, ok: !isError });
|
|
91708
|
+
this.lastEventAt = Date.now();
|
|
91530
91709
|
},
|
|
91531
91710
|
reportError2
|
|
91532
91711
|
)
|
|
@@ -91549,7 +91728,11 @@ var Worker = class {
|
|
|
91549
91728
|
};
|
|
91550
91729
|
this.currentText = "";
|
|
91551
91730
|
this.currentTools = [];
|
|
91731
|
+
this.activeTools.clear();
|
|
91732
|
+
this.startedAt = null;
|
|
91733
|
+
this.lastEventAt = null;
|
|
91552
91734
|
this.status = "idle";
|
|
91735
|
+
this.queue.removeStuckFor(this.name);
|
|
91553
91736
|
bossStore.appendWorkerEvent(summary);
|
|
91554
91737
|
this.queue.push({ kind: "worker_turn_complete", summary });
|
|
91555
91738
|
},
|
|
@@ -91598,6 +91781,19 @@ var EventQueue = class {
|
|
|
91598
91781
|
size() {
|
|
91599
91782
|
return this.user.length + this.rest.length;
|
|
91600
91783
|
}
|
|
91784
|
+
/**
|
|
91785
|
+
* Drop any queued `worker_stuck` events for the given project. Called when a
|
|
91786
|
+
* `worker_turn_complete` or `worker_error` fires — the worker is no longer
|
|
91787
|
+
* running, so any pending stuck ping is now stale and would mislead the boss
|
|
91788
|
+
* (e.g. tell it to cancel a worker that already finished).
|
|
91789
|
+
*
|
|
91790
|
+
* Returns the number of events dropped.
|
|
91791
|
+
*/
|
|
91792
|
+
removeStuckFor(project) {
|
|
91793
|
+
const before = this.rest.length;
|
|
91794
|
+
this.rest = this.rest.filter((e) => !(e.kind === "worker_stuck" && e.project === project));
|
|
91795
|
+
return before - this.rest.length;
|
|
91796
|
+
}
|
|
91601
91797
|
};
|
|
91602
91798
|
|
|
91603
91799
|
// src/settings.ts
|
|
@@ -91659,6 +91855,7 @@ Every user-role message is one of:
|
|
|
91659
91855
|
1. A direct user message \u2014 respond to the user.
|
|
91660
91856
|
2. \`[event:worker_turn_complete]\` \u2014 a worker finished a turn. Contains project, turn number, tools used (\u2713/\u2717), the worker's final text, AND a trailing \`other_workers:\` line listing every other project's current status (e.g. \`other_workers: B(working) C(idle) D(working)\`).
|
|
91661
91857
|
3. \`[event:worker_error]\` \u2014 a worker hit an error. Diagnose, then retry or surface to the user. Same \`other_workers:\` trailer.
|
|
91858
|
+
4. \`[event:worker_stuck]\` \u2014 a queued ping from the orchestrator's watchdog: a worker has been silent or running unusually long. Includes \`reason\` (silent | long_running), \`working_seconds\`, \`silent_seconds\`, \`active_tools\`, \`completed_this_turn\`, and a \`text_tail\` snippet. The worker is STILL RUNNING \u2014 this is informational, not an error. Decide: wait (most cases), \`cancel_worker\`, or surface. The watchdog won't ping again for the same worker until it emits new activity AND stalls again, so you won't be spammed.
|
|
91662
91859
|
|
|
91663
91860
|
**Always read the \`other_workers:\` trailer before deciding "the run is done".** During a parallel dispatch you receive ONE event per finishing worker, in arrival order. It is wrong to treat the event you're processing as "the last one" unless \`other_workers:\` shows every other worker is \`idle\` (or \`error\`). If any are \`working\`, more events are coming \u2014 finish your routing for THIS event, then wait.
|
|
91664
91861
|
|
|
@@ -91678,6 +91875,9 @@ Worker dispatch:
|
|
|
91678
91875
|
- \`get_worker_status(project)\` \u2014 single-project status check.
|
|
91679
91876
|
- \`prompt_worker(project, message, fresh?)\` \u2014 send a prompt directly to a worker. FIRE-AND-FORGET. Returns immediately; you'll get \`worker_turn_complete\` later. NEVER call this on a worker whose status is "working".
|
|
91680
91877
|
- \`get_worker_summary(project)\` \u2014 most recent turn summary. Use to inspect what was actually done.
|
|
91878
|
+
- \`get_worker_activity(project)\` \u2014 mid-turn peek: working/silent seconds, active tools, text tail. Use ONLY when a worker has been \`working\` long enough to wonder if it's stuck.
|
|
91879
|
+
- \`cancel_worker(project)\` \u2014 abort the current turn. Surfaces as a \`worker_error\` ("Cancelled by boss."). Other workers untouched.
|
|
91880
|
+
- \`reset_worker(project)\` \u2014 last resort: cancel + wipe history + force idle. Only when re-prompting can't recover.
|
|
91681
91881
|
|
|
91682
91882
|
Task plan (persistent backlog, visible in the user's Ctrl+T overlay):
|
|
91683
91883
|
|
|
@@ -91796,6 +91996,30 @@ The worker has full context of its prior turn (you set fresh=false), so don't re
|
|
|
91796
91996
|
|
|
91797
91997
|
This keeps the loop bounded \u2014 workers don't grind forever on a stuck task.
|
|
91798
91998
|
|
|
91999
|
+
# Recoverable error tags on worker_error
|
|
92000
|
+
|
|
92001
|
+
Worker errors are pre-classified \u2014 the message starts with a tag like \`[context_overflow]\`, \`[rate_limited]\`, \`[billing]\`, or \`[auth]\` when recovery is well-defined. Route off the tag, NOT a generic re-prompt:
|
|
92002
|
+
|
|
92003
|
+
- \`[context_overflow]\` \u2014 conversation outgrew the model's window. Call \`reset_worker(project)\` first, THEN re-prompt with the task. Re-prompting without reset fails the same way. Tell the user briefly that you reset.
|
|
92004
|
+
- \`[rate_limited]\` \u2014 wait for the next event (~30s of natural delay) or briefly note to user, then re-prompt the same worker. No reset.
|
|
92005
|
+
- \`[billing]\` / \`[auth]\` \u2014 surface to the user. Do not retry. The user must fix it.
|
|
92006
|
+
- Untagged \u2014 fall back to the normal BLOCKED handling (one corrective re-prompt, then surface).
|
|
92007
|
+
|
|
92008
|
+
# Checking on a stuck or slow worker
|
|
92009
|
+
|
|
92010
|
+
The orchestrator's watchdog will queue a \`[event:worker_stuck]\` ping if a worker is silent for too long. **It arrives like every other event \u2014 you process it AFTER finishing your current turn.** It does NOT interrupt you. Don't drop what you're doing to chase it; just route it when it's its turn.
|
|
92011
|
+
|
|
92012
|
+
When a stuck ping arrives (or you otherwise suspect a hang):
|
|
92013
|
+
|
|
92014
|
+
1. The ping itself usually has enough info (\`silent_seconds\`, \`active_tools\`, \`text_tail\`). Only call \`get_worker_activity(project)\` if you need a fresher snapshot \u2014 the ping data may already be 30+ seconds old by the time you read it.
|
|
92015
|
+
- Active tool + recent activity \u2192 it's working, leave it alone. Stay silent or briefly note to the user.
|
|
92016
|
+
- High \`silent_seconds\` with no active tool \u2192 likely a stalled stream. Cancel.
|
|
92017
|
+
- Active \`bash\` for several minutes \u2192 probably a long command (test suite, build). Wait unless the user is impatient.
|
|
92018
|
+
2. \`cancel_worker(project)\` if you decide to intervene. A \`worker_error\` arrives; treat it as a normal failed turn (re-prompt with a tighter instruction, or surface to the user).
|
|
92019
|
+
3. \`reset_worker(project)\` ONLY if the worker is in \`error\` and re-prompting fails repeatedly, OR its context is clearly poisoned. Reset wipes history \u2014 the worker forgets everything. Always tell the user when you reset.
|
|
92020
|
+
|
|
92021
|
+
Don't poll \`get_worker_activity\` \u2014 call it at most once per concern. Don't cancel routinely; the user is mostly fine waiting.
|
|
92022
|
+
|
|
91799
92023
|
# Style
|
|
91800
92024
|
|
|
91801
92025
|
- Terse with the user. They want results, not narration.
|
|
@@ -91859,6 +92083,15 @@ var promptWorkerParams = external_exports.object({
|
|
|
91859
92083
|
var getWorkerSummaryParams = external_exports.object({
|
|
91860
92084
|
project: external_exports.string().describe("Project name as listed by list_workers.")
|
|
91861
92085
|
});
|
|
92086
|
+
var getWorkerActivityParams = external_exports.object({
|
|
92087
|
+
project: external_exports.string().describe("Project name as listed by list_workers.")
|
|
92088
|
+
});
|
|
92089
|
+
var cancelWorkerParams = external_exports.object({
|
|
92090
|
+
project: external_exports.string().describe("Project name as listed by list_workers.")
|
|
92091
|
+
});
|
|
92092
|
+
var resetWorkerParams = external_exports.object({
|
|
92093
|
+
project: external_exports.string().describe("Project name as listed by list_workers.")
|
|
92094
|
+
});
|
|
91862
92095
|
function createBossTools(deps) {
|
|
91863
92096
|
const { workers, lastSummaries } = deps;
|
|
91864
92097
|
const listWorkers = {
|
|
@@ -91921,7 +92154,58 @@ Final text:
|
|
|
91921
92154
|
${summary.finalText || "(empty)"}`;
|
|
91922
92155
|
}
|
|
91923
92156
|
};
|
|
91924
|
-
|
|
92157
|
+
const getWorkerActivity = {
|
|
92158
|
+
name: "get_worker_activity",
|
|
92159
|
+
description: "Peek at what a worker is doing RIGHT NOW (mid-turn). Returns working/silent durations in seconds, currently-running tool names, completed tools so far, and the tail of its streamed text. Use this when a worker has been `working` for a long time to decide whether it's making progress, hung, or worth cancelling.",
|
|
92160
|
+
parameters: getWorkerActivityParams,
|
|
92161
|
+
execute(args) {
|
|
92162
|
+
const w = workers.get(args.project);
|
|
92163
|
+
if (!w) return `Unknown project: ${args.project}`;
|
|
92164
|
+
const a = w.getActivity();
|
|
92165
|
+
const lines = [
|
|
92166
|
+
`Project: ${args.project}`,
|
|
92167
|
+
`Status: ${a.status}`,
|
|
92168
|
+
`Working: ${a.workingSeconds}s (last event ${a.silentSeconds}s ago)`,
|
|
92169
|
+
`Active tools: ${a.activeTools.length > 0 ? a.activeTools.join(", ") : "(none)"}`,
|
|
92170
|
+
`Completed this turn: ${a.completedTools.length > 0 ? a.completedTools.map((t) => `${t.ok ? "\u2713" : "\u2717"}${t.name}`).join(", ") : "(none)"}`,
|
|
92171
|
+
"",
|
|
92172
|
+
"Text tail:",
|
|
92173
|
+
a.textTail || "(no text yet)"
|
|
92174
|
+
];
|
|
92175
|
+
return lines.join("\n");
|
|
92176
|
+
}
|
|
92177
|
+
};
|
|
92178
|
+
const cancelWorker = {
|
|
92179
|
+
name: "cancel_worker",
|
|
92180
|
+
description: "Abort a worker's current turn. Other workers are untouched. The cancelled worker emits a `worker_error` event with message 'Cancelled by boss.' so any in-flight task is cleared. After cancelling, you can re-prompt or reset the worker. No-op if the worker isn't `working`.",
|
|
92181
|
+
parameters: cancelWorkerParams,
|
|
92182
|
+
execute(args) {
|
|
92183
|
+
const w = workers.get(args.project);
|
|
92184
|
+
if (!w) return `Unknown project: ${args.project}`;
|
|
92185
|
+
const cancelled = w.cancel();
|
|
92186
|
+
return cancelled ? `Cancellation requested for "${args.project}". A worker_error event will arrive shortly.` : `Worker "${args.project}" is not working \u2014 nothing to cancel (status: ${w.getStatus()}).`;
|
|
92187
|
+
}
|
|
92188
|
+
};
|
|
92189
|
+
const resetWorker = {
|
|
92190
|
+
name: "reset_worker",
|
|
92191
|
+
description: "Hard reset: cancel any in-flight turn, wipe conversation history, force status back to idle. Use as a last resort when a worker is wedged in `error` or its context is so polluted that re-prompting won't recover. Equivalent to `fresh: true` plus a forced unstuck. After reset, the worker is ready for a new prompt_worker call.",
|
|
92192
|
+
parameters: resetWorkerParams,
|
|
92193
|
+
async execute(args) {
|
|
92194
|
+
const w = workers.get(args.project);
|
|
92195
|
+
if (!w) return `Unknown project: ${args.project}`;
|
|
92196
|
+
await w.reset();
|
|
92197
|
+
return `Worker "${args.project}" reset \u2014 status is idle, history wiped.`;
|
|
92198
|
+
}
|
|
92199
|
+
};
|
|
92200
|
+
return [
|
|
92201
|
+
listWorkers,
|
|
92202
|
+
getWorkerStatus,
|
|
92203
|
+
promptWorker,
|
|
92204
|
+
getWorkerSummary,
|
|
92205
|
+
getWorkerActivity,
|
|
92206
|
+
cancelWorker,
|
|
92207
|
+
resetWorker
|
|
92208
|
+
];
|
|
91925
92209
|
}
|
|
91926
92210
|
|
|
91927
92211
|
// src/task-tools.ts
|
|
@@ -92224,7 +92508,7 @@ function createTaskTools(deps) {
|
|
|
92224
92508
|
|
|
92225
92509
|
// src/audio.ts
|
|
92226
92510
|
init_esm_shims();
|
|
92227
|
-
import { spawn as spawn6 } from "child_process";
|
|
92511
|
+
import { spawn as spawn6, execFileSync as execFileSync3 } from "child_process";
|
|
92228
92512
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
92229
92513
|
import path28 from "path";
|
|
92230
92514
|
import fs29 from "fs";
|
|
@@ -92328,6 +92612,79 @@ function trySpawn(cmd, args) {
|
|
|
92328
92612
|
}
|
|
92329
92613
|
});
|
|
92330
92614
|
}
|
|
92615
|
+
function isWsl() {
|
|
92616
|
+
if (process.platform !== "linux") return false;
|
|
92617
|
+
return !!process.env.WSL_DISTRO_NAME || fs29.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
92618
|
+
}
|
|
92619
|
+
async function tryPlayOnWindowsHost(file2) {
|
|
92620
|
+
try {
|
|
92621
|
+
const here = path28.dirname(fileURLToPath2(import.meta.url));
|
|
92622
|
+
const devAssets = path28.resolve(here, "..", "assets");
|
|
92623
|
+
const resolved = path28.resolve(file2);
|
|
92624
|
+
const inDist = resolved === here || resolved.startsWith(here + path28.sep);
|
|
92625
|
+
const inAssets = resolved === devAssets || resolved.startsWith(devAssets + path28.sep);
|
|
92626
|
+
if (!inDist && !inAssets) {
|
|
92627
|
+
return false;
|
|
92628
|
+
}
|
|
92629
|
+
const winPath = execFileSync3("wslpath", ["-w", resolved], {
|
|
92630
|
+
encoding: "utf8",
|
|
92631
|
+
timeout: 2e3
|
|
92632
|
+
}).trim();
|
|
92633
|
+
const script = [
|
|
92634
|
+
"Add-Type -AssemblyName presentationCore;",
|
|
92635
|
+
"$p = New-Object System.Windows.Media.MediaPlayer;",
|
|
92636
|
+
"$p.Open([uri]$env:GGBOSS_AUDIO_PATH);",
|
|
92637
|
+
"$p.Play();",
|
|
92638
|
+
// Same reason as the win32 branch: MediaPlayer is async, so we have
|
|
92639
|
+
// to keep powershell.exe alive long enough to actually emit audio.
|
|
92640
|
+
"Start-Sleep -Seconds 5;"
|
|
92641
|
+
].join(" ");
|
|
92642
|
+
return new Promise((resolve3) => {
|
|
92643
|
+
let resolved2 = false;
|
|
92644
|
+
try {
|
|
92645
|
+
const child = spawn6(
|
|
92646
|
+
"powershell.exe",
|
|
92647
|
+
["-NoProfile", "-WindowStyle", "Hidden", "-Command", script],
|
|
92648
|
+
{
|
|
92649
|
+
detached: true,
|
|
92650
|
+
stdio: "ignore",
|
|
92651
|
+
env: {
|
|
92652
|
+
...process.env,
|
|
92653
|
+
GGBOSS_AUDIO_PATH: winPath,
|
|
92654
|
+
// WSLENV propagates listed vars across the WSL→Windows boundary.
|
|
92655
|
+
// Append rather than replace so we don't clobber existing rules.
|
|
92656
|
+
WSLENV: (process.env.WSLENV ? process.env.WSLENV + ":" : "") + "GGBOSS_AUDIO_PATH"
|
|
92657
|
+
}
|
|
92658
|
+
}
|
|
92659
|
+
);
|
|
92660
|
+
child.once("error", () => {
|
|
92661
|
+
if (!resolved2) {
|
|
92662
|
+
resolved2 = true;
|
|
92663
|
+
resolve3(false);
|
|
92664
|
+
}
|
|
92665
|
+
});
|
|
92666
|
+
child.once("spawn", () => {
|
|
92667
|
+
if (!resolved2) {
|
|
92668
|
+
resolved2 = true;
|
|
92669
|
+
child.unref();
|
|
92670
|
+
resolve3(true);
|
|
92671
|
+
}
|
|
92672
|
+
});
|
|
92673
|
+
setTimeout(() => {
|
|
92674
|
+
if (!resolved2) {
|
|
92675
|
+
resolved2 = true;
|
|
92676
|
+
child.unref();
|
|
92677
|
+
resolve3(true);
|
|
92678
|
+
}
|
|
92679
|
+
}, 50);
|
|
92680
|
+
} catch {
|
|
92681
|
+
resolve3(false);
|
|
92682
|
+
}
|
|
92683
|
+
});
|
|
92684
|
+
} catch {
|
|
92685
|
+
return false;
|
|
92686
|
+
}
|
|
92687
|
+
}
|
|
92331
92688
|
async function playFile(file2) {
|
|
92332
92689
|
if (!fs29.existsSync(file2)) return;
|
|
92333
92690
|
const platform2 = process.platform;
|
|
@@ -92350,6 +92707,7 @@ async function playFile(file2) {
|
|
|
92350
92707
|
await trySpawn("powershell.exe", ["-NoProfile", "-WindowStyle", "Hidden", "-Command", script]);
|
|
92351
92708
|
return;
|
|
92352
92709
|
}
|
|
92710
|
+
if (isWsl() && await tryPlayOnWindowsHost(file2)) return;
|
|
92353
92711
|
const linuxCandidates = [
|
|
92354
92712
|
{ cmd: "mpv", args: ["--really-quiet", "--no-video", file2] },
|
|
92355
92713
|
{ cmd: "ffplay", args: ["-nodisp", "-autoexit", "-loglevel", "quiet", file2] },
|
|
@@ -92479,6 +92837,9 @@ async function getSessionById(id2) {
|
|
|
92479
92837
|
}
|
|
92480
92838
|
|
|
92481
92839
|
// src/orchestrator.ts
|
|
92840
|
+
var WATCHDOG_INTERVAL_MS = 3e4;
|
|
92841
|
+
var SILENT_THRESHOLD_SEC = 90;
|
|
92842
|
+
var WORKING_THRESHOLD_SEC = 600;
|
|
92482
92843
|
var GGBoss = class {
|
|
92483
92844
|
workers = /* @__PURE__ */ new Map();
|
|
92484
92845
|
lastSummaries = /* @__PURE__ */ new Map();
|
|
@@ -92517,6 +92878,23 @@ var GGBoss = class {
|
|
|
92517
92878
|
* message that didn't dispatch any workers.
|
|
92518
92879
|
*/
|
|
92519
92880
|
hadWorkerActivitySinceReady = false;
|
|
92881
|
+
/**
|
|
92882
|
+
* Watchdog for stuck workers. Fires every WATCHDOG_INTERVAL_MS; if any
|
|
92883
|
+
* "working" worker has been silent past SILENT_THRESHOLD_SEC or running
|
|
92884
|
+
* past WORKING_THRESHOLD_SEC, push a `worker_stuck` event onto the queue.
|
|
92885
|
+
* The boss processes it like any other event — AFTER its current turn
|
|
92886
|
+
* (queue is FIFO, boss is single-event-at-a-time), so this never
|
|
92887
|
+
* interrupts an in-flight boss turn.
|
|
92888
|
+
*/
|
|
92889
|
+
watchdogTimer = null;
|
|
92890
|
+
/**
|
|
92891
|
+
* Per-project debounce. Stores the worker's lastEventAtMs at the moment we
|
|
92892
|
+
* pushed the stuck event. If the worker's lastEventAt advances past that,
|
|
92893
|
+
* we know the worker recovered (emitted a new event), so we clear the entry
|
|
92894
|
+
* and become eligible to fire again on the next stall. Also cleared on
|
|
92895
|
+
* worker_turn_complete / worker_error.
|
|
92896
|
+
*/
|
|
92897
|
+
stuckPushedAt = /* @__PURE__ */ new Map();
|
|
92520
92898
|
constructor(opts) {
|
|
92521
92899
|
this.opts = opts;
|
|
92522
92900
|
}
|
|
@@ -92810,6 +93188,7 @@ var GGBoss = class {
|
|
|
92810
93188
|
}
|
|
92811
93189
|
async run() {
|
|
92812
93190
|
this.running = true;
|
|
93191
|
+
this.startWatchdog();
|
|
92813
93192
|
while (this.running) {
|
|
92814
93193
|
try {
|
|
92815
93194
|
await this.runIteration();
|
|
@@ -92832,6 +93211,7 @@ var GGBoss = class {
|
|
|
92832
93211
|
}
|
|
92833
93212
|
let finishedTaskId = null;
|
|
92834
93213
|
if (event.kind === "worker_turn_complete") {
|
|
93214
|
+
this.stuckPushedAt.delete(event.summary.project);
|
|
92835
93215
|
void playDoneAudio();
|
|
92836
93216
|
this.hadWorkerActivitySinceReady = true;
|
|
92837
93217
|
this.lastSummaries.set(event.summary.project, event.summary);
|
|
@@ -92861,6 +93241,7 @@ var GGBoss = class {
|
|
|
92861
93241
|
}
|
|
92862
93242
|
if (event.kind === "worker_error") {
|
|
92863
93243
|
this.hadWorkerActivitySinceReady = true;
|
|
93244
|
+
this.stuckPushedAt.delete(event.project);
|
|
92864
93245
|
log2("ERROR", "worker_error", event.message, { project: event.project });
|
|
92865
93246
|
const taskId = this.inFlightTaskByProject.get(event.project);
|
|
92866
93247
|
if (taskId) {
|
|
@@ -92871,6 +93252,14 @@ var GGBoss = class {
|
|
|
92871
93252
|
});
|
|
92872
93253
|
}
|
|
92873
93254
|
}
|
|
93255
|
+
if (event.kind === "worker_stuck") {
|
|
93256
|
+
log2("WARN", "worker_stuck", `worker silent\u2014pinging boss`, {
|
|
93257
|
+
project: event.project,
|
|
93258
|
+
reason: event.reason,
|
|
93259
|
+
silentSeconds: event.snapshot.silentSeconds,
|
|
93260
|
+
workingSeconds: event.snapshot.workingSeconds
|
|
93261
|
+
});
|
|
93262
|
+
}
|
|
92874
93263
|
await this.runCompaction(false);
|
|
92875
93264
|
const workerSnapshot = [...this.workers.entries()].map(([name, w]) => ({
|
|
92876
93265
|
name,
|
|
@@ -92978,8 +93367,62 @@ var GGBoss = class {
|
|
|
92978
93367
|
await this.dispatchTaskByDescription(project, next.description, next.fresh === true, next.id);
|
|
92979
93368
|
this.pendingAutoChainNotices.push({ project, title: next.title });
|
|
92980
93369
|
}
|
|
93370
|
+
/**
|
|
93371
|
+
* Start the stuck-worker watchdog. Idempotent.
|
|
93372
|
+
*
|
|
93373
|
+
* Safety properties:
|
|
93374
|
+
* - Pushes onto the same FIFO queue the boss already drains, so the boss
|
|
93375
|
+
* never gets interrupted mid-turn — stuck pings are processed AFTER
|
|
93376
|
+
* whatever it's currently doing.
|
|
93377
|
+
* - Per-worker debounce (`stuckPushedAt`) prevents spam; a worker only
|
|
93378
|
+
* gets re-flagged after it emits a new event AND stalls again, or after
|
|
93379
|
+
* it completes/errors and stalls on a fresh turn.
|
|
93380
|
+
*/
|
|
93381
|
+
startWatchdog() {
|
|
93382
|
+
if (this.watchdogTimer) return;
|
|
93383
|
+
this.watchdogTimer = setInterval(() => {
|
|
93384
|
+
try {
|
|
93385
|
+
this.checkStuckWorkers();
|
|
93386
|
+
} catch (err) {
|
|
93387
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
93388
|
+
log2("ERROR", "watchdog", "tick threw", { message });
|
|
93389
|
+
}
|
|
93390
|
+
}, WATCHDOG_INTERVAL_MS);
|
|
93391
|
+
this.watchdogTimer.unref?.();
|
|
93392
|
+
}
|
|
93393
|
+
stopWatchdog() {
|
|
93394
|
+
if (this.watchdogTimer) {
|
|
93395
|
+
clearInterval(this.watchdogTimer);
|
|
93396
|
+
this.watchdogTimer = null;
|
|
93397
|
+
}
|
|
93398
|
+
}
|
|
93399
|
+
checkStuckWorkers() {
|
|
93400
|
+
for (const [name, worker] of this.workers) {
|
|
93401
|
+
const decision = decideStuckEvent({
|
|
93402
|
+
status: worker.getStatus(),
|
|
93403
|
+
activity: worker.getStatus() === "working" ? worker.getActivity() : null,
|
|
93404
|
+
lastPushedAt: this.stuckPushedAt.has(name) ? this.stuckPushedAt.get(name) ?? null : void 0,
|
|
93405
|
+
silentThresholdSec: SILENT_THRESHOLD_SEC,
|
|
93406
|
+
workingThresholdSec: WORKING_THRESHOLD_SEC
|
|
93407
|
+
});
|
|
93408
|
+
if (decision.kind === "clear_debounce") {
|
|
93409
|
+
this.stuckPushedAt.delete(name);
|
|
93410
|
+
continue;
|
|
93411
|
+
}
|
|
93412
|
+
if (decision.kind === "skip") continue;
|
|
93413
|
+
this.stuckPushedAt.set(name, decision.lastEventAtMs);
|
|
93414
|
+
this.queue.push({
|
|
93415
|
+
kind: "worker_stuck",
|
|
93416
|
+
project: name,
|
|
93417
|
+
reason: decision.reason,
|
|
93418
|
+
snapshot: decision.snapshot,
|
|
93419
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
93420
|
+
});
|
|
93421
|
+
}
|
|
93422
|
+
}
|
|
92981
93423
|
async dispose() {
|
|
92982
93424
|
this.running = false;
|
|
93425
|
+
this.stopWatchdog();
|
|
92983
93426
|
this.ac.abort();
|
|
92984
93427
|
this.queue.push({
|
|
92985
93428
|
kind: "user_message",
|
|
@@ -93004,6 +93447,37 @@ function reportedToTaskStatus(reported, anyToolFailed) {
|
|
|
93004
93447
|
if (reported === "UNVERIFIED" || reported === "PARTIAL") return "in_progress";
|
|
93005
93448
|
return anyToolFailed ? "blocked" : "done";
|
|
93006
93449
|
}
|
|
93450
|
+
function decideStuckEvent(input) {
|
|
93451
|
+
const { status, activity, lastPushedAt, silentThresholdSec, workingThresholdSec } = input;
|
|
93452
|
+
if (status !== "working" || !activity) {
|
|
93453
|
+
return lastPushedAt !== void 0 ? { kind: "clear_debounce" } : { kind: "skip" };
|
|
93454
|
+
}
|
|
93455
|
+
if (lastPushedAt !== void 0) {
|
|
93456
|
+
const lastEvent = activity.lastEventAtMs;
|
|
93457
|
+
if (lastEvent === null || lastPushedAt === null || lastEvent <= lastPushedAt) {
|
|
93458
|
+
return { kind: "skip" };
|
|
93459
|
+
}
|
|
93460
|
+
}
|
|
93461
|
+
let reason = null;
|
|
93462
|
+
if (activity.lastEventAtMs !== null && activity.silentSeconds >= silentThresholdSec) {
|
|
93463
|
+
reason = "silent";
|
|
93464
|
+
} else if (activity.workingSeconds >= workingThresholdSec) {
|
|
93465
|
+
reason = "long_running";
|
|
93466
|
+
}
|
|
93467
|
+
if (!reason) return { kind: "skip" };
|
|
93468
|
+
return {
|
|
93469
|
+
kind: "push",
|
|
93470
|
+
reason,
|
|
93471
|
+
lastEventAtMs: activity.lastEventAtMs,
|
|
93472
|
+
snapshot: {
|
|
93473
|
+
workingSeconds: activity.workingSeconds,
|
|
93474
|
+
silentSeconds: activity.silentSeconds,
|
|
93475
|
+
activeTools: activity.activeTools,
|
|
93476
|
+
completedTools: activity.completedTools,
|
|
93477
|
+
textTail: activity.textTail
|
|
93478
|
+
}
|
|
93479
|
+
};
|
|
93480
|
+
}
|
|
93007
93481
|
function formatEventForBoss(event, workerSnapshot, autoChainNotices) {
|
|
93008
93482
|
if (event.kind === "user_message") {
|
|
93009
93483
|
return event.text;
|
|
@@ -93021,15 +93495,27 @@ auto_dispatched_since_last_event:
|
|
|
93021
93495
|
${lines.join("\n")}`;
|
|
93022
93496
|
};
|
|
93023
93497
|
if (event.kind === "worker_turn_complete") {
|
|
93024
|
-
const
|
|
93025
|
-
const tools =
|
|
93026
|
-
return `[event:worker_turn_complete] project="${
|
|
93498
|
+
const s2 = event.summary;
|
|
93499
|
+
const tools = s2.toolsUsed.length > 0 ? s2.toolsUsed.map((t) => `${t.ok ? "\u2713" : "\u2717"}${t.name}`).join(", ") : "(none)";
|
|
93500
|
+
return `[event:worker_turn_complete] project="${s2.project}" turn=${s2.turnIndex} timestamp=${s2.timestamp}
|
|
93027
93501
|
tools_used: ${tools}
|
|
93028
93502
|
final_text:
|
|
93029
|
-
${
|
|
93503
|
+
${s2.finalText || "(empty)"}${renderOthers(s2.project)}${renderAutoChain()}`;
|
|
93030
93504
|
}
|
|
93031
|
-
|
|
93505
|
+
if (event.kind === "worker_error") {
|
|
93506
|
+
return `[event:worker_error] project="${event.project}" timestamp=${event.timestamp}
|
|
93032
93507
|
${event.message}${renderOthers(event.project)}${renderAutoChain()}`;
|
|
93508
|
+
}
|
|
93509
|
+
const s = event.snapshot;
|
|
93510
|
+
const completed = s.completedTools.length > 0 ? s.completedTools.map((t) => `${t.ok ? "\u2713" : "\u2717"}${t.name}`).join(", ") : "(none)";
|
|
93511
|
+
const active = s.activeTools.length > 0 ? s.activeTools.join(", ") : "(none)";
|
|
93512
|
+
return `[event:worker_stuck] project="${event.project}" reason=${event.reason} timestamp=${event.timestamp}
|
|
93513
|
+
working_seconds: ${s.workingSeconds}
|
|
93514
|
+
silent_seconds: ${s.silentSeconds}
|
|
93515
|
+
active_tools: ${active}
|
|
93516
|
+
completed_this_turn: ${completed}
|
|
93517
|
+
text_tail:
|
|
93518
|
+
${s.textTail || "(no text yet)"}${renderOthers(event.project)}${renderAutoChain()}`;
|
|
93033
93519
|
}
|
|
93034
93520
|
function computeContextUsed(usage, provider) {
|
|
93035
93521
|
const inputContext = (usage.inputTokens ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
|
|
@@ -93219,4 +93705,4 @@ react/cjs/react-jsx-runtime.development.js:
|
|
|
93219
93705
|
* LICENSE file in the root directory of this source tree.
|
|
93220
93706
|
*)
|
|
93221
93707
|
*/
|
|
93222
|
-
//# sourceMappingURL=chunk-
|
|
93708
|
+
//# sourceMappingURL=chunk-WGJRDNT6.js.map
|