@evident-ai/cli 3.4.1-dev.0b03f1c → 3.4.1-dev.10920a8
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 +723 -70
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -623,6 +623,19 @@ function isInteractive(jsonOutput) {
|
|
|
623
623
|
return true;
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
// src/lib/subscription-usage-report.ts
|
|
627
|
+
function toReportedSubscription(collected) {
|
|
628
|
+
if (!collected) return null;
|
|
629
|
+
if (collected.ownerEmail === null && collected.planType === null && collected.organizationName === null) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
owner_email: collected.ownerEmail,
|
|
634
|
+
plan_type: collected.planType,
|
|
635
|
+
organization_name: collected.organizationName
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
626
639
|
// src/commands/agent-lookup.ts
|
|
627
640
|
async function readErrorMessage(response) {
|
|
628
641
|
const text = await response.text().catch(() => "");
|
|
@@ -722,14 +735,6 @@ function toReportedWindow(window) {
|
|
|
722
735
|
if (!window) return null;
|
|
723
736
|
return { utilization: window.utilization, resets_at: window.resetsAt };
|
|
724
737
|
}
|
|
725
|
-
function toReportedOwner(snapshot) {
|
|
726
|
-
if (!snapshot.owner) return null;
|
|
727
|
-
return {
|
|
728
|
-
email: snapshot.owner.email,
|
|
729
|
-
organization_name: snapshot.owner.organizationName,
|
|
730
|
-
rate_limit_tier: snapshot.owner.rateLimitTier
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
738
|
async function reportClaudeUsage(agentId, authHeader, snapshot) {
|
|
734
739
|
try {
|
|
735
740
|
const apiUrl = getApiUrlConfig();
|
|
@@ -739,7 +744,7 @@ async function reportClaudeUsage(agentId, authHeader, snapshot) {
|
|
|
739
744
|
body: JSON.stringify({
|
|
740
745
|
five_hour: toReportedWindow(snapshot.fiveHour),
|
|
741
746
|
seven_day: toReportedWindow(snapshot.sevenDay),
|
|
742
|
-
|
|
747
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
743
748
|
}),
|
|
744
749
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
745
750
|
});
|
|
@@ -763,13 +768,6 @@ function toReportedOpenAiWindow(window) {
|
|
|
763
768
|
resets_at: window.resetsAt
|
|
764
769
|
};
|
|
765
770
|
}
|
|
766
|
-
function toReportedOpenAiSubscription(snapshot) {
|
|
767
|
-
if (!snapshot.subscription) return null;
|
|
768
|
-
return {
|
|
769
|
-
owner_email: snapshot.subscription.ownerEmail,
|
|
770
|
-
plan_type: snapshot.subscription.planType
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
771
|
async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
774
772
|
try {
|
|
775
773
|
const apiUrl = getApiUrlConfig();
|
|
@@ -781,7 +779,7 @@ async function reportOpenAiUsage(agentId, authHeader, snapshot) {
|
|
|
781
779
|
secondary: toReportedOpenAiWindow(snapshot.secondary),
|
|
782
780
|
has_credits: snapshot.hasCredits,
|
|
783
781
|
credits_unlimited: snapshot.creditsUnlimited,
|
|
784
|
-
subscription:
|
|
782
|
+
subscription: toReportedSubscription(snapshot.subscription)
|
|
785
783
|
}),
|
|
786
784
|
signal: AbortSignal.timeout(BEST_EFFORT_NOTIFY_TIMEOUT_MS)
|
|
787
785
|
});
|
|
@@ -1097,7 +1095,7 @@ function ownerLookupFailure(error2) {
|
|
|
1097
1095
|
}
|
|
1098
1096
|
async function getClaudeUsageOwner(accessToken) {
|
|
1099
1097
|
if (cachedOwner?.accessToken === accessToken) {
|
|
1100
|
-
return {
|
|
1098
|
+
return { subscription: cachedOwner.owner, ownerLookupError: null };
|
|
1101
1099
|
}
|
|
1102
1100
|
try {
|
|
1103
1101
|
const response = await fetch(CLAUDE_PROFILE_URL, {
|
|
@@ -1109,27 +1107,27 @@ async function getClaudeUsageOwner(accessToken) {
|
|
|
1109
1107
|
signal: AbortSignal.timeout(CLAUDE_PROFILE_TIMEOUT_MS)
|
|
1110
1108
|
});
|
|
1111
1109
|
if (!response.ok) {
|
|
1112
|
-
return {
|
|
1110
|
+
return { subscription: null, ownerLookupError: `HTTP ${response.status}` };
|
|
1113
1111
|
}
|
|
1114
1112
|
let body;
|
|
1115
1113
|
try {
|
|
1116
1114
|
body = await response.json();
|
|
1117
1115
|
} catch (error2) {
|
|
1118
|
-
return {
|
|
1116
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1119
1117
|
}
|
|
1120
1118
|
const profile = body;
|
|
1121
1119
|
if (typeof profile.account?.email !== "string" || !profile.account.email.trim()) {
|
|
1122
|
-
return {
|
|
1120
|
+
return { subscription: null, ownerLookupError: "malformed response" };
|
|
1123
1121
|
}
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1122
|
+
const subscription = {
|
|
1123
|
+
ownerEmail: profile.account.email,
|
|
1126
1124
|
organizationName: typeof profile.organization?.name === "string" && profile.organization.name.trim() ? profile.organization.name.trim() : null,
|
|
1127
|
-
|
|
1125
|
+
planType: typeof profile.organization?.rate_limit_tier === "string" && profile.organization.rate_limit_tier.trim() ? profile.organization.rate_limit_tier.trim() : null
|
|
1128
1126
|
};
|
|
1129
|
-
cachedOwner = { accessToken, owner };
|
|
1130
|
-
return {
|
|
1127
|
+
cachedOwner = { accessToken, owner: subscription };
|
|
1128
|
+
return { subscription, ownerLookupError: null };
|
|
1131
1129
|
} catch (error2) {
|
|
1132
|
-
return {
|
|
1130
|
+
return { subscription: null, ownerLookupError: ownerLookupFailure(error2) };
|
|
1133
1131
|
}
|
|
1134
1132
|
}
|
|
1135
1133
|
async function getClaudeUsage() {
|
|
@@ -1158,11 +1156,11 @@ async function getClaudeUsage() {
|
|
|
1158
1156
|
throw new ClaudeUsageError(`Claude usage request failed: HTTP ${res.status}`, "request_failed");
|
|
1159
1157
|
}
|
|
1160
1158
|
const body = await res.json();
|
|
1161
|
-
const {
|
|
1159
|
+
const { subscription, ownerLookupError } = await getClaudeUsageOwner(credentials2.accessToken);
|
|
1162
1160
|
return {
|
|
1163
1161
|
fiveHour: toWindow(body.five_hour),
|
|
1164
1162
|
sevenDay: toWindow(body.seven_day),
|
|
1165
|
-
|
|
1163
|
+
subscription,
|
|
1166
1164
|
ownerLookupError
|
|
1167
1165
|
};
|
|
1168
1166
|
}
|
|
@@ -3263,21 +3261,74 @@ function collectSubagentSessions(messages, userMessageId) {
|
|
|
3263
3261
|
}
|
|
3264
3262
|
return refs;
|
|
3265
3263
|
}
|
|
3266
|
-
function
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3264
|
+
function finiteNumber(value) {
|
|
3265
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
3266
|
+
}
|
|
3267
|
+
function taskCallModel(value) {
|
|
3268
|
+
if (!value || typeof value !== "object") return null;
|
|
3269
|
+
const model = value;
|
|
3270
|
+
const modelID = typeof model.modelID === "string" ? model.modelID : void 0;
|
|
3271
|
+
const providerID = typeof model.providerID === "string" ? model.providerID : void 0;
|
|
3272
|
+
return modelID || providerID ? { modelID, providerID } : null;
|
|
3273
|
+
}
|
|
3274
|
+
function collectTaskCalls(messages, userMessageId) {
|
|
3275
|
+
if (!messages || messages.length === 0) return [];
|
|
3276
|
+
const calls = [];
|
|
3277
|
+
for (const message of messages) {
|
|
3278
|
+
if (roleOf(message) !== "assistant" || parentIdOf(message) !== userMessageId) continue;
|
|
3279
|
+
for (const part of message.parts ?? []) {
|
|
3280
|
+
if (part.tool !== "task" || !part.callID || !part.state || part.state.status === "pending") {
|
|
3281
|
+
continue;
|
|
3282
|
+
}
|
|
3283
|
+
const rawName = part.state.input?.subagent_type;
|
|
3284
|
+
const subagentName = typeof rawName === "string" && rawName.trim().length > 0 ? rawName : rawName === void 0 ? "general" : "unknown";
|
|
3285
|
+
const metadata = part.state.metadata;
|
|
3286
|
+
calls.push({
|
|
3287
|
+
callID: part.callID,
|
|
3288
|
+
subagentName,
|
|
3289
|
+
childSessionId: typeof metadata?.sessionId === "string" ? metadata.sessionId : null,
|
|
3290
|
+
parentSessionId: typeof metadata?.parentSessionId === "string" ? metadata.parentSessionId : null,
|
|
3291
|
+
model: taskCallModel(metadata?.model),
|
|
3292
|
+
status: part.state.status ?? "unknown",
|
|
3293
|
+
timeStart: finiteNumber(part.state.time?.start),
|
|
3294
|
+
timeEnd: finiteNumber(part.state.time?.end)
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
return calls;
|
|
3299
|
+
}
|
|
3300
|
+
function attributeTaskCallUsage(messages, windows) {
|
|
3301
|
+
const eligibleWindows = windows.filter(
|
|
3302
|
+
(window) => window.timeStart !== null && Number.isFinite(window.timeStart)
|
|
3270
3303
|
);
|
|
3271
|
-
const
|
|
3272
|
-
const
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
const
|
|
3278
|
-
|
|
3304
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
3305
|
+
for (const window of eligibleWindows) assignments.set(window.callID, []);
|
|
3306
|
+
const unattributed = [];
|
|
3307
|
+
for (const message of messages ?? []) {
|
|
3308
|
+
if (roleOf(message) !== "assistant") continue;
|
|
3309
|
+
const created = finiteNumber(createdOf(message));
|
|
3310
|
+
const matching = created === null ? [] : eligibleWindows.filter(
|
|
3311
|
+
(window) => window.timeStart <= created && (window.timeEnd === null || window.timeEnd === void 0 || created <= window.timeEnd)
|
|
3312
|
+
);
|
|
3313
|
+
if (matching.length === 0) {
|
|
3314
|
+
unattributed.push(message);
|
|
3315
|
+
continue;
|
|
3316
|
+
}
|
|
3317
|
+
matching.sort((a, b) => a.timeStart - b.timeStart);
|
|
3318
|
+
assignments.get(matching[0].callID)?.push(message);
|
|
3279
3319
|
}
|
|
3280
|
-
|
|
3320
|
+
return {
|
|
3321
|
+
invocations: eligibleWindows.map((window) => {
|
|
3322
|
+
const assigned = assignments.get(window.callID) ?? [];
|
|
3323
|
+
return { callID: window.callID, messages: assigned, usage: sumAssistantUsage(assigned) };
|
|
3324
|
+
}),
|
|
3325
|
+
unattributed
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
function sumAssistantUsage(messages) {
|
|
3329
|
+
if (!messages || messages.length === 0) return null;
|
|
3330
|
+
const nonErrored = messages.filter((message) => errorOf(message) == null);
|
|
3331
|
+
const selected = nonErrored.length > 0 ? nonErrored : messages;
|
|
3281
3332
|
let sawAnyUsage = false;
|
|
3282
3333
|
let inputSum = 0;
|
|
3283
3334
|
let outputSum = 0;
|
|
@@ -3288,7 +3339,7 @@ function messageUsage(messages, userMessageId) {
|
|
|
3288
3339
|
let sawCost = false;
|
|
3289
3340
|
let modelId = null;
|
|
3290
3341
|
let providerId = null;
|
|
3291
|
-
for (const m of
|
|
3342
|
+
for (const m of selected) {
|
|
3292
3343
|
const info = m.info;
|
|
3293
3344
|
if (!info) continue;
|
|
3294
3345
|
const tokens = info.tokens;
|
|
@@ -3323,12 +3374,28 @@ function messageUsage(messages, userMessageId) {
|
|
|
3323
3374
|
usage_tokens_reasoning: reasoningSum,
|
|
3324
3375
|
usage_tokens_cache_read: cacheReadSum,
|
|
3325
3376
|
usage_tokens_cache_write: cacheWriteSum,
|
|
3326
|
-
// NULL means
|
|
3327
|
-
//
|
|
3328
|
-
// `sawCost` true with `costSum === 0`.
|
|
3377
|
+
// NULL means OpenCode never reported a cost; it is distinct from a genuine
|
|
3378
|
+
// zero-cost message, which sets `sawCost` with `costSum === 0`.
|
|
3329
3379
|
usage_cost_usd: sawCost ? costSum : null
|
|
3330
3380
|
};
|
|
3331
3381
|
}
|
|
3382
|
+
function messageUsage(messages, userMessageId) {
|
|
3383
|
+
if (!messages || messages.length === 0) return null;
|
|
3384
|
+
const byParentAll = messages.filter(
|
|
3385
|
+
(m) => roleOf(m) === "assistant" && parentIdOf(m) === userMessageId
|
|
3386
|
+
);
|
|
3387
|
+
const byParentNonErrored = byParentAll.filter((m) => errorOf(m) == null);
|
|
3388
|
+
const byParent = byParentNonErrored.length > 0 ? byParentNonErrored : byParentAll;
|
|
3389
|
+
let correlated;
|
|
3390
|
+
if (byParent.length > 0) {
|
|
3391
|
+
correlated = byParent;
|
|
3392
|
+
} else {
|
|
3393
|
+
const reply = findAssistantReplyAfter(messages, userMessageId);
|
|
3394
|
+
correlated = reply ? [reply] : [];
|
|
3395
|
+
}
|
|
3396
|
+
if (correlated.length === 0) return null;
|
|
3397
|
+
return sumAssistantUsage(correlated);
|
|
3398
|
+
}
|
|
3332
3399
|
function messageRunState(messages, userMessageId) {
|
|
3333
3400
|
if (!messages || messages.length === 0) return "unknown";
|
|
3334
3401
|
const hasUser = messages.some((m) => idOf(m) === userMessageId);
|
|
@@ -3458,6 +3525,28 @@ function hasRunningAssistantExcept(messages, exceptUserMessageId) {
|
|
|
3458
3525
|
(m) => roleOf(m) === "assistant" && parentIdOf(m) !== exceptUserMessageId && isAssistantInFlight(m)
|
|
3459
3526
|
);
|
|
3460
3527
|
}
|
|
3528
|
+
function hasLaterSiblingTurnStarted(messages, userMessageId, siblingUserMessageIds) {
|
|
3529
|
+
if (!messages || messages.length === 0) return false;
|
|
3530
|
+
const userIndex = messages.findIndex((message) => idOf(message) === userMessageId);
|
|
3531
|
+
if (userIndex === -1) return false;
|
|
3532
|
+
let hasLaterUser = false;
|
|
3533
|
+
let hasStartedLaterUser = false;
|
|
3534
|
+
for (let i = userIndex + 1; i < messages.length; i++) {
|
|
3535
|
+
const message = messages[i];
|
|
3536
|
+
if (roleOf(message) !== "user") continue;
|
|
3537
|
+
hasLaterUser = true;
|
|
3538
|
+
const laterUserMessageId = idOf(message);
|
|
3539
|
+
if (laterUserMessageId === void 0 || !siblingUserMessageIds.has(laterUserMessageId)) {
|
|
3540
|
+
return false;
|
|
3541
|
+
}
|
|
3542
|
+
if (messages.some(
|
|
3543
|
+
(candidate) => roleOf(candidate) === "assistant" && parentIdOf(candidate) === laterUserMessageId
|
|
3544
|
+
)) {
|
|
3545
|
+
hasStartedLaterUser = true;
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
return hasLaterUser && hasStartedLaterUser;
|
|
3549
|
+
}
|
|
3461
3550
|
async function hasAnyConfiguredProvider(port) {
|
|
3462
3551
|
try {
|
|
3463
3552
|
const res = await timedFetch(`${opencodeBase(port)}/config/providers`);
|
|
@@ -3489,6 +3578,76 @@ async function hasAnyConfiguredProvider(port) {
|
|
|
3489
3578
|
return null;
|
|
3490
3579
|
}
|
|
3491
3580
|
}
|
|
3581
|
+
function sessionErrorReason(error2) {
|
|
3582
|
+
const record = typeof error2 === "object" && error2 !== null ? error2 : null;
|
|
3583
|
+
const data = record?.data;
|
|
3584
|
+
const dataRecord = typeof data === "object" && data !== null ? data : null;
|
|
3585
|
+
const rawReason = typeof dataRecord?.message === "string" && dataRecord.message || typeof record?.message === "string" && record.message || typeof error2 === "string" && error2 || typeof record?.name === "string" && record.name || "OpenCode reported a session error with no details";
|
|
3586
|
+
const reason = rawReason.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
3587
|
+
return reason || "OpenCode reported a session error with no details";
|
|
3588
|
+
}
|
|
3589
|
+
function parseSessionErrorFrame(data) {
|
|
3590
|
+
let parsed;
|
|
3591
|
+
try {
|
|
3592
|
+
parsed = JSON.parse(data);
|
|
3593
|
+
} catch (error2) {
|
|
3594
|
+
void error2;
|
|
3595
|
+
return null;
|
|
3596
|
+
}
|
|
3597
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
3598
|
+
const parsedRecord = parsed;
|
|
3599
|
+
const payload = parsedRecord.payload;
|
|
3600
|
+
const event = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : parsedRecord;
|
|
3601
|
+
if (event.type !== "session.error") return null;
|
|
3602
|
+
const properties = event.properties;
|
|
3603
|
+
if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
|
|
3604
|
+
return null;
|
|
3605
|
+
}
|
|
3606
|
+
const propertiesRecord = properties;
|
|
3607
|
+
const sessionId = propertiesRecord.sessionID;
|
|
3608
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) return null;
|
|
3609
|
+
return {
|
|
3610
|
+
sessionId,
|
|
3611
|
+
reason: sessionErrorReason(propertiesRecord.error)
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
async function readSessionErrorStream(port, options) {
|
|
3615
|
+
let reader = null;
|
|
3616
|
+
try {
|
|
3617
|
+
const response = await fetch(`${opencodeBase(port)}/event`, {
|
|
3618
|
+
headers: { accept: "text/event-stream" },
|
|
3619
|
+
signal: options.signal
|
|
3620
|
+
});
|
|
3621
|
+
if (!response.ok || !response.body) {
|
|
3622
|
+
return { reason: "unavailable", detail: `HTTP ${response.status}` };
|
|
3623
|
+
}
|
|
3624
|
+
reader = response.body.getReader();
|
|
3625
|
+
const decoder = new TextDecoder();
|
|
3626
|
+
let buffer = "";
|
|
3627
|
+
const processLine = (line) => {
|
|
3628
|
+
const trimmed = line.trimEnd();
|
|
3629
|
+
if (!trimmed.startsWith("data:")) return;
|
|
3630
|
+
const event = parseSessionErrorFrame(trimmed.slice("data:".length).replace(/^ /, ""));
|
|
3631
|
+
if (event) options.onSessionError(event);
|
|
3632
|
+
};
|
|
3633
|
+
while (true) {
|
|
3634
|
+
const { done, value } = await reader.read();
|
|
3635
|
+
if (done) return { reason: "ended" };
|
|
3636
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3637
|
+
const lines = buffer.split("\n");
|
|
3638
|
+
buffer = lines.pop() ?? "";
|
|
3639
|
+
for (const line of lines) processLine(line);
|
|
3640
|
+
}
|
|
3641
|
+
} catch (err) {
|
|
3642
|
+
if (options.signal.aborted) return { reason: "aborted" };
|
|
3643
|
+
return {
|
|
3644
|
+
reason: "unavailable",
|
|
3645
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
3646
|
+
};
|
|
3647
|
+
} finally {
|
|
3648
|
+
if (reader) void reader.cancel().catch(() => void 0);
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3492
3651
|
async function reloadProviderCache(port) {
|
|
3493
3652
|
try {
|
|
3494
3653
|
const res = await timedFetch(`${opencodeBase(port)}/config`, {
|
|
@@ -4384,7 +4543,7 @@ function parseChatGptIdentity(accessToken) {
|
|
|
4384
4543
|
const auth = payload["https://api.openai.com/auth"];
|
|
4385
4544
|
const ownerEmail = typeof profile?.email === "string" ? profile.email.trim() || null : null;
|
|
4386
4545
|
const planType = typeof auth?.chatgpt_plan_type === "string" ? auth.chatgpt_plan_type.trim() || null : null;
|
|
4387
|
-
return ownerEmail === null && planType === null ? null : { ownerEmail, planType };
|
|
4546
|
+
return ownerEmail === null && planType === null ? null : { ownerEmail, planType, organizationName: null };
|
|
4388
4547
|
}
|
|
4389
4548
|
function toWindow2(headers, name) {
|
|
4390
4549
|
const utilizationHeader = headers.get(`x-codex-${name}-used-percent`);
|
|
@@ -5228,6 +5387,10 @@ var DEFAULT_RETRY_POLICY = {
|
|
|
5228
5387
|
baseDelayMs: 500,
|
|
5229
5388
|
maxDelayMs: 3e4
|
|
5230
5389
|
};
|
|
5390
|
+
var SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS = 2e3;
|
|
5391
|
+
var SESSION_ERROR_STREAM_HEALTHY_MS = 5e3;
|
|
5392
|
+
var SESSION_ERROR_BUFFER_TTL_MS = SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
5393
|
+
var MAX_BUFFERED_SESSION_ERRORS = 256;
|
|
5231
5394
|
var DEFAULT_PAUSED_POLL_INTERVAL_MS = 2e3;
|
|
5232
5395
|
var DEFAULT_PAUSED_MAX_WAIT_MS = 10 * 60 * 1e3;
|
|
5233
5396
|
var DEFAULT_STUCK_QUEUED_MS = 6e4;
|
|
@@ -5368,6 +5531,17 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5368
5531
|
* message; it is removed once its in-flight set empties.
|
|
5369
5532
|
*/
|
|
5370
5533
|
watchers = /* @__PURE__ */ new Map();
|
|
5534
|
+
sessionErrorStream = null;
|
|
5535
|
+
/**
|
|
5536
|
+
* Session-error failures currently being reported; entries are empty at rest
|
|
5537
|
+
* because each handoff deletes its id in `finally`.
|
|
5538
|
+
*/
|
|
5539
|
+
sessionErrorHandled = /* @__PURE__ */ new Set();
|
|
5540
|
+
/**
|
|
5541
|
+
* Session errors that arrived before their dispatch was registered. Bounded FIFO
|
|
5542
|
+
* with a short TTL so an unmatched session cannot retain an event indefinitely.
|
|
5543
|
+
*/
|
|
5544
|
+
bufferedSessionErrors = /* @__PURE__ */ new Map();
|
|
5371
5545
|
/**
|
|
5372
5546
|
* AUTHORITATIVE local dedup (WI-3): Evident message ids that have been
|
|
5373
5547
|
* dispatched and are still in-flight. A message in this set is never
|
|
@@ -5551,6 +5725,13 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5551
5725
|
* no watcher) can resolve the title.
|
|
5552
5726
|
*/
|
|
5553
5727
|
sessionTitles = /* @__PURE__ */ new Map();
|
|
5728
|
+
/** One best-effort terminal subagent collection per Evident message id. */
|
|
5729
|
+
subagentInvocationCollections = /* @__PURE__ */ new Map();
|
|
5730
|
+
/**
|
|
5731
|
+
* Early snapshots are only liveness hints; they must not become the terminal
|
|
5732
|
+
* collection when the task parts or child transcript have advanced.
|
|
5733
|
+
*/
|
|
5734
|
+
subagentInvocationPrefetches = /* @__PURE__ */ new Map();
|
|
5554
5735
|
/** Serialises drains so a reconnect during a drain doesn't double-process. */
|
|
5555
5736
|
draining = false;
|
|
5556
5737
|
/**
|
|
@@ -5765,6 +5946,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5765
5946
|
}
|
|
5766
5947
|
return ids;
|
|
5767
5948
|
}
|
|
5949
|
+
/**
|
|
5950
|
+
* OpenCode user-message ids tracked for other Evident messages in a session.
|
|
5951
|
+
* Excluding this message makes an unattributed later row fail safe; a missing
|
|
5952
|
+
* watcher yields no attributions, per `hasLaterSiblingTurnStarted`'s docblock.
|
|
5953
|
+
*/
|
|
5954
|
+
siblingOpencodeMessageIds(watcher, ownEvidentMessageId) {
|
|
5955
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5956
|
+
if (!watcher) return ids;
|
|
5957
|
+
for (const inFlight of watcher.inFlight.values()) {
|
|
5958
|
+
if (inFlight.evidentMessageId !== ownEvidentMessageId) {
|
|
5959
|
+
ids.add(inFlight.opencodeMessageId);
|
|
5960
|
+
}
|
|
5961
|
+
}
|
|
5962
|
+
return ids;
|
|
5963
|
+
}
|
|
5768
5964
|
/**
|
|
5769
5965
|
* File-pull work, for `run.ts`'s idle accounting (#559).
|
|
5770
5966
|
*
|
|
@@ -5831,6 +6027,8 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5831
6027
|
*/
|
|
5832
6028
|
stop() {
|
|
5833
6029
|
this.stopped = true;
|
|
6030
|
+
this.sessionErrorStream?.abort.abort();
|
|
6031
|
+
this.sessionErrorStream = null;
|
|
5834
6032
|
}
|
|
5835
6033
|
/**
|
|
5836
6034
|
* The server clears this request when a new MicroVM identity is recorded, so a
|
|
@@ -5905,6 +6103,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
5905
6103
|
*/
|
|
5906
6104
|
async processConversation(conv) {
|
|
5907
6105
|
const { sessionId, refusedSessionId, created: sessionCreated } = await this.ensureSession(conv);
|
|
6106
|
+
this.ensureSessionErrorStream();
|
|
5908
6107
|
const messages = await this.getPendingMessages(conv.id);
|
|
5909
6108
|
let dispatched = 0;
|
|
5910
6109
|
let skippedAlreadyDispatched = 0;
|
|
@@ -6251,6 +6450,23 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6251
6450
|
if (state === "running" || state === "queued") {
|
|
6252
6451
|
const ongoing = await isSessionOngoing(this.port, sessionId);
|
|
6253
6452
|
if (ongoing === true) {
|
|
6453
|
+
if (state === "queued") {
|
|
6454
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(
|
|
6455
|
+
this.watchers.get(sessionId),
|
|
6456
|
+
message.id
|
|
6457
|
+
);
|
|
6458
|
+
if (hasLaterSiblingTurnStarted(messages, ocId ?? "", siblingOcIds)) {
|
|
6459
|
+
this.log({
|
|
6460
|
+
level: "warn",
|
|
6461
|
+
message: `Re-drive: OpenCode already served a later, different Evident message's turn in session ${sessionId.slice(0, 8)} while message ${message.id.slice(0, 8)} produced no reply \u2014 re-dispatching instead of reattaching to someone else's turn`,
|
|
6462
|
+
conversation_id: conv.id,
|
|
6463
|
+
message_id: message.id
|
|
6464
|
+
});
|
|
6465
|
+
this.clearRedriveUnresolved(message.id);
|
|
6466
|
+
void this.postSignal(conv.id, message.id, "redrive_redispatched");
|
|
6467
|
+
return "dispatch";
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6254
6470
|
return this.reattachRedrive(conv, sessionId, message, ocId);
|
|
6255
6471
|
}
|
|
6256
6472
|
if (ongoing === false) {
|
|
@@ -6340,16 +6556,37 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6340
6556
|
if (state === "done") {
|
|
6341
6557
|
const title = await this.resolveSessionTitle(sessionId, conv.id);
|
|
6342
6558
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6559
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6560
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6561
|
+
messages,
|
|
6562
|
+
ocId ?? "",
|
|
6563
|
+
message.id
|
|
6564
|
+
);
|
|
6343
6565
|
this.log({
|
|
6344
6566
|
level: "info",
|
|
6345
6567
|
message: `Re-drive: message ${message.id.slice(0, 8)} completed while its row was wrongly reclaimed to pending \u2014 marking done instead of re-dispatching`,
|
|
6346
6568
|
conversation_id: conv.id,
|
|
6347
6569
|
message_id: message.id
|
|
6348
6570
|
});
|
|
6349
|
-
await this.markDone(
|
|
6571
|
+
await this.markDone(
|
|
6572
|
+
conv.id,
|
|
6573
|
+
message.id,
|
|
6574
|
+
sessionId,
|
|
6575
|
+
ocId,
|
|
6576
|
+
title,
|
|
6577
|
+
usage,
|
|
6578
|
+
usageAgentName,
|
|
6579
|
+
subagentInvocations
|
|
6580
|
+
);
|
|
6350
6581
|
} else {
|
|
6351
6582
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
6352
6583
|
const usage = messageUsage(messages, ocId ?? "");
|
|
6584
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
6585
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
6586
|
+
messages,
|
|
6587
|
+
ocId ?? "",
|
|
6588
|
+
message.id
|
|
6589
|
+
);
|
|
6353
6590
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
6354
6591
|
this.log({
|
|
6355
6592
|
level: "error",
|
|
@@ -6357,7 +6594,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6357
6594
|
conversation_id: conv.id,
|
|
6358
6595
|
message_id: message.id
|
|
6359
6596
|
});
|
|
6360
|
-
await this.markFailed(
|
|
6597
|
+
await this.markFailed(
|
|
6598
|
+
conv.id,
|
|
6599
|
+
message.id,
|
|
6600
|
+
sessionId,
|
|
6601
|
+
error2,
|
|
6602
|
+
usage,
|
|
6603
|
+
failure,
|
|
6604
|
+
usageAgentName,
|
|
6605
|
+
subagentInvocations
|
|
6606
|
+
);
|
|
6361
6607
|
}
|
|
6362
6608
|
if (ocId !== null) {
|
|
6363
6609
|
await this.reportSubagentAuthFailures(conv.id, ocId, message.id, messages);
|
|
@@ -6901,6 +7147,24 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
6901
7147
|
ambiguousPinnedSinceMs: 0,
|
|
6902
7148
|
ambiguousResolved: false
|
|
6903
7149
|
});
|
|
7150
|
+
const buffered = this.bufferedSessionErrors.get(sessionId);
|
|
7151
|
+
if (!buffered) return;
|
|
7152
|
+
this.bufferedSessionErrors.delete(sessionId);
|
|
7153
|
+
if (this.now() - buffered.receivedAt < SESSION_ERROR_BUFFER_TTL_MS) {
|
|
7154
|
+
this.handleSessionError(buffered.event);
|
|
7155
|
+
}
|
|
7156
|
+
}
|
|
7157
|
+
bufferSessionError(event) {
|
|
7158
|
+
this.bufferedSessionErrors.delete(event.sessionId);
|
|
7159
|
+
this.bufferedSessionErrors.set(event.sessionId, {
|
|
7160
|
+
event,
|
|
7161
|
+
receivedAt: this.now()
|
|
7162
|
+
});
|
|
7163
|
+
while (this.bufferedSessionErrors.size > MAX_BUFFERED_SESSION_ERRORS) {
|
|
7164
|
+
const oldest = this.bufferedSessionErrors.keys().next().value;
|
|
7165
|
+
if (typeof oldest !== "string") break;
|
|
7166
|
+
this.bufferedSessionErrors.delete(oldest);
|
|
7167
|
+
}
|
|
6904
7168
|
}
|
|
6905
7169
|
/**
|
|
6906
7170
|
* Build a fresh `SessionWatcher`. Seeds `lastTickAt`/`lastObservedTickAt`
|
|
@@ -7105,6 +7369,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7105
7369
|
ensureWatcherRunning(sessionId) {
|
|
7106
7370
|
const watcher = this.watchers.get(sessionId);
|
|
7107
7371
|
if (!watcher) return;
|
|
7372
|
+
this.ensureSessionErrorStream();
|
|
7108
7373
|
if (watcher.loop) return;
|
|
7109
7374
|
if (watcher.inFlight.size === 0) {
|
|
7110
7375
|
this.watchers.delete(sessionId);
|
|
@@ -7120,6 +7385,154 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7120
7385
|
});
|
|
7121
7386
|
watcher.loop = loop;
|
|
7122
7387
|
}
|
|
7388
|
+
ensureSessionErrorStream() {
|
|
7389
|
+
if (this.sessionErrorStream || this.stopped) return;
|
|
7390
|
+
const abort = new AbortController();
|
|
7391
|
+
const loop = this.runSessionErrorStream(abort.signal);
|
|
7392
|
+
this.sessionErrorStream = { abort, loop };
|
|
7393
|
+
}
|
|
7394
|
+
async runSessionErrorStream(signal) {
|
|
7395
|
+
let attempt = 0;
|
|
7396
|
+
let warned = false;
|
|
7397
|
+
while (!this.stopped && !signal.aborted) {
|
|
7398
|
+
const openedAt = this.now();
|
|
7399
|
+
try {
|
|
7400
|
+
const outcome = await readSessionErrorStream(this.port, {
|
|
7401
|
+
signal,
|
|
7402
|
+
onSessionError: (event) => this.handleSessionError(event)
|
|
7403
|
+
});
|
|
7404
|
+
if (outcome.reason === "aborted" || signal.aborted) return;
|
|
7405
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7406
|
+
if (outcome.reason === "unavailable" || outcome.reason === "ended") {
|
|
7407
|
+
if (!healthy) {
|
|
7408
|
+
const detail = outcome.reason === "unavailable" ? outcome.detail : "stream ended";
|
|
7409
|
+
this.log({
|
|
7410
|
+
level: warned ? "debug" : "warn",
|
|
7411
|
+
message: `OpenCode session-error stream ${warned ? "still unavailable" : "unavailable"} (${detail}); transcript polling remains the evidence path`
|
|
7412
|
+
});
|
|
7413
|
+
warned = true;
|
|
7414
|
+
}
|
|
7415
|
+
}
|
|
7416
|
+
if (healthy) {
|
|
7417
|
+
if (warned) {
|
|
7418
|
+
this.log({
|
|
7419
|
+
level: "info",
|
|
7420
|
+
message: "OpenCode session-error stream reconnected; transcript polling remains the evidence path"
|
|
7421
|
+
});
|
|
7422
|
+
warned = false;
|
|
7423
|
+
}
|
|
7424
|
+
attempt = 0;
|
|
7425
|
+
} else {
|
|
7426
|
+
attempt += 1;
|
|
7427
|
+
}
|
|
7428
|
+
if (this.stopped || signal.aborted) return;
|
|
7429
|
+
await this.sleep(backoffDelay(healthy ? 0 : attempt - 1, this.retry));
|
|
7430
|
+
} catch (err) {
|
|
7431
|
+
if (this.stopped || signal.aborted) return;
|
|
7432
|
+
this.log({
|
|
7433
|
+
level: "error",
|
|
7434
|
+
message: `OpenCode session-error stream failed unexpectedly: ${err instanceof Error ? err.message : String(err)}`
|
|
7435
|
+
});
|
|
7436
|
+
const healthy = this.now() - openedAt >= SESSION_ERROR_STREAM_HEALTHY_MS;
|
|
7437
|
+
const delayAttempt = healthy ? 0 : attempt;
|
|
7438
|
+
attempt = healthy ? 0 : attempt + 1;
|
|
7439
|
+
try {
|
|
7440
|
+
await this.sleep(backoffDelay(delayAttempt, this.retry));
|
|
7441
|
+
} catch (sleepErr) {
|
|
7442
|
+
this.log({
|
|
7443
|
+
level: "error",
|
|
7444
|
+
message: `OpenCode session-error stream backoff failed unexpectedly: ${sleepErr instanceof Error ? sleepErr.message : String(sleepErr)}`
|
|
7445
|
+
});
|
|
7446
|
+
}
|
|
7447
|
+
}
|
|
7448
|
+
}
|
|
7449
|
+
}
|
|
7450
|
+
handleSessionError(event) {
|
|
7451
|
+
try {
|
|
7452
|
+
const watcher = this.watchers.get(event.sessionId);
|
|
7453
|
+
if (!watcher) {
|
|
7454
|
+
this.bufferSessionError(event);
|
|
7455
|
+
this.log({
|
|
7456
|
+
level: "debug",
|
|
7457
|
+
message: `Ignoring session error for unknown session ${event.sessionId.slice(0, 8)}`
|
|
7458
|
+
});
|
|
7459
|
+
return;
|
|
7460
|
+
}
|
|
7461
|
+
if ([...watcher.inFlight.values()].some((message) => message.started && !message.done)) {
|
|
7462
|
+
this.log({
|
|
7463
|
+
level: "debug",
|
|
7464
|
+
message: `A turn is already running in session ${event.sessionId.slice(0, 8)} \u2014 deferring to transcript polling`,
|
|
7465
|
+
conversation_id: watcher.conv.id
|
|
7466
|
+
});
|
|
7467
|
+
return;
|
|
7468
|
+
}
|
|
7469
|
+
const inFlight = [...watcher.inFlight.values()].filter((message) => !message.started && !message.done).sort((a, b) => a.dispatchedAt - b.dispatchedAt)[0];
|
|
7470
|
+
if (!inFlight) {
|
|
7471
|
+
this.bufferSessionError(event);
|
|
7472
|
+
this.log({
|
|
7473
|
+
level: "debug",
|
|
7474
|
+
message: `No queued in-flight turn to correlate with session error in ${event.sessionId.slice(0, 8)}`,
|
|
7475
|
+
conversation_id: watcher.conv.id
|
|
7476
|
+
});
|
|
7477
|
+
return;
|
|
7478
|
+
}
|
|
7479
|
+
if (this.sessionErrorHandled.has(inFlight.evidentMessageId)) return;
|
|
7480
|
+
this.sessionErrorHandled.add(inFlight.evidentMessageId);
|
|
7481
|
+
void this.failFromSessionError(watcher, event, inFlight);
|
|
7482
|
+
} catch (err) {
|
|
7483
|
+
this.log({
|
|
7484
|
+
level: "error",
|
|
7485
|
+
message: `Failed to handle OpenCode session error for ${event.sessionId.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`
|
|
7486
|
+
});
|
|
7487
|
+
}
|
|
7488
|
+
}
|
|
7489
|
+
async failFromSessionError(watcher, event, inFlight) {
|
|
7490
|
+
try {
|
|
7491
|
+
const messages = await getSessionMessages(this.port, event.sessionId);
|
|
7492
|
+
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7493
|
+
if (state !== "queued") {
|
|
7494
|
+
this.log({
|
|
7495
|
+
level: "debug",
|
|
7496
|
+
message: `Session error for message ${inFlight.evidentMessageId.slice(0, 8)} observed state ${state}; leaving it to transcript polling`,
|
|
7497
|
+
conversation_id: watcher.conv.id,
|
|
7498
|
+
message_id: inFlight.evidentMessageId
|
|
7499
|
+
});
|
|
7500
|
+
return;
|
|
7501
|
+
}
|
|
7502
|
+
this.log({
|
|
7503
|
+
level: "error",
|
|
7504
|
+
message: `OpenCode could not run message ${inFlight.evidentMessageId.slice(0, 8)} in session ${event.sessionId.slice(0, 8)}: ${event.reason}`,
|
|
7505
|
+
conversation_id: watcher.conv.id,
|
|
7506
|
+
message_id: inFlight.evidentMessageId
|
|
7507
|
+
});
|
|
7508
|
+
await this.markFailed(
|
|
7509
|
+
watcher.conv.id,
|
|
7510
|
+
inFlight.evidentMessageId,
|
|
7511
|
+
event.sessionId,
|
|
7512
|
+
`OpenCode could not run this turn: ${event.reason}`
|
|
7513
|
+
);
|
|
7514
|
+
inFlight.done = true;
|
|
7515
|
+
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7516
|
+
} catch (err) {
|
|
7517
|
+
if (err instanceof ChannelAuthError) {
|
|
7518
|
+
this.log({
|
|
7519
|
+
level: "warn",
|
|
7520
|
+
message: `OpenCode session error could not mark message ${inFlight.evidentMessageId.slice(0, 8)} failed because authentication failed: ${err.message}; leaving it to transcript polling / the existing give-up path`,
|
|
7521
|
+
conversation_id: watcher.conv.id,
|
|
7522
|
+
message_id: inFlight.evidentMessageId
|
|
7523
|
+
});
|
|
7524
|
+
} else {
|
|
7525
|
+
this.log({
|
|
7526
|
+
level: "warn",
|
|
7527
|
+
message: `OpenCode session error could not mark message ${inFlight.evidentMessageId.slice(0, 8)} failed: ${err instanceof Error ? err.message : String(err)}; leaving it to transcript polling / the existing give-up path`,
|
|
7528
|
+
conversation_id: watcher.conv.id,
|
|
7529
|
+
message_id: inFlight.evidentMessageId
|
|
7530
|
+
});
|
|
7531
|
+
}
|
|
7532
|
+
} finally {
|
|
7533
|
+
this.sessionErrorHandled.delete(inFlight.evidentMessageId);
|
|
7534
|
+
}
|
|
7535
|
+
}
|
|
7123
7536
|
/**
|
|
7124
7537
|
* The per-session polling loop (WI-3). Once per tick it:
|
|
7125
7538
|
* 1. polls `GET /session/:id/message` once and, per in-flight message,
|
|
@@ -7232,6 +7645,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7232
7645
|
const conv = watcher.conv;
|
|
7233
7646
|
const state = messageRunState(messages, inFlight.opencodeMessageId);
|
|
7234
7647
|
const id = inFlight.evidentMessageId;
|
|
7648
|
+
if (messages && collectTaskCalls(messages, inFlight.opencodeMessageId).length > 0 && !this.subagentInvocationPrefetches.has(id)) {
|
|
7649
|
+
void this.resolveSubagentInvocations(
|
|
7650
|
+
messages,
|
|
7651
|
+
inFlight.opencodeMessageId,
|
|
7652
|
+
id,
|
|
7653
|
+
"prefetch"
|
|
7654
|
+
).catch((err) => {
|
|
7655
|
+
this.log({
|
|
7656
|
+
level: "warn",
|
|
7657
|
+
message: `Best-effort subagent usage prefetch failed for message ${id.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
7658
|
+
conversation_id: conv.id,
|
|
7659
|
+
message_id: id
|
|
7660
|
+
});
|
|
7661
|
+
});
|
|
7662
|
+
}
|
|
7235
7663
|
if (openQuestions.has(id)) inFlight.pausedOnQuestion = true;
|
|
7236
7664
|
else if (questionsPolledOk) inFlight.pausedOnQuestion = false;
|
|
7237
7665
|
if (openPermissions.has(id)) inFlight.pausedOnPermission = true;
|
|
@@ -7285,6 +7713,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7285
7713
|
message_id: inFlight.evidentMessageId
|
|
7286
7714
|
});
|
|
7287
7715
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7716
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7717
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7718
|
+
messages,
|
|
7719
|
+
inFlight.opencodeMessageId,
|
|
7720
|
+
inFlight.evidentMessageId
|
|
7721
|
+
);
|
|
7288
7722
|
const failure = await this.classifyModelAuthFailure(messages, inFlight.opencodeMessageId);
|
|
7289
7723
|
try {
|
|
7290
7724
|
await this.markFailed(
|
|
@@ -7293,7 +7727,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7293
7727
|
sessionId,
|
|
7294
7728
|
error2,
|
|
7295
7729
|
usage,
|
|
7296
|
-
failure
|
|
7730
|
+
failure,
|
|
7731
|
+
usageAgentName,
|
|
7732
|
+
subagentInvocations
|
|
7297
7733
|
);
|
|
7298
7734
|
} catch (err) {
|
|
7299
7735
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7336,9 +7772,11 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7336
7772
|
this.removeInFlight(watcher, inFlight.evidentMessageId);
|
|
7337
7773
|
return;
|
|
7338
7774
|
}
|
|
7775
|
+
const siblingOcIds = this.siblingOpencodeMessageIds(watcher, inFlight.evidentMessageId);
|
|
7776
|
+
const skippedByOpencode = state === "queued" && hasLaterSiblingTurnStarted(messages, inFlight.opencodeMessageId, siblingOcIds);
|
|
7339
7777
|
const pastStuckBound = this.now() - inFlight.dispatchedAt >= this.stuckQueuedMs;
|
|
7340
7778
|
const sessionIdle = state === "queued" && !hasRunningAssistantExcept(messages, inFlight.opencodeMessageId);
|
|
7341
|
-
if (state === "queued" && pastStuckBound && sessionIdle && !inFlight.stuckReported) {
|
|
7779
|
+
if (state === "queued" && pastStuckBound && (sessionIdle || skippedByOpencode) && !inFlight.stuckReported) {
|
|
7342
7780
|
inFlight.stuckReported = true;
|
|
7343
7781
|
void this.postSignal(conv.id, inFlight.evidentMessageId, "stuck_queued", {
|
|
7344
7782
|
stuck_for_ms: this.now() - inFlight.dispatchedAt
|
|
@@ -7499,7 +7937,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7499
7937
|
const hasActivelyRunningSibling = [...watcher.inFlight.values()].some(
|
|
7500
7938
|
(sib) => sib.evidentMessageId !== inFlight.evidentMessageId && messageRunState(messages, sib.opencodeMessageId) === "running" && !siblingPaused(sib)
|
|
7501
7939
|
);
|
|
7502
|
-
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling;
|
|
7940
|
+
const queuedBehindRunningSibling = state === "queued" && hasActivelyRunningSibling && !skippedByOpencode;
|
|
7503
7941
|
if (!activelyRunning && !queuedBehindRunningSibling && this.now() >= inFlight.deadline) {
|
|
7504
7942
|
this.log({
|
|
7505
7943
|
level: "debug",
|
|
@@ -7534,6 +7972,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7534
7972
|
});
|
|
7535
7973
|
const title = await this.resolveSessionTitle(sessionId, watcher.conv.id);
|
|
7536
7974
|
const usage = messageUsage(messages, inFlight.opencodeMessageId);
|
|
7975
|
+
const usageAgentName = this.usageAgentName(messages ?? [], inFlight.opencodeMessageId);
|
|
7976
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
7977
|
+
messages,
|
|
7978
|
+
inFlight.opencodeMessageId,
|
|
7979
|
+
inFlight.evidentMessageId
|
|
7980
|
+
);
|
|
7537
7981
|
try {
|
|
7538
7982
|
await this.markDone(
|
|
7539
7983
|
conv.id,
|
|
@@ -7541,7 +7985,9 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7541
7985
|
sessionId,
|
|
7542
7986
|
inFlight.opencodeMessageId,
|
|
7543
7987
|
title,
|
|
7544
|
-
usage
|
|
7988
|
+
usage,
|
|
7989
|
+
usageAgentName,
|
|
7990
|
+
subagentInvocations
|
|
7545
7991
|
);
|
|
7546
7992
|
} catch (err) {
|
|
7547
7993
|
if (err instanceof ChannelAuthError) throw err;
|
|
@@ -7720,6 +8166,12 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7720
8166
|
if (state === "failed" && !restartAborted) {
|
|
7721
8167
|
const error2 = messageError(messages, ocId ?? "") ?? void 0;
|
|
7722
8168
|
const usage = messageUsage(messages, ocId ?? "");
|
|
8169
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8170
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8171
|
+
messages,
|
|
8172
|
+
ocId ?? "",
|
|
8173
|
+
row.id
|
|
8174
|
+
);
|
|
7723
8175
|
const failure = await this.classifyModelAuthFailure(messages, ocId ?? "");
|
|
7724
8176
|
this.log({
|
|
7725
8177
|
level: "error",
|
|
@@ -7728,7 +8180,16 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7728
8180
|
message_id: row.id
|
|
7729
8181
|
});
|
|
7730
8182
|
try {
|
|
7731
|
-
await this.markFailed(
|
|
8183
|
+
await this.markFailed(
|
|
8184
|
+
row.conversation_id,
|
|
8185
|
+
row.id,
|
|
8186
|
+
sessionId,
|
|
8187
|
+
error2,
|
|
8188
|
+
usage,
|
|
8189
|
+
failure,
|
|
8190
|
+
usageAgentName,
|
|
8191
|
+
subagentInvocations
|
|
8192
|
+
);
|
|
7732
8193
|
} catch (err) {
|
|
7733
8194
|
if (err instanceof ChannelAuthError) throw err;
|
|
7734
8195
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -7890,7 +8351,22 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
7890
8351
|
try {
|
|
7891
8352
|
const title = await this.resolveSessionTitle(sessionId, row.conversation_id);
|
|
7892
8353
|
const usage = messageUsage(messages, ocId ?? "");
|
|
7893
|
-
|
|
8354
|
+
const usageAgentName = this.usageAgentName(messages, ocId ?? "");
|
|
8355
|
+
const subagentInvocations = await this.resolveSubagentInvocations(
|
|
8356
|
+
messages,
|
|
8357
|
+
ocId ?? "",
|
|
8358
|
+
row.id
|
|
8359
|
+
);
|
|
8360
|
+
await this.markDone(
|
|
8361
|
+
row.conversation_id,
|
|
8362
|
+
row.id,
|
|
8363
|
+
sessionId,
|
|
8364
|
+
ocId,
|
|
8365
|
+
title,
|
|
8366
|
+
usage,
|
|
8367
|
+
usageAgentName,
|
|
8368
|
+
subagentInvocations
|
|
8369
|
+
);
|
|
7894
8370
|
} catch (err) {
|
|
7895
8371
|
if (err instanceof ChannelAuthError) throw err;
|
|
7896
8372
|
if (err instanceof ChannelTerminalError) {
|
|
@@ -8294,6 +8770,166 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8294
8770
|
if (parent !== void 0) this.sessionParents.set(sessionId, parent);
|
|
8295
8771
|
return parent;
|
|
8296
8772
|
}
|
|
8773
|
+
usageAgentName(messages, userMessageId) {
|
|
8774
|
+
const reply = findLastAssistantReplyFor(messages, userMessageId);
|
|
8775
|
+
const mode = reply?.info?.mode;
|
|
8776
|
+
if (typeof mode === "string" && mode.length > 0) return mode;
|
|
8777
|
+
const agent = reply?.info?.agent;
|
|
8778
|
+
return typeof agent === "string" && agent.length > 0 ? agent : null;
|
|
8779
|
+
}
|
|
8780
|
+
async resolveSubagentInvocations(messages, userMessageId, messageId, phase = "terminal") {
|
|
8781
|
+
if (!messages) return void 0;
|
|
8782
|
+
const cache = phase === "prefetch" ? this.subagentInvocationPrefetches : this.subagentInvocationCollections;
|
|
8783
|
+
const cached = cache.get(messageId);
|
|
8784
|
+
if (cached) return cached;
|
|
8785
|
+
const collection = this.buildSubagentInvocations(messages, userMessageId, messageId).catch(
|
|
8786
|
+
(err) => {
|
|
8787
|
+
this.log({
|
|
8788
|
+
level: "warn",
|
|
8789
|
+
message: `Best-effort subagent usage collection failed for message ${messageId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
8790
|
+
message_id: messageId
|
|
8791
|
+
});
|
|
8792
|
+
return void 0;
|
|
8793
|
+
}
|
|
8794
|
+
);
|
|
8795
|
+
cache.set(messageId, collection);
|
|
8796
|
+
const result = await collection;
|
|
8797
|
+
if (result === void 0 && cache.get(messageId) === collection) cache.delete(messageId);
|
|
8798
|
+
return result;
|
|
8799
|
+
}
|
|
8800
|
+
clearSubagentInvocationCaches(messageId) {
|
|
8801
|
+
this.subagentInvocationCollections.delete(messageId);
|
|
8802
|
+
this.subagentInvocationPrefetches.delete(messageId);
|
|
8803
|
+
}
|
|
8804
|
+
async buildSubagentInvocations(messages, userMessageId, messageId) {
|
|
8805
|
+
const rootCalls = collectTaskCalls(messages, userMessageId);
|
|
8806
|
+
if (rootCalls.length === 0) return void 0;
|
|
8807
|
+
const childMessages = /* @__PURE__ */ new Map();
|
|
8808
|
+
const seenCallIds = new Set(rootCalls.map((call) => call.callID));
|
|
8809
|
+
const work = rootCalls.map((call) => ({
|
|
8810
|
+
call,
|
|
8811
|
+
depth: 1
|
|
8812
|
+
}));
|
|
8813
|
+
const payload = [];
|
|
8814
|
+
const fetchChildMessages = (sessionId) => {
|
|
8815
|
+
const cached = childMessages.get(sessionId);
|
|
8816
|
+
if (cached) return cached;
|
|
8817
|
+
const pending = (async () => {
|
|
8818
|
+
try {
|
|
8819
|
+
const res = await this.fetchImpl(`${this.opencodeBase}/session/${sessionId}/message`);
|
|
8820
|
+
if (!res.ok) {
|
|
8821
|
+
this.log({
|
|
8822
|
+
level: "warn",
|
|
8823
|
+
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} returned HTTP ${res.status} \u2014 omitting invocation telemetry`,
|
|
8824
|
+
message_id: messageId
|
|
8825
|
+
});
|
|
8826
|
+
return null;
|
|
8827
|
+
}
|
|
8828
|
+
const body = await res.json();
|
|
8829
|
+
if (!Array.isArray(body)) throw new Error("response body was not a message array");
|
|
8830
|
+
return body;
|
|
8831
|
+
} catch (err) {
|
|
8832
|
+
this.log({
|
|
8833
|
+
level: "warn",
|
|
8834
|
+
message: `Best-effort subagent session fetch failed for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} \u2014 omitting invocation telemetry: ${err instanceof Error ? err.message : String(err)}`,
|
|
8835
|
+
message_id: messageId
|
|
8836
|
+
});
|
|
8837
|
+
return null;
|
|
8838
|
+
}
|
|
8839
|
+
})();
|
|
8840
|
+
childMessages.set(sessionId, pending);
|
|
8841
|
+
return pending;
|
|
8842
|
+
};
|
|
8843
|
+
const fetchChildWithoutBlocking = async (sessionId) => {
|
|
8844
|
+
const pending = fetchChildMessages(sessionId);
|
|
8845
|
+
let timer;
|
|
8846
|
+
const timeout = new Promise((resolve4) => {
|
|
8847
|
+
timer = setTimeout(() => {
|
|
8848
|
+
this.log({
|
|
8849
|
+
level: "warn",
|
|
8850
|
+
message: `Best-effort subagent session fetch for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} was slow \u2014 omitting invocation telemetry without delaying completion`,
|
|
8851
|
+
message_id: messageId
|
|
8852
|
+
});
|
|
8853
|
+
resolve4(null);
|
|
8854
|
+
}, SUBAGENT_TELEMETRY_FETCH_TIMEOUT_MS);
|
|
8855
|
+
});
|
|
8856
|
+
try {
|
|
8857
|
+
return await Promise.race([pending, timeout]);
|
|
8858
|
+
} finally {
|
|
8859
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
8860
|
+
}
|
|
8861
|
+
};
|
|
8862
|
+
while (work.length > 0) {
|
|
8863
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8864
|
+
for (const item of work.splice(0)) {
|
|
8865
|
+
const group = groups.get(item.call.childSessionId) ?? [];
|
|
8866
|
+
group.push(item);
|
|
8867
|
+
groups.set(item.call.childSessionId, group);
|
|
8868
|
+
}
|
|
8869
|
+
const groupResults = await Promise.all(
|
|
8870
|
+
[...groups].map(async ([sessionId, items]) => ({
|
|
8871
|
+
sessionId,
|
|
8872
|
+
items,
|
|
8873
|
+
messages: sessionId === null ? [] : await fetchChildWithoutBlocking(sessionId)
|
|
8874
|
+
}))
|
|
8875
|
+
);
|
|
8876
|
+
for (const { sessionId, items, messages: child } of groupResults) {
|
|
8877
|
+
if (sessionId !== null && child === null) continue;
|
|
8878
|
+
const attribution = sessionId === null ? { invocations: [], unattributed: [] } : attributeTaskCallUsage(
|
|
8879
|
+
child,
|
|
8880
|
+
items.map(({ call }) => ({
|
|
8881
|
+
callID: call.callID,
|
|
8882
|
+
timeStart: call.timeStart,
|
|
8883
|
+
timeEnd: call.timeEnd
|
|
8884
|
+
}))
|
|
8885
|
+
);
|
|
8886
|
+
if (sessionId !== null && attribution.unattributed.length > 0) {
|
|
8887
|
+
this.log({
|
|
8888
|
+
level: "warn",
|
|
8889
|
+
message: `Omitted ${attribution.unattributed.length} unattributable assistant message(s) from subagent usage for message ${messageId.slice(0, 8)} and child session ${sessionId.slice(0, 8)} \u2014 assigned to no invocation window`,
|
|
8890
|
+
message_id: messageId
|
|
8891
|
+
});
|
|
8892
|
+
}
|
|
8893
|
+
const usageByCall = new Map(
|
|
8894
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.usage])
|
|
8895
|
+
);
|
|
8896
|
+
const messagesByCall = new Map(
|
|
8897
|
+
attribution.invocations.map((invocation) => [invocation.callID, invocation.messages])
|
|
8898
|
+
);
|
|
8899
|
+
for (const { call, depth } of items) {
|
|
8900
|
+
const usage = usageByCall.get(call.callID) ?? null;
|
|
8901
|
+
payload.push({
|
|
8902
|
+
tool_call_id: call.callID,
|
|
8903
|
+
agent_name: call.subagentName,
|
|
8904
|
+
opencode_session_id: call.childSessionId,
|
|
8905
|
+
parent_opencode_session_id: call.parentSessionId,
|
|
8906
|
+
depth,
|
|
8907
|
+
status: call.status,
|
|
8908
|
+
started_at: call.timeStart === null ? null : new Date(call.timeStart).toISOString(),
|
|
8909
|
+
ended_at: call.timeEnd === null ? null : new Date(call.timeEnd).toISOString(),
|
|
8910
|
+
usage_provider_id: usage?.usage_provider_id ?? call.model?.providerID ?? null,
|
|
8911
|
+
usage_model_id: usage?.usage_model_id ?? call.model?.modelID ?? null,
|
|
8912
|
+
usage_tokens_input: usage?.usage_tokens_input ?? null,
|
|
8913
|
+
usage_tokens_output: usage?.usage_tokens_output ?? null,
|
|
8914
|
+
usage_tokens_reasoning: usage?.usage_tokens_reasoning ?? null,
|
|
8915
|
+
usage_tokens_cache_read: usage?.usage_tokens_cache_read ?? null,
|
|
8916
|
+
usage_tokens_cache_write: usage?.usage_tokens_cache_write ?? null,
|
|
8917
|
+
usage_cost_usd: usage?.usage_cost_usd ?? null
|
|
8918
|
+
});
|
|
8919
|
+
for (const assigned of messagesByCall.get(call.callID) ?? []) {
|
|
8920
|
+
const parentId = assigned.info?.parentID ?? assigned.parentID;
|
|
8921
|
+
if (!parentId) continue;
|
|
8922
|
+
for (const nested of collectTaskCalls([assigned], parentId)) {
|
|
8923
|
+
if (seenCallIds.has(nested.callID)) continue;
|
|
8924
|
+
seenCallIds.add(nested.callID);
|
|
8925
|
+
work.push({ call: nested, depth: depth + 1 });
|
|
8926
|
+
}
|
|
8927
|
+
}
|
|
8928
|
+
}
|
|
8929
|
+
}
|
|
8930
|
+
}
|
|
8931
|
+
return payload.length > 0 ? payload : void 0;
|
|
8932
|
+
}
|
|
8297
8933
|
/**
|
|
8298
8934
|
* OpenCode's synchronous default session title (e.g.
|
|
8299
8935
|
* `"New session - 1737800000000"`), assigned immediately when a session is
|
|
@@ -8777,7 +9413,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8777
9413
|
* watcher retries next tick within the
|
|
8778
9414
|
* deadline, Finding 4).
|
|
8779
9415
|
*/
|
|
8780
|
-
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage) {
|
|
9416
|
+
async markDone(conversationId, messageId, sessionId, opencodeMessageId, title, usage, usageAgentName, subagentInvocations) {
|
|
8781
9417
|
const res = await this.fetchImpl(
|
|
8782
9418
|
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
8783
9419
|
{
|
|
@@ -8793,15 +9429,21 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8793
9429
|
opencode_session_id: sessionId,
|
|
8794
9430
|
...opencodeMessageId ? { opencode_message_id: opencodeMessageId } : {},
|
|
8795
9431
|
...title ? { title } : {},
|
|
8796
|
-
...usage ? usage : {}
|
|
9432
|
+
...usage ? usage : {},
|
|
9433
|
+
...usageAgentName ? { usage_agent_name: usageAgentName } : {},
|
|
9434
|
+
...subagentInvocations && subagentInvocations.length > 0 ? { subagent_invocations: subagentInvocations } : {}
|
|
8797
9435
|
})
|
|
8798
9436
|
}
|
|
8799
9437
|
);
|
|
8800
9438
|
this.assertAuth(res, "marking message as done");
|
|
8801
|
-
if (res.ok)
|
|
9439
|
+
if (res.ok) {
|
|
9440
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
9441
|
+
return;
|
|
9442
|
+
}
|
|
8802
9443
|
if (isRetryableStatus(res.status)) {
|
|
8803
9444
|
throw new Error(`marking message as done: HTTP ${res.status}`);
|
|
8804
9445
|
}
|
|
9446
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8805
9447
|
throw new ChannelTerminalError(`marking message as done: HTTP ${res.status}`, res.status);
|
|
8806
9448
|
}
|
|
8807
9449
|
/**
|
|
@@ -8816,7 +9458,7 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8816
9458
|
* exists but is wedged, so the next attempt must get a fresh one
|
|
8817
9459
|
* instead of reusing it — see WI-1's server-side null-clearing PATCH).
|
|
8818
9460
|
*/
|
|
8819
|
-
async markFailed(conversationId, messageId, sessionId, error2, usage, failure) {
|
|
9461
|
+
async markFailed(conversationId, messageId, sessionId, error2, usage, failure, usageAgentName, subagentInvocations) {
|
|
8820
9462
|
const body = { status: "failed" };
|
|
8821
9463
|
if (sessionId === null) {
|
|
8822
9464
|
body.opencode_session_id = null;
|
|
@@ -8825,23 +9467,33 @@ var ChannelDriver = class _ChannelDriver {
|
|
|
8825
9467
|
}
|
|
8826
9468
|
if (error2 !== void 0) body.error = error2;
|
|
8827
9469
|
if (usage) Object.assign(body, usage);
|
|
9470
|
+
if (usageAgentName) body.usage_agent_name = usageAgentName;
|
|
9471
|
+
if (subagentInvocations && subagentInvocations.length > 0) {
|
|
9472
|
+
body.subagent_invocations = subagentInvocations;
|
|
9473
|
+
}
|
|
8828
9474
|
if (failure) {
|
|
8829
9475
|
body.failure_kind = failure.kind;
|
|
8830
9476
|
body.failure_provider_id = failure.providerId;
|
|
8831
9477
|
body.failure_model_id = failure.modelId;
|
|
8832
9478
|
body.failure_reason = failure.reason;
|
|
8833
9479
|
}
|
|
8834
|
-
|
|
8835
|
-
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
9480
|
+
try {
|
|
9481
|
+
await this.callWithRetry(
|
|
9482
|
+
"marking message as failed",
|
|
9483
|
+
() => this.fetchImpl(
|
|
9484
|
+
`${this.apiUrl}/runners/${this.agentId}/threads/${conversationId}/messages/${messageId}`,
|
|
9485
|
+
{
|
|
9486
|
+
method: "PATCH",
|
|
9487
|
+
headers: { Authorization: this.getAuthHeader(), "Content-Type": "application/json" },
|
|
9488
|
+
body: JSON.stringify(body)
|
|
9489
|
+
}
|
|
9490
|
+
)
|
|
9491
|
+
);
|
|
9492
|
+
} catch (err) {
|
|
9493
|
+
if (err instanceof ChannelTerminalError) this.clearSubagentInvocationCaches(messageId);
|
|
9494
|
+
throw err;
|
|
9495
|
+
}
|
|
9496
|
+
this.clearSubagentInvocationCaches(messageId);
|
|
8845
9497
|
}
|
|
8846
9498
|
/**
|
|
8847
9499
|
* Classify an errored turn as a model-auth failure (#736 P1-4), for `markFailed`.
|
|
@@ -11269,6 +11921,7 @@ async function run(options) {
|
|
|
11269
11921
|
logActivity(state, { type: "info", level: "warn", message: versionWarning });
|
|
11270
11922
|
}
|
|
11271
11923
|
}
|
|
11924
|
+
await reloadProviderCache(state.port);
|
|
11272
11925
|
const noProviderWarning = buildNoProviderWarning(
|
|
11273
11926
|
await hasAnyConfiguredProvider(state.port)
|
|
11274
11927
|
);
|