@zq-silk/yui 0.7.1 → 0.8.2
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/ARCHITECTURE.md +27 -28
- package/README.md +79 -71
- package/dist/cli/commandCatalog.js +283 -136
- package/dist/cli/completion.js +3 -3
- package/dist/cli/helpRenderer.js +3 -0
- package/dist/cli/interactionPolicy.js +48 -33
- package/dist/cli/interactiveSelection.js +1 -1
- package/dist/cli/invocationRouter.js +3 -2
- package/dist/cli/roleWizard.js +8 -8
- package/dist/cli.js +189 -93
- package/dist/commands/agentCommands.js +5 -5
- package/dist/commands/configCommands.js +351 -104
- package/dist/commands/configOverview.js +60 -0
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/globalRoleCommands.js +9 -9
- package/dist/commands/profileCommands.js +8 -8
- package/dist/commands/resourcesCommands.js +6 -5
- package/dist/commands/taskCommands.js +111 -59
- package/dist/commands/taskRoleRuntimeStatus.js +3 -1
- package/dist/commands/telemetryCommands.js +11 -6
- package/dist/config/configCatalog.js +42 -0
- package/dist/config/yuiConfig.js +80 -35
- package/dist/context/sessionBootstrapManifest.js +1 -1
- package/dist/controller/clientRuntime.js +0 -2
- package/dist/controller/controller.js +21 -9
- package/dist/controller/fileSchedulerStoreAdapter.js +409 -79
- package/dist/controller/resourceInventory.js +9 -5
- package/dist/controller/runtime.js +112 -25
- package/dist/controller/runtimeLaunchCoordinator.js +18 -78
- package/dist/controller/structuredProviderObservation.js +273 -0
- package/dist/doctor/doctor.js +2 -2
- package/dist/executor/agentAdapter.js +40 -0
- package/dist/executor/agentExecutor.js +31 -7
- package/dist/executor/executorRegistry.js +11 -49
- package/dist/executor/fileRoleLaunchPlanner.js +115 -37
- package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
- package/dist/resources/autoResourceGc.js +3 -1
- package/dist/review/reviewConfig.js +0 -2
- package/dist/run/agentRun.js +2 -2
- package/dist/run/providerRetry.js +29 -16
- package/dist/run/providerRetryConfig.js +5 -3
- package/dist/runtime/agentHost.js +767 -158
- package/dist/runtime/builtinAgentDrivers.js +1 -5
- package/dist/runtime/codexAppServerRuntime.js +67 -60
- package/dist/runtime/exactControlPlane.js +7 -2
- package/dist/runtime/index.js +6 -2
- package/dist/runtime/launchBroker.js +30 -8
- package/dist/runtime/launchDiagnostics.js +1 -1
- package/dist/runtime/providerAuthorityFence.js +24 -0
- package/dist/runtime/providerControl.js +63 -0
- package/dist/runtime/providerRecoveryDecision.js +55 -0
- package/dist/runtime/providerRuntimeIdentity.js +269 -19
- package/dist/runtime/runtimeBinding.js +20 -11
- package/dist/runtime/structuredProviderHost.js +476 -0
- package/dist/runtime/tmuxAdapters.js +143 -42
- package/dist/scheduler/activeRoleRunDelivery.js +206 -120
- package/dist/scheduler/leaderWakeupProcessor.js +141 -16
- package/dist/scheduler/roleRunStall.js +12 -9
- package/dist/setup/setupCommand.js +153 -492
- package/dist/storage/compatibleTaskStore.js +9 -5
- package/dist/storage/migration/productionRegistry.js +169 -0
- package/dist/storage/taskStore.js +22 -3
- package/dist/telemetry/sqliteTelemetryStore.js +9 -1
- package/dist/telemetry/telemetryConfig.js +1 -18
- package/dist/telemetry/telemetryStore.js +2 -2
- package/dist/telemetry/telemetryWiring.js +6 -5
- package/dist/tmux/tmuxManager.js +1 -1
- package/dist/web/webSnapshot.js +5 -3
- package/i18n/README.zh-CN.md +48 -40
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +12 -5
- package/skills/yui-operator/SKILL.md +44 -6
- package/skills/yui-runtime/SKILL.md +1 -1
|
@@ -24,6 +24,7 @@ import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment }
|
|
|
24
24
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
25
25
|
import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
26
26
|
import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
|
|
27
|
+
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
27
28
|
/** Builds managed native Agent launches from the authoritative Task records. */
|
|
28
29
|
export class FileRoleLaunchPlanner {
|
|
29
30
|
home;
|
|
@@ -223,7 +224,7 @@ export class FileRoleLaunchPlanner {
|
|
|
223
224
|
if (input.mode === "resume" && !compatibleExisting) {
|
|
224
225
|
throw new Error(`Task Role resume effective snapshot drifted: ${task.id}/${role.name}.`);
|
|
225
226
|
}
|
|
226
|
-
return this.#compile(role, input, { scope: "task", taskId: task.id }, taskRoleSessionTitle(task, role.name), compatibleExisting ? existing.nativeSessionId : undefined, runWorkspace, effective, {
|
|
227
|
+
return this.#compile(role, input, { scope: "task", taskId: task.id }, taskRoleSessionTitle(task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, runWorkspace, effective, {
|
|
227
228
|
purpose: activeRun?.purpose ?? "execution"
|
|
228
229
|
});
|
|
229
230
|
}
|
|
@@ -331,6 +332,9 @@ export class FileRoleLaunchPlanner {
|
|
|
331
332
|
const managedRun = owner.scope === "task" && input.runId !== undefined
|
|
332
333
|
? this.store.getAgentRun(owner.taskId, input.runId)
|
|
333
334
|
: null;
|
|
335
|
+
const managedSessionSet = owner.scope === "task" && input.runId !== undefined
|
|
336
|
+
? this.store.getTaskRoleSessionSet(owner.taskId, role.name)
|
|
337
|
+
: null;
|
|
334
338
|
const driver = builtinAgentDriverRegistry().require(builtinDriverIdForAdapter(configured.adapterId));
|
|
335
339
|
if (owner.scope === "task" && input.runId !== undefined) {
|
|
336
340
|
const admission = managedRuntimeAdmission(driver.capabilities);
|
|
@@ -369,12 +373,33 @@ export class FileRoleLaunchPlanner {
|
|
|
369
373
|
const launchMode = resumeNativeSessionId === undefined
|
|
370
374
|
? "new"
|
|
371
375
|
: "resume";
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
376
|
+
const managedControl = owner.scope === "task" && input.runId !== undefined;
|
|
377
|
+
const managedProviderEnvironment = managedControl
|
|
378
|
+
&& configured.adapterId === "codex"
|
|
379
|
+
? {
|
|
380
|
+
// Managed Codex Runs are non-interactive. Use the Codex execution
|
|
381
|
+
// identity for provider requests while clientInfo still identifies Yui.
|
|
382
|
+
CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "codex_exec"
|
|
383
|
+
}
|
|
384
|
+
: {};
|
|
385
|
+
const preallocatedManagedNativeSessionId = managedControl
|
|
386
|
+
&& binding.adapterId === "claude"
|
|
387
|
+
&& resumeNativeSessionId === undefined
|
|
388
|
+
? requireText(input.launchId === undefined
|
|
389
|
+
? this.#createNativeSessionId()
|
|
390
|
+
: nativeSessionIdForLaunch(this.home, input.launchId, input.agentId, input.adapterId), "Native session id")
|
|
391
|
+
: resumeNativeSessionId;
|
|
392
|
+
const managedCompiled = managedControl
|
|
393
|
+
? adapter.compileManagedControl(compileInput, launchMode, preallocatedManagedNativeSessionId)
|
|
394
|
+
: undefined;
|
|
395
|
+
const compiled = managedCompiled !== undefined
|
|
396
|
+
? managedCompiled
|
|
397
|
+
: launchMode === "resume"
|
|
398
|
+
? adapter.compileResume({
|
|
399
|
+
...compileInput,
|
|
400
|
+
nativeSessionId: resumeNativeSessionId
|
|
401
|
+
})
|
|
402
|
+
: adapter.compileNew(compileInput);
|
|
378
403
|
for (const path of [
|
|
379
404
|
bootstrap.manifestPath,
|
|
380
405
|
bootstrap.sessionCliPath,
|
|
@@ -397,39 +422,32 @@ export class FileRoleLaunchPlanner {
|
|
|
397
422
|
if (owner.scope !== "task" || input.runId === undefined) {
|
|
398
423
|
args = addCodexSessionNotify(args, launchMode, this.#cliPath);
|
|
399
424
|
}
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// submitted only after Codex completes startup; never race terminal bytes
|
|
403
|
-
// and Enter against TUI initialization.
|
|
425
|
+
// Managed Codex runs use App Server lifecycle hooks and structured Turn
|
|
426
|
+
// submission. No Run prompt is placed in argv or written as terminal input.
|
|
404
427
|
if (owner.scope === "task" && input.runId !== undefined) {
|
|
405
428
|
if (managedRun === null || managedRun.status !== "active") {
|
|
406
429
|
throw new Error(`Managed Codex Run is no longer active: ${input.runId}.`);
|
|
407
430
|
}
|
|
408
|
-
args =
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if (managedRun.pushedAt === undefined) {
|
|
412
|
-
args.push("--", managedRunLaunchEnvelope(managedRun, input.mode));
|
|
413
|
-
}
|
|
431
|
+
args = managedControl
|
|
432
|
+
? addCodexManagedLifecycleHooks(args, this.#cliPath)
|
|
433
|
+
: addCodexLifecycleHooks(args, launchMode, this.#cliPath);
|
|
414
434
|
}
|
|
415
435
|
session = launchMode === "resume"
|
|
416
436
|
? readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective)
|
|
417
437
|
: null;
|
|
418
438
|
}
|
|
419
439
|
else if (launchMode === "new") {
|
|
420
|
-
if (
|
|
421
|
-
args.push("-p", "--output-format", "stream-json", "--input-format", "stream-json", "--verbose");
|
|
440
|
+
if (managedControl)
|
|
422
441
|
args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
442
|
+
const nativeSessionId = requireText(preallocatedManagedNativeSessionId, "Native session id");
|
|
443
|
+
if (!managedControl)
|
|
444
|
+
args.push("--session-id", nativeSessionId);
|
|
445
|
+
else if (!args.includes("--session-id"))
|
|
446
|
+
args.push("--session-id", nativeSessionId);
|
|
428
447
|
session = readySession(input.agentId, binding.adapterId, nativeSessionId, effective);
|
|
429
448
|
}
|
|
430
449
|
else {
|
|
431
|
-
if (
|
|
432
|
-
args.push("-p", "--output-format", "stream-json", "--input-format", "stream-json", "--verbose");
|
|
450
|
+
if (managedControl) {
|
|
433
451
|
args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
|
|
434
452
|
}
|
|
435
453
|
session = readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective);
|
|
@@ -469,6 +487,36 @@ export class FileRoleLaunchPlanner {
|
|
|
469
487
|
if (owner.scope === "task" && (input.mode === "new" || input.mode === "resume")) {
|
|
470
488
|
jobCallerKey = randomBytes(32).toString("hex");
|
|
471
489
|
}
|
|
490
|
+
const carriesInitialTurn = managedControl
|
|
491
|
+
&& managedRun?.pushedAt === undefined
|
|
492
|
+
&& (managedSessionSet?.providerBinding === null
|
|
493
|
+
|| managedSessionSet?.providerBinding === undefined);
|
|
494
|
+
const providerAuthority = managedControl
|
|
495
|
+
? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId)
|
|
496
|
+
: undefined;
|
|
497
|
+
const providerNativeSessionId = binding.adapterId === "claude"
|
|
498
|
+
? preallocatedManagedNativeSessionId
|
|
499
|
+
: resumeNativeSessionId;
|
|
500
|
+
const providerControl = managedControl
|
|
501
|
+
? {
|
|
502
|
+
schemaVersion: 1,
|
|
503
|
+
adapterId: binding.adapterId,
|
|
504
|
+
transport: managedCompiled.transport,
|
|
505
|
+
mode: resumeNativeSessionId === undefined ? "new" : "resume",
|
|
506
|
+
...(providerNativeSessionId === undefined
|
|
507
|
+
? {}
|
|
508
|
+
: { nativeSessionId: providerNativeSessionId }),
|
|
509
|
+
authority: providerAuthority,
|
|
510
|
+
...(carriesInitialTurn
|
|
511
|
+
? {
|
|
512
|
+
initialTurn: {
|
|
513
|
+
attemptId: formatAgentRunReceiptId(owner.taskId, input.runId),
|
|
514
|
+
boundedText: managedRunLaunchEnvelope(managedRun, input.mode)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
: {})
|
|
518
|
+
}
|
|
519
|
+
: undefined;
|
|
472
520
|
const launch = {
|
|
473
521
|
command,
|
|
474
522
|
args,
|
|
@@ -476,16 +524,10 @@ export class FileRoleLaunchPlanner {
|
|
|
476
524
|
&& managedRun.providerRetry.state !== "dispatching"
|
|
477
525
|
? { deferProviderStart: true }
|
|
478
526
|
: {}),
|
|
479
|
-
...(
|
|
480
|
-
? {
|
|
481
|
-
providerInput: {
|
|
482
|
-
kind: "stdin-json-user-message",
|
|
483
|
-
boundedText: managedRunLaunchEnvelope(managedRun, input.mode)
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
: {}),
|
|
527
|
+
...(providerControl === undefined ? {} : { providerControl }),
|
|
487
528
|
env: {
|
|
488
529
|
...launchEnvironment,
|
|
530
|
+
...managedProviderEnvironment,
|
|
489
531
|
YUI_HOME: resolve(this.home),
|
|
490
532
|
YUI_SESSION_SCOPE: owner.scope,
|
|
491
533
|
...(owner.scope === "task" ? { YUI_TASK_ID: owner.taskId } : {}),
|
|
@@ -536,12 +578,36 @@ export class FileRoleLaunchPlanner {
|
|
|
536
578
|
},
|
|
537
579
|
launch: scopedLaunch,
|
|
538
580
|
session,
|
|
539
|
-
...(
|
|
540
|
-
|
|
541
|
-
? { initialPromptRunId: input.runId }
|
|
581
|
+
...(carriesInitialTurn && input.runId !== undefined
|
|
582
|
+
? { initialTurnRunId: input.runId }
|
|
542
583
|
: {})
|
|
543
584
|
};
|
|
544
585
|
}
|
|
586
|
+
#providerAuthorityForLaunch(taskId, roleName, launchId) {
|
|
587
|
+
const activationId = requireText(launchId, "Managed Provider Activation id");
|
|
588
|
+
const binding = this.store.getTaskRoleSessionSet(taskId, roleName)?.providerBinding;
|
|
589
|
+
if (binding === null || binding === undefined) {
|
|
590
|
+
return { epoch: 1, owner: "controller", holderId: activationId };
|
|
591
|
+
}
|
|
592
|
+
if (binding.authority.owner === "controller") {
|
|
593
|
+
return {
|
|
594
|
+
epoch: binding.authority.epoch,
|
|
595
|
+
owner: "controller",
|
|
596
|
+
holderId: binding.authority.holderId
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
if (binding.authority.owner === "human") {
|
|
600
|
+
throw new Error(`Provider writer is held by a human: ${taskId}/${roleName}.`);
|
|
601
|
+
}
|
|
602
|
+
if (binding.authority.owner === "none") {
|
|
603
|
+
return {
|
|
604
|
+
epoch: binding.authority.epoch + 1,
|
|
605
|
+
owner: "controller",
|
|
606
|
+
holderId: activationId
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
throw new Error(`Provider writer authority is unknown: ${taskId}/${roleName}.`);
|
|
610
|
+
}
|
|
545
611
|
#applyWorkspaceScope(taskId, role, launch, workspaceOverride) {
|
|
546
612
|
const workspace = workspaceOverride
|
|
547
613
|
?? (role.name === "leader"
|
|
@@ -786,6 +852,18 @@ function addCodexLifecycleHooks(args, mode, cliPath) {
|
|
|
786
852
|
}
|
|
787
853
|
return [...args.slice(0, -2), ...managed, ...args.slice(-2)];
|
|
788
854
|
}
|
|
855
|
+
function addCodexManagedLifecycleHooks(args, cliPath) {
|
|
856
|
+
if (args.length < 2 || args.at(-2) !== "app-server" || args.at(-1) !== "--stdio") {
|
|
857
|
+
throw new Error("Managed Codex App Server launch shape is invalid.");
|
|
858
|
+
}
|
|
859
|
+
return [
|
|
860
|
+
...args.slice(0, -2),
|
|
861
|
+
"--enable", "hooks",
|
|
862
|
+
"--config", codexLifecycleHooksConfig(cliPath),
|
|
863
|
+
"--dangerously-bypass-hook-trust",
|
|
864
|
+
...args.slice(-2)
|
|
865
|
+
];
|
|
866
|
+
}
|
|
789
867
|
function readySession(agentId, adapterId, nativeSessionId, effective) {
|
|
790
868
|
return {
|
|
791
869
|
agentId,
|
|
@@ -185,9 +185,12 @@ export function foldCanonicalLifecycleEvent(event, expectation) {
|
|
|
185
185
|
// Only an identity-matched durable native event can move accepted/delivered,
|
|
186
186
|
// and only after the independently committed transport receipt. Provider
|
|
187
187
|
// acceptance and transport acknowledgement are deliberately separate
|
|
188
|
-
// evidence layers: neither may repair or infer the other.
|
|
188
|
+
// evidence layers: neither may repair or infer the other. A fresh managed
|
|
189
|
+
// Host can publish native acceptance immediately before its launch call
|
|
190
|
+
// returns and lets the scheduler persist the transport receipt. Retain
|
|
191
|
+
// that exact fenced fact for replay instead of misclassifying it as stale.
|
|
189
192
|
if (!expectation.pushed)
|
|
190
|
-
return { outcome: "
|
|
193
|
+
return { outcome: "deferred", reason: "accept-before-push" };
|
|
191
194
|
if (expectation.terminal)
|
|
192
195
|
return { outcome: "obsolete", reason: "accept-after-terminal" };
|
|
193
196
|
if (expectation.accepted)
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* sources) applies identically.
|
|
12
12
|
*/
|
|
13
13
|
import { applyResourceGc, planResourceGc } from "./resourceGc.js";
|
|
14
|
-
import { resolveResourcesGcAutoQuarantine, resolveResourcesGcMode } from "../config/yuiConfig.js";
|
|
14
|
+
import { resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours } from "../config/yuiConfig.js";
|
|
15
15
|
/**
|
|
16
16
|
* Create the Controller's automatic Resource GC hook. The hook self-skips
|
|
17
17
|
* unless `resourcesGcMode=quarantine` and `resourcesGcAutoQuarantine=true`.
|
|
@@ -35,6 +35,7 @@ export function createResourceAutoGc(options) {
|
|
|
35
35
|
taskStatusById,
|
|
36
36
|
mode: "quarantine",
|
|
37
37
|
now,
|
|
38
|
+
quarantineTtlHours: resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours),
|
|
38
39
|
environment,
|
|
39
40
|
activeWorkspaceOwnerPaths: collectActiveWorkspaceOwnerPaths(store)
|
|
40
41
|
};
|
|
@@ -75,6 +76,7 @@ export async function runAutoResourceGc(store, options = {}) {
|
|
|
75
76
|
taskStatusById,
|
|
76
77
|
mode: "quarantine",
|
|
77
78
|
now,
|
|
79
|
+
quarantineTtlHours: resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours),
|
|
78
80
|
activeWorkspaceOwnerPaths: collectActiveWorkspaceOwnerPaths(store)
|
|
79
81
|
};
|
|
80
82
|
const plan = await planResourceGc(input);
|
|
@@ -11,8 +11,6 @@ export const REVIEW_DELTA_RECHECK_MODES = ["enabled", "disabled"];
|
|
|
11
11
|
/** Issue 07: conservative defaults for whether a delta attempt is allowed. */
|
|
12
12
|
export const DEFAULT_DELTA_RECHECK_MAX_CHANGED_LINES = 200;
|
|
13
13
|
export const DEFAULT_DELTA_RECHECK_MAX_CHANGED_FILES = 5;
|
|
14
|
-
/** The Reviewer Role seeded in a new Home by `yui setup`. */
|
|
15
|
-
export const DEFAULT_REVIEWER_ROLE = "reviewer";
|
|
16
14
|
export function validateReviewConfig(config) {
|
|
17
15
|
requireIdentity(config.roleName, "Review Role");
|
|
18
16
|
if (!REVIEW_TRIGGERS.includes(config.trigger)) {
|
package/dist/run/agentRun.js
CHANGED
|
@@ -329,7 +329,7 @@ export function withProviderRetry(run, retry) {
|
|
|
329
329
|
* remain historical facts; the new delivery receipt and `dispatching` retry
|
|
330
330
|
* state make the delivery path send only the short continuation envelope.
|
|
331
331
|
*/
|
|
332
|
-
export function reopenRunForProviderRetry(run, receiptId, now) {
|
|
332
|
+
export function reopenRunForProviderRetry(run, receiptId, now, mode = "resume") {
|
|
333
333
|
if (run.status !== "active" || run.providerRetry === undefined) {
|
|
334
334
|
throw new Error(`Agent run is not waiting for a provider retry: ${run.id}.`);
|
|
335
335
|
}
|
|
@@ -337,7 +337,7 @@ export function reopenRunForProviderRetry(run, receiptId, now) {
|
|
|
337
337
|
const { providerRetry, ...rest } = run;
|
|
338
338
|
return validateAgentRun({
|
|
339
339
|
...rest,
|
|
340
|
-
mode
|
|
340
|
+
mode,
|
|
341
341
|
deliveryReceiptId: receiptId,
|
|
342
342
|
updatedAt: timestamp,
|
|
343
343
|
providerRetry: prepareProviderRetryDispatch(providerRetry, receiptId, now)
|
|
@@ -1,24 +1,22 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS, DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS, MAX_PROVIDER_RETRY_ATTEMPTS } from "../config/yuiConfig.js";
|
|
2
3
|
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
3
|
-
export const PROVIDER_RETRY_DELAYS_MS = Object.freeze(
|
|
4
|
-
export const
|
|
5
|
-
export
|
|
6
|
-
/** Durable configuration keeps the historical name for compatibility. */
|
|
7
|
-
export const PROVIDER_RETRY_MAX_WINDOW_MS = PROVIDER_RETRY_EPISODE_WINDOW_MS;
|
|
8
|
-
export function nextProviderRetryDelayMs(retryIndex) {
|
|
4
|
+
export const PROVIDER_RETRY_DELAYS_MS = Object.freeze(DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS.map((seconds) => seconds * 1_000));
|
|
5
|
+
export const PROVIDER_RETRY_EPISODE_WINDOW_MS = DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS * 1_000;
|
|
6
|
+
export function nextProviderRetryDelayMs(retryIndex, delaysMs = PROVIDER_RETRY_DELAYS_MS) {
|
|
9
7
|
if (!Number.isSafeInteger(retryIndex)
|
|
10
8
|
|| retryIndex < 1
|
|
11
|
-
|| retryIndex >
|
|
9
|
+
|| retryIndex > delaysMs.length) {
|
|
12
10
|
throw new Error(`Provider retry index is out of range: ${String(retryIndex)}.`);
|
|
13
11
|
}
|
|
14
|
-
return
|
|
12
|
+
return delaysMs[retryIndex - 1];
|
|
15
13
|
}
|
|
16
14
|
/**
|
|
17
15
|
* True when the retry lineage has used its total wall-clock budget. The
|
|
18
16
|
* budget is measured from the first classified failure, so repeated failures
|
|
19
17
|
* never extend it.
|
|
20
18
|
*/
|
|
21
|
-
export function providerRetryBudgetExhausted(value, now, maxWindowMs =
|
|
19
|
+
export function providerRetryBudgetExhausted(value, now, maxWindowMs = PROVIDER_RETRY_EPISODE_WINDOW_MS) {
|
|
22
20
|
if (!Number.isSafeInteger(maxWindowMs) || maxWindowMs <= 0) {
|
|
23
21
|
throw new Error(`Provider retry max window must be a positive integer: ${String(maxWindowMs)}.`);
|
|
24
22
|
}
|
|
@@ -43,7 +41,9 @@ export function validateAgentRunProviderRetry(value) {
|
|
|
43
41
|
|| value.dispatchedRetries > value.maxRetries) {
|
|
44
42
|
throw new Error("Agent run providerRetry dispatchedRetries is invalid.");
|
|
45
43
|
}
|
|
46
|
-
if (value.maxRetries
|
|
44
|
+
if (!Number.isSafeInteger(value.maxRetries)
|
|
45
|
+
|| value.maxRetries < 1
|
|
46
|
+
|| value.maxRetries > MAX_PROVIDER_RETRY_ATTEMPTS) {
|
|
47
47
|
throw new Error("Agent run providerRetry maxRetries is invalid.");
|
|
48
48
|
}
|
|
49
49
|
requireTimestamp(value.firstFailureAt, "Agent run providerRetry firstFailureAt");
|
|
@@ -83,18 +83,22 @@ export function validateAgentRunProviderRetry(value) {
|
|
|
83
83
|
return value;
|
|
84
84
|
}
|
|
85
85
|
/** Advance one failure episode without ever changing the native Session. */
|
|
86
|
-
export function scheduleProviderRetry(previous, input, now
|
|
86
|
+
export function scheduleProviderRetry(previous, input, now, policy = {
|
|
87
|
+
delaysMs: PROVIDER_RETRY_DELAYS_MS,
|
|
88
|
+
maxWindowMs: PROVIDER_RETRY_EPISODE_WINDOW_MS
|
|
89
|
+
}) {
|
|
90
|
+
validateRetrySchedulePolicy(policy);
|
|
87
91
|
const at = now.toISOString();
|
|
88
92
|
const firstFailureAt = previous?.firstFailureAt ?? at;
|
|
89
93
|
const episodeDeadlineAt = previous?.episodeDeadlineAt
|
|
90
|
-
?? new Date(now.getTime() +
|
|
94
|
+
?? new Date(now.getTime() + policy.maxWindowMs).toISOString();
|
|
91
95
|
if (now.getTime() >= Date.parse(episodeDeadlineAt)) {
|
|
92
96
|
return Object.freeze({ outcome: "exhausted", reason: "window" });
|
|
93
97
|
}
|
|
94
98
|
const consecutiveFailures = (previous?.consecutiveFailures ?? 0) + 1;
|
|
95
99
|
const dispatchedRetries = previous?.dispatchedRetries ?? 0;
|
|
96
100
|
const schedule = input.scheduleNextAttempt ?? true;
|
|
97
|
-
if (schedule && dispatchedRetries >=
|
|
101
|
+
if (schedule && dispatchedRetries >= policy.delaysMs.length) {
|
|
98
102
|
return Object.freeze({ outcome: "exhausted", reason: "attempts" });
|
|
99
103
|
}
|
|
100
104
|
const state = schedule ? "scheduled" : "blocked";
|
|
@@ -103,7 +107,7 @@ export function scheduleProviderRetry(previous, input, now) {
|
|
|
103
107
|
throw new Error("Provider retry Retry-After must be a positive safe integer.");
|
|
104
108
|
}
|
|
105
109
|
const delayMs = schedule
|
|
106
|
-
? Math.max(nextProviderRetryDelayMs(dispatchedRetries + 1), retryAfterMs ?? 0)
|
|
110
|
+
? Math.max(nextProviderRetryDelayMs(dispatchedRetries + 1, policy.delaysMs), retryAfterMs ?? 0)
|
|
107
111
|
: undefined;
|
|
108
112
|
const nextAttemptAt = delayMs === undefined
|
|
109
113
|
? undefined
|
|
@@ -125,7 +129,7 @@ export function scheduleProviderRetry(previous, input, now) {
|
|
|
125
129
|
errorClass: input.errorClass,
|
|
126
130
|
consecutiveFailures,
|
|
127
131
|
dispatchedRetries,
|
|
128
|
-
maxRetries:
|
|
132
|
+
maxRetries: policy.delaysMs.length,
|
|
129
133
|
firstFailureAt,
|
|
130
134
|
lastFailureAt: at,
|
|
131
135
|
episodeDeadlineAt,
|
|
@@ -139,6 +143,15 @@ export function scheduleProviderRetry(previous, input, now) {
|
|
|
139
143
|
});
|
|
140
144
|
return Object.freeze({ outcome: schedule ? "scheduled" : "blocked", retry });
|
|
141
145
|
}
|
|
146
|
+
function validateRetrySchedulePolicy(policy) {
|
|
147
|
+
if (!Number.isSafeInteger(policy.maxWindowMs) || policy.maxWindowMs < 1) {
|
|
148
|
+
throw new Error("Provider retry max window must be a positive safe integer.");
|
|
149
|
+
}
|
|
150
|
+
if (policy.delaysMs.length < 1 || policy.delaysMs.length > MAX_PROVIDER_RETRY_ATTEMPTS
|
|
151
|
+
|| policy.delaysMs.some((delay) => !Number.isSafeInteger(delay) || delay < 1)) {
|
|
152
|
+
throw new Error(`Provider retry delay schedule must contain 1-${MAX_PROVIDER_RETRY_ATTEMPTS} positive safe integers.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
142
155
|
/** Mark that one short continuation request was dispatched and now awaits any correlated progress. */
|
|
143
156
|
export function prepareProviderRetryDispatch(value, receiptId, now) {
|
|
144
157
|
if (value.state !== "scheduled" || value.nextAttemptAt === undefined)
|
|
@@ -203,7 +216,7 @@ export function serializeProviderRetryEnvelope(input) {
|
|
|
203
216
|
return [
|
|
204
217
|
"Yui managed in-Session continuation retry.",
|
|
205
218
|
`task=${requireIdentity(input.taskId, "Provider retry task id")} run=${requireIdentity(input.runId, "Provider retry run id")} role=${requireIdentity(input.roleName, "Provider retry role")}`,
|
|
206
|
-
`episode=${input.retry.episodeId} retry=${retryOrdinal}/${
|
|
219
|
+
`episode=${input.retry.episodeId} retry=${retryOrdinal}/${input.retry.maxRetries} receipt=${input.retry.lastRetryReceiptId ?? "pending"}`,
|
|
207
220
|
`failureEvent=${input.retry.failureEventId}`,
|
|
208
221
|
...(input.retry.failedNativeTurnId === undefined
|
|
209
222
|
? []
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveProviderRetryAdapters,
|
|
1
|
+
import { resolveProviderRetryAdapters, resolveProviderRetryDelaysSeconds, resolveProviderRetryMaxWindowSeconds, resolveProviderRetryMode } from "../config/yuiConfig.js";
|
|
2
2
|
/**
|
|
3
3
|
* Resolves the retry flags from the durable Yui config. Homes without the
|
|
4
4
|
* fields get the safe defaults: enforce mode, all supported adapters, receipt
|
|
@@ -10,8 +10,10 @@ export function providerRetryConfig(config) {
|
|
|
10
10
|
return {
|
|
11
11
|
mode: adapters.length === 0 ? "off" : mode,
|
|
12
12
|
adapters,
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
delaysMs: resolveProviderRetryDelaysSeconds(config.providerRetryDelaysSeconds)
|
|
14
|
+
.map((seconds) => seconds * 1_000),
|
|
15
|
+
maxWindowMs: resolveProviderRetryMaxWindowSeconds(config.providerRetryMaxWindowSeconds)
|
|
16
|
+
* 1_000
|
|
15
17
|
};
|
|
16
18
|
}
|
|
17
19
|
/** Whether the adapter has in-place retry enabled in the given mode. */
|