@bivy/bivy 0.10.1-staging.243 → 0.10.1-staging.244
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/server.js
CHANGED
|
@@ -385,6 +385,15 @@ function replayPendingInteractions(sessionId) {
|
|
|
385
385
|
broadcast({ type: "approval.created", approval: a });
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
|
+
const record = resolveSession(sessionId);
|
|
389
|
+
if (record?.turnAttention) {
|
|
390
|
+
const { trigger, idleMs, at } = record.turnAttention;
|
|
391
|
+
const mins = Math.max(1, Math.round(idleMs / 60_000));
|
|
392
|
+
const message = trigger === "wedged"
|
|
393
|
+
? `A tool call has run for ${mins} min without making progress. Stop it or keep waiting?`
|
|
394
|
+
: `The agent has been quiet for ${mins} min. Stop it or keep waiting?`;
|
|
395
|
+
broadcast({ type: "session.turn_attention", sessionId, trigger, idleMs, at, message });
|
|
396
|
+
}
|
|
388
397
|
}
|
|
389
398
|
function persistApprovalRequest(request, resolvedAt) {
|
|
390
399
|
metadata.recordApproval({
|
|
@@ -1909,7 +1918,17 @@ const RELAY_COMMANDS = {
|
|
|
1909
1918
|
const record = resolveSession(msg.sessionId);
|
|
1910
1919
|
if (!record || !sessionBusy(record))
|
|
1911
1920
|
return;
|
|
1912
|
-
|
|
1921
|
+
if (record.turnAttention)
|
|
1922
|
+
turnWatchdog.resolveTurnAttention(record, "stop");
|
|
1923
|
+
else
|
|
1924
|
+
abortSessionRecord(record, ctx.broadcast);
|
|
1925
|
+
},
|
|
1926
|
+
"session.turn_attention.resolve"(msg) {
|
|
1927
|
+
const record = resolveSession(msg.sessionId);
|
|
1928
|
+
const action = msg.action === "stop" ? "stop" : msg.action === "continue" ? "continue" : undefined;
|
|
1929
|
+
if (!record || !action)
|
|
1930
|
+
return;
|
|
1931
|
+
turnWatchdog.resolveTurnAttention(record, action);
|
|
1913
1932
|
},
|
|
1914
1933
|
async "session.command.invoke"(msg, ctx) {
|
|
1915
1934
|
// Invoke a protocol-mode agent command (AgentCommand.mode === "protocol")
|
|
@@ -2064,6 +2083,7 @@ const RELAY_COMMANDS = {
|
|
|
2064
2083
|
// (SessionList.tsx's periodic safety-net poll) clobbering the correct
|
|
2065
2084
|
// needs_action the live session.question broadcast had just set.
|
|
2066
2085
|
const pendingApproval = rec ? sessionHasPendingApproval(rec) : approvals.list().some((a) => a.sessionId === s.id && a.status === "pending");
|
|
2086
|
+
const needsAction = pendingApproval || Boolean(rec?.turnAttention);
|
|
2067
2087
|
return {
|
|
2068
2088
|
path: s.path,
|
|
2069
2089
|
id: s.id,
|
|
@@ -2079,10 +2099,10 @@ const RELAY_COMMANDS = {
|
|
|
2079
2099
|
sandbox: rec?.sandbox ?? normalizeSandboxTier(meta?.sandbox),
|
|
2080
2100
|
prUrl: rec?.prUrl ?? meta?.prUrl,
|
|
2081
2101
|
prs: rec?.prs ?? meta?.prs,
|
|
2082
|
-
status:
|
|
2102
|
+
status: needsAction ? "needs_action" : (rec ? sessionState(rec).displayStatus : "saved"),
|
|
2083
2103
|
sessionState: rec ? sessionState(rec) : undefined,
|
|
2084
2104
|
open: Boolean(rec),
|
|
2085
|
-
needsAction
|
|
2105
|
+
needsAction,
|
|
2086
2106
|
bivySession: bivySessionEnvelopeFromSummary(s, rec, meta),
|
|
2087
2107
|
};
|
|
2088
2108
|
});
|
|
@@ -5873,6 +5893,7 @@ function sessionState(record) {
|
|
|
5873
5893
|
awaitingInput: sessionHasPendingApproval(record),
|
|
5874
5894
|
workspace: record.workspaceState ?? "clean",
|
|
5875
5895
|
lastTurnFailed: Boolean(record.lastFailureAt),
|
|
5896
|
+
turnNeedsAttention: Boolean(record.turnAttention),
|
|
5876
5897
|
});
|
|
5877
5898
|
}
|
|
5878
5899
|
/** Push axis-only transitions (checkpointing, question/approval changes) that
|
|
@@ -6627,11 +6648,25 @@ const stallSweepMs = stallSweepBasis.length ? Math.max(15_000, Math.min(60_000,
|
|
|
6627
6648
|
// The stall/timeout orchestration lives in ./session/turn-watchdog-runtime; its
|
|
6628
6649
|
// whole coupling surface to the daemon is this deps object. The narrow
|
|
6629
6650
|
// WatchdogSession it operates on is structurally satisfied by SessionRecord.
|
|
6651
|
+
const stallAction = process.env.BIVY_TURN_STALL_ACTION?.trim().toLowerCase() === "recover" ? "recover" : "notify";
|
|
6630
6652
|
const turnWatchdog = createTurnWatchdog({
|
|
6631
6653
|
turnTimeoutMs,
|
|
6632
6654
|
turnStallMs,
|
|
6633
6655
|
turnActivityStallMs,
|
|
6656
|
+
stallAction,
|
|
6634
6657
|
broadcast,
|
|
6658
|
+
broadcastSessionState: (record) => broadcastSessionState(record),
|
|
6659
|
+
notifyTurnAttention: (record, message) => {
|
|
6660
|
+
const session = record;
|
|
6661
|
+
void sendNotificationHint({
|
|
6662
|
+
kind: "agent_stalled",
|
|
6663
|
+
sessionId: session.id,
|
|
6664
|
+
targetSessionId: session.id,
|
|
6665
|
+
attentionId: session.id,
|
|
6666
|
+
title: `${sessionNotifyLabel(session)} may be stuck`,
|
|
6667
|
+
body: message,
|
|
6668
|
+
});
|
|
6669
|
+
},
|
|
6635
6670
|
markSessionFailed: (id) => metadata.touchSession(id, "failed"),
|
|
6636
6671
|
abortSessionRecord: (record) => abortSessionRecord(record),
|
|
6637
6672
|
evaluateEphemeralTeardown,
|
|
@@ -6662,8 +6697,10 @@ function markSessionWorking(record, activity, opts) {
|
|
|
6662
6697
|
// separately anchors the wedged band. Raw subprocess output (tool_execution_
|
|
6663
6698
|
// update) bumps lastProgressAt above but NOT this, so a chatty-but-hung tool
|
|
6664
6699
|
// still trips the wedged watchdog. Non-event callers default to structural.
|
|
6665
|
-
|
|
6700
|
+
const structural = opts?.structural !== false;
|
|
6701
|
+
if (structural)
|
|
6666
6702
|
record.lastStructuralProgressAt = now;
|
|
6703
|
+
turnWatchdog.clearTurnAttentionOnProgress(record, structural);
|
|
6667
6704
|
// A new attempt resolves the prior turn's failure condition at its source.
|
|
6668
6705
|
record.lastFailureAt = undefined;
|
|
6669
6706
|
metadata.touchSession(record.id, "working");
|
|
@@ -6673,6 +6710,7 @@ function markSessionWorking(record, activity, opts) {
|
|
|
6673
6710
|
}
|
|
6674
6711
|
}
|
|
6675
6712
|
function clearSessionWorking(record, forcedStatus) {
|
|
6713
|
+
turnWatchdog.clearTurnAttentionOnProgress(record, true);
|
|
6676
6714
|
turnWatchdog.clearTurnWatchdog(record);
|
|
6677
6715
|
touchSession(record);
|
|
6678
6716
|
record.isWorking = false;
|
|
@@ -10188,8 +10226,27 @@ app.post("/api/session/abort", (req, res, next) => {
|
|
|
10188
10226
|
const record = resolveSession(req.body?.sessionId);
|
|
10189
10227
|
if (!record)
|
|
10190
10228
|
return res.status(404).json({ error: "No active session" });
|
|
10191
|
-
if (sessionBusy(record))
|
|
10192
|
-
|
|
10229
|
+
if (sessionBusy(record)) {
|
|
10230
|
+
if (record.turnAttention)
|
|
10231
|
+
turnWatchdog.resolveTurnAttention(record, "stop");
|
|
10232
|
+
else
|
|
10233
|
+
abortSessionRecord(record);
|
|
10234
|
+
}
|
|
10235
|
+
res.json({ ok: true });
|
|
10236
|
+
}
|
|
10237
|
+
catch (error) {
|
|
10238
|
+
next(error);
|
|
10239
|
+
}
|
|
10240
|
+
});
|
|
10241
|
+
app.post("/api/session/turn-attention", (req, res, next) => {
|
|
10242
|
+
try {
|
|
10243
|
+
const record = resolveSession(req.body?.sessionId);
|
|
10244
|
+
if (!record)
|
|
10245
|
+
return res.status(404).json({ error: "No active session" });
|
|
10246
|
+
const action = req.body?.action === "stop" ? "stop" : req.body?.action === "continue" ? "continue" : undefined;
|
|
10247
|
+
if (!action)
|
|
10248
|
+
return res.status(400).json({ error: "Action must be stop or continue" });
|
|
10249
|
+
turnWatchdog.resolveTurnAttention(record, action);
|
|
10193
10250
|
res.json({ ok: true });
|
|
10194
10251
|
}
|
|
10195
10252
|
catch (error) {
|
|
@@ -22,6 +22,10 @@ export function deriveSessionState(evidence) {
|
|
|
22
22
|
displayStatus = "needs_attention";
|
|
23
23
|
else if (process === "exited")
|
|
24
24
|
displayStatus = "failed";
|
|
25
|
+
// A working turn the watchdog flagged for review is actionable even though the
|
|
26
|
+
// agent is still technically "working" — surface it so the user sees the card.
|
|
27
|
+
else if (agent === "working" && evidence.turnNeedsAttention)
|
|
28
|
+
displayStatus = "needs_attention";
|
|
25
29
|
else if (agent === "working")
|
|
26
30
|
displayStatus = "working";
|
|
27
31
|
else if (evidence.lastTurnFailed)
|
|
@@ -63,6 +63,12 @@ export function createTurnWatchdog(deps) {
|
|
|
63
63
|
return `A tool call ran for ${mins} min without making progress and was recovered. Send a message to continue.`;
|
|
64
64
|
return `The agent stopped responding (no activity for ${mins} min) and was recovered. Send a message to continue.`;
|
|
65
65
|
}
|
|
66
|
+
function turnAttentionMessage(idleMs, trigger) {
|
|
67
|
+
const mins = Math.max(1, Math.round(idleMs / 60_000));
|
|
68
|
+
if (trigger === "wedged")
|
|
69
|
+
return `A tool call has run for ${mins} min without making progress. Stop it or keep waiting?`;
|
|
70
|
+
return `The agent has been quiet for ${mins} min. Stop it or keep waiting?`;
|
|
71
|
+
}
|
|
66
72
|
/** Idle used for a stall's diagnostic/message. A `wedged` turn is measured from
|
|
67
73
|
* the last STRUCTURAL progress (raw output kept flowing), every other trigger
|
|
68
74
|
* from the last progress of any kind. */
|
|
@@ -97,6 +103,10 @@ export function createTurnWatchdog(deps) {
|
|
|
97
103
|
if (record.turnTimedOut)
|
|
98
104
|
return; // already recovering this turn
|
|
99
105
|
record.turnTimedOut = true;
|
|
106
|
+
// A real recovery (user hit Stop, wall-clock cap, or a dead subprocess)
|
|
107
|
+
// supersedes any pending "needs your decision" card — drop it so the client
|
|
108
|
+
// closes it and the failed/outcome events below take over.
|
|
109
|
+
record.turnAttention = undefined;
|
|
100
110
|
record.lastFailureAt = now();
|
|
101
111
|
const trigger = diag?.trigger ?? "stalled";
|
|
102
112
|
const turnMs = record.workingStartedAt ? record.lastFailureAt - record.workingStartedAt : undefined;
|
|
@@ -152,9 +162,53 @@ export function createTurnWatchdog(deps) {
|
|
|
152
162
|
activityStallMs: turnActivityStallMs,
|
|
153
163
|
});
|
|
154
164
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
function clearTurnAttention(record) {
|
|
166
|
+
if (!record.turnAttention)
|
|
167
|
+
return;
|
|
168
|
+
record.turnAttention = undefined;
|
|
169
|
+
deps.broadcast({ type: "session.turn_attention.resolved", sessionId: record.id });
|
|
170
|
+
deps.broadcastSessionState(record);
|
|
171
|
+
}
|
|
172
|
+
function flagTurnForReview(record, trigger, idleMs, at) {
|
|
173
|
+
if (record.turnAttention)
|
|
174
|
+
return;
|
|
175
|
+
const message = turnAttentionMessage(idleMs, trigger);
|
|
176
|
+
record.turnAttention = { trigger, idleMs, at };
|
|
177
|
+
deps.broadcast({ type: "session.turn_attention", sessionId: record.id, trigger, idleMs, at, message });
|
|
178
|
+
deps.broadcastSessionState(record);
|
|
179
|
+
deps.notifyTurnAttention(record, message);
|
|
180
|
+
}
|
|
181
|
+
function resolveTurnAttention(record, action) {
|
|
182
|
+
const attention = record.turnAttention;
|
|
183
|
+
if (!attention)
|
|
184
|
+
return;
|
|
185
|
+
if (action === "stop") {
|
|
186
|
+
recoverStuckTurn(record, turnStallMessage(attention.idleMs, attention.trigger), attention);
|
|
187
|
+
// recoverStuckTurn clears the field, but it intentionally emits failure
|
|
188
|
+
// frames rather than the ordinary resolved frame. Emit this one too so a
|
|
189
|
+
// client can deterministically remove its card before those frames arrive.
|
|
190
|
+
deps.broadcast({ type: "session.turn_attention.resolved", sessionId: record.id });
|
|
191
|
+
deps.broadcastSessionState(record);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const at = now();
|
|
195
|
+
record.lastProgressAt = at;
|
|
196
|
+
record.lastStructuralProgressAt = at;
|
|
197
|
+
clearTurnAttention(record);
|
|
198
|
+
}
|
|
199
|
+
function clearTurnAttentionOnProgress(record, structural) {
|
|
200
|
+
const attention = record.turnAttention;
|
|
201
|
+
if (!attention)
|
|
202
|
+
return;
|
|
203
|
+
// Silence means any activity disproves the warning. A wedged tool can keep
|
|
204
|
+
// producing raw output forever, so only structural progress dismisses it.
|
|
205
|
+
if (attention.trigger === "wedged" && !structural)
|
|
206
|
+
return;
|
|
207
|
+
clearTurnAttention(record);
|
|
208
|
+
}
|
|
209
|
+
/** Periodic sweep: dead subprocesses are always recovered. Time-based soft
|
|
210
|
+
* stalls normally ask the user instead; operators can opt into the legacy
|
|
211
|
+
* automatic recovery with stallAction="recover". */
|
|
158
212
|
function sweepStalledTurns() {
|
|
159
213
|
if (turnStallMs <= 0 && turnActivityStallMs <= 0)
|
|
160
214
|
return;
|
|
@@ -164,7 +218,12 @@ export function createTurnWatchdog(deps) {
|
|
|
164
218
|
if (!trigger)
|
|
165
219
|
continue;
|
|
166
220
|
const idleMs = stallIdleMs(record, trigger, at);
|
|
167
|
-
|
|
221
|
+
if (trigger === "pid_dead" || deps.stallAction === "recover") {
|
|
222
|
+
recoverStuckTurn(record, turnStallMessage(idleMs, trigger), { trigger, idleMs });
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
flagTurnForReview(record, trigger, idleMs, at);
|
|
226
|
+
}
|
|
168
227
|
}
|
|
169
228
|
}
|
|
170
229
|
/**
|
|
@@ -216,5 +275,7 @@ export function createTurnWatchdog(deps) {
|
|
|
216
275
|
stallTriggerFor,
|
|
217
276
|
sweepStalledTurns,
|
|
218
277
|
turnRecoveryStats,
|
|
278
|
+
resolveTurnAttention,
|
|
279
|
+
clearTurnAttentionOnProgress,
|
|
219
280
|
};
|
|
220
281
|
}
|
package/package.json
CHANGED