@evident-ai/cli 3.1.1-dev.0d1732f → 3.1.1-dev.0d69ecc
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/index.js +343 -60
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1532,6 +1532,9 @@ function isPreamblePinnedRunning(messages, userMessageId) {
|
|
|
1532
1532
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
1533
1533
|
return completedOf(reply) != null && finishOf(reply) === "tool-calls";
|
|
1534
1534
|
}
|
|
1535
|
+
function isB2AbandonmentConfirmed(params) {
|
|
1536
|
+
return params.pinnedForMs >= params.minPinnedMs && params.descendantOngoing === false;
|
|
1537
|
+
}
|
|
1535
1538
|
function messageError(messages, userMessageId) {
|
|
1536
1539
|
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
1537
1540
|
const error2 = errorOf(reply);
|
|
@@ -1545,6 +1548,42 @@ function messageError(messages, userMessageId) {
|
|
|
1545
1548
|
}
|
|
1546
1549
|
return "The agent run failed.";
|
|
1547
1550
|
}
|
|
1551
|
+
function messageFailure(messages, userMessageId) {
|
|
1552
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
1553
|
+
const error2 = errorOf(reply);
|
|
1554
|
+
if (error2 == null || typeof error2 !== "object") return null;
|
|
1555
|
+
const e = error2;
|
|
1556
|
+
const replyProviderId = reply?.info?.providerID ?? null;
|
|
1557
|
+
const replyModelId = reply?.info?.modelID ?? null;
|
|
1558
|
+
if (e.name === "ProviderAuthError") {
|
|
1559
|
+
const data = e.data;
|
|
1560
|
+
const providerId = typeof data?.providerID === "string" && data.providerID || replyProviderId;
|
|
1561
|
+
return { kind: "model_auth", providerId, modelId: replyModelId, reason: "missing" };
|
|
1562
|
+
}
|
|
1563
|
+
if (e.name === "APIError") {
|
|
1564
|
+
const data = e.data;
|
|
1565
|
+
const statusCode = data?.statusCode;
|
|
1566
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
1567
|
+
return {
|
|
1568
|
+
kind: "model_auth",
|
|
1569
|
+
providerId: replyProviderId,
|
|
1570
|
+
modelId: replyModelId,
|
|
1571
|
+
reason: "rejected"
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
function applyZeroProviderFallback(classified, hasConfiguredProvider, replyProviderId, replyModelId = null) {
|
|
1578
|
+
if (classified != null) return classified;
|
|
1579
|
+
if (hasConfiguredProvider !== false) return null;
|
|
1580
|
+
return {
|
|
1581
|
+
kind: "model_auth",
|
|
1582
|
+
providerId: replyProviderId,
|
|
1583
|
+
modelId: replyModelId,
|
|
1584
|
+
reason: "missing"
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1548
1587
|
function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
1549
1588
|
if (!messages || messages.length === 0) return false;
|
|
1550
1589
|
return messages.some(
|
|
@@ -2073,6 +2112,18 @@ var RunnerConnection = class {
|
|
|
2073
2112
|
}
|
|
2074
2113
|
};
|
|
2075
2114
|
|
|
2115
|
+
// src/lib/tunnel/ready-marker.ts
|
|
2116
|
+
import { writeFileSync } from "fs";
|
|
2117
|
+
function writeTunnelReadyMarker(path, agentId) {
|
|
2118
|
+
try {
|
|
2119
|
+
writeFileSync(path, `${agentId}
|
|
2120
|
+
`);
|
|
2121
|
+
return { ok: true };
|
|
2122
|
+
} catch (error2) {
|
|
2123
|
+
return { ok: false, error: error2 instanceof Error ? error2.message : String(error2) };
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2076
2127
|
// src/lib/channels/driver.ts
|
|
2077
2128
|
import { homedir } from "os";
|
|
2078
2129
|
|
|
@@ -2499,6 +2550,8 @@ var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
|
2499
2550
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
2500
2551
|
var HEARTBEAT_MS = 6e4;
|
|
2501
2552
|
var ABSOLUTE_MAX_PROCESSING_MS = 6 * 60 * 60 * 1e3;
|
|
2553
|
+
var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
|
|
2554
|
+
var B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;
|
|
2502
2555
|
var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
|
|
2503
2556
|
var MAX_SUPERSEDED_CONVERSATIONS = 256;
|
|
2504
2557
|
var ChannelAuthError = class extends Error {
|
|
@@ -3324,7 +3377,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3324
3377
|
pausedOnPermission: false,
|
|
3325
3378
|
pausedClearConfirmed: false,
|
|
3326
3379
|
pausedInFlight: false,
|
|
3327
|
-
deliveryDeadlineAnchored: false
|
|
3380
|
+
deliveryDeadlineAnchored: false,
|
|
3381
|
+
b2PinnedSinceMs: 0,
|
|
3382
|
+
b2LastDescendantCheckMs: 0,
|
|
3383
|
+
b2AbandonedSignalled: false
|
|
3328
3384
|
});
|
|
3329
3385
|
}
|
|
3330
3386
|
/**
|
|
@@ -3399,7 +3455,10 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3399
3455
|
pausedOnPermission: false,
|
|
3400
3456
|
pausedClearConfirmed: false,
|
|
3401
3457
|
pausedInFlight: false,
|
|
3402
|
-
deliveryDeadlineAnchored: false
|
|
3458
|
+
deliveryDeadlineAnchored: false,
|
|
3459
|
+
b2PinnedSinceMs: 0,
|
|
3460
|
+
b2LastDescendantCheckMs: 0,
|
|
3461
|
+
b2AbandonedSignalled: false
|
|
3403
3462
|
});
|
|
3404
3463
|
}
|
|
3405
3464
|
/**
|
|
@@ -3561,58 +3620,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3561
3620
|
}
|
|
3562
3621
|
}
|
|
3563
3622
|
if (state === "done") {
|
|
3564
|
-
this.
|
|
3565
|
-
if (!inFlight.done) {
|
|
3566
|
-
this.log({
|
|
3567
|
-
level: "info",
|
|
3568
|
-
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} completed \u2014 marking done`,
|
|
3569
|
-
conversation_id: conv.id,
|
|
3570
|
-
message_id: inFlight.evidentMessageId
|
|
3571
|
-
});
|
|
3572
|
-
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
3573
|
-
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
3574
|
-
try {
|
|
3575
|
-
await this.markDone(
|
|
3576
|
-
conv.id,
|
|
3577
|
-
inFlight.evidentMessageId,
|
|
3578
|
-
sessionId,
|
|
3579
|
-
inFlight.opencodeMessageId,
|
|
3580
|
-
title,
|
|
3581
|
-
usage
|
|
3582
|
-
);
|
|
3583
|
-
} catch (err) {
|
|
3584
|
-
if (err instanceof ChannelAuthError) throw err;
|
|
3585
|
-
if (err instanceof ChannelTerminalError) {
|
|
3586
|
-
this.log({
|
|
3587
|
-
level: "warn",
|
|
3588
|
-
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (terminal HTTP ${err.status}) \u2014 leaving for the cron safety net: ${err.message}`,
|
|
3589
|
-
conversation_id: conv.id,
|
|
3590
|
-
message_id: inFlight.evidentMessageId
|
|
3591
|
-
});
|
|
3592
|
-
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
3593
|
-
return;
|
|
3594
|
-
}
|
|
3595
|
-
if (this.now() >= inFlight.deadline) {
|
|
3596
|
-
this.log({
|
|
3597
|
-
level: "warn",
|
|
3598
|
-
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done within the watch window \u2014 leaving for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,
|
|
3599
|
-
conversation_id: conv.id,
|
|
3600
|
-
message_id: inFlight.evidentMessageId
|
|
3601
|
-
});
|
|
3602
|
-
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
3603
|
-
return;
|
|
3604
|
-
}
|
|
3605
|
-
this.log({
|
|
3606
|
-
level: "warn",
|
|
3607
|
-
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
3608
|
-
conversation_id: conv.id,
|
|
3609
|
-
message_id: inFlight.evidentMessageId
|
|
3610
|
-
});
|
|
3611
|
-
return;
|
|
3612
|
-
}
|
|
3613
|
-
inFlight.done = true;
|
|
3614
|
-
}
|
|
3615
|
-
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
3623
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
3616
3624
|
return;
|
|
3617
3625
|
}
|
|
3618
3626
|
if (state === "failed") {
|
|
@@ -3626,8 +3634,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3626
3634
|
message_id: inFlight.evidentMessageId
|
|
3627
3635
|
});
|
|
3628
3636
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
3637
|
+
const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
|
|
3629
3638
|
try {
|
|
3630
|
-
await this.markFailed(
|
|
3639
|
+
await this.markFailed(
|
|
3640
|
+
conv.id,
|
|
3641
|
+
inFlight.evidentMessageId,
|
|
3642
|
+
sessionId,
|
|
3643
|
+
error2,
|
|
3644
|
+
usage,
|
|
3645
|
+
failure
|
|
3646
|
+
);
|
|
3631
3647
|
} catch (err) {
|
|
3632
3648
|
if (err instanceof ChannelAuthError) throw err;
|
|
3633
3649
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -3672,6 +3688,44 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3672
3688
|
});
|
|
3673
3689
|
}
|
|
3674
3690
|
const activelyRunning = state === "running" && !awaitingHuman;
|
|
3691
|
+
const pinnedNow = activelyRunning && isPreamblePinnedRunning(messages, inFlight.opencodeMessageId);
|
|
3692
|
+
const snapshotReadable = messages != null && messages.length > 0;
|
|
3693
|
+
if (!pinnedNow) {
|
|
3694
|
+
if (snapshotReadable) {
|
|
3695
|
+
inFlight.b2PinnedSinceMs = 0;
|
|
3696
|
+
inFlight.b2LastDescendantCheckMs = 0;
|
|
3697
|
+
inFlight.b2AbandonedSignalled = false;
|
|
3698
|
+
}
|
|
3699
|
+
} else {
|
|
3700
|
+
if (inFlight.b2AbandonedSignalled) {
|
|
3701
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3704
|
+
if (inFlight.b2PinnedSinceMs === 0) inFlight.b2PinnedSinceMs = this.now();
|
|
3705
|
+
const pinnedForMs = this.now() - inFlight.b2PinnedSinceMs;
|
|
3706
|
+
if (pinnedForMs >= B2_ABANDONMENT_MIN_PINNED_MS && this.now() - inFlight.b2LastDescendantCheckMs >= B2_ABANDONMENT_RECHECK_MS) {
|
|
3707
|
+
inFlight.b2LastDescendantCheckMs = this.now();
|
|
3708
|
+
const descendantOngoing = await this.isAnyDescendantSessionOngoing(sessionId);
|
|
3709
|
+
if (isB2AbandonmentConfirmed({
|
|
3710
|
+
pinnedForMs,
|
|
3711
|
+
minPinnedMs: B2_ABANDONMENT_MIN_PINNED_MS,
|
|
3712
|
+
descendantOngoing
|
|
3713
|
+
})) {
|
|
3714
|
+
inFlight.b2AbandonedSignalled = true;
|
|
3715
|
+
this.log({
|
|
3716
|
+
level: "warn",
|
|
3717
|
+
message: `Message ${id.slice(0, 8)} b2-pinned for ${Math.round(pinnedForMs / 1e3)}s with no ongoing descendant sub-agent session (status-map confirmed) \u2014 treating the delegated/tool turn as abandoned, resolving done`,
|
|
3718
|
+
conversation_id: conv.id,
|
|
3719
|
+
message_id: id
|
|
3720
|
+
});
|
|
3721
|
+
void this.postSignal(conv.id, id, "b2_abandoned_resolved", {
|
|
3722
|
+
watched_for_ms: pinnedForMs
|
|
3723
|
+
});
|
|
3724
|
+
await this.settleMessageDone(sessionId, watcher, inFlight, messages);
|
|
3725
|
+
return;
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3675
3729
|
if (activelyRunning && this.now() - inFlight.processingAnchorMs >= ABSOLUTE_MAX_PROCESSING_MS) {
|
|
3676
3730
|
this.log({
|
|
3677
3731
|
level: "warn",
|
|
@@ -3740,6 +3794,70 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3740
3794
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
3741
3795
|
}
|
|
3742
3796
|
}
|
|
3797
|
+
/**
|
|
3798
|
+
* Settle a message whose run-state has resolved `'done'` — extracted verbatim
|
|
3799
|
+
* (pure refactor, no behavior change) from `serviceInFlightMessage`'s former
|
|
3800
|
+
* inline `state === 'done'` branch body, so a SECOND caller (the #721
|
|
3801
|
+
* b2-abandonment resolution) can reach the exact same completion behavior
|
|
3802
|
+
* (delivery-deadline anchoring, title resolution, usage extraction, and
|
|
3803
|
+
* `markDone`'s auth/terminal/transient-retry discipline) without duplicating it
|
|
3804
|
+
* and risking the two copies silently drifting apart.
|
|
3805
|
+
*/
|
|
3806
|
+
async settleMessageDone(sessionId, watcher, inFlight, messages) {
|
|
3807
|
+
const conv = watcher.conv;
|
|
3808
|
+
this.anchorDeliveryDeadline(inFlight);
|
|
3809
|
+
if (!inFlight.done) {
|
|
3810
|
+
this.log({
|
|
3811
|
+
level: "info",
|
|
3812
|
+
message: `Message ${inFlight.evidentMessageId.slice(0, 8)} completed \u2014 marking done`,
|
|
3813
|
+
conversation_id: conv.id,
|
|
3814
|
+
message_id: inFlight.evidentMessageId
|
|
3815
|
+
});
|
|
3816
|
+
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
3817
|
+
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
3818
|
+
try {
|
|
3819
|
+
await this.markDone(
|
|
3820
|
+
conv.id,
|
|
3821
|
+
inFlight.evidentMessageId,
|
|
3822
|
+
sessionId,
|
|
3823
|
+
inFlight.opencodeMessageId,
|
|
3824
|
+
title,
|
|
3825
|
+
usage
|
|
3826
|
+
);
|
|
3827
|
+
} catch (err) {
|
|
3828
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
3829
|
+
if (err instanceof ChannelTerminalError) {
|
|
3830
|
+
this.log({
|
|
3831
|
+
level: "warn",
|
|
3832
|
+
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (terminal HTTP ${err.status}) \u2014 leaving for the cron safety net: ${err.message}`,
|
|
3833
|
+
conversation_id: conv.id,
|
|
3834
|
+
message_id: inFlight.evidentMessageId
|
|
3835
|
+
});
|
|
3836
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
3837
|
+
return;
|
|
3838
|
+
}
|
|
3839
|
+
if (this.now() >= inFlight.deadline) {
|
|
3840
|
+
this.log({
|
|
3841
|
+
level: "warn",
|
|
3842
|
+
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done within the watch window \u2014 leaving for the cron safety net: ${err instanceof Error ? err.message : String(err)}`,
|
|
3843
|
+
conversation_id: conv.id,
|
|
3844
|
+
message_id: inFlight.evidentMessageId
|
|
3845
|
+
});
|
|
3846
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
3847
|
+
return;
|
|
3848
|
+
}
|
|
3849
|
+
this.log({
|
|
3850
|
+
level: "warn",
|
|
3851
|
+
message: `Failed to mark message ${inFlight.evidentMessageId.slice(0, 8)} done (will retry next tick): ${err instanceof Error ? err.message : String(err)}`,
|
|
3852
|
+
conversation_id: conv.id,
|
|
3853
|
+
message_id: inFlight.evidentMessageId
|
|
3854
|
+
});
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3857
|
+
inFlight.done = true;
|
|
3858
|
+
}
|
|
3859
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
3860
|
+
}
|
|
3743
3861
|
// Restart recovery: re-adopt `processing` messages (ADR-0046, WI-3/4/5)
|
|
3744
3862
|
/**
|
|
3745
3863
|
* Re-adopt this agent's `processing` messages on drain (ADR-0046 Decision §1).
|
|
@@ -3906,6 +4024,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3906
4024
|
if (state === "failed") {
|
|
3907
4025
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
3908
4026
|
const usage = messageUsage(messages, ocId ?? "");
|
|
4027
|
+
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
3909
4028
|
this.log({
|
|
3910
4029
|
level: "error",
|
|
3911
4030
|
message: `Re-adopt: message ${row.id.slice(0, 8)} errored while unwatched \u2014 marking failed: ${error2 ?? "(no error text)"}`,
|
|
@@ -3913,7 +4032,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3913
4032
|
message_id: row.id
|
|
3914
4033
|
});
|
|
3915
4034
|
try {
|
|
3916
|
-
await this.markFailed(row.conversation_id, row.id, sessionId, error2, usage);
|
|
4035
|
+
await this.markFailed(row.conversation_id, row.id, sessionId, error2, usage, failure);
|
|
3917
4036
|
} catch (err) {
|
|
3918
4037
|
if (err instanceof ChannelAuthError) throw err;
|
|
3919
4038
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -4319,6 +4438,47 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4319
4438
|
}
|
|
4320
4439
|
return false;
|
|
4321
4440
|
}
|
|
4441
|
+
/**
|
|
4442
|
+
* Tri-state variant of the upward parentID membership walk (#721), used ONLY
|
|
4443
|
+
* by `isAnyDescendantSessionOngoing`. Walks the SAME cached
|
|
4444
|
+
* `resolveSessionParent` chain `sessionBelongsTo` uses above, but — unlike
|
|
4445
|
+
* `sessionBelongsTo`, which deliberately collapses "confirmed not a
|
|
4446
|
+
* descendant" and "the walk's fetch failed" into the same `false` (safe for
|
|
4447
|
+
* its OTHER callers: interaction attribution and the recovery-path
|
|
4448
|
+
* `isAnyDescendantSessionAlive`, both of which just retry next tick with no
|
|
4449
|
+
* safety consequence either way) — this variant keeps those two outcomes
|
|
4450
|
+
* SEPARATE, because `isAnyDescendantSessionOngoing`'s caller
|
|
4451
|
+
* (`isB2AbandonmentConfirmed`) must never treat "couldn't tell" as "confirmed
|
|
4452
|
+
* not ongoing".
|
|
4453
|
+
*
|
|
4454
|
+
* Return contract:
|
|
4455
|
+
* - `true` → the walk reached `rootSessionId` — `sessionId` IS a descendant.
|
|
4456
|
+
* - `false` → the walk reached a definitive, parent-less root session
|
|
4457
|
+
* WITHOUT ever matching `rootSessionId` — `sessionId` is
|
|
4458
|
+
* CONFIRMED NOT a descendant of it.
|
|
4459
|
+
* - `null` → INDETERMINATE: a `GET /session/:id` fetch failed partway
|
|
4460
|
+
* through the walk (`resolveSessionParent` returned `undefined`),
|
|
4461
|
+
* or the depth cap (32) was hit without a definitive answer (a
|
|
4462
|
+
* pathological/cyclic chain proves nothing either way). NEVER
|
|
4463
|
+
* treat this the same as `false` — see `sessionBelongsTo`'s own
|
|
4464
|
+
* doc comment above for why that collapse is safe THERE but not
|
|
4465
|
+
* here.
|
|
4466
|
+
*
|
|
4467
|
+
* `sessionBelongsTo` itself is UNCHANGED — this is an additive helper scoped
|
|
4468
|
+
* to the live-path descendant check, not a modification of shared code used
|
|
4469
|
+
* by interaction attribution or the recovery path.
|
|
4470
|
+
*/
|
|
4471
|
+
async resolveSessionMembership(sessionId, rootSessionId) {
|
|
4472
|
+
let current = sessionId;
|
|
4473
|
+
for (let depth = 0; current && depth < 32; depth++) {
|
|
4474
|
+
if (current === rootSessionId) return true;
|
|
4475
|
+
const parent = await this.resolveSessionParent(current);
|
|
4476
|
+
if (parent === void 0) return null;
|
|
4477
|
+
if (parent === null) return false;
|
|
4478
|
+
current = parent;
|
|
4479
|
+
}
|
|
4480
|
+
return null;
|
|
4481
|
+
}
|
|
4322
4482
|
/**
|
|
4323
4483
|
* Resolve (and cache) a session's `parentID` via `GET /session/:id`. Returns
|
|
4324
4484
|
* `null` for a root session (no parent) and `undefined` when opencode is
|
|
@@ -4509,6 +4669,84 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4509
4669
|
}
|
|
4510
4670
|
return false;
|
|
4511
4671
|
}
|
|
4672
|
+
/**
|
|
4673
|
+
* LIVE-PATH descendant-liveness check (#721): is any descendant (`task`
|
|
4674
|
+
* sub-agent) session under `rootSessionId` currently ONGOING per OpenCode's own
|
|
4675
|
+
* in-memory status map (`isSessionOngoing` — `busy`/`retry`)?
|
|
4676
|
+
*
|
|
4677
|
+
* Deliberately NOT `isAnyDescendantSessionAlive` (the RECOVERY-path
|
|
4678
|
+
* cross-check above): that method judges liveness from the child's OWN
|
|
4679
|
+
* TRANSCRIPT (`isSessionActivelyGenerating`), which is the right (only) option
|
|
4680
|
+
* on the recovery path because a restart WIPES `SessionStatus`. On the LIVE
|
|
4681
|
+
* path the local opencode server IS running, so its in-memory status map is
|
|
4682
|
+
* live and authoritative — and per ADR-0047 §4a ("the child has its own entry
|
|
4683
|
+
* [in the map]"), a `task` descendant's OWN busy/retry entry reflects its
|
|
4684
|
+
* ENTIRE turn (including any tool call it is itself executing), not a
|
|
4685
|
+
* per-message transcript snapshot. This sidesteps the "child's own tool is
|
|
4686
|
+
* executing, between its step's completion and the next generation step"
|
|
4687
|
+
* transcript gap that a transcript-based check would need a second,
|
|
4688
|
+
* sustained-window bound to guard against — it is simply not derived from
|
|
4689
|
+
* message timestamps at all.
|
|
4690
|
+
*
|
|
4691
|
+
* Why not just check `isSessionOngoing(port, rootSessionId)` (the ROOT's own
|
|
4692
|
+
* status, as the recovery path does per §4a)? Because on the LIVE path the
|
|
4693
|
+
* root session can be shared: a SECOND, unrelated user message can land on the
|
|
4694
|
+
* SAME session (issue #721's own root cause) and keep the root `busy` for a
|
|
4695
|
+
* reason that has nothing to do with THIS message's delegation. A `task`
|
|
4696
|
+
* descendant session is spawned for exactly one delegated turn and never
|
|
4697
|
+
* reused, so its OWN status-map entry is unambiguous evidence about that one
|
|
4698
|
+
* delegation — which the root's status is not.
|
|
4699
|
+
*
|
|
4700
|
+
* Why membership is checked via `resolveSessionMembership`, NOT
|
|
4701
|
+
* `sessionBelongsTo`: `sessionBelongsTo` collapses a transient
|
|
4702
|
+
* `GET /session/:id` fetch failure into "not a descendant", which would
|
|
4703
|
+
* silently drop a genuinely-live candidate from consideration on the one
|
|
4704
|
+
* unlucky tick its membership-walk fetch hiccups (#721).
|
|
4705
|
+
* `resolveSessionMembership` keeps that failure mode as a distinct `null`
|
|
4706
|
+
* (indeterminate) so it is folded into THIS method's own `indeterminate` flag
|
|
4707
|
+
* instead.
|
|
4708
|
+
*
|
|
4709
|
+
* Return contract (note the DIFFERENT judge vs. `isAnyDescendantSessionAlive`):
|
|
4710
|
+
* - `true` → some descendant session is `busy`/`retry` (genuinely ongoing).
|
|
4711
|
+
* - `false` → enumeration succeeded, EVERY candidate's MEMBERSHIP was
|
|
4712
|
+
* confirmed either way (`resolveSessionMembership` never
|
|
4713
|
+
* returned `null`), and every CONFIRMED descendant's status read
|
|
4714
|
+
* succeeded and is not ongoing (includes "no descendant session
|
|
4715
|
+
* exists at all" — e.g. a plain, non-`task` tool call).
|
|
4716
|
+
* - `null` → INDETERMINATE: `listSessions` failed, OR at least one
|
|
4717
|
+
* candidate's MEMBERSHIP could not be confirmed
|
|
4718
|
+
* (`resolveSessionMembership` returned `null` — a fetch failure
|
|
4719
|
+
* or pathological chain partway through the parent walk), OR at
|
|
4720
|
+
* least one CONFIRMED descendant's `isSessionOngoing` read
|
|
4721
|
+
* failed — and no OTHER candidate was already confirmed `true`.
|
|
4722
|
+
* The caller MUST NOT treat `null` the same as `false` here
|
|
4723
|
+
* (unlike the recovery cross-check's contract) — see
|
|
4724
|
+
* `isB2AbandonmentConfirmed`.
|
|
4725
|
+
*/
|
|
4726
|
+
async isAnyDescendantSessionOngoing(rootSessionId) {
|
|
4727
|
+
const sessions = await listSessions(this.port);
|
|
4728
|
+
if (!sessions) {
|
|
4729
|
+
this.log({
|
|
4730
|
+
level: "warn",
|
|
4731
|
+
message: `Could not enumerate sessions to check descendant liveness for root ${rootSessionId} (listSessions failed) \u2014 treating descendant liveness as indeterminate`
|
|
4732
|
+
});
|
|
4733
|
+
return null;
|
|
4734
|
+
}
|
|
4735
|
+
let indeterminate = false;
|
|
4736
|
+
for (const candidate of sessions) {
|
|
4737
|
+
if (!candidate?.id || candidate.id === rootSessionId) continue;
|
|
4738
|
+
const membership = await this.resolveSessionMembership(candidate.id, rootSessionId);
|
|
4739
|
+
if (membership === null) {
|
|
4740
|
+
indeterminate = true;
|
|
4741
|
+
continue;
|
|
4742
|
+
}
|
|
4743
|
+
if (membership === false) continue;
|
|
4744
|
+
const ongoing = await isSessionOngoing(this.port, candidate.id);
|
|
4745
|
+
if (ongoing === true) return true;
|
|
4746
|
+
if (ongoing === null) indeterminate = true;
|
|
4747
|
+
}
|
|
4748
|
+
return indeterminate ? null : false;
|
|
4749
|
+
}
|
|
4512
4750
|
/**
|
|
4513
4751
|
* Cheap decision-telemetry label for a running row's LAST correlated reply
|
|
4514
4752
|
* (WI-2 Task 2.2): which running SHAPE it is, for the status-gated recovery log.
|
|
@@ -4772,7 +5010,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4772
5010
|
* exists but is wedged, so the next attempt must get a fresh one
|
|
4773
5011
|
* instead of reusing it — see WI-1's server-side null-clearing PATCH).
|
|
4774
5012
|
*/
|
|
4775
|
-
async markFailed(conversationId, messageId, sessionId, error2, usage) {
|
|
5013
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
|
|
4776
5014
|
const body = { status: "failed" };
|
|
4777
5015
|
if (sessionId === null) {
|
|
4778
5016
|
body.opencode_session_id = null;
|
|
@@ -4781,6 +5019,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4781
5019
|
}
|
|
4782
5020
|
if (error2 !== void 0) body.error = error2;
|
|
4783
5021
|
if (usage) Object.assign(body, usage);
|
|
5022
|
+
if (failure) {
|
|
5023
|
+
body.failure_kind = failure.kind;
|
|
5024
|
+
body.failure_provider_id = failure.providerId;
|
|
5025
|
+
body.failure_model_id = failure.modelId;
|
|
5026
|
+
body.failure_reason = failure.reason;
|
|
5027
|
+
}
|
|
4784
5028
|
await this.callWithRetry(
|
|
4785
5029
|
"marking message as failed",
|
|
4786
5030
|
() => this.fetchImpl(
|
|
@@ -4793,6 +5037,29 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4793
5037
|
)
|
|
4794
5038
|
);
|
|
4795
5039
|
}
|
|
5040
|
+
/**
|
|
5041
|
+
* Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
|
|
5042
|
+
*
|
|
5043
|
+
* `messageFailure` alone (structured OpenCode error → `model_auth`) covers
|
|
5044
|
+
* most cases; when it returns `null` on this ALREADY-FAILED turn, fall back
|
|
5045
|
+
* to the P1-2b zero-provider check — one extra loopback call to
|
|
5046
|
+
* `hasAnyConfiguredProvider`, only reached when the structured classifier
|
|
5047
|
+
* couldn't place it. Fails open (never throws): a fallback probe failure
|
|
5048
|
+
* (`null`/indeterminate) leaves the classification `null`, which produces
|
|
5049
|
+
* today's byte-identical PATCH body via `markFailed`'s `if (failure)` guard.
|
|
5050
|
+
*/
|
|
5051
|
+
async classifyModelAuthFailure(messages, userMessageId) {
|
|
5052
|
+
const classified = messageFailure(messages, userMessageId);
|
|
5053
|
+
if (classified != null) return classified;
|
|
5054
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
5055
|
+
const hasProvider = await hasAnyConfiguredProvider(this.port);
|
|
5056
|
+
return applyZeroProviderFallback(
|
|
5057
|
+
classified,
|
|
5058
|
+
hasProvider,
|
|
5059
|
+
reply?.info?.providerID ?? null,
|
|
5060
|
+
reply?.info?.modelID ?? null
|
|
5061
|
+
);
|
|
5062
|
+
}
|
|
4796
5063
|
/**
|
|
4797
5064
|
* Best-effort, SINGLE-ATTEMPT runner→server telemetry ping
|
|
4798
5065
|
* (queued-followup-redrive). `POST .../messages/:id/signal {signal, ...extra}`
|
|
@@ -4969,7 +5236,7 @@ async function ensureOpenCodeRunning(ctx) {
|
|
|
4969
5236
|
console.log(chalk5.yellow("Tip: Run with the correct port:"));
|
|
4970
5237
|
console.log(
|
|
4971
5238
|
chalk5.dim(
|
|
4972
|
-
` ${getCliName()} run --
|
|
5239
|
+
` ${getCliName()} run --runner ${ctx.agentId} --port ${runningInstances[0].port}`
|
|
4973
5240
|
)
|
|
4974
5241
|
);
|
|
4975
5242
|
}
|
|
@@ -5099,7 +5366,7 @@ async function resolveAgentIdFromKey(authHeader) {
|
|
|
5099
5366
|
return { agent_id: data.agent_id };
|
|
5100
5367
|
}
|
|
5101
5368
|
return {
|
|
5102
|
-
error: "Cannot resolve runner ID: auth type is not agent_key. Please provide --
|
|
5369
|
+
error: "Cannot resolve runner ID: auth type is not agent_key. Please provide --runner explicitly."
|
|
5103
5370
|
};
|
|
5104
5371
|
} catch (error2) {
|
|
5105
5372
|
const message = error2 instanceof Error ? error2.message : "Unknown error";
|
|
@@ -5895,6 +6162,18 @@ async function run(options) {
|
|
|
5895
6162
|
type: "info",
|
|
5896
6163
|
message: `Tunnel ${isReconnect ? "reconnected" : "connected"} (runner: ${agentId})`
|
|
5897
6164
|
});
|
|
6165
|
+
if (options.tunnelReadyFile) {
|
|
6166
|
+
const marker = writeTunnelReadyMarker(options.tunnelReadyFile, agentId);
|
|
6167
|
+
if (marker.ok) {
|
|
6168
|
+
log2(state, `Wrote tunnel readiness marker to ${options.tunnelReadyFile}`, "debug");
|
|
6169
|
+
} else {
|
|
6170
|
+
log2(
|
|
6171
|
+
state,
|
|
6172
|
+
`Failed to write tunnel readiness marker to ${options.tunnelReadyFile}: ${marker.error}`,
|
|
6173
|
+
"error"
|
|
6174
|
+
);
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
5898
6177
|
emitAgentConnected(state.agentId, {
|
|
5899
6178
|
port: state.port,
|
|
5900
6179
|
cli_version: getCliVersion(),
|
|
@@ -6059,6 +6338,9 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
6059
6338
|
"Allow Evident to write files into this directory (repeatable). Omit to disable file sync entirely.",
|
|
6060
6339
|
(value, previous) => previous.concat([value]),
|
|
6061
6340
|
[]
|
|
6341
|
+
).option(
|
|
6342
|
+
"--tunnel-ready-file <path>",
|
|
6343
|
+
"Path to write once the tunnel is connected (set by the MicroVM hooks; unused on a developer machine)"
|
|
6062
6344
|
).action(
|
|
6063
6345
|
(options) => {
|
|
6064
6346
|
run({
|
|
@@ -6078,7 +6360,8 @@ program.command("run").description("Connect to Evident and process messages").op
|
|
|
6078
6360
|
sessionCleanupInterval: options.sessionCleanupInterval,
|
|
6079
6361
|
// Raw values — expansion/validation is single-sourced in run.ts's
|
|
6080
6362
|
// resolveFileSyncDirectories.
|
|
6081
|
-
enableFileSyncTo: options.enableFileSyncTo
|
|
6363
|
+
enableFileSyncTo: options.enableFileSyncTo,
|
|
6364
|
+
tunnelReadyFile: options.tunnelReadyFile
|
|
6082
6365
|
});
|
|
6083
6366
|
}
|
|
6084
6367
|
);
|