@zq-silk/yui 2.21.0 → 2.22.0
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 +5 -0
- package/dist/cli/commandCatalog.js +85 -25
- package/dist/cli/dynamicCompletion.js +4 -1
- package/dist/cli/helpRenderer.js +12 -1
- package/dist/cli/interactionPolicy.js +2 -0
- package/dist/cli/invocationRouter.js +5 -5
- package/dist/cli/roleWizard.js +5 -1
- package/dist/commands/globalRoleCommands.js +33 -8
- package/dist/commands/inboxCommands.js +39 -12
- package/dist/commands/operatorCommands.js +2 -5
- package/dist/commands/releaseCommands.js +16 -2
- package/dist/commands/roleConfiguration.js +25 -2
- package/dist/commands/sessionCommands.js +5 -2
- package/dist/commands/taskCommands.js +217 -64
- package/dist/commands/taskContextCommand.js +26 -4
- package/dist/commands/taskFactCommands.js +15 -2
- package/dist/commands/taskIntegrationCommands.js +3 -3
- package/dist/commands/taskRoleRuntimeStatus.js +15 -5
- package/dist/commands/textInput.js +2 -2
- package/dist/context/contextFamilies.js +10 -0
- package/dist/context/currentSkills.js +6 -4
- package/dist/context/runContextPack.js +44 -5
- package/dist/context/runInputContract.js +1 -1
- package/dist/context/sessionBootstrapManifest.js +7 -5
- package/dist/context/sessionSkillNotice.js +53 -0
- package/dist/context/taskContext.js +100 -16
- package/dist/controlPlaneCli.js +17 -7
- package/dist/controller/agentRuntimeObserver.js +57 -12
- package/dist/controller/clientRuntime.js +37 -8
- package/dist/controller/controller.js +3 -1
- package/dist/controller/diagnosticJournal.js +10 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +72 -24
- package/dist/controller/messageHealth.js +4 -3
- package/dist/controller/providerRetryAdmission.js +15 -1
- package/dist/controller/providerRetryDelivery.js +18 -2
- package/dist/controller/runtime.js +15 -5
- package/dist/controller/runtimeEventInbox.js +107 -13
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/controller/taskRunRecovery.js +153 -0
- package/dist/core/controllerClient.js +16 -2
- package/dist/core/controllerServer.js +14 -5
- package/dist/core/controllerTelemetry.js +51 -5
- package/dist/errors/cliFailure.js +14 -5
- package/dist/executor/agentAdapter.js +32 -4
- package/dist/executor/agentConfigurationFields.js +2 -0
- package/dist/integration/gitIntegrationService.js +3 -2
- package/dist/integration/integrationSourceApplication.js +12 -1
- package/dist/interaction/notificationPresentation.js +11 -16
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/message/messageContinuation.js +3 -1
- package/dist/output/boundedRead.js +36 -7
- package/dist/output/rolePresentation.js +3 -1
- package/dist/runtime/agentHost.js +12 -3
- package/dist/runtime/codexAppServerRuntime.js +110 -24
- package/dist/runtime/codexInteractiveHost.js +46 -0
- package/dist/runtime/providerRetry.js +5 -2
- package/dist/runtime/providerRuntimeIdentity.js +1 -1
- package/dist/runtime/structuredProviderHost.js +32 -34
- package/dist/scheduler/wakeupQueue.js +10 -4
- package/dist/storage/sqliteSchema.js +9 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/tmux/tmuxManager.js +4 -2
- package/dist/web/webSettings.js +2 -0
- package/docs/authorization.md +18 -3
- package/docs/cli-information-contract.md +228 -6
- package/docs/cli-information-contract.zh-CN.md +95 -5
- package/docs/managed-turn-and-session-runtime.md +82 -21
- package/docs/managed-turn-and-session-runtime.zh-CN.md +56 -13
- package/docs/provider-retry.md +11 -0
- package/docs/release-workflow.md +37 -0
- package/docs/release-workflow.zh-CN.md +28 -0
- package/docs/roles-and-configuration.md +28 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/storage-baseline.md +4 -1
- package/docs/storage-baseline.zh-CN.md +1 -1
- package/docs/task-delivery.md +7 -0
- package/docs/task-delivery.zh-CN.md +5 -0
- package/docs/testing/review-evaluation.md +7 -7
- package/docs/testing/review-evaluation.zh-CN.md +7 -7
- package/docs/testing/verification-levels.md +45 -2
- package/docs/testing/verification-levels.zh-CN.md +21 -2
- package/i18n/README.zh-CN.md +6 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +98 -193
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/artifacts.md +41 -0
- package/skills/yui-leader/references/assignment-execution.md +140 -0
- package/skills/yui-leader/references/completion.md +79 -0
- package/skills/yui-leader/references/delivery-scope.md +26 -0
- package/skills/yui-leader/references/execution.md +16 -487
- package/skills/yui-leader/references/implementation.md +81 -0
- package/skills/yui-leader/references/planning.md +7 -45
- package/skills/yui-leader/references/review.md +90 -0
- package/skills/yui-leader/references/task-progress.md +23 -0
- package/skills/yui-leader/references/work-allocation.md +118 -0
- package/skills/yui-operator/SKILL.md +77 -333
- package/skills/yui-operator/VERSION +1 -1
- package/skills/yui-operator/references/body-transport.md +62 -0
- package/skills/yui-operator/references/configuration.md +47 -0
- package/skills/yui-operator/references/lifecycle.md +41 -0
- package/skills/yui-operator/references/progress.md +39 -0
- package/skills/yui-operator/references/routing.md +57 -0
- package/skills/yui-operator/references/submission.md +56 -0
- package/skills/yui-operator/references/task-delivery.md +1 -1
- package/skills/yui-operator/references/web-lifecycle.md +19 -0
- package/skills/yui-operator/scripts/verify-submission.mjs +37 -0
- package/skills/yui-runtime/SKILL.md +177 -531
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/archive.md +51 -0
- package/skills/yui-runtime/references/command-discovery.md +41 -0
- package/skills/yui-runtime/references/completed-followup.md +65 -0
- package/skills/yui-runtime/references/context-ordering.md +3 -34
- package/skills/yui-runtime/references/context-reads.md +61 -0
- package/skills/yui-runtime/references/dispatched-run.md +61 -0
- package/skills/yui-runtime/references/execution-failure.md +105 -0
- package/skills/yui-runtime/references/global-context.md +18 -0
- package/skills/yui-runtime/references/global-input-controls.md +44 -0
- package/skills/yui-runtime/references/host-custody.md +30 -0
- package/skills/yui-runtime/references/inbox-recovery.md +77 -0
- package/skills/yui-runtime/references/incremental-context.md +40 -0
- package/skills/yui-runtime/references/publication.md +4 -111
- package/skills/yui-runtime/references/recovery.md +32 -192
- package/skills/yui-runtime/references/session-identity.md +18 -0
- package/skills/yui-runtime/references/session-replacement.md +45 -0
- package/skills/yui-runtime/references/uncertain-execution.md +29 -0
|
@@ -87,7 +87,7 @@ class CodexAdapter extends BaseAdapter {
|
|
|
87
87
|
throw new Error("Codex permission strategy is required.");
|
|
88
88
|
}
|
|
89
89
|
if (config.permission.strategy === "configured") {
|
|
90
|
-
exact(config.permission, ["strategy", "sandbox", "approval"], "Codex permission config");
|
|
90
|
+
exact(config.permission, ["strategy", "sandbox", "approval", "excludeTmpdirEnvVar", "excludeSlashTmp"], "Codex permission config");
|
|
91
91
|
if (config.permission.sandbox !== undefined
|
|
92
92
|
&& !SANDBOXES.includes(config.permission.sandbox)) {
|
|
93
93
|
throw new Error("Codex sandbox is invalid.");
|
|
@@ -96,6 +96,16 @@ class CodexAdapter extends BaseAdapter {
|
|
|
96
96
|
&& !APPROVALS.includes(config.permission.approval)) {
|
|
97
97
|
throw new Error("Codex approval is invalid.");
|
|
98
98
|
}
|
|
99
|
+
for (const key of ["excludeTmpdirEnvVar", "excludeSlashTmp"]) {
|
|
100
|
+
const value = config.permission[key];
|
|
101
|
+
if (value === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
if (typeof value !== "boolean")
|
|
104
|
+
throw new Error(`Codex ${key} must be boolean.`);
|
|
105
|
+
if (config.permission.sandbox !== "workspace-write") {
|
|
106
|
+
throw new Error(`Codex ${key} requires an explicit workspace-write sandbox.`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
99
109
|
requireConfiguredPermissionOption(config.permission, "Codex");
|
|
100
110
|
}
|
|
101
111
|
else {
|
|
@@ -118,7 +128,8 @@ class CodexAdapter extends BaseAdapter {
|
|
|
118
128
|
...(config.permission.sandbox === undefined
|
|
119
129
|
? [] : ["--sandbox", config.permission.sandbox]),
|
|
120
130
|
...(config.permission.approval === undefined
|
|
121
|
-
? [] : ["--ask-for-approval", config.permission.approval])
|
|
131
|
+
? [] : ["--ask-for-approval", config.permission.approval]),
|
|
132
|
+
...Object.entries(codexWorkspaceWriteOptions(config)).flatMap(([key, value]) => ["--config", `sandbox_workspace_write.${key}=${value}`])
|
|
122
133
|
]
|
|
123
134
|
: []),
|
|
124
135
|
...(config.search === true ? ["--search"] : []),
|
|
@@ -158,7 +169,12 @@ class CodexAdapter extends BaseAdapter {
|
|
|
158
169
|
// A managed Codex thread is an ordinary user thread. Its Yui guidance is
|
|
159
170
|
// part of the durable Task message, not a developer_instructions override
|
|
160
171
|
// that would shadow the user's native project/developer configuration.
|
|
161
|
-
|
|
172
|
+
let argv = withoutCodexConfigOverride(launch.argv, "developer_instructions");
|
|
173
|
+
// Managed exclusions belong to thread/start and thread/resume configuration,
|
|
174
|
+
// never the shared daemon/proxy startup defaults.
|
|
175
|
+
for (const key of Object.keys(codexWorkspaceWriteOptions(config))) {
|
|
176
|
+
argv = withoutCodexConfigOverride(argv, `sandbox_workspace_write.${key}`);
|
|
177
|
+
}
|
|
162
178
|
if (config.model !== undefined) {
|
|
163
179
|
const modelFlag = argv.findIndex((value, index) => (value === "--model" && argv[index + 1] === config.model));
|
|
164
180
|
if (modelFlag < 0)
|
|
@@ -475,7 +491,10 @@ function codexThreadOptions(input, config) {
|
|
|
475
491
|
// Keep the trust override on this thread instead of mutating global config.
|
|
476
492
|
projects: { [resolve(input.workspace)]: { trust_level: "trusted" } },
|
|
477
493
|
...(config.effort === undefined ? {} : { model_reasoning_effort: config.effort }),
|
|
478
|
-
...(config.search === true ? { web_search: "live" } : {})
|
|
494
|
+
...(config.search === true ? { web_search: "live" } : {}),
|
|
495
|
+
...(Object.keys(codexWorkspaceWriteOptions(config)).length === 0 ? {} : {
|
|
496
|
+
sandbox_workspace_write: codexWorkspaceWriteOptions(config)
|
|
497
|
+
})
|
|
479
498
|
};
|
|
480
499
|
const permission = config.permission.strategy === "bypass"
|
|
481
500
|
? { approvalPolicy: "never", sandbox: "danger-full-access" }
|
|
@@ -496,6 +515,15 @@ function codexThreadOptions(input, config) {
|
|
|
496
515
|
...(Object.keys(threadConfig).length === 0 ? {} : { config: threadConfig })
|
|
497
516
|
};
|
|
498
517
|
}
|
|
518
|
+
function codexWorkspaceWriteOptions(config) {
|
|
519
|
+
if (config.permission.strategy !== "configured")
|
|
520
|
+
return {};
|
|
521
|
+
const { excludeTmpdirEnvVar, excludeSlashTmp } = config.permission;
|
|
522
|
+
return {
|
|
523
|
+
...(excludeTmpdirEnvVar === undefined ? {} : { exclude_tmpdir_env_var: excludeTmpdirEnvVar }),
|
|
524
|
+
...(excludeSlashTmp === undefined ? {} : { exclude_slash_tmp: excludeSlashTmp })
|
|
525
|
+
};
|
|
526
|
+
}
|
|
499
527
|
function codexSessionInstructions(input) {
|
|
500
528
|
if (input.sessionManifestPath !== undefined) {
|
|
501
529
|
return sessionContextInstructions(input.sessionManifestPath, input.sessionManifestDigest);
|
|
@@ -15,6 +15,8 @@ export function staticAgentConfigurationFields(id) {
|
|
|
15
15
|
...fields,
|
|
16
16
|
staticField("permission.sandbox", CODEX_SANDBOXES, false),
|
|
17
17
|
staticField("permission.approval", CODEX_APPROVALS, false, "Native versions may retire values accepted by the adapter."),
|
|
18
|
+
staticField("permission.excludeTmpdirEnvVar", ["true", "false"], false, "Requires explicit workspace-write; omission follows the native default."),
|
|
19
|
+
staticField("permission.excludeSlashTmp", ["true", "false"], false, "Requires explicit workspace-write; omission follows the native default."),
|
|
18
20
|
staticField("search", ["true"], false),
|
|
19
21
|
staticField("profile", [], true),
|
|
20
22
|
staticField("additionalDirectories", [], true)
|
|
@@ -79,7 +79,7 @@ export class GitIntegrationService {
|
|
|
79
79
|
#targetCheckout(attempt) {
|
|
80
80
|
return taskIntegrationTargetCheckout(this.store, this.environment, attempt.taskId, attempt.projectId, attempt.targetRef);
|
|
81
81
|
}
|
|
82
|
-
async integrate(taskId, integrationId, signal) {
|
|
82
|
+
async integrate(taskId, integrationId, signal, intent) {
|
|
83
83
|
let initial = requireIntegration(this.store, taskId, integrationId);
|
|
84
84
|
this.#targetCheckout(initial);
|
|
85
85
|
if (!["running", "conflicted", "blocked", "validating"].includes(initial.status)) {
|
|
@@ -185,7 +185,8 @@ export class GitIntegrationService {
|
|
|
185
185
|
try {
|
|
186
186
|
current = await applyIntegrationSource({
|
|
187
187
|
attempt: current, workspace, remoteUrl: project.remoteUrl,
|
|
188
|
-
git: this.git, store: this.store, now: this.now
|
|
188
|
+
git: this.git, store: this.store, now: this.now,
|
|
189
|
+
continueOwnedOperation: intent === "continue"
|
|
189
190
|
});
|
|
190
191
|
if (current.status === "conflicted" || current.status === "blocked") {
|
|
191
192
|
return { status: current.status, attempt: current, workspace };
|
|
@@ -135,7 +135,18 @@ export async function applyIntegrationSource(input) {
|
|
|
135
135
|
return pause(unresolved, kind);
|
|
136
136
|
}
|
|
137
137
|
if (!fresh && current.status !== "conflicted") {
|
|
138
|
-
|
|
138
|
+
if (input.continueOwnedOperation !== true) {
|
|
139
|
+
throw new Error("Git operation is present but no paused conflict is recorded; inspect its execution before continuing.");
|
|
140
|
+
}
|
|
141
|
+
// Git can have stopped before the transaction recording its conflict
|
|
142
|
+
// failed. Existing progress and assertOperation above bind this exact
|
|
143
|
+
// operation; explicit intent may continue only a staged resolution.
|
|
144
|
+
// Do not manufacture a conflict record or restart the source command.
|
|
145
|
+
if (!await succeeds(path, ["diff", "--quiet"])
|
|
146
|
+
|| await succeeds(path, ["diff", "--cached", "--quiet"])
|
|
147
|
+
|| (await command(path, ["ls-files", "--others", "--exclude-standard"])).trim() !== "") {
|
|
148
|
+
throw new Error("Owned Git recovery requires a fully staged resolution with no unstaged or untracked files.");
|
|
149
|
+
}
|
|
139
150
|
}
|
|
140
151
|
try {
|
|
141
152
|
if (operation !== undefined) {
|
|
@@ -63,32 +63,27 @@ export function taskNotificationPresentation(wake, store) {
|
|
|
63
63
|
`Changes: ${labels.slice(0, 4).join("; ")}${labels.length > 4 ? "; more in wake" : ""}.`,
|
|
64
64
|
...messages.slice(0, 4).map(message => {
|
|
65
65
|
const item = taskMessagePresentation(message, (task, run) => store.getRun(task, run));
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
// Keep failure/disposition facts here. Authored summaries remain in the
|
|
67
|
+
// mandatory wake/original reads, not a second copy in every delivery.
|
|
68
|
+
return `- ${message.id} [${message.author.type}${message.author.type === "role" ? `:${message.author.roleName}` : ""}]: ${item.fact}.`;
|
|
68
69
|
}),
|
|
69
70
|
...(messages.length > 4 ? [`${messages.length - 4} more messages in the fixed wake.`] : []),
|
|
70
71
|
`Refresh: yui task context ${wake.taskId} --json`,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
// Match Runtime's receiving-entry -> claim -> originals order. The claim
|
|
73
|
+
// returns the exact focused read; do not suggest reading it once beforehand.
|
|
74
|
+
`Read receiving entry: yui inbox show ${wake.id} --json`,
|
|
75
|
+
`Inbox: ${wake.id}; follow Runtime claim → original → disposition → ACK.`,
|
|
74
76
|
...(wake.reasons.includes("session-replaced") ? [
|
|
75
|
-
"Session
|
|
76
|
-
|
|
77
|
-
"Preserve original claims and delivery targets. Unknown external effects require evidence, not another execution."
|
|
78
|
-
] : []),
|
|
79
|
-
"Check current Skills each turn: yui session skills --json. Apply current yui-leader; read relevant originals in full and advance authorized work."
|
|
77
|
+
"Session replaced: yui inbox list --view history --json; follow Runtime inbox recovery before settling retained claims. Do not replay unknown effects."
|
|
78
|
+
] : [])
|
|
80
79
|
].join("\n");
|
|
81
80
|
}
|
|
82
81
|
export function globalMessagePresentation(roleName, messageId, message) {
|
|
83
|
-
const summary = authoredNotificationSummary(message?.body);
|
|
84
82
|
return [
|
|
85
83
|
`Yui Global Message: role=${roleName} message=${messageId}.`,
|
|
86
84
|
`Changes: ${message?.author.type ?? "Recorded"} input received.`,
|
|
87
|
-
...(summary === undefined ? [] : [`Author summary (message-body): ${JSON.stringify(summary)}`]),
|
|
88
85
|
"Refresh Session Context using the current Manifest's loadCommand.",
|
|
89
|
-
`
|
|
90
|
-
`
|
|
91
|
-
"Routing to a Task or asking a necessary user question can settle this input without completing the Task. Missing ACK is not permission to repeat effects.",
|
|
92
|
-
`The Global loadCommand checks current Skill versions each turn. Apply current ${roleName === "operator" ? "yui-operator" : "yui-runtime"} within existing authority.`
|
|
86
|
+
`Original: reuse the complete matching currentOriginals record from Context; if absent, yui inbox show ${messageId} --json.`,
|
|
87
|
+
`Inbox: ${messageId}; follow Runtime claim → original → disposition → ACK.`
|
|
93
88
|
].join("\n");
|
|
94
89
|
}
|
|
@@ -10,7 +10,7 @@ export function createOperatorBatchPresentation(batchId, items) {
|
|
|
10
10
|
receiptId: `operator-batch:${batchId}`,
|
|
11
11
|
text: [
|
|
12
12
|
"[Yui updates]",
|
|
13
|
-
"
|
|
13
|
+
"Apply current yui-operator and refresh Global Context; inspect these originals and present material changes or decisions.",
|
|
14
14
|
...items.flatMap(renderItem)
|
|
15
15
|
].join("\n")
|
|
16
16
|
};
|
|
@@ -12,6 +12,7 @@ import { sourceRunContextValue } from "../context/sourceRunContext.js";
|
|
|
12
12
|
import { requireManagedTaskCaller } from "../runtime/managedCaller.js";
|
|
13
13
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
14
14
|
import { isTaskRecordRetired, operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
15
|
+
import { recoveredMessageContinuation } from "../controller/taskRunRecovery.js";
|
|
15
16
|
export function resolveMessageRecipient(store, taskId, roleName, scope) {
|
|
16
17
|
if (scope.reviewRoundId !== undefined) {
|
|
17
18
|
const round = store.getReviewRound(taskId, scope.reviewRoundId);
|
|
@@ -80,7 +81,8 @@ export function messageAssignmentBlocker(store, message) {
|
|
|
80
81
|
const latest = store.listRuns(message.taskId).filter((run) => run.workItemId === owner.workItemId && run.reviewRoundId === owner.reviewRoundId
|
|
81
82
|
&& run.roleName === owner.roleName).at(-1);
|
|
82
83
|
if (latest !== undefined && latest.id !== owner.id
|
|
83
|
-
&& latest.inputs[0]?.input.source.channel !== "message-continuation"
|
|
84
|
+
&& latest.inputs[0]?.input.source.channel !== "message-continuation"
|
|
85
|
+
&& !recoveredMessageContinuation(store, message, latest))
|
|
84
86
|
return "assignment-changed";
|
|
85
87
|
if (target.reviewRoundId !== undefined) {
|
|
86
88
|
const validation = validateExactRunReviewRound(store, latest ?? owner, { allowTerminal: true });
|
|
@@ -35,10 +35,17 @@ export function readDigest(value) {
|
|
|
35
35
|
}
|
|
36
36
|
/** A save receipt is not another read of the submitted body. Keep the exact
|
|
37
37
|
* delivery/control facts and identity; body/history are read on demand.
|
|
38
|
+
* bodySha256 hashes the saved string's UTF-8 bytes, not its JSON encoding or
|
|
39
|
+
* record metadata. It permits transport comparison, not semantic read credit.
|
|
38
40
|
*/
|
|
39
41
|
export function messageReceipt(message) {
|
|
40
42
|
const { body: _body, ...facts } = message;
|
|
41
|
-
return {
|
|
43
|
+
return {
|
|
44
|
+
...facts,
|
|
45
|
+
digest: readDigest(message),
|
|
46
|
+
bodyBytes: Buffer.byteLength(message.body, "utf8"),
|
|
47
|
+
bodySha256: createHash("sha256").update(message.body, "utf8").digest("hex")
|
|
48
|
+
};
|
|
42
49
|
}
|
|
43
50
|
export function encodeReadCursor(cursor) {
|
|
44
51
|
return Buffer.from(JSON.stringify(cursor)).toString("base64url");
|
|
@@ -117,15 +124,37 @@ export function boundedDocument(value, source, cursor) {
|
|
|
117
124
|
const offset = cursor === undefined ? 0 : decodeReadCursor(cursor, source, digest);
|
|
118
125
|
if (offset >= text.length && offset !== 0)
|
|
119
126
|
throw usageError("Read cursor exceeds this document.");
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
end--;
|
|
124
|
-
return { contentPage: {
|
|
127
|
+
const boundary = (end) => end < text.length && /[\uD800-\uDBFF]/u.test(text[end - 1])
|
|
128
|
+
? end - 1 : end;
|
|
129
|
+
const page = (end) => ({ contentPage: {
|
|
125
130
|
source, digest, encoding: "json", offset, totalCharacters: text.length,
|
|
126
131
|
totalBytes, text: text.slice(offset, end), complete: end === text.length,
|
|
127
132
|
nextCursor: end < text.length ? encodeReadCursor({ source, digest, offset: end }) : null
|
|
128
|
-
} };
|
|
133
|
+
} });
|
|
134
|
+
// A code unit costs at least one encoded byte. Bound each trial independently
|
|
135
|
+
// of document size, and budget the entire response including escaped identity.
|
|
136
|
+
const upper = boundary(Math.min(offset + READ_PAGE_BYTES, text.length));
|
|
137
|
+
const largest = page(upper);
|
|
138
|
+
if (fitsReadBudget(largest))
|
|
139
|
+
return largest;
|
|
140
|
+
// The terminal page drops its cursor: test it above, separately from the
|
|
141
|
+
// monotone non-terminal search. Raw indices advance even at surrogate pairs.
|
|
142
|
+
let low = offset + 1;
|
|
143
|
+
let high = upper - 1;
|
|
144
|
+
let end = offset;
|
|
145
|
+
while (low <= high) {
|
|
146
|
+
const middle = Math.floor((low + high) / 2);
|
|
147
|
+
const candidate = boundary(middle);
|
|
148
|
+
if (fitsReadBudget(page(candidate))) {
|
|
149
|
+
end = candidate;
|
|
150
|
+
low = middle + 1;
|
|
151
|
+
}
|
|
152
|
+
else
|
|
153
|
+
high = middle - 1;
|
|
154
|
+
}
|
|
155
|
+
if (end === offset)
|
|
156
|
+
throw usageError("Read identity and text exceed the response budget.");
|
|
157
|
+
return page(end);
|
|
129
158
|
}
|
|
130
159
|
/** Shared parser for list/detail command tails; no options reach mutations. */
|
|
131
160
|
export function readOptions(args, flags = ["--cursor", "--limit"]) {
|
|
@@ -122,7 +122,9 @@ function permission(binding) {
|
|
|
122
122
|
return "bypass";
|
|
123
123
|
return [
|
|
124
124
|
permission.sandbox === undefined ? undefined : `sandbox=${permission.sandbox}`,
|
|
125
|
-
permission.approval === undefined ? undefined : `approval=${permission.approval}
|
|
125
|
+
permission.approval === undefined ? undefined : `approval=${permission.approval}`,
|
|
126
|
+
permission.excludeTmpdirEnvVar === undefined ? undefined : `excludeTmpdirEnvVar=${permission.excludeTmpdirEnvVar}`,
|
|
127
|
+
permission.excludeSlashTmp === undefined ? undefined : `excludeSlashTmp=${permission.excludeSlashTmp}`
|
|
126
128
|
].filter((value) => value !== undefined).join("; ");
|
|
127
129
|
}
|
|
128
130
|
// ACP's strategy decides the Session mode Yui requests over the protocol. It
|
|
@@ -4,7 +4,7 @@ import { createConnection, createServer } from "node:net";
|
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { createInterface } from "node:readline";
|
|
7
|
-
import {
|
|
7
|
+
import { createSessionSkillNotice } from "../context/sessionSkillNotice.js";
|
|
8
8
|
import { publishStructuredConversationRecoverability, publishStructuredProviderAccepted, publishStructuredProviderActivity, publishStructuredProviderAttachmentExit, publishStructuredProviderConnection, publishStructuredProviderDiagnostic, publishStructuredProviderGoal, publishStructuredProviderInputObserved, publishStructuredProviderInputSettlement, publishStructuredProviderOpened, publishStructuredProviderStarted, publishStructuredProviderTerminal, replayStructuredProviderPersistence, structuredProviderEventDelivery } from "../controller/structuredProviderObservation.js";
|
|
9
9
|
import { callController, controllerCallMayHaveApplied, ControllerClientError } from "../core/controllerClient.js";
|
|
10
10
|
import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
|
|
@@ -82,6 +82,7 @@ export async function runAgentHost(input) {
|
|
|
82
82
|
return runCodexInteractiveHost(input.home, payload, openAgentHostControl);
|
|
83
83
|
}
|
|
84
84
|
let session;
|
|
85
|
+
const skillNotice = createSessionSkillNotice(input.home);
|
|
85
86
|
// This process really holds the Endpoint code its Session runs on. The lease
|
|
86
87
|
// spans the Session's Turns, so installing a newer generation does not change
|
|
87
88
|
// the implementation under a Session that is still using this one.
|
|
@@ -405,6 +406,7 @@ export async function runAgentHost(input) {
|
|
|
405
406
|
throw new Error("Codex client reconnect lost its Endpoint implementation lease.");
|
|
406
407
|
}
|
|
407
408
|
const started = await endpointLease.resume(reconnectPayload);
|
|
409
|
+
skillNotice.invalidate();
|
|
408
410
|
session = started.session;
|
|
409
411
|
sessionPayload = currentPayload;
|
|
410
412
|
started.session.events((event) => {
|
|
@@ -660,6 +662,7 @@ export async function runAgentHost(input) {
|
|
|
660
662
|
session = started.session;
|
|
661
663
|
sessionPayload = next;
|
|
662
664
|
await recordProviderConnection(input.home, next, started.session);
|
|
665
|
+
skillNotice.invalidate();
|
|
663
666
|
lastTerminalPayload = undefined;
|
|
664
667
|
lastTerminal = undefined;
|
|
665
668
|
directObservation = undefined;
|
|
@@ -909,9 +912,10 @@ export async function runAgentHost(input) {
|
|
|
909
912
|
try {
|
|
910
913
|
// beginDurableProviderTurn validated current execution-environment,
|
|
911
914
|
// authority and Session identity before this native write.
|
|
915
|
+
const notice = skillNotice.prepare(recoveryInput?.text ?? request.run.boundedText, activeRunPayload.environment, session.nativeSessionId, { recovery: recoveryInput !== undefined });
|
|
912
916
|
const receipt = endpointReceipt(await session.submit({
|
|
913
917
|
...request.run,
|
|
914
|
-
boundedText:
|
|
918
|
+
boundedText: notice.text,
|
|
915
919
|
...(recoveryInput === undefined ? {} : { requireQuiescent: true }),
|
|
916
920
|
...(recoveryInput?.expectedFailedNativeTurnId === undefined ? {} : {
|
|
917
921
|
expectedFailedNativeTurnId: recoveryInput.expectedFailedNativeTurnId
|
|
@@ -920,6 +924,8 @@ export async function runAgentHost(input) {
|
|
|
920
924
|
}), request.run.attemptId);
|
|
921
925
|
transportAccepted = true;
|
|
922
926
|
providerAccepted = receipt.acceptance === "provider";
|
|
927
|
+
if (providerAccepted)
|
|
928
|
+
notice.commit();
|
|
923
929
|
activeNativeTurnId = receipt.nativeTurnId;
|
|
924
930
|
try {
|
|
925
931
|
await publishStructuredProviderAccepted({
|
|
@@ -1029,14 +1035,17 @@ export async function runAgentHost(input) {
|
|
|
1029
1035
|
}
|
|
1030
1036
|
try {
|
|
1031
1037
|
await assertHostExecutionEnvironment(input.home, sessionPayload, session.nativeSessionId);
|
|
1038
|
+
const notice = skillNotice.prepare(request.run.boundedText, sessionPayload.environment, session.nativeSessionId);
|
|
1032
1039
|
const receipt = endpointReceipt(await session.steer({
|
|
1033
1040
|
...request.run,
|
|
1034
|
-
boundedText:
|
|
1041
|
+
boundedText: notice.text,
|
|
1035
1042
|
inputRef: request.run.attemptId
|
|
1036
1043
|
}), request.run.attemptId);
|
|
1037
1044
|
if (receipt.nativeTurnId !== request.nativeTurnId) {
|
|
1038
1045
|
throw new ProviderDeliveryUnknownError("Provider accepted steer against an unexpected native Turn.", request.run.attemptId);
|
|
1039
1046
|
}
|
|
1047
|
+
if (receipt.acceptance === "provider")
|
|
1048
|
+
notice.commit();
|
|
1040
1049
|
}
|
|
1041
1050
|
catch (error) {
|
|
1042
1051
|
const unknown = error instanceof ProviderDeliveryUnknownError;
|
|
@@ -49,9 +49,10 @@ export class CodexAppServerRuntime {
|
|
|
49
49
|
const result = await this.transport.request("thread/resume", {
|
|
50
50
|
threadId: id,
|
|
51
51
|
...(options.cwd === undefined ? {} : { cwd: text(options.cwd, "Codex thread cwd") }),
|
|
52
|
-
...threadOptions(options)
|
|
52
|
+
...threadOptions(options),
|
|
53
|
+
excludeTurns: true
|
|
53
54
|
});
|
|
54
|
-
return parseThreadSnapshot(result, id, true);
|
|
55
|
+
return this.withLatestTurn(parseThreadSnapshot(result, id, true));
|
|
55
56
|
}
|
|
56
57
|
async setConversationName(input) {
|
|
57
58
|
await this.transport.request("thread/name/set", {
|
|
@@ -63,9 +64,81 @@ export class CodexAppServerRuntime {
|
|
|
63
64
|
const id = text(conversationId, "Codex thread id");
|
|
64
65
|
const result = await this.transport.request("thread/read", {
|
|
65
66
|
threadId: id,
|
|
66
|
-
includeTurns:
|
|
67
|
+
includeTurns: false
|
|
67
68
|
});
|
|
68
|
-
|
|
69
|
+
const snapshot = parseThreadSnapshot(result, id, "unknown");
|
|
70
|
+
return options.includeTurns === false ? snapshot : this.withLatestTurn(snapshot);
|
|
71
|
+
}
|
|
72
|
+
/** Current execution metadata, never an unbounded transcript hydration. */
|
|
73
|
+
async withLatestTurn(snapshot) {
|
|
74
|
+
const page = await this.transport.request("thread/turns/list", {
|
|
75
|
+
threadId: snapshot.threadId, limit: 1, sortDirection: "desc", itemsView: "notLoaded"
|
|
76
|
+
});
|
|
77
|
+
const records = codexPage(page, new Set()).data;
|
|
78
|
+
if (records.length > 1)
|
|
79
|
+
throw new Error("Codex latest Turn page exceeded its requested bound.");
|
|
80
|
+
const turns = records.map(parseTurnSnapshot);
|
|
81
|
+
const latest = turns[0];
|
|
82
|
+
return {
|
|
83
|
+
...snapshot,
|
|
84
|
+
// The metadata response may contain no Turns. Do not carry an old active
|
|
85
|
+
// identity over a separately observed latest Turn.
|
|
86
|
+
activeTurnId: latest?.status === "inProgress" ? latest.turnId : undefined,
|
|
87
|
+
latestTurnStatus: latest?.status,
|
|
88
|
+
turns
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Recover only the persisted owned Turn. Scan metadata, then its items,
|
|
92
|
+
* newest first, stopping at the last nonempty assistant message. A malformed
|
|
93
|
+
* page or oversized individual item remains an error, never guessed success. */
|
|
94
|
+
async readTurn(conversationId, turnId) {
|
|
95
|
+
const threadId = text(conversationId, "Codex thread id");
|
|
96
|
+
const expectedTurnId = text(turnId, "Codex owned Turn id");
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
let cursor;
|
|
99
|
+
for (;;) {
|
|
100
|
+
const page = codexPage(await this.transport.request("thread/turns/list", {
|
|
101
|
+
threadId, limit: 50, sortDirection: "desc", itemsView: "notLoaded",
|
|
102
|
+
...(cursor === undefined ? {} : { cursor })
|
|
103
|
+
}), seen);
|
|
104
|
+
if (page.data.length > 50)
|
|
105
|
+
throw new Error("Codex Turn page exceeded its requested bound.");
|
|
106
|
+
const found = page.data.map(parseTurnSnapshot).find(turn => turn.turnId === expectedTurnId);
|
|
107
|
+
if (found !== undefined) {
|
|
108
|
+
if (found.status === "inProgress")
|
|
109
|
+
return found;
|
|
110
|
+
const output = await this.readTurnOutput(threadId, expectedTurnId);
|
|
111
|
+
return { ...found, ...(output === undefined ? {} : { output }) };
|
|
112
|
+
}
|
|
113
|
+
if (page.nextCursor === null)
|
|
114
|
+
return undefined;
|
|
115
|
+
cursor = page.nextCursor;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async readTurnOutput(threadId, turnId) {
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
let cursor;
|
|
121
|
+
for (;;) {
|
|
122
|
+
const page = codexPage(await this.transport.request("thread/items/list", {
|
|
123
|
+
threadId, turnId, limit: 1, sortDirection: "desc",
|
|
124
|
+
...(cursor === undefined ? {} : { cursor })
|
|
125
|
+
}), seen);
|
|
126
|
+
if (page.data.length > 1)
|
|
127
|
+
throw new Error("Codex item page exceeded its requested bound.");
|
|
128
|
+
for (const value of page.data) {
|
|
129
|
+
const entry = object(value);
|
|
130
|
+
const item = object(entry?.item);
|
|
131
|
+
if (entry?.turnId !== turnId || item === null) {
|
|
132
|
+
throw new Error("Codex item page does not belong to the owned Turn.");
|
|
133
|
+
}
|
|
134
|
+
if (item.type === "agentMessage" && typeof item.text === "string" && item.text.trim().length > 0) {
|
|
135
|
+
return item.text;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (page.nextCursor === null)
|
|
139
|
+
return undefined;
|
|
140
|
+
cursor = page.nextCursor;
|
|
141
|
+
}
|
|
69
142
|
}
|
|
70
143
|
async readGoal(conversationId) {
|
|
71
144
|
const id = text(conversationId, "Codex thread id");
|
|
@@ -330,26 +403,7 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
|
|
|
330
403
|
throw new Error("Codex thread status is missing or unsupported.");
|
|
331
404
|
if (!Array.isArray(thread.turns))
|
|
332
405
|
throw new Error("Codex thread turns are missing or invalid.");
|
|
333
|
-
const turnSnapshots = thread.turns.map(
|
|
334
|
-
const turn = object(value);
|
|
335
|
-
if (turn === null)
|
|
336
|
-
throw new Error("Codex thread contains an invalid Turn.");
|
|
337
|
-
const turnId = text(optionalId(turn.id), "Codex response Turn id");
|
|
338
|
-
const status = optionalTurnStatus(turn.status);
|
|
339
|
-
if (status === undefined)
|
|
340
|
-
throw new Error("Codex Turn status is missing or unsupported.");
|
|
341
|
-
const output = codexTurnOutput(turn);
|
|
342
|
-
const error = providerError(turn.error);
|
|
343
|
-
return {
|
|
344
|
-
turnId,
|
|
345
|
-
status,
|
|
346
|
-
...(output === undefined ? {} : { output }),
|
|
347
|
-
...(error === undefined ? {} : { error }),
|
|
348
|
-
...(turn.error === undefined || turn.error === null ? {} : {
|
|
349
|
-
rawError: serializeAgentErrorRaw(turn.error)
|
|
350
|
-
})
|
|
351
|
-
};
|
|
352
|
-
});
|
|
406
|
+
const turnSnapshots = thread.turns.map(parseTurnSnapshot);
|
|
353
407
|
const active = [...turnSnapshots].reverse().find((turn) => turn.status === "inProgress");
|
|
354
408
|
const latestStatus = turnSnapshots.at(-1)?.status;
|
|
355
409
|
return {
|
|
@@ -365,6 +419,38 @@ function parseThreadSnapshot(result, expectedThreadId, loaded) {
|
|
|
365
419
|
raw: result
|
|
366
420
|
};
|
|
367
421
|
}
|
|
422
|
+
function parseTurnSnapshot(value) {
|
|
423
|
+
const turn = object(value);
|
|
424
|
+
if (turn === null)
|
|
425
|
+
throw new Error("Codex thread contains an invalid Turn.");
|
|
426
|
+
const turnId = text(optionalId(turn.id), "Codex response Turn id");
|
|
427
|
+
const status = optionalTurnStatus(turn.status);
|
|
428
|
+
if (status === undefined)
|
|
429
|
+
throw new Error("Codex Turn status is missing or unsupported.");
|
|
430
|
+
const output = codexTurnOutput(turn);
|
|
431
|
+
const error = providerError(turn.error);
|
|
432
|
+
return {
|
|
433
|
+
turnId,
|
|
434
|
+
status,
|
|
435
|
+
...(output === undefined ? {} : { output }),
|
|
436
|
+
...(error === undefined ? {} : { error }),
|
|
437
|
+
...(turn.error === undefined || turn.error === null ? {} : {
|
|
438
|
+
rawError: serializeAgentErrorRaw(turn.error)
|
|
439
|
+
})
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function codexPage(page, seen) {
|
|
443
|
+
if (!Array.isArray(page.data)
|
|
444
|
+
|| !(page.nextCursor === null || typeof page.nextCursor === "string" && page.nextCursor.length > 0)) {
|
|
445
|
+
throw new Error("Codex history page is missing data or its continuation boundary.");
|
|
446
|
+
}
|
|
447
|
+
if (typeof page.nextCursor === "string") {
|
|
448
|
+
if (seen.has(page.nextCursor))
|
|
449
|
+
throw new Error("Codex history cursor repeated.");
|
|
450
|
+
seen.add(page.nextCursor);
|
|
451
|
+
}
|
|
452
|
+
return { data: page.data, nextCursor: page.nextCursor };
|
|
453
|
+
}
|
|
368
454
|
/** Extract the last assistant message from one native Codex Turn without rewriting it. */
|
|
369
455
|
export function codexTurnOutput(turn) {
|
|
370
456
|
for (const value of [...arrayMember(turn, "items")].reverse()) {
|
|
@@ -4,6 +4,7 @@ import { once } from "node:events";
|
|
|
4
4
|
import WebSocket, { WebSocketServer } from "ws";
|
|
5
5
|
import { AGENT_HOST_CONTROL_PROTOCOL } from "./agentHostProtocol.js";
|
|
6
6
|
import { openCodexInteractiveConnection } from "./structuredProviderHost.js";
|
|
7
|
+
import { createSessionSkillNotice } from "../context/sessionSkillNotice.js";
|
|
7
8
|
/**
|
|
8
9
|
* Keep the native TUI and its transparent App Server attachment in one pane.
|
|
9
10
|
* The existing Host acknowledgement carries the ID from that TUI's exact
|
|
@@ -15,6 +16,8 @@ import { openCodexInteractiveConnection } from "./structuredProviderHost.js";
|
|
|
15
16
|
*/
|
|
16
17
|
export async function runCodexInteractiveHost(home, payload, openAgentHostControl) {
|
|
17
18
|
const environment = payload.environment;
|
|
19
|
+
const skillNotice = createSessionSkillNotice(home);
|
|
20
|
+
const pendingNotices = new Map();
|
|
18
21
|
if (environment.YUI_SESSION_SCOPE !== "global" || environment.YUI_ADAPTER_ID !== "codex"
|
|
19
22
|
|| payload.providerControl !== undefined) {
|
|
20
23
|
throw new Error("Interactive Codex Host requires a global native TUI launch.");
|
|
@@ -106,6 +109,23 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
106
109
|
startupRequestId = message.id;
|
|
107
110
|
message.params = codexInteractiveStartupParameters(payload, params);
|
|
108
111
|
}
|
|
112
|
+
if (message.method === "turn/start" || message.method === "turn/steer") {
|
|
113
|
+
if (snapshot.nativeSessionId === undefined)
|
|
114
|
+
throw new Error("Skill check requires the reserved native Thread.");
|
|
115
|
+
if (typeof message.id !== "string" && typeof message.id !== "number"
|
|
116
|
+
|| pendingNotices.has(message.id))
|
|
117
|
+
throw new Error("Turn input requires a unique correlation id.");
|
|
118
|
+
const params = jsonObject(message.params);
|
|
119
|
+
if (params.threadId !== snapshot.nativeSessionId)
|
|
120
|
+
throw new Error("Turn input targets another Thread.");
|
|
121
|
+
const prepared = codexInteractiveSkillInput(skillNotice, environment, snapshot.nativeSessionId, params);
|
|
122
|
+
message.params = prepared.params;
|
|
123
|
+
pendingNotices.set(message.id, {
|
|
124
|
+
commit: prepared.commit,
|
|
125
|
+
...(message.method === "turn/steer" && typeof params.expectedTurnId === "string"
|
|
126
|
+
? { expectedTurnId: params.expectedTurnId } : {})
|
|
127
|
+
});
|
|
128
|
+
}
|
|
109
129
|
void connection.send(message).catch(fail);
|
|
110
130
|
}
|
|
111
131
|
catch (error) {
|
|
@@ -115,6 +135,17 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
115
135
|
});
|
|
116
136
|
connection.onMessage((message) => {
|
|
117
137
|
try {
|
|
138
|
+
if (message.method === undefined && (typeof message.id === "string" || typeof message.id === "number")) {
|
|
139
|
+
const pending = pendingNotices.get(message.id);
|
|
140
|
+
if (pending !== undefined) {
|
|
141
|
+
pendingNotices.delete(message.id);
|
|
142
|
+
const result = message.result;
|
|
143
|
+
const turnId = result?.turn?.id ?? result?.turnId;
|
|
144
|
+
if (message.error === undefined && typeof turnId === "string" && turnId.length > 0
|
|
145
|
+
&& (pending.expectedTurnId === undefined || pending.expectedTurnId === turnId))
|
|
146
|
+
pending.commit();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
118
149
|
let nativeSessionId;
|
|
119
150
|
if (startupRequestId !== undefined && message.id === startupRequestId
|
|
120
151
|
&& message.method === undefined) {
|
|
@@ -183,6 +214,21 @@ export async function runCodexInteractiveHost(home, payload, openAgentHostContro
|
|
|
183
214
|
await control.close();
|
|
184
215
|
}
|
|
185
216
|
}
|
|
217
|
+
/** Preserve all native content parts (including images), adding only Host metadata. */
|
|
218
|
+
export function codexInteractiveSkillInput(notice, environment, nativeSessionId, params) {
|
|
219
|
+
if (!Array.isArray(params.input))
|
|
220
|
+
throw new Error("Turn input must be an array.");
|
|
221
|
+
const prepared = notice.prepare("", environment, nativeSessionId);
|
|
222
|
+
return {
|
|
223
|
+
params: {
|
|
224
|
+
...params,
|
|
225
|
+
input: prepared.text.length === 0 ? params.input : [
|
|
226
|
+
{ type: "text", text: prepared.text.trimEnd(), text_elements: [] }, ...params.input
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
commit: prepared.commit
|
|
230
|
+
};
|
|
231
|
+
}
|
|
186
232
|
/** The remote TUI can omit local launch configuration. Apply the reserved
|
|
187
233
|
* workspace and managed identity on its actual startup, never by starting a
|
|
188
234
|
* second Thread or changing daemon-wide configuration. */
|
|
@@ -211,7 +211,7 @@ export function providerRetryProjection(binding) {
|
|
|
211
211
|
}
|
|
212
212
|
/** Entry-point facts, never accumulated retry history or raw diagnostics.
|
|
213
213
|
* Exact Session retry/show and failureRef retain the original evidence. */
|
|
214
|
-
export function providerRetrySummary(binding) {
|
|
214
|
+
export function providerRetrySummary(binding, detailCommand) {
|
|
215
215
|
const view = providerRetryProjection(binding);
|
|
216
216
|
if (view === null)
|
|
217
217
|
return null;
|
|
@@ -224,7 +224,10 @@ export function providerRetrySummary(binding) {
|
|
|
224
224
|
currentTurn: currentTurn === null ? null : {
|
|
225
225
|
attemptId: currentTurn.attemptId, status: currentTurn.status, nativeTurnId: currentTurn.nativeTurnId
|
|
226
226
|
},
|
|
227
|
-
historyCount: history.length, detailOmitted: true
|
|
227
|
+
historyCount: history.length, detailOmitted: true,
|
|
228
|
+
// This reads current Session evidence, not a frozen Run snapshot. The
|
|
229
|
+
// returned chain/attempt identities must still be compared by the caller.
|
|
230
|
+
...(detailCommand === undefined ? {} : { currentDetailCommand: detailCommand })
|
|
228
231
|
};
|
|
229
232
|
}
|
|
230
233
|
export function controlProviderRetry(binding, action, at) {
|
|
@@ -462,7 +462,7 @@ function orderedRunTimestamp(run, value, label) {
|
|
|
462
462
|
}
|
|
463
463
|
return normalized;
|
|
464
464
|
}
|
|
465
|
-
function providerTurnIsActive(run) {
|
|
465
|
+
export function providerTurnIsActive(run) {
|
|
466
466
|
return run !== null && ["submitting", "accepted", "delivery-unknown"]
|
|
467
467
|
.includes(run.status);
|
|
468
468
|
}
|