@zq-silk/yui 0.6.13 → 0.6.14
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/README.md +34 -5
- package/dist/cli/commandCatalog.js +173 -57
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli.js +99 -11
- package/dist/commands/configCommands.js +521 -171
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +56 -3
- package/dist/commands/projectCommands.js +504 -2
- package/dist/commands/releaseCommands.js +0 -1
- package/dist/commands/taskActor.js +17 -0
- package/dist/commands/taskBaseCommands.js +29 -0
- package/dist/commands/taskCommands.js +577 -126
- package/dist/commands/taskContextCommand.js +36 -2
- package/dist/commands/taskNextActionCommand.js +48 -3
- package/dist/commands/taskPublicationCommands.js +319 -0
- package/dist/commands/taskRoleRuntimeStatus.js +83 -59
- package/dist/commands/telemetryCommands.js +14 -13
- package/dist/config/yuiConfig.js +161 -8
- package/dist/context/sessionContextBudget.js +68 -0
- package/dist/context/wakeNotification.js +65 -0
- package/dist/controller/clientRuntime.js +2 -1
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
- package/dist/controller/jobSupervisor.js +5 -4
- package/dist/controller/resourceCleanupLinux.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +3 -3
- package/dist/controller/runtime.js +88 -10
- package/dist/controller/updateReconciliation.js +4 -3
- package/dist/doctor/doctor.js +26 -7
- package/dist/executor/agentConfigurationCatalog.js +18 -0
- package/dist/executor/fileRoleLaunchPlanner.js +3 -3
- package/dist/lifecycle/contextBudgetRollover.js +81 -0
- package/dist/lifecycle/exactRunTerminalization.js +11 -1
- package/dist/lifecycle/providerErrorClass.js +33 -12
- package/dist/observability/executionAudit.js +214 -6
- package/dist/output/table.js +18 -0
- package/dist/repository/gitWorkspace.js +92 -0
- package/dist/repository/project.js +218 -4
- package/dist/repository/taskBaseFreshness.js +318 -0
- package/dist/repository/taskWorkspacePreparer.js +16 -2
- package/dist/review/deltaRecheck.js +232 -0
- package/dist/review/reviewConfig.js +31 -0
- package/dist/review/reviewFindingLedger.js +5 -1
- package/dist/review/reviewRound.js +156 -1
- package/dist/run/providerRetry.js +21 -3
- package/dist/run/providerRetryConfig.js +13 -60
- package/dist/run/recoveryProjection.js +199 -0
- package/dist/runtime/builtinAgentDrivers.js +3 -0
- package/dist/runtime/builtinTranscriptUsage.js +76 -32
- package/dist/runtime/continuationManager.js +17 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/launchDiagnostics.js +154 -0
- package/dist/runtime/lifecycleReservation.js +13 -0
- package/dist/runtime/providerContinuation.js +38 -0
- package/dist/runtime/providerContinuationReconciliationService.js +1 -0
- package/dist/runtime/providerErrorCodes.js +278 -0
- package/dist/runtime/runtimeHealthPolicy.js +20 -0
- package/dist/runtime/runtimeObservation.js +1 -0
- package/dist/runtime/runtimeProjection.js +115 -23
- package/dist/runtime/tmuxAdapters.js +242 -48
- package/dist/scheduler/activeRoleRunDelivery.js +139 -3
- package/dist/scheduler/activeTaskProgress.js +4 -3
- package/dist/scheduler/leaderWakeupProcessor.js +105 -15
- package/dist/scheduler/roleRunLiveness.js +2 -1
- package/dist/scheduler/roleRunStall.js +3 -2
- package/dist/scheduler/taskWake.js +72 -0
- package/dist/scheduler/wakeReason.js +64 -0
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/migration/productionRegistry.js +325 -1
- package/dist/storage/sqliteSchema.js +61 -2
- package/dist/storage/sqliteStore.js +129 -2
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskStore.js +262 -5
- package/dist/storage/upgrade/recordVersions.js +6 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
- package/dist/task/completionReadiness.js +282 -0
- package/dist/task/publicationReference.js +123 -0
- package/dist/task/taskRecordReference.js +3 -1
- package/dist/telemetry/telemetryConfig.js +23 -18
- package/dist/telemetry/telemetryWiring.js +8 -8
- package/dist/tmux/tmuxManager.js +50 -9
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/view.js +18 -0
- package/dist/web/webSnapshot.js +100 -10
- package/i18n/README.zh-CN.md +5 -5
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +49 -10
- package/skills/yui-operator/SKILL.md +13 -3
- package/skills/yui-worker/SKILL.md +8 -0
|
@@ -12,7 +12,7 @@ const MAX_RECONCILIATION_PASSES = 4;
|
|
|
12
12
|
* and inode fingerprints. Agent, tmux, app, and foreign-Home resources are
|
|
13
13
|
* deliberately outside this operation.
|
|
14
14
|
*/
|
|
15
|
-
export async function reconcileControllerResourcesForUpdate(home, environment = process.env) {
|
|
15
|
+
export async function reconcileControllerResourcesForUpdate(home, environment = process.env, tmuxBin) {
|
|
16
16
|
const resolvedHome = resolve(home);
|
|
17
17
|
const releaseLock = await acquireHomeLifecycleLock(resolvedHome, {
|
|
18
18
|
removeStaleOwner: true
|
|
@@ -23,7 +23,8 @@ export async function reconcileControllerResourcesForUpdate(home, environment =
|
|
|
23
23
|
const snapshot = await scanControllerResourceInventory({
|
|
24
24
|
currentHome: resolvedHome,
|
|
25
25
|
scope: "current",
|
|
26
|
-
environment
|
|
26
|
+
environment,
|
|
27
|
+
...(tmuxBin === undefined ? {} : { tmuxBin })
|
|
27
28
|
});
|
|
28
29
|
assertCertainSnapshot(snapshot, resolvedHome);
|
|
29
30
|
const resources = controllerResources(snapshot, resolvedHome);
|
|
@@ -53,7 +54,7 @@ export async function reconcileControllerResourcesForUpdate(home, environment =
|
|
|
53
54
|
}
|
|
54
55
|
for (const candidate of candidates) {
|
|
55
56
|
try {
|
|
56
|
-
await cleanControllerResource(candidate, { environment });
|
|
57
|
+
await cleanControllerResource(candidate, { environment, ...(tmuxBin === undefined ? {} : { tmuxBin }) });
|
|
57
58
|
cleaned.add(candidate.id);
|
|
58
59
|
}
|
|
59
60
|
catch (error) {
|
package/dist/doctor/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@ import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/a
|
|
|
5
5
|
import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
|
|
6
6
|
import { inspectAgentCapabilities, resolveAgentAdapter } from "../executor/agentAdapter.js";
|
|
7
7
|
import { inspectCodexLaunchConfig } from "../executor/codexConfigConflict.js";
|
|
8
|
+
import { nativeAdditionalDirectories, nativeAgentWorkspace, withNativeProjectDirectories } from "../executor/fileRoleLaunchPlanner.js";
|
|
8
9
|
import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
9
10
|
import { compileRoleSessionContext } from "../context/roleSessionContext.js";
|
|
10
11
|
import { usageError } from "../errors/cliError.js";
|
|
@@ -15,6 +16,7 @@ import { inspectStorageSchema } from "../storage/storageSchema.js";
|
|
|
15
16
|
import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
|
|
16
17
|
import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
|
|
17
18
|
import { classifyHome } from "../storage/upgrade/homeClassification.js";
|
|
19
|
+
import { resolveGitBin, resolveTmuxBin } from "../config/yuiConfig.js";
|
|
18
20
|
import { readMigrationReceipt } from "../storage/upgrade/migrationReceipt.js";
|
|
19
21
|
import { latestStorageVersionState } from "../storage/upgrade/recordVersions.js";
|
|
20
22
|
import { COMMITTED_DATABASE_FILENAME } from "../storage/upgrade/sqliteStateMigration.js";
|
|
@@ -127,9 +129,10 @@ function inspectDoctor(env, executor, storageOptions = {}) {
|
|
|
127
129
|
const schemaCheck = checkSchema(schema, compatibility);
|
|
128
130
|
const storage = inspectState(home, homeCheck, schema, compatibility);
|
|
129
131
|
const domain = checkEphemeralDomain(home);
|
|
132
|
+
const durableConfig = readDurableConfigSafely(home, compatibility.storageOptions);
|
|
130
133
|
const toolChecks = [
|
|
131
|
-
checkExecutable("git",
|
|
132
|
-
checkExecutable("tmux",
|
|
134
|
+
checkExecutable("git", durableConfig.gitBin, ["--version"], executor),
|
|
135
|
+
checkExecutable("tmux", durableConfig.tmuxBin, ["-V"], executor)
|
|
133
136
|
];
|
|
134
137
|
const agentChecks = storage.agents.flatMap((agent) => checkAgent(agent, executor, env));
|
|
135
138
|
const review = inspectReview({ ...storage.review, home }, agentChecks, env);
|
|
@@ -776,9 +779,8 @@ function checkReviewerLaunch(agent, role, binding, adapter, environment, home) {
|
|
|
776
779
|
const definition = configuredAgentToDefinition(agent);
|
|
777
780
|
const launchEnvironment = resolveDoctorAgentEnvironment(definition, environment);
|
|
778
781
|
const effective = resolveEffectiveLaunch({ role, purpose: "execution" });
|
|
779
|
-
const agentWorkspace = effective.workspace
|
|
780
|
-
|
|
781
|
-
: effective.workspace.root;
|
|
782
|
+
const agentWorkspace = nativeAgentWorkspace(effective.workspace);
|
|
783
|
+
const launchConfig = withNativeProjectDirectories(binding.config, nativeAdditionalDirectories(effective.workspace, agentWorkspace));
|
|
782
784
|
const codexConfig = adapter.id === "codex"
|
|
783
785
|
? inspectCodexLaunchConfig({
|
|
784
786
|
environment: launchEnvironment,
|
|
@@ -801,7 +803,7 @@ function checkReviewerLaunch(agent, role, binding, adapter, environment, home) {
|
|
|
801
803
|
: undefined;
|
|
802
804
|
const compiled = adapter.compileNew({
|
|
803
805
|
agent: definition,
|
|
804
|
-
config:
|
|
806
|
+
config: launchConfig,
|
|
805
807
|
workspace: agentWorkspace,
|
|
806
808
|
sessionTitle: "reviewer",
|
|
807
809
|
...(reviewerContext === undefined
|
|
@@ -817,7 +819,7 @@ function checkReviewerLaunch(agent, role, binding, adapter, environment, home) {
|
|
|
817
819
|
return {
|
|
818
820
|
name: "reviewer launch",
|
|
819
821
|
status: "ok",
|
|
820
|
-
detail: `adapter=${adapter.id} strategy=${compiled.sessionStrategy} command=${agent.command}`
|
|
822
|
+
detail: `adapter=${adapter.id} strategy=${compiled.sessionStrategy} command=${agent.command} addDirs=${launchConfig.additionalDirectories?.length ?? 0}`
|
|
821
823
|
};
|
|
822
824
|
}
|
|
823
825
|
catch (error) {
|
|
@@ -973,3 +975,20 @@ function systemCode(error) {
|
|
|
973
975
|
function errorMessage(error) {
|
|
974
976
|
return error instanceof Error ? error.message : String(error);
|
|
975
977
|
}
|
|
978
|
+
/**
|
|
979
|
+
* Read the durable config for executable paths. Falls back to defaults when
|
|
980
|
+
* the Home store cannot be opened (the doctor must still run on broken Homes).
|
|
981
|
+
*/
|
|
982
|
+
function readDurableConfigSafely(home, storageOptions) {
|
|
983
|
+
try {
|
|
984
|
+
const store = openCompatibleFileTaskStore(home, storageOptions ?? {});
|
|
985
|
+
const config = store.getConfig();
|
|
986
|
+
return {
|
|
987
|
+
tmuxBin: resolveTmuxBin(config.tmuxBin),
|
|
988
|
+
gitBin: resolveGitBin(config.gitBin)
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
return { tmuxBin: "tmux", gitBin: "git" };
|
|
993
|
+
}
|
|
994
|
+
}
|
|
@@ -156,6 +156,24 @@ export function modelChoice(catalog, value) {
|
|
|
156
156
|
? defaultModel(catalog)
|
|
157
157
|
: catalog.models.find((model) => model.value === value);
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Validates the launch-time model/effort against a live or cached capability
|
|
161
|
+
* catalog. A fallback catalog intentionally carries no model list, so an
|
|
162
|
+
* unavailable Provider probe does not block a supported launch.
|
|
163
|
+
*/
|
|
164
|
+
export function validateAgentLaunchConfiguration(catalog, config) {
|
|
165
|
+
if (catalog.models.length === 0)
|
|
166
|
+
return;
|
|
167
|
+
const model = modelChoice(catalog, config.model);
|
|
168
|
+
if (model === undefined) {
|
|
169
|
+
throw new Error(`Unsupported ${catalog.adapterId} launch configuration: field=model actual=${JSON.stringify(config.model ?? "")} supported=${JSON.stringify(catalog.models.map(({ value }) => value))}.`);
|
|
170
|
+
}
|
|
171
|
+
if (config.effort !== undefined
|
|
172
|
+
&& model.efforts.length > 0
|
|
173
|
+
&& !model.efforts.some((effort) => effort.value === config.effort)) {
|
|
174
|
+
throw new Error(`Unsupported ${catalog.adapterId} launch configuration: field=effort actual=${JSON.stringify(config.effort)} model=${JSON.stringify(model.value)} supported=${JSON.stringify(model.efforts.map(({ value }) => value))}.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
159
177
|
function field(key, choices, allowCustom) {
|
|
160
178
|
return { key, choices, allowCustom };
|
|
161
179
|
}
|
|
@@ -547,16 +547,16 @@ export class FileRoleLaunchPlanner {
|
|
|
547
547
|
return selectEnvironment(source, names);
|
|
548
548
|
}
|
|
549
549
|
}
|
|
550
|
-
function nativeAgentWorkspace(workspace) {
|
|
550
|
+
export function nativeAgentWorkspace(workspace) {
|
|
551
551
|
return workspace.entries.length === 1
|
|
552
552
|
? workspace.entries[0].path
|
|
553
553
|
: workspace.root;
|
|
554
554
|
}
|
|
555
|
-
function nativeAdditionalDirectories(workspace, agentWorkspace) {
|
|
555
|
+
export function nativeAdditionalDirectories(workspace, agentWorkspace) {
|
|
556
556
|
return [workspace.root, ...workspace.entries.map(({ path }) => path)]
|
|
557
557
|
.filter((path) => path !== agentWorkspace);
|
|
558
558
|
}
|
|
559
|
-
function withNativeProjectDirectories(config, projectDirectories) {
|
|
559
|
+
export function withNativeProjectDirectories(config, projectDirectories) {
|
|
560
560
|
if (projectDirectories.length === 0)
|
|
561
561
|
return config;
|
|
562
562
|
return {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
2
|
+
import { validateRoleSessionSet } from "../executor/agentExecutor.js";
|
|
3
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
4
|
+
import { RUNTIME_CLEANUP_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
5
|
+
/**
|
|
6
|
+
* Issue 04 (context token budget): retires one native Session generation that
|
|
7
|
+
* crossed the hard context budget so the next Leader wake starts a fresh
|
|
8
|
+
* generation. This is a controlled rollover, not a failure: the durable Task
|
|
9
|
+
* records are the checkpoint, the bounded context snapshot re-establishes
|
|
10
|
+
* working context for the new generation, and the retired Session is kept in
|
|
11
|
+
* history as `stopped` with an auditable event. The Controller separately
|
|
12
|
+
* owns verified process cleanup through the runtime cleanup lane.
|
|
13
|
+
*/
|
|
14
|
+
export const CONTEXT_BUDGET_ROLLOVER_REASON = "context-budget-hard-limit";
|
|
15
|
+
export function rolloverTaskRoleSessionForContextBudget(store, taskId, roleName, evidence, now) {
|
|
16
|
+
const task = store.getTask(taskId);
|
|
17
|
+
if (task === null)
|
|
18
|
+
throw new Error(`Task not found: ${taskId}.`);
|
|
19
|
+
if (task.status !== "active") {
|
|
20
|
+
throw new Error(`Task is not active: ${task.id}/${task.status}.`);
|
|
21
|
+
}
|
|
22
|
+
const role = store.getRole(task.id, roleName);
|
|
23
|
+
if (role === null)
|
|
24
|
+
throw new Error(`Role not found: ${task.id}/${roleName}.`);
|
|
25
|
+
const set = store.getTaskRoleSessionSet(task.id, role.name);
|
|
26
|
+
const current = set?.sessions[set.activeAgentId];
|
|
27
|
+
if (set === null || set === undefined || current === undefined) {
|
|
28
|
+
// No live generation to retire; the caller can launch fresh directly.
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const timestamp = now.toISOString();
|
|
32
|
+
const reason = `${CONTEXT_BUDGET_ROLLOVER_REASON} (peak ${evidence.peakTokens} >= hard ${evidence.hardTokens} tokens)`;
|
|
33
|
+
enqueueWork(store, runtimeLifecycleTarget({ scope: "task", taskId: task.id, roleName: role.name }), RUNTIME_CLEANUP_REQUIRED_REASON, now, [{ type: "task", id: task.id }]);
|
|
34
|
+
const retired = retireStoppedTaskRoleSession(set, timestamp);
|
|
35
|
+
store.saveTaskRoleSessionSet(retired);
|
|
36
|
+
const eventId = store.nextEventId(task.id);
|
|
37
|
+
store.saveEvent(task.id, createTaskEvent(eventId, task.id, "runtime.role-session-reset", {
|
|
38
|
+
roleName: role.name,
|
|
39
|
+
reason,
|
|
40
|
+
peakTokens: String(evidence.peakTokens),
|
|
41
|
+
hardTokens: String(evidence.hardTokens),
|
|
42
|
+
...(current.nativeSessionId === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: { nativeSessionId: current.nativeSessionId }),
|
|
45
|
+
...(current.launchId === undefined ? {} : { launchId: current.launchId })
|
|
46
|
+
}, now));
|
|
47
|
+
return Object.freeze({
|
|
48
|
+
taskId: task.id,
|
|
49
|
+
roleName: role.name,
|
|
50
|
+
eventId,
|
|
51
|
+
...(current.nativeSessionId === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { retiredNativeSessionId: current.nativeSessionId }),
|
|
54
|
+
...(current.launchId === undefined ? {} : { retiredLaunchId: current.launchId })
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Retires the current generation to history as `stopped` (not `broken`:
|
|
59
|
+
* a budget rollover is not a Session failure) and clears the Turn fence so
|
|
60
|
+
* the next launch is a fresh generation.
|
|
61
|
+
*/
|
|
62
|
+
function retireStoppedTaskRoleSession(set, timestamp) {
|
|
63
|
+
validateRoleSessionSet(set);
|
|
64
|
+
const current = set.sessions[set.activeAgentId];
|
|
65
|
+
const sessions = { ...set.sessions };
|
|
66
|
+
delete sessions[set.activeAgentId];
|
|
67
|
+
const history = current === undefined
|
|
68
|
+
? set.history
|
|
69
|
+
: [
|
|
70
|
+
...(set.history ?? []),
|
|
71
|
+
{ ...current, status: "stopped", updatedAt: timestamp }
|
|
72
|
+
];
|
|
73
|
+
return validateRoleSessionSet({
|
|
74
|
+
...set,
|
|
75
|
+
sessions,
|
|
76
|
+
...(history === undefined ? {} : { history }),
|
|
77
|
+
inFlight: null,
|
|
78
|
+
providerBinding: null,
|
|
79
|
+
updatedAt: timestamp
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -13,6 +13,7 @@ import { recordExecutionLaneResult } from "../execution/executionGroup.js";
|
|
|
13
13
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
14
14
|
import { isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
15
15
|
import { clearMatchingLeaderStallAttention, latestRunDurableProgressAt, RUN_RECOVERY_APPLIED_EVENT, RUN_RECOVERY_REQUESTED_EVENT } from "../scheduler/roleRunStall.js";
|
|
16
|
+
import { markTaskWakeConsumed } from "../scheduler/taskWake.js";
|
|
16
17
|
import { workItemExecutionGroupById, updateWorkItemExecutionGroup, updateWorkItemStatus } from "../workItem/workItem.js";
|
|
17
18
|
/**
|
|
18
19
|
* Validate every immutable identity and frozen Project head needed before a
|
|
@@ -283,7 +284,9 @@ export function terminalizeExactTaskRun(store, input, now) {
|
|
|
283
284
|
...(input.reviewResult.findings === undefined ? {} : { findings: input.reviewResult.findings }),
|
|
284
285
|
...(input.reviewResult.evidence === undefined ? {} : { evidence: input.reviewResult.evidence }),
|
|
285
286
|
...(input.reviewResult.evidenceCommit === undefined ? {} : { evidenceCommit: input.reviewResult.evidenceCommit }),
|
|
286
|
-
...(input.reviewResult.gitSnapshot === undefined ? {} : { gitSnapshot: input.reviewResult.gitSnapshot })
|
|
287
|
+
...(input.reviewResult.gitSnapshot === undefined ? {} : { gitSnapshot: input.reviewResult.gitSnapshot }),
|
|
288
|
+
...(input.reviewResult.deltaDisposition === undefined ? {} : { deltaDisposition: input.reviewResult.deltaDisposition }),
|
|
289
|
+
...(input.reviewResult.deltaReasoning === undefined ? {} : { deltaReasoning: input.reviewResult.deltaReasoning })
|
|
287
290
|
}
|
|
288
291
|
})
|
|
289
292
|
}, now))
|
|
@@ -310,6 +313,13 @@ export function terminalizeExactTaskRun(store, input, now) {
|
|
|
310
313
|
}
|
|
311
314
|
}
|
|
312
315
|
store.saveAgentRun(terminal);
|
|
316
|
+
if (terminal.roleName === "leader") {
|
|
317
|
+
const wake = store.listTaskWakes(input.taskId)
|
|
318
|
+
.find((candidate) => candidate.runId === terminal.id && candidate.status === "dispatched");
|
|
319
|
+
if (wake !== undefined) {
|
|
320
|
+
store.saveTaskWake(input.taskId, markTaskWakeConsumed(wake, now));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
313
323
|
if (terminal.executionGroupId !== undefined && terminal.executionLaneId !== undefined) {
|
|
314
324
|
store.clearActiveExecutionLaneRun(input.taskId, terminal.executionGroupId, terminal.executionLaneId);
|
|
315
325
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Issue 04 — Provider error classification.
|
|
3
3
|
*
|
|
4
|
-
* Provider failures arrive as opaque free text (Claude
|
|
5
|
-
* `error`/`errorDetails`, Codex turn-completion summaries).
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Provider failures arrive at the driver boundary as opaque free text (Claude
|
|
5
|
+
* StopFailure `error`/`errorDetails`, Codex turn-completion summaries). Each
|
|
6
|
+
* driver parses its own Provider's format into a structured
|
|
7
|
+
* {@link ProviderErrorCode} at the driver boundary. This module maps those
|
|
8
|
+
* codes to provider-neutral error classes by lookup, falling back to text
|
|
9
|
+
* matching only when the driver could not produce a structured code.
|
|
10
|
+
*
|
|
11
|
+
* The retry-in-place coordinator needs a stable, provider-neutral error class
|
|
12
|
+
* before it can decide whether the original Session may be retried.
|
|
9
13
|
*
|
|
10
14
|
* Classes (Issue 04 §2):
|
|
11
15
|
* - `transient-provider` — 500/502/504, connection reset, backend capacity;
|
|
@@ -25,6 +29,7 @@
|
|
|
25
29
|
* terminalize-immediately behavior) while remaining
|
|
26
30
|
* observable in shadow metrics.
|
|
27
31
|
*/
|
|
32
|
+
import { PROVIDER_ERROR_CODE_CLASS } from "../runtime/providerErrorCodes.js";
|
|
28
33
|
/** Classes for which the original Session may be retried in place. */
|
|
29
34
|
export const RETRYABLE_PROVIDER_ERROR_CLASSES = [
|
|
30
35
|
"transient-provider",
|
|
@@ -69,8 +74,11 @@ const INVALID_REQUEST_PATTERNS = [
|
|
|
69
74
|
];
|
|
70
75
|
const TRANSIENT_PROVIDER_PATTERNS = [
|
|
71
76
|
{ pattern: /\b50[024]\b/u, label: "http-5xx" },
|
|
72
|
-
{ pattern: /server[
|
|
77
|
+
{ pattern: /server[\s_-]?error/iu, label: "server-error" },
|
|
73
78
|
{ pattern: /internal server error/iu, label: "internal-server-error" },
|
|
79
|
+
// HTTP/2 RST_STREAM / gRPC status carried by Claude Code and Codex streams
|
|
80
|
+
// (Task-27: "stream error: stream ID …; INTERNAL_ERROR; received from peer").
|
|
81
|
+
{ pattern: /\binternal[\s_-]?error\b/iu, label: "internal-error" },
|
|
74
82
|
{ pattern: /connection lost/iu, label: "connection-lost" },
|
|
75
83
|
{ pattern: /connection reset/iu, label: "connection-reset" },
|
|
76
84
|
{ pattern: /econnreset/iu, label: "econnreset" },
|
|
@@ -91,6 +99,9 @@ const TRANSPORT_UNCERTAIN_PATTERNS = [
|
|
|
91
99
|
{ pattern: /etimedout/iu, label: "etimedout" },
|
|
92
100
|
{ pattern: /response lost/iu, label: "response-lost" },
|
|
93
101
|
{ pattern: /lost response/iu, label: "lost-response" },
|
|
102
|
+
// A stream-level failure means the response may have been cut mid-turn;
|
|
103
|
+
// the retry path consults durable completion facts before any resend.
|
|
104
|
+
{ pattern: /stream error/iu, label: "stream-error" },
|
|
94
105
|
{ pattern: /stream interrupted/iu, label: "stream-interrupted" },
|
|
95
106
|
{ pattern: /interrupted function/iu, label: "interrupted-function" },
|
|
96
107
|
{ pattern: /controller timeout/iu, label: "controller-timeout" },
|
|
@@ -106,21 +117,31 @@ const CLASS_TABLE = [
|
|
|
106
117
|
{ errorClass: "transport-uncertain", patterns: TRANSPORT_UNCERTAIN_PATTERNS }
|
|
107
118
|
];
|
|
108
119
|
/**
|
|
109
|
-
* Classifies one provider failure.
|
|
110
|
-
*
|
|
111
|
-
* text
|
|
120
|
+
* Classifies one provider failure. When the driver produced a structured
|
|
121
|
+
* {@link ProviderErrorCode}, the class is looked up directly. Otherwise the
|
|
122
|
+
* raw text fields are matched against the fallback pattern tables. Every
|
|
123
|
+
* available text field is concatenated so a class can be recognized
|
|
124
|
+
* regardless of which field carried it.
|
|
112
125
|
*/
|
|
113
126
|
export function classifyProviderError(input) {
|
|
127
|
+
// Structured path: the driver already parsed the Provider's error format.
|
|
128
|
+
if (input.errorCode !== undefined) {
|
|
129
|
+
const errorClass = PROVIDER_ERROR_CODE_CLASS[input.errorCode];
|
|
130
|
+
if (errorClass !== undefined) {
|
|
131
|
+
return { errorClass, matched: input.errorCode, basis: "structured" };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Text fallback: for drivers that cannot yet produce a structured code.
|
|
114
135
|
const text = [input.error, input.errorDetails, input.summary]
|
|
115
136
|
.filter((value) => typeof value === "string" && value.length > 0)
|
|
116
137
|
.join("\n");
|
|
117
138
|
if (text.length === 0)
|
|
118
|
-
return { errorClass: "unclassified", matched: "none" };
|
|
139
|
+
return { errorClass: "unclassified", matched: "none", basis: "text" };
|
|
119
140
|
for (const { errorClass, patterns } of CLASS_TABLE) {
|
|
120
141
|
for (const { pattern, label } of patterns) {
|
|
121
142
|
if (pattern.test(text))
|
|
122
|
-
return { errorClass, matched: label };
|
|
143
|
+
return { errorClass, matched: label, basis: "text" };
|
|
123
144
|
}
|
|
124
145
|
}
|
|
125
|
-
return { errorClass: "unclassified", matched: "none" };
|
|
146
|
+
return { errorClass: "unclassified", matched: "none", basis: "text" };
|
|
126
147
|
}
|