@evident-ai/cli 3.1.1-dev.1c351d6 → 3.1.1-dev.1e270a5
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 +245 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3141,6 +3141,7 @@ var B2_ABANDONMENT_MIN_PINNED_MS = 3 * 6e4;
|
|
|
3141
3141
|
var B2_ABANDONMENT_RECHECK_MS = HEARTBEAT_MS;
|
|
3142
3142
|
var POLL_MISS_GRACE_MS = HEARTBEAT_MS;
|
|
3143
3143
|
var MAX_SUPERSEDED_CONVERSATIONS = 256;
|
|
3144
|
+
var MAX_IDENTICAL_REDRIVE_POLL_FAILURES = 5;
|
|
3144
3145
|
var ChannelAuthError = class extends Error {
|
|
3145
3146
|
constructor(message) {
|
|
3146
3147
|
super(message);
|
|
@@ -3163,6 +3164,10 @@ function backoffDelay(attempt, policy) {
|
|
|
3163
3164
|
function isRetryableStatus(status2) {
|
|
3164
3165
|
return status2 === 429 || status2 >= 500 && status2 <= 599;
|
|
3165
3166
|
}
|
|
3167
|
+
var VOLATILE_BODY_FIELD_PATTERN = /("(?:ref|requestId|request_id|traceId|trace_id)"\s*:\s*)"[^"]*"/gi;
|
|
3168
|
+
function normalizeRedrivePollFailureBody(body) {
|
|
3169
|
+
return body.replace(VOLATILE_BODY_FIELD_PATTERN, '$1"<redacted>"').replace(/\s+/g, " ").trim().slice(0, 200);
|
|
3170
|
+
}
|
|
3166
3171
|
var ChannelDriver = class _ChannelDriver {
|
|
3167
3172
|
agentId;
|
|
3168
3173
|
port;
|
|
@@ -3294,6 +3299,40 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3294
3299
|
* ADR-0047's own "unreachable ⇒ bounded" rule). Cleared on any other outcome.
|
|
3295
3300
|
*/
|
|
3296
3301
|
redriveUnresolvedSince = /* @__PURE__ */ new Map();
|
|
3302
|
+
/**
|
|
3303
|
+
* Consecutive-identical-poll-failure streak for the re-drive fence (#1348),
|
|
3304
|
+
* keyed by Evident **message id** (not session) so `clearRedriveUnresolved`
|
|
3305
|
+
* can drop it with the other two trackers and it cannot leak. `sessionId` is
|
|
3306
|
+
* carried inside the entry, not the key: a session change is a different
|
|
3307
|
+
* situation and resets the streak, which gives the `(sessionId, message.id)`
|
|
3308
|
+
* pairing #1348 asks for without a composite map key.
|
|
3309
|
+
*/
|
|
3310
|
+
redrivePollFailures = /* @__PURE__ */ new Map();
|
|
3311
|
+
/**
|
|
3312
|
+
* "Already emitted `redrive_outcome_unreported` for THIS (message, outcome)
|
|
3313
|
+
* streak" (Class B, #1340: the runner DECIDED reattach/settle/fail_permanent
|
|
3314
|
+
* but its own PATCH to record it failed — distinct from Class A's
|
|
3315
|
+
* `redrive_poll_failed`, where opencode itself can't be observed). Keyed by
|
|
3316
|
+
* message id, valued by the outcome currently failing to report, so a
|
|
3317
|
+
* change of outcome starts a fresh signal. Cleared by
|
|
3318
|
+
* `clearRedriveUnresolved` the instant either PATCH succeeds.
|
|
3319
|
+
*/
|
|
3320
|
+
redriveOutcomeUnreportedSignalled = /* @__PURE__ */ new Map();
|
|
3321
|
+
/**
|
|
3322
|
+
* Consecutive-UNCONFIRMED-dispatch streak for a `pending` row with NO stored
|
|
3323
|
+
* `opencode_message_id` yet — i.e. one that has never even reached the
|
|
3324
|
+
* re-drive fence above. `sendPromptAsync`'s POST may 2xx, but its own
|
|
3325
|
+
* read-back retries can never confirm the assigned id when the session's
|
|
3326
|
+
* message list is PERMANENTLY unreadable (e.g. a corrupted local opencode
|
|
3327
|
+
* SQLite DB, #1345/#1348's exact fault, just hit BEFORE the row is ever
|
|
3328
|
+
* dispatched instead of after). Unlike an already-dispatched row, THIS row has
|
|
3329
|
+
* no other safety net at all: the lifecycle cron only reclaims `status =
|
|
3330
|
+
* 'processing'` rows, and a row stuck here never reaches `processing`. Keyed
|
|
3331
|
+
* by message id, carrying `sessionId` so a session change (a fresh one bound
|
|
3332
|
+
* after abandonment) starts a new streak rather than inheriting the old
|
|
3333
|
+
* session's count — same shape as `redrivePollFailures` above.
|
|
3334
|
+
*/
|
|
3335
|
+
unconfirmedDispatchFailures = /* @__PURE__ */ new Map();
|
|
3297
3336
|
/**
|
|
3298
3337
|
* "A null-id re-adopt re-dispatch is in flight, awaiting its read-back" (WI-5
|
|
3299
3338
|
* Task 5.4, High-2). Since we no longer send a caller-supplied id, a re-dispatch
|
|
@@ -3618,7 +3657,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3618
3657
|
* @returns the count of messages NEWLY dispatched (not already in-flight).
|
|
3619
3658
|
*/
|
|
3620
3659
|
async processConversation(conv) {
|
|
3621
|
-
const { sessionId, refusedSessionId } = await this.ensureSession(conv);
|
|
3660
|
+
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
3622
3661
|
const messages = await this.getPendingMessages(conv.id);
|
|
3623
3662
|
let dispatched = 0;
|
|
3624
3663
|
let skippedAlreadyDispatched = 0;
|
|
@@ -3634,7 +3673,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3634
3673
|
continue;
|
|
3635
3674
|
}
|
|
3636
3675
|
if (message.opencode_message_id) {
|
|
3637
|
-
const outcome = await this.resolveRedrive(conv, sessionId, message,
|
|
3676
|
+
const outcome = await this.resolveRedrive(conv, sessionId, message, sessionCreated);
|
|
3638
3677
|
if (outcome !== "dispatch") {
|
|
3639
3678
|
break;
|
|
3640
3679
|
}
|
|
@@ -3705,14 +3744,37 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3705
3744
|
break;
|
|
3706
3745
|
}
|
|
3707
3746
|
if (opencodeMessageId === null) {
|
|
3747
|
+
const streak = this.recordUnconfirmedDispatch(message.id, sessionId);
|
|
3748
|
+
if (streak < MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
3749
|
+
this.log({
|
|
3750
|
+
level: "warn",
|
|
3751
|
+
message: `Message ${message.id.slice(0, 8)} dispatched but its opencode id could not be read back (${streak}/${MAX_IDENTICAL_REDRIVE_POLL_FAILURES}) \u2014 leaving un-tracked to retry next tick`,
|
|
3752
|
+
conversation_id: conv.id,
|
|
3753
|
+
message_id: message.id
|
|
3754
|
+
});
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
this.unconfirmedDispatchFailures.delete(message.id);
|
|
3758
|
+
this.sessions.delete(conv.id);
|
|
3759
|
+
this.supersede(conv.id, sessionId);
|
|
3760
|
+
const errorMessage = `OpenCode accepted this message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
3708
3761
|
this.log({
|
|
3709
|
-
level: "
|
|
3710
|
-
message:
|
|
3762
|
+
level: "error",
|
|
3763
|
+
message: errorMessage,
|
|
3711
3764
|
conversation_id: conv.id,
|
|
3712
3765
|
message_id: message.id
|
|
3713
3766
|
});
|
|
3714
|
-
|
|
3767
|
+
await this.markFailed(conv.id, message.id, null, errorMessage).catch((markErr) => {
|
|
3768
|
+
this.log({
|
|
3769
|
+
level: "warn",
|
|
3770
|
+
message: `markFailed PATCH for message ${message.id.slice(0, 8)} (conversation ${conv.id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
3771
|
+
conversation_id: conv.id,
|
|
3772
|
+
message_id: message.id
|
|
3773
|
+
});
|
|
3774
|
+
});
|
|
3775
|
+
break;
|
|
3715
3776
|
}
|
|
3777
|
+
this.unconfirmedDispatchFailures.delete(message.id);
|
|
3716
3778
|
this.dispatched.add(message.id);
|
|
3717
3779
|
this.registerInFlight(conv, sessionId, message, opencodeMessageId);
|
|
3718
3780
|
dispatched += 1;
|
|
@@ -3733,21 +3795,29 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3733
3795
|
* INJECTED `fetchImpl` — NOT the imported `getSessionMessages` helper, which
|
|
3734
3796
|
* hits the global `fetch` and would bypass the same override every other
|
|
3735
3797
|
* opencode poll in this file respects. Mirrors `readoptProcessing`'s own
|
|
3736
|
-
* snapshot fetch (`:3081-3111`).
|
|
3737
|
-
*
|
|
3738
|
-
*
|
|
3798
|
+
* snapshot fetch (`:3081-3111`).
|
|
3799
|
+
*
|
|
3800
|
+
* Returns `{ ok: true, messages }` on a readable snapshot, or
|
|
3801
|
+
* `{ ok: false, signature }` on failure — `signature` is a string that
|
|
3802
|
+
* repeats across attempts for the SAME underlying fault (used by the
|
|
3803
|
+
* consecutive-identical-failure bound, #1348), or `null` for a thrown
|
|
3804
|
+
* exception, which is NOT countable toward that bound (a network blip / an
|
|
3805
|
+
* opencode restart also throws identically every tick, and must keep
|
|
3806
|
+
* retrying unbounded rather than ever being treated as permanent).
|
|
3739
3807
|
*/
|
|
3740
3808
|
async pollSessionMessagesForRedrive(conv, message, sessionId) {
|
|
3741
3809
|
try {
|
|
3742
3810
|
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
3743
3811
|
if (!res.ok) {
|
|
3812
|
+
const rawBody = await res.text();
|
|
3813
|
+
const normalized = normalizeRedrivePollFailureBody(rawBody);
|
|
3744
3814
|
this.log({
|
|
3745
3815
|
level: "warn",
|
|
3746
|
-
message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned HTTP ${res.status} \u2014 treating as unreadable this tick`,
|
|
3816
|
+
message: `Re-drive: polling session ${sessionId.slice(0, 8)} for message ${message.id.slice(0, 8)} returned HTTP ${res.status}${normalized ? `: ${normalized}` : ""} \u2014 treating as unreadable this tick`,
|
|
3747
3817
|
conversation_id: conv.id,
|
|
3748
3818
|
message_id: message.id
|
|
3749
3819
|
});
|
|
3750
|
-
return
|
|
3820
|
+
return { ok: false, signature: `HTTP ${res.status}${normalized ? `: ${normalized}` : ""}` };
|
|
3751
3821
|
}
|
|
3752
3822
|
const body = await res.json();
|
|
3753
3823
|
if (!Array.isArray(body)) {
|
|
@@ -3757,9 +3827,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3757
3827
|
conversation_id: conv.id,
|
|
3758
3828
|
message_id: message.id
|
|
3759
3829
|
});
|
|
3760
|
-
return
|
|
3830
|
+
return { ok: false, signature: "non-array message body" };
|
|
3761
3831
|
}
|
|
3762
|
-
return body;
|
|
3832
|
+
return { ok: true, messages: body };
|
|
3763
3833
|
} catch (err) {
|
|
3764
3834
|
this.log({
|
|
3765
3835
|
level: "warn",
|
|
@@ -3767,7 +3837,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3767
3837
|
conversation_id: conv.id,
|
|
3768
3838
|
message_id: message.id
|
|
3769
3839
|
});
|
|
3770
|
-
return null;
|
|
3840
|
+
return { ok: false, signature: null };
|
|
3771
3841
|
}
|
|
3772
3842
|
}
|
|
3773
3843
|
/**
|
|
@@ -3779,21 +3849,34 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3779
3849
|
* without this fence the drain loop would re-`prompt_async` the SAME turn a
|
|
3780
3850
|
* second time against live GitHub state. Mirrors `readoptOne`'s job for the
|
|
3781
3851
|
* `processing` re-adopt path, but simpler: no b1/b2 preamble cross-check is
|
|
3782
|
-
* needed here because `
|
|
3783
|
-
*
|
|
3852
|
+
* needed here because `sessionCreated` already handles the cases (a #553
|
|
3853
|
+
* abandoned session, a #190 vanished one) that path exists for.
|
|
3784
3854
|
*
|
|
3785
|
-
* Only `ChannelAuthError` propagates
|
|
3786
|
-
* `
|
|
3855
|
+
* Only `ChannelAuthError` propagates. A poll that fails identically
|
|
3856
|
+
* `MAX_IDENTICAL_REDRIVE_POLL_FAILURES` times in a row reports the message
|
|
3857
|
+
* failed instead of retrying it (#1348) — SEPARATE from, not a replacement
|
|
3858
|
+
* for, `resolveRedriveUnresolved`'s own `pausedMaxWaitMs` bound below. Every
|
|
3859
|
+
* other failure resolves to `unresolved` and is retried whole on the next
|
|
3860
|
+
* ~2s drain tick.
|
|
3787
3861
|
*/
|
|
3788
|
-
async resolveRedrive(conv, sessionId, message,
|
|
3862
|
+
async resolveRedrive(conv, sessionId, message, sessionCreated) {
|
|
3789
3863
|
const ocId = message.opencode_message_id ?? null;
|
|
3790
|
-
if (
|
|
3864
|
+
if (sessionCreated) {
|
|
3791
3865
|
this.clearRedriveUnresolved(message.id);
|
|
3792
3866
|
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
3793
3867
|
return "dispatch";
|
|
3794
3868
|
}
|
|
3795
|
-
const
|
|
3796
|
-
if (
|
|
3869
|
+
const polled = await this.pollSessionMessagesForRedrive(conv, message, sessionId);
|
|
3870
|
+
if (!polled.ok) {
|
|
3871
|
+
const streak = this.recordRedrivePollFailure(message.id, sessionId, polled.signature);
|
|
3872
|
+
if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES && polled.signature !== null) {
|
|
3873
|
+
return this.failRedrivePollPermanent(conv, sessionId, message, polled.signature, streak);
|
|
3874
|
+
}
|
|
3875
|
+
return this.resolveRedriveUnresolved(conv, message);
|
|
3876
|
+
}
|
|
3877
|
+
this.redrivePollFailures.delete(message.id);
|
|
3878
|
+
const messages = polled.messages;
|
|
3879
|
+
if (messages.length === 0) {
|
|
3797
3880
|
return this.resolveRedriveUnresolved(conv, message);
|
|
3798
3881
|
}
|
|
3799
3882
|
const state = messageRunState(messages, ocId ?? "");
|
|
@@ -3860,6 +3943,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3860
3943
|
conversation_id: conv.id,
|
|
3861
3944
|
message_id: message.id
|
|
3862
3945
|
});
|
|
3946
|
+
this.signalRedriveOutcomeUnreported(conv, message, "reattach");
|
|
3863
3947
|
return "unresolved";
|
|
3864
3948
|
}
|
|
3865
3949
|
this.clearRedriveUnresolved(message.id);
|
|
@@ -3920,12 +4004,53 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3920
4004
|
conversation_id: conv.id,
|
|
3921
4005
|
message_id: message.id
|
|
3922
4006
|
});
|
|
4007
|
+
this.signalRedriveOutcomeUnreported(conv, message, "settle");
|
|
3923
4008
|
return "unresolved";
|
|
3924
4009
|
}
|
|
3925
4010
|
this.clearRedriveUnresolved(message.id);
|
|
3926
4011
|
void this.postSignal(conv.id, message.id, "redrive_settled");
|
|
3927
4012
|
return "settled";
|
|
3928
4013
|
}
|
|
4014
|
+
/**
|
|
4015
|
+
* The permanent-failure outcome (#1348): the fence's own poll of this session
|
|
4016
|
+
* failed with the SAME opencode-answered signature
|
|
4017
|
+
* `MAX_IDENTICAL_REDRIVE_POLL_FAILURES` times in a row — a transient blip
|
|
4018
|
+
* would have varied or eventually cleared (see `pollSessionMessagesForRedrive`
|
|
4019
|
+
* and `recordRedrivePollFailure`), so this is a durable fault (e.g. #1345's
|
|
4020
|
+
* corrupted opencode session) rather than something worth retrying forever.
|
|
4021
|
+
* Mirrors `settleRedrive`'s error discipline: no `usage`/`failure` args to
|
|
4022
|
+
* `markFailed` (no opencode snapshot to extract them from — this poll never
|
|
4023
|
+
* got a readable one).
|
|
4024
|
+
*/
|
|
4025
|
+
async failRedrivePollPermanent(conv, sessionId, message, signature, streak) {
|
|
4026
|
+
this.log({
|
|
4027
|
+
level: "error",
|
|
4028
|
+
message: `Re-drive: message ${message.id.slice(0, 8)} (session ${sessionId.slice(0, 8)}) failed to poll with the identical signature "${signature}" ${streak} times in a row \u2014 reporting the message failed instead of retrying forever`,
|
|
4029
|
+
conversation_id: conv.id,
|
|
4030
|
+
message_id: message.id
|
|
4031
|
+
});
|
|
4032
|
+
try {
|
|
4033
|
+
await this.markFailed(
|
|
4034
|
+
conv.id,
|
|
4035
|
+
message.id,
|
|
4036
|
+
sessionId,
|
|
4037
|
+
`The runner could not read this conversation's state from OpenCode (${signature}). The same failure repeated ${streak} times in a row, so the message was not retried further.`
|
|
4038
|
+
);
|
|
4039
|
+
} catch (err) {
|
|
4040
|
+
if (err instanceof ChannelAuthError) throw err;
|
|
4041
|
+
this.log({
|
|
4042
|
+
level: "warn",
|
|
4043
|
+
message: `Re-drive: failed to report message ${message.id.slice(0, 8)} permanently failed (will retry next drain): ${err instanceof Error ? err.message : String(err)}`,
|
|
4044
|
+
conversation_id: conv.id,
|
|
4045
|
+
message_id: message.id
|
|
4046
|
+
});
|
|
4047
|
+
this.signalRedriveOutcomeUnreported(conv, message, "fail_permanent");
|
|
4048
|
+
return "unresolved";
|
|
4049
|
+
}
|
|
4050
|
+
this.clearRedriveUnresolved(message.id);
|
|
4051
|
+
void this.postSignal(conv.id, message.id, "redrive_poll_failed");
|
|
4052
|
+
return "settled";
|
|
4053
|
+
}
|
|
3929
4054
|
/**
|
|
3930
4055
|
* The bounded `unresolved` outcome (Task 3.4): opencode's state could not be
|
|
3931
4056
|
* observed (snapshot unreadable/empty, or `isSessionOngoing` returned `null`).
|
|
@@ -3951,10 +4076,68 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3951
4076
|
}
|
|
3952
4077
|
return "unresolved";
|
|
3953
4078
|
}
|
|
3954
|
-
/** Clear
|
|
4079
|
+
/** Clear all `unresolved`/failure-streak trackers for a row (any non-`unresolved` outcome). */
|
|
3955
4080
|
clearRedriveUnresolved(messageId) {
|
|
3956
4081
|
this.redriveUnresolvedSince.delete(messageId);
|
|
3957
4082
|
this.redriveUnresolvedSignalled.delete(messageId);
|
|
4083
|
+
this.redrivePollFailures.delete(messageId);
|
|
4084
|
+
this.redriveOutcomeUnreportedSignalled.delete(messageId);
|
|
4085
|
+
}
|
|
4086
|
+
/**
|
|
4087
|
+
* Class B (#1340): the runner DECIDED an outcome (reattach/settle/fail_permanent)
|
|
4088
|
+
* but its own PATCH to record it failed. Deliberately NOT bounded like
|
|
4089
|
+
* `resolveRedriveUnresolved`, for two load-bearing reasons: (1) the only
|
|
4090
|
+
* remedy is a PATCH to the server — the very call that is failing, so a bound
|
|
4091
|
+
* would act by retrying it; (2) this class is self-clearing by construction
|
|
4092
|
+
* (the next successful PATCH resolves it via `clearRedriveUnresolved`),
|
|
4093
|
+
* unlike #1340's deterministic, permanent shape. Fires at most once per
|
|
4094
|
+
* (message, outcome) streak.
|
|
4095
|
+
*/
|
|
4096
|
+
signalRedriveOutcomeUnreported(conv, message, outcome) {
|
|
4097
|
+
if (this.redriveOutcomeUnreportedSignalled.get(message.id) === outcome) return;
|
|
4098
|
+
this.redriveOutcomeUnreportedSignalled.set(message.id, outcome);
|
|
4099
|
+
void this.postSignal(conv.id, message.id, "redrive_outcome_unreported", {
|
|
4100
|
+
attempted_outcome: outcome
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
/**
|
|
4104
|
+
* Record one poll outcome toward the re-drive fence's consecutive-identical-
|
|
4105
|
+
* failure streak (#1348) and return the resulting count. `signature === null`
|
|
4106
|
+
* (a thrown exception, H1) always clears the streak and returns `0` — it is
|
|
4107
|
+
* never countable. Otherwise the streak continues only when BOTH the session
|
|
4108
|
+
* and the signature match the previous failure; anything else (a different
|
|
4109
|
+
* session, or the same session failing a DIFFERENT way) starts a fresh streak
|
|
4110
|
+
* at `1`.
|
|
4111
|
+
*/
|
|
4112
|
+
recordRedrivePollFailure(messageId, sessionId, signature) {
|
|
4113
|
+
if (signature === null) {
|
|
4114
|
+
this.redrivePollFailures.delete(messageId);
|
|
4115
|
+
return 0;
|
|
4116
|
+
}
|
|
4117
|
+
const existing = this.redrivePollFailures.get(messageId);
|
|
4118
|
+
if (existing && existing.sessionId === sessionId && existing.signature === signature) {
|
|
4119
|
+
existing.count += 1;
|
|
4120
|
+
return existing.count;
|
|
4121
|
+
}
|
|
4122
|
+
this.redrivePollFailures.set(messageId, { sessionId, signature, count: 1 });
|
|
4123
|
+
return 1;
|
|
4124
|
+
}
|
|
4125
|
+
/**
|
|
4126
|
+
* Record one UNCONFIRMED-dispatch outcome (a `pending` row with no stored
|
|
4127
|
+
* `opencode_message_id` whose `sendPromptAsync` returned `null`) toward the
|
|
4128
|
+
* bound in `processConversation`'s dispatch loop, and return the resulting
|
|
4129
|
+
* count. Mirrors `recordRedrivePollFailure`'s session-scoping: a session
|
|
4130
|
+
* change starts a fresh streak at `1` rather than inheriting the old one's
|
|
4131
|
+
* count, since a new session is a genuinely different attempt.
|
|
4132
|
+
*/
|
|
4133
|
+
recordUnconfirmedDispatch(messageId, sessionId) {
|
|
4134
|
+
const existing = this.unconfirmedDispatchFailures.get(messageId);
|
|
4135
|
+
if (existing && existing.sessionId === sessionId) {
|
|
4136
|
+
existing.count += 1;
|
|
4137
|
+
return existing.count;
|
|
4138
|
+
}
|
|
4139
|
+
this.unconfirmedDispatchFailures.set(messageId, { sessionId, count: 1 });
|
|
4140
|
+
return 1;
|
|
3958
4141
|
}
|
|
3959
4142
|
/**
|
|
3960
4143
|
* Record that `sessionId` is no longer a valid binding for `conversationId`
|
|
@@ -3980,6 +4163,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3980
4163
|
* `refusedSessionId` is set when the #553 guard fired — i.e. the persisted
|
|
3981
4164
|
* binding was an id this runner had abandoned, so a resurrection genuinely
|
|
3982
4165
|
* happened and a fresh session was bound instead. The caller reports it.
|
|
4166
|
+
*
|
|
4167
|
+
* `created` says the returned session was made JUST NOW, so it provably holds
|
|
4168
|
+
* no prior turn. The re-drive fence needs that as CONTRARY evidence ("nothing
|
|
4169
|
+
* to reconcile against") — distinct from the ambiguous "I polled and saw an
|
|
4170
|
+
* empty transcript", which stays a deferral. Keep it separate from
|
|
4171
|
+
* `refusedSessionId`: only the latter means a #553 resurrection happened, and
|
|
4172
|
+
* only it may drive the `session_superseded` signal.
|
|
3983
4173
|
*/
|
|
3984
4174
|
async ensureSession(conv) {
|
|
3985
4175
|
const bound = this.sessions.get(conv.id) ?? conv.opencode_session_id ?? null;
|
|
@@ -3990,7 +4180,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
3990
4180
|
conversation_id: conv.id
|
|
3991
4181
|
});
|
|
3992
4182
|
this.sessions.delete(conv.id);
|
|
3993
|
-
return {
|
|
4183
|
+
return {
|
|
4184
|
+
sessionId: await this.createAndBindSession(conv.id),
|
|
4185
|
+
refusedSessionId: bound,
|
|
4186
|
+
created: true
|
|
4187
|
+
};
|
|
3994
4188
|
}
|
|
3995
4189
|
if (bound) {
|
|
3996
4190
|
const exists = await sessionExists(this.port, bound);
|
|
@@ -4001,12 +4195,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
4001
4195
|
conversation_id: conv.id
|
|
4002
4196
|
});
|
|
4003
4197
|
this.sessions.delete(conv.id);
|
|
4004
|
-
return { sessionId: await this.createAndBindSession(conv.id) };
|
|
4198
|
+
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
4005
4199
|
}
|
|
4006
4200
|
this.sessions.set(conv.id, bound);
|
|
4007
|
-
return { sessionId: bound };
|
|
4201
|
+
return { sessionId: bound, created: false };
|
|
4008
4202
|
}
|
|
4009
|
-
return { sessionId: await this.createAndBindSession(conv.id) };
|
|
4203
|
+
return { sessionId: await this.createAndBindSession(conv.id), created: true };
|
|
4010
4204
|
}
|
|
4011
4205
|
/**
|
|
4012
4206
|
* Create a fresh OpenCode session for a conversation, cache the binding, and
|
|
@@ -5092,15 +5286,39 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5092
5286
|
}
|
|
5093
5287
|
if (ocId === null) {
|
|
5094
5288
|
this.awaitingReadopt.delete(row.id);
|
|
5289
|
+
const streak = this.recordUnconfirmedDispatch(row.id, sessionId);
|
|
5290
|
+
if (streak >= MAX_IDENTICAL_REDRIVE_POLL_FAILURES) {
|
|
5291
|
+
this.unconfirmedDispatchFailures.delete(row.id);
|
|
5292
|
+
this.sessions.delete(readoptConv.id);
|
|
5293
|
+
this.supersede(readoptConv.id, sessionId);
|
|
5294
|
+
const errorMessage = `OpenCode accepted this re-dispatched message ${streak} times in a row but never confirmed its assigned id (session ${sessionId.slice(0, 8)}'s message list could not be read back) \u2014 the session was abandoned; a fresh one is used for further messages.`;
|
|
5295
|
+
this.log({
|
|
5296
|
+
level: "error",
|
|
5297
|
+
message: errorMessage,
|
|
5298
|
+
conversation_id: row.conversation_id,
|
|
5299
|
+
message_id: row.id
|
|
5300
|
+
});
|
|
5301
|
+
await this.markFailed(row.conversation_id, row.id, null, errorMessage).catch((markErr) => {
|
|
5302
|
+
this.log({
|
|
5303
|
+
level: "warn",
|
|
5304
|
+
message: `markFailed PATCH for message ${row.id.slice(0, 8)} (conversation ${row.conversation_id.slice(0, 8)}) failed (best-effort, not retried): ${markErr instanceof Error ? markErr.message : String(markErr)}`,
|
|
5305
|
+
conversation_id: row.conversation_id,
|
|
5306
|
+
message_id: row.id
|
|
5307
|
+
});
|
|
5308
|
+
});
|
|
5309
|
+
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
5310
|
+
return;
|
|
5311
|
+
}
|
|
5095
5312
|
this.log({
|
|
5096
5313
|
level: "warn",
|
|
5097
|
-
message: `Re-adopt: message ${row.id.slice(0, 8)} re-dispatched but its opencode id could not be read back \u2014 leaving un-tracked to retry next drain`,
|
|
5314
|
+
message: `Re-adopt: message ${row.id.slice(0, 8)} re-dispatched but its opencode id could not be read back (${streak}/${MAX_IDENTICAL_REDRIVE_POLL_FAILURES}) \u2014 leaving un-tracked to retry next drain`,
|
|
5098
5315
|
conversation_id: row.conversation_id,
|
|
5099
5316
|
message_id: row.id
|
|
5100
5317
|
});
|
|
5101
5318
|
void this.postSignal(row.conversation_id, row.id, "readopt_orphan_unsent");
|
|
5102
5319
|
return;
|
|
5103
5320
|
}
|
|
5321
|
+
this.unconfirmedDispatchFailures.delete(row.id);
|
|
5104
5322
|
this.registerReadopted(readoptConv, sessionId, readoptMessage, ocId, this.processedAtMs(row));
|
|
5105
5323
|
this.dispatched.add(row.id);
|
|
5106
5324
|
this.readopted.add(row.id);
|