@zq-silk/yui 2.21.0 → 2.23.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 +87 -27
- 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/cli/updateCommand.js +20 -3
- package/dist/cli/updateOrchestrator.js +13 -0
- package/dist/cli/updatePorts.js +27 -0
- 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 +19 -5
- package/dist/commands/textInput.js +2 -2
- package/dist/context/contextFamilies.js +10 -0
- package/dist/context/currentSkills.js +37 -4
- package/dist/context/runContextPack.js +44 -5
- package/dist/context/runInputContract.js +1 -1
- package/dist/context/sessionBootstrapManifest.js +21 -6
- package/dist/context/sessionSkillNotice.js +74 -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 +66 -18
- package/dist/controller/controller.js +19 -2
- package/dist/controller/diagnosticJournal.js +10 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +74 -24
- package/dist/controller/hostLifecycle.js +153 -0
- package/dist/controller/hostLifecycleChecks.js +27 -0
- package/dist/controller/hostUpgrade.js +344 -0
- 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 +82 -21
- package/dist/controller/runtimeEventInbox.js +107 -13
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/controller/sessionNotify.js +5 -46
- 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/execution/codexThreadNaming.js +54 -43
- package/dist/executor/agentAdapter.js +32 -4
- package/dist/executor/agentConfigurationFields.js +2 -0
- package/dist/executor/fileRoleLaunchPlanner.js +18 -3
- 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/agentEndpoint.js +11 -1
- package/dist/runtime/agentEndpointIdentity.js +2 -0
- package/dist/runtime/agentHost.js +174 -40
- package/dist/runtime/agentHostImplementation.js +3 -1
- package/dist/runtime/codexAppServerRuntime.js +113 -24
- package/dist/runtime/codexInteractiveHost.js +102 -11
- package/dist/runtime/codexNativeAccess.js +1 -1
- package/dist/runtime/contextRecovery.js +15 -0
- package/dist/runtime/hostSessionLifecycle.js +83 -0
- package/dist/runtime/hostUpgradeGate.js +43 -0
- package/dist/runtime/lifecycleChecks.js +47 -0
- package/dist/runtime/lifecycleReservation.js +9 -1
- package/dist/runtime/nativeTitleReceipt.js +31 -0
- package/dist/runtime/nativeTitleSync.js +192 -0
- package/dist/runtime/providerRetry.js +9 -6
- package/dist/runtime/providerRuntimeIdentity.js +1 -1
- package/dist/runtime/structuredProviderHost.js +65 -35
- package/dist/scheduler/wakeupQueue.js +10 -4
- package/dist/storage/sqliteSchema.js +29 -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 +177 -21
- package/docs/managed-turn-and-session-runtime.zh-CN.md +126 -14
- package/docs/provider-retry.md +11 -0
- package/docs/release-workflow.md +65 -0
- package/docs/release-workflow.zh-CN.md +48 -0
- package/docs/roles-and-configuration.md +28 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/storage-baseline.md +13 -1
- package/docs/storage-baseline.zh-CN.md +5 -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 +182 -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
import { closeSync, constants, existsSync, fchmodSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
4
5
|
import { join } from "node:path";
|
|
5
6
|
import { createRuntimeObservation, isRuntimeUsageEvidence } from "../runtime/runtimeObservation.js";
|
|
6
7
|
import { boundedRunFailureDiagnostic, transportAgentResult } from "../domain/agentResultTransport.js";
|
|
@@ -19,6 +20,9 @@ export class FileRuntimeEventInbox {
|
|
|
19
20
|
now;
|
|
20
21
|
hooks;
|
|
21
22
|
directory;
|
|
23
|
+
// Serialize this producer's async admissions, not the consumer/ACK path.
|
|
24
|
+
// No persistent index or cache: each scan retains the existing validation.
|
|
25
|
+
#asyncAdmission = Promise.resolve();
|
|
22
26
|
constructor(home, now = () => new Date(), hooks = {}) {
|
|
23
27
|
this.home = home;
|
|
24
28
|
this.now = now;
|
|
@@ -26,6 +30,31 @@ export class FileRuntimeEventInbox {
|
|
|
26
30
|
this.directory = join(home, RUNTIME_EVENT_DIRECTORY);
|
|
27
31
|
}
|
|
28
32
|
enqueueObservation(input, host) {
|
|
33
|
+
return this.publish(this.observationEvent(input, host));
|
|
34
|
+
}
|
|
35
|
+
/** Controller sampler ingress. The full usage dedup scan yields to control
|
|
36
|
+
* I/O; publication keeps the existing atomic fsync/link/conflict boundary.
|
|
37
|
+
* Callers must await before advancing cursor/state. Throughput remains O(U*E)
|
|
38
|
+
* and a scan still retains O(E) payload memory. */
|
|
39
|
+
enqueueObservationAsync(input, host) {
|
|
40
|
+
const event = this.observationEvent(input, host);
|
|
41
|
+
const admission = this.#asyncAdmission.then(async () => {
|
|
42
|
+
this.hooks.afterAdmission?.();
|
|
43
|
+
if (isRuntimeUsageEvidence(event.observation)) {
|
|
44
|
+
const existing = (await this.listAsync()).find(candidate => (candidate.type === "runtime-observation"
|
|
45
|
+
&& candidate.taskId === event.taskId
|
|
46
|
+
&& candidate.observation.eventId === event.observation.eventId));
|
|
47
|
+
if (existing !== undefined)
|
|
48
|
+
return {
|
|
49
|
+
event: existing, created: false
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return this.publishUnlocked(event);
|
|
53
|
+
});
|
|
54
|
+
this.#asyncAdmission = admission.catch(() => undefined);
|
|
55
|
+
return admission;
|
|
56
|
+
}
|
|
57
|
+
observationEvent(input, host) {
|
|
29
58
|
const observation = createRuntimeObservation(input);
|
|
30
59
|
const scope = observation.fence.taskId === undefined ? "global" : "task";
|
|
31
60
|
const event = Object.freeze({
|
|
@@ -40,7 +69,7 @@ export class FileRuntimeEventInbox {
|
|
|
40
69
|
observation,
|
|
41
70
|
...(host === undefined ? {} : { host: readAgentHostObservationSource(host) })
|
|
42
71
|
});
|
|
43
|
-
return
|
|
72
|
+
return event;
|
|
44
73
|
}
|
|
45
74
|
enqueueRunTerminal(input) {
|
|
46
75
|
const normalized = normalizeNativeTurnTerminalInput(input);
|
|
@@ -89,6 +118,59 @@ export class FileRuntimeEventInbox {
|
|
|
89
118
|
}
|
|
90
119
|
return events.sort(compareRuntimeEvents);
|
|
91
120
|
}
|
|
121
|
+
/** Controller consumers must not synchronously read an entire retained
|
|
122
|
+
* backlog before accepting control sockets. Keep the same validated,
|
|
123
|
+
* sorted snapshot; the fold/ACK batch and durable identities are unchanged. */
|
|
124
|
+
async listAsync() {
|
|
125
|
+
let names;
|
|
126
|
+
try {
|
|
127
|
+
names = await readdir(this.directory);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (isNodeError(error, "ENOENT"))
|
|
131
|
+
return [];
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
const events = [];
|
|
135
|
+
for (const name of names.filter((entry) => entry.endsWith(".json"))) {
|
|
136
|
+
try {
|
|
137
|
+
const id = name.slice(0, -".json".length);
|
|
138
|
+
assertEventId(id);
|
|
139
|
+
const path = this.eventPath(id);
|
|
140
|
+
let metadata;
|
|
141
|
+
try {
|
|
142
|
+
metadata = await lstat(path);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (isNodeError(error, "ENOENT"))
|
|
146
|
+
continue;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
assertEventFile(metadata, id);
|
|
150
|
+
let content;
|
|
151
|
+
try {
|
|
152
|
+
content = await readFile(path, "utf8");
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
throw invalidEvent(`Runtime event JSON is invalid: ${id}`);
|
|
156
|
+
}
|
|
157
|
+
const event = parseEventFile(content, id);
|
|
158
|
+
events.push(event);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
if (!(error instanceof RuntimeEventInboxError)
|
|
162
|
+
|| error.code !== "RUNTIME_EVENT_INVALID")
|
|
163
|
+
throw error;
|
|
164
|
+
try {
|
|
165
|
+
this.quarantine(name);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Match list(): an invalid entry cannot block independent facts.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return events.sort(compareRuntimeEvents);
|
|
173
|
+
}
|
|
92
174
|
read(id) {
|
|
93
175
|
assertEventId(id);
|
|
94
176
|
const path = this.eventPath(id);
|
|
@@ -101,23 +183,15 @@ export class FileRuntimeEventInbox {
|
|
|
101
183
|
return null;
|
|
102
184
|
throw error;
|
|
103
185
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|| metadata.size > MAX_RUNTIME_EVENT_FILE_BYTES) {
|
|
107
|
-
throw invalidEvent(`Runtime event file is invalid: ${id}`);
|
|
108
|
-
}
|
|
109
|
-
let value;
|
|
186
|
+
assertEventFile(metadata, id);
|
|
187
|
+
let content;
|
|
110
188
|
try {
|
|
111
|
-
|
|
189
|
+
content = readFileSync(path, "utf8");
|
|
112
190
|
}
|
|
113
191
|
catch {
|
|
114
192
|
throw invalidEvent(`Runtime event JSON is invalid: ${id}`);
|
|
115
193
|
}
|
|
116
|
-
|
|
117
|
-
if (event.id !== id || runtimeEventId(event.type, event) !== id) {
|
|
118
|
-
throw invalidEvent(`Runtime event identity is invalid: ${id}`);
|
|
119
|
-
}
|
|
120
|
-
return event;
|
|
194
|
+
return parseEventFile(content, id);
|
|
121
195
|
}
|
|
122
196
|
/** Producer-local delivery diagnostics need only existence, never a read of
|
|
123
197
|
* another version's payload or permission to quarantine another producer. */
|
|
@@ -252,6 +326,26 @@ export class FileRuntimeEventInbox {
|
|
|
252
326
|
}
|
|
253
327
|
}
|
|
254
328
|
}
|
|
329
|
+
function assertEventFile(metadata, id) {
|
|
330
|
+
if (!metadata.isFile() || (metadata.mode & 0o777) !== 0o600
|
|
331
|
+
|| metadata.size > MAX_RUNTIME_EVENT_FILE_BYTES) {
|
|
332
|
+
throw invalidEvent(`Runtime event file is invalid: ${id}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function parseEventFile(content, id) {
|
|
336
|
+
let value;
|
|
337
|
+
try {
|
|
338
|
+
value = JSON.parse(content);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
throw invalidEvent(`Runtime event JSON is invalid: ${id}`);
|
|
342
|
+
}
|
|
343
|
+
const event = parseRuntimeEvent(decodeInboxV1(value));
|
|
344
|
+
if (event.id !== id || runtimeEventId(event.type, event) !== id) {
|
|
345
|
+
throw invalidEvent(`Runtime event identity is invalid: ${id}`);
|
|
346
|
+
}
|
|
347
|
+
return event;
|
|
348
|
+
}
|
|
255
349
|
export class RuntimeEventInboxError extends Error {
|
|
256
350
|
code;
|
|
257
351
|
constructor(code, message) {
|
|
@@ -530,8 +530,8 @@ export function createAsyncRuntimeObserver(invoke) {
|
|
|
530
530
|
/**
|
|
531
531
|
* The async counterpart to {@link FileRuntimeEventProcessor}: folds immutable
|
|
532
532
|
* Hook facts one at a time (awaiting the worker-hosted observer) before
|
|
533
|
-
* acknowledging them.
|
|
534
|
-
*
|
|
533
|
+
* acknowledging them. File reads are asynchronous so a retained backlog
|
|
534
|
+
* cannot hold the control event loop before the worker's bounded fold.
|
|
535
535
|
*/
|
|
536
536
|
export class AsyncRuntimeEventProcessor {
|
|
537
537
|
inbox;
|
|
@@ -549,7 +549,7 @@ export class AsyncRuntimeEventProcessor {
|
|
|
549
549
|
const failed = [];
|
|
550
550
|
let events;
|
|
551
551
|
try {
|
|
552
|
-
events = this.inbox.list();
|
|
552
|
+
events = this.inbox.listAsync === undefined ? this.inbox.list() : await this.inbox.listAsync();
|
|
553
553
|
}
|
|
554
554
|
catch (error) {
|
|
555
555
|
return emptyDrainFailure(error);
|
|
@@ -2,10 +2,9 @@ import { callController } from "../core/controllerClient.js";
|
|
|
2
2
|
import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
|
|
3
3
|
import { transportAgentResult } from "../domain/agentResultTransport.js";
|
|
4
4
|
import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
|
|
5
|
-
import { setCodexThreadName } from "../execution/codexThreadNaming.js";
|
|
6
5
|
import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
7
6
|
/** Hidden CLI entrypoint used by Codex's structured notify hook. */
|
|
8
|
-
export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call
|
|
7
|
+
export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call) {
|
|
9
8
|
const params = parseCodexSessionNotification(payloadArgument, environment);
|
|
10
9
|
// Old global TUIs can still emit their invocation-local hook. It is not
|
|
11
10
|
// evidence of a shared-daemon Thread's identity or process lifecycle.
|
|
@@ -15,7 +14,7 @@ export async function runSessionNotifyCommand(payloadArgument, environment = pro
|
|
|
15
14
|
const home = requireText(environment.YUI_HOME, "YUI_HOME");
|
|
16
15
|
// The caller reports native identity; durable state supplies the current AgentRun.
|
|
17
16
|
const current = currentNotifyRun(home, params);
|
|
18
|
-
|
|
17
|
+
new FileRuntimeEventInbox(home).enqueueRunTerminal({
|
|
19
18
|
scope: params.scope,
|
|
20
19
|
...(params.scope === "task" ? { taskId: params.taskId } : {}),
|
|
21
20
|
roleName: params.roleName,
|
|
@@ -42,11 +41,9 @@ export async function runSessionNotifyCommand(payloadArgument, environment = pro
|
|
|
42
41
|
roleName: params.roleName
|
|
43
42
|
})
|
|
44
43
|
}, { timeoutMs: 100 }).catch(() => { });
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await setThreadName(request).catch(() => { });
|
|
49
|
-
}
|
|
44
|
+
// Title synchronization belongs to the Host's separately fenced metadata
|
|
45
|
+
// lane. A terminal notification is neither a rename receipt nor permission
|
|
46
|
+
// to replay an unknown write or overwrite a native user's name.
|
|
50
47
|
}
|
|
51
48
|
/**
|
|
52
49
|
* Resolve the active AgentRun only when the notifying native Session matches.
|
|
@@ -130,44 +127,6 @@ function requireText(value, label) {
|
|
|
130
127
|
throw new Error(`${label} is invalid.`);
|
|
131
128
|
return text;
|
|
132
129
|
}
|
|
133
|
-
function shouldSetThreadName(home, params, runId) {
|
|
134
|
-
if (params.scope !== "task"
|
|
135
|
-
|| params.title === undefined
|
|
136
|
-
|| runId === undefined)
|
|
137
|
-
return false;
|
|
138
|
-
try {
|
|
139
|
-
const store = openCurrentTaskStore(home);
|
|
140
|
-
return store.getRun(params.taskId, runId)?.mode === "new";
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function threadNameRequest(params, environment) {
|
|
147
|
-
if (params.title === undefined)
|
|
148
|
-
return null;
|
|
149
|
-
const command = environment.YUI_AGENT_COMMAND;
|
|
150
|
-
const encodedBaseArgs = environment.YUI_AGENT_BASE_ARGS;
|
|
151
|
-
if (command === undefined || encodedBaseArgs === undefined)
|
|
152
|
-
return null;
|
|
153
|
-
try {
|
|
154
|
-
const parsed = JSON.parse(encodedBaseArgs);
|
|
155
|
-
if (!Array.isArray(parsed)
|
|
156
|
-
|| parsed.some((value) => typeof value !== "string" || value.includes("\0"))) {
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
command: requireText(command, "YUI_AGENT_COMMAND"),
|
|
161
|
-
baseArgs: parsed,
|
|
162
|
-
environment,
|
|
163
|
-
threadId: params.nativeSessionId,
|
|
164
|
-
name: params.title
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
130
|
function isObject(value) {
|
|
172
131
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
173
132
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { readRunContextSnapshot } from "../context/runContextPack.js";
|
|
3
|
+
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
4
|
+
/**
|
|
5
|
+
* An explicit current-owner retry may follow an undelivered message
|
|
6
|
+
* continuation, rather than the retry's original business input. Recover the
|
|
7
|
+
* native anchor from durable evidence only; never schedule here or renew its
|
|
8
|
+
* budget. Native latest-failed-Turn and background checks still run at submit.
|
|
9
|
+
*/
|
|
10
|
+
export function taskRunRecovery(store, binding, current) {
|
|
11
|
+
if (!verifiedRecoveryChain(store, binding, current, false))
|
|
12
|
+
return undefined;
|
|
13
|
+
const { currentAttemptId: _attempt, successorRunId: _successor, successorReviewRoundId: _round, ...retained } = binding.retry;
|
|
14
|
+
return { ...retained, input: binding.run.input, status: "waiting", successorAutomatic: false,
|
|
15
|
+
previousRunId: current.id, failedAttemptId: binding.run.attemptId };
|
|
16
|
+
}
|
|
17
|
+
/** Read-only handling authority for the exact original Message. Its recipient,
|
|
18
|
+
* continuation, claim and delivery receipts are never rewritten or requeued. */
|
|
19
|
+
export function recoveredMessageContinuation(store, message, current) {
|
|
20
|
+
const target = message.continuation?.runId;
|
|
21
|
+
if (target === undefined || message.id === undefined || message.taskId !== current.taskId
|
|
22
|
+
|| message.recipient?.roleName !== current.roleName
|
|
23
|
+
|| message.recipient.workItemId !== current.workItemId || message.recipient.reviewRoundId !== current.reviewRoundId)
|
|
24
|
+
return false;
|
|
25
|
+
const binding = store.getTaskRoleSessionSet(current.taskId, current.roleName)?.providerBinding;
|
|
26
|
+
if (binding == null || !verifiedRecoveryChain(store, binding, current, true))
|
|
27
|
+
return false;
|
|
28
|
+
const events = store.listEventsByType(current.taskId, ["run.retried"]);
|
|
29
|
+
const visited = new Set();
|
|
30
|
+
let id = current.id;
|
|
31
|
+
while (id !== target) {
|
|
32
|
+
if (visited.has(id))
|
|
33
|
+
return false;
|
|
34
|
+
visited.add(id);
|
|
35
|
+
const edge = events.find(e => e.payload.runId === id);
|
|
36
|
+
if (edge?.payload.previousRunId === undefined)
|
|
37
|
+
return false;
|
|
38
|
+
id = edge.payload.previousRunId;
|
|
39
|
+
}
|
|
40
|
+
const source = store.getRun(current.taskId, target);
|
|
41
|
+
if (source?.inputs[0]?.input.source.channel !== "message-continuation")
|
|
42
|
+
return false;
|
|
43
|
+
return readRunContextSnapshot(store, current).resources.some(entry => {
|
|
44
|
+
if (entry.ref.store !== "task-message" || entry.ref.refId !== message.id)
|
|
45
|
+
return false;
|
|
46
|
+
const frozen = entry.value;
|
|
47
|
+
return frozen.body === message.body && frozen.createdAt === message.createdAt
|
|
48
|
+
&& isDeepStrictEqual(frozen.recipient, message.recipient);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function verifiedRecoveryChain(store, binding, current, admitted) {
|
|
52
|
+
const retry = binding.retry;
|
|
53
|
+
const turn = binding.run;
|
|
54
|
+
const conversation = binding.conversations.find(c => c.epoch === binding.currentConversationEpoch);
|
|
55
|
+
if (binding.providerNamespace !== "openai/codex"
|
|
56
|
+
|| retry === undefined || retry.status === "recovered" || retry.failedNativeTurnId === undefined
|
|
57
|
+
|| binding.authority.owner !== "controller" || retry.authorityEpoch !== binding.authority.epoch
|
|
58
|
+
|| conversation?.conversationId !== retry.nativeSessionId
|
|
59
|
+
|| turn?.runId !== current.id
|
|
60
|
+
|| turn.authorityEpoch !== binding.authority.epoch || turn.retrySupported !== true
|
|
61
|
+
|| turn.input?.kind !== "run" || turn.input.taskId !== current.taskId
|
|
62
|
+
|| current.purpose !== "execution")
|
|
63
|
+
return false;
|
|
64
|
+
if (admitted
|
|
65
|
+
? current.status !== "active" || turn.status !== "accepted"
|
|
66
|
+
|| retry.currentAttemptId !== turn.attemptId || retry.successorRunId !== current.id
|
|
67
|
+
|| retry.successorAutomatic !== false
|
|
68
|
+
: current.status !== "failed" || turn.status !== "rejected"
|
|
69
|
+
|| turn.nativeTurnId !== undefined || turn.input.runId !== current.id)
|
|
70
|
+
return false;
|
|
71
|
+
const runs = store.listRuns(current.taskId).filter(r => r.roleName === current.roleName);
|
|
72
|
+
const start = runs.findIndex(r => r.result?.provider?.attemptId === retry.failureAttemptId
|
|
73
|
+
&& r.result.provider.nativeTurnId === retry.failedNativeTurnId
|
|
74
|
+
&& r.result.provider.conversationId === retry.nativeSessionId
|
|
75
|
+
&& r.result.provider.status === "failed" && r.status === "failed");
|
|
76
|
+
const end = runs.findIndex(r => r.id === current.id);
|
|
77
|
+
if (start < 0 || end <= start || end !== runs.length - 1)
|
|
78
|
+
return false;
|
|
79
|
+
if (runs.slice(start, end + 1).some(run => run.purpose !== current.purpose
|
|
80
|
+
|| run.workItemId !== current.workItemId || run.reviewRoundId !== current.reviewRoundId
|
|
81
|
+
|| run.executionGroupId !== current.executionGroupId || run.executionLaneId !== current.executionLaneId
|
|
82
|
+
|| run.sourceExecutionGroupId !== current.sourceExecutionGroupId
|
|
83
|
+
|| !isDeepStrictEqual(run.effective, current.effective)
|
|
84
|
+
|| !isDeepStrictEqual(run.workspace, current.workspace)))
|
|
85
|
+
return false;
|
|
86
|
+
const events = store.listEventsByType(current.taskId, ["runtime.agent-error", "runtime.observation", "run.retried", "message.continuation"]);
|
|
87
|
+
const anchor = events.findIndex(event => {
|
|
88
|
+
const observation = runtimeObservationFromTaskEvent(event);
|
|
89
|
+
return observation?.kind === "turn.failed"
|
|
90
|
+
&& observation.fence.roleName === current.roleName
|
|
91
|
+
&& observation.fence.runId === runs[start].id
|
|
92
|
+
&& observation.fence.nativeSessionId === retry.nativeSessionId
|
|
93
|
+
&& observation.fence.receiptId === retry.failureAttemptId
|
|
94
|
+
&& observation.fence.nativeTurnId === retry.failedNativeTurnId
|
|
95
|
+
&& observation.payload.failure?.error.inputDisposition === "accepted"
|
|
96
|
+
&& observation.payload.failure.error.sessionDisposition === "recoverable";
|
|
97
|
+
});
|
|
98
|
+
if (anchor < 0)
|
|
99
|
+
return false;
|
|
100
|
+
// Reject any intervening accepted/unknown input, including an unowned native
|
|
101
|
+
// Turn. Timestamps or absence of a Run result are not non-submission proof.
|
|
102
|
+
for (const event of events.slice(anchor + 1)) {
|
|
103
|
+
if (event.payload.roleName !== current.roleName
|
|
104
|
+
|| event.payload.nativeSessionId !== retry.nativeSessionId)
|
|
105
|
+
continue;
|
|
106
|
+
if (event.type === "runtime.agent-error" && event.payload.inputDisposition !== "not-accepted"
|
|
107
|
+
&& !(event.payload.attemptId === retry.failureAttemptId
|
|
108
|
+
&& event.payload.nativeTurnId === retry.failedNativeTurnId
|
|
109
|
+
&& event.payload.phase === "turn-execute" && event.payload.inputDisposition === "accepted"))
|
|
110
|
+
return false;
|
|
111
|
+
if (event.type !== "runtime.observation")
|
|
112
|
+
continue;
|
|
113
|
+
const observation = runtimeObservationFromTaskEvent(event);
|
|
114
|
+
if (admitted && observation?.fence.receiptId === turn.attemptId
|
|
115
|
+
&& observation.fence.runId === current.id && observation.fence.nativeTurnId === turn.nativeTurnId
|
|
116
|
+
&& observation.kind !== "input.delivery-unknown")
|
|
117
|
+
continue;
|
|
118
|
+
if (observation === null || observation.kind === "input.accepted"
|
|
119
|
+
|| observation.kind === "input.delivery-unknown"
|
|
120
|
+
|| observation.kind.startsWith("turn.")
|
|
121
|
+
&& observation.fence.nativeTurnId !== retry.failedNativeTurnId)
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
for (let i = start + 1; i <= end; i++) {
|
|
125
|
+
const prior = runs[i - 1];
|
|
126
|
+
const run = runs[i];
|
|
127
|
+
if (!(admitted && i === end)) {
|
|
128
|
+
if (run.status !== "failed" || run.result?.provider !== undefined
|
|
129
|
+
|| run.result?.failureReason !== "runtime-failed")
|
|
130
|
+
return false;
|
|
131
|
+
const errors = events.filter(e => e.type === "runtime.agent-error" && e.payload.runId === run.id);
|
|
132
|
+
const proof = errors.find(({ payload: p }) => p.roleName === current.roleName
|
|
133
|
+
&& p.agentId === run.effective.agentId && p.nativeSessionId === retry.nativeSessionId
|
|
134
|
+
&& p.source === "host" && p.adapterId === "codex" && p.driverId === "openai/codex"
|
|
135
|
+
&& p.phase === "turn-submit" && p.inputDisposition === "not-accepted"
|
|
136
|
+
&& p.errorName === "CodexPreSubmissionError" && p.registrationDisposition === "committed"
|
|
137
|
+
&& p.attemptId !== undefined);
|
|
138
|
+
if (proof === undefined || errors.some(e => e.payload.inputDisposition !== "not-accepted")
|
|
139
|
+
|| i === end && proof.payload.attemptId !== turn.attemptId)
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
if (events.some(e => e.type === "run.retried"
|
|
143
|
+
&& e.payload.runId === run.id && e.payload.previousRunId === prior.id))
|
|
144
|
+
continue;
|
|
145
|
+
if (run.inputs[0]?.input.source.channel !== "message-continuation"
|
|
146
|
+
|| !events.some(e => e.type === "message.continuation" && e.payload.runId === run.id))
|
|
147
|
+
return false;
|
|
148
|
+
const snapshot = readRunContextSnapshot(store, run);
|
|
149
|
+
if (!snapshot.resources.some(r => r.ref.store === "source-run" && r.ref.refId === prior.id))
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { isControllerSocketPathForHome } from "./controllerEndpoint.js";
|
|
6
6
|
import { findLiveControllerProcessForHome, inspectLiveControllerProcess } from "./controllerProcessIdentity.js";
|
|
7
7
|
import { readHomeFilesystemId } from "./homeFilesystemIdentity.js";
|
|
8
|
+
import { monotonicMilliseconds } from "./controllerTelemetry.js";
|
|
8
9
|
import { CONTROLLER_DISCOVERY_PATH, ControllerProtocolError, encodeControllerRequest, FILE_TASK_CONTROLLER_PROTOCOL_VERSION, MAX_CONTROLLER_MESSAGE_BYTES, parseControllerDiscovery, parseControllerResponse } from "./protocol.js";
|
|
9
10
|
export class ControllerClientError extends Error {
|
|
10
11
|
code;
|
|
@@ -169,6 +170,10 @@ export async function stopOrphanedFileTaskController(home, timeoutMs, options =
|
|
|
169
170
|
}
|
|
170
171
|
function exchange(socketPath, requestLine, expectedId, timeoutMs, method) {
|
|
171
172
|
return new Promise((resolve, reject) => {
|
|
173
|
+
const startedAtMs = monotonicMilliseconds();
|
|
174
|
+
let connectedAtMs = null;
|
|
175
|
+
let writeCompletedAtMs = null;
|
|
176
|
+
let responseAtMs = null;
|
|
172
177
|
const socket = createConnection(socketPath);
|
|
173
178
|
let buffer = Buffer.alloc(0);
|
|
174
179
|
let settled = false;
|
|
@@ -194,19 +199,28 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs, method) {
|
|
|
194
199
|
delivery: error.diagnostic?.delivery ?? (deliveryStarted ? "unconfirmed" : "not-sent"),
|
|
195
200
|
method,
|
|
196
201
|
requestId: expectedId,
|
|
197
|
-
socketPath
|
|
202
|
+
socketPath,
|
|
203
|
+
timing: { startedAtMs, connectedAtMs, writeCompletedAtMs, responseAtMs,
|
|
204
|
+
endedAtMs: monotonicMilliseconds() }
|
|
198
205
|
}));
|
|
199
206
|
};
|
|
200
207
|
socket.on("connect", () => {
|
|
208
|
+
if (settled)
|
|
209
|
+
return;
|
|
210
|
+
connectedAtMs = monotonicMilliseconds();
|
|
201
211
|
// Once write begins, a missing response cannot prove that the Controller
|
|
202
212
|
// did not commit the request. Callers must use their domain identity to
|
|
203
213
|
// decide whether an explicit retry is safe.
|
|
204
214
|
deliveryStarted = true;
|
|
205
|
-
socket.write(requestLine
|
|
215
|
+
socket.write(requestLine, error => {
|
|
216
|
+
if (error == null)
|
|
217
|
+
writeCompletedAtMs = monotonicMilliseconds();
|
|
218
|
+
});
|
|
206
219
|
});
|
|
207
220
|
socket.on("data", (chunk) => {
|
|
208
221
|
if (settled)
|
|
209
222
|
return;
|
|
223
|
+
responseAtMs ??= monotonicMilliseconds();
|
|
210
224
|
buffer = Buffer.concat([buffer, chunk]);
|
|
211
225
|
const newline = buffer.indexOf(0x0a);
|
|
212
226
|
if (newline < 0) {
|
|
@@ -17,7 +17,7 @@ import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
|
|
|
17
17
|
import { CommandExecutionError } from "../tmux/commandExecutor.js";
|
|
18
18
|
import { redactAgentErrorText } from "../runtime/agentError.js";
|
|
19
19
|
import { detectRunningRelease, readActiveReleasePointer, readHandoverFence, verifyReleaseIntegrity, writeHandoverFence, writeRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
20
|
-
import { isBuiltinControllerMethod, ControllerCommandObserver, ControllerEventLoopDelay } from "./controllerTelemetry.js";
|
|
20
|
+
import { isBuiltinControllerMethod, ControllerCommandObserver, ControllerEventLoopDelay, monotonicMilliseconds } from "./controllerTelemetry.js";
|
|
21
21
|
import { parseLinuxProcessStartIdentity, removeEphemeralDomainIdentity, readEphemeralDomainIdentity, writeEphemeralDomainIdentity } from "../controller/domainIdentity.js";
|
|
22
22
|
/** Methods that stay available while a handover fence is active. */
|
|
23
23
|
const HANDOVER_ALLOWED_METHODS = new Set([
|
|
@@ -54,7 +54,7 @@ async function startControllerServerLocked(home, dispatcher, beforeDiscoveryRemo
|
|
|
54
54
|
await chmod(socketDirectory, 0o700);
|
|
55
55
|
const token = randomBytes(32).toString("hex");
|
|
56
56
|
let closeRunning = async () => undefined;
|
|
57
|
-
const commandObserver = new ControllerCommandObserver();
|
|
57
|
+
const commandObserver = new ControllerCommandObserver(undefined, options.onRequestDiagnostic);
|
|
58
58
|
const eventLoopDelay = new ControllerEventLoopDelay();
|
|
59
59
|
const telemetry = Object.freeze({
|
|
60
60
|
commandObserver,
|
|
@@ -245,6 +245,7 @@ async function writeDiscoveryAtomically(discoveryPath, discovery) {
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
function receiveRequest(socket, token, dispatcher, stop, status, telemetry, handover, home, homeId, homeFilesystemId, controllerInstanceId, getPrimaryIdentity) {
|
|
248
|
+
const acceptedAtMs = monotonicMilliseconds();
|
|
248
249
|
let buffer = Buffer.alloc(0);
|
|
249
250
|
let complete = false;
|
|
250
251
|
const fail = (code, message, id = "invalid") => {
|
|
@@ -273,7 +274,7 @@ function receiveRequest(socket, token, dispatcher, stop, status, telemetry, hand
|
|
|
273
274
|
return;
|
|
274
275
|
}
|
|
275
276
|
complete = true;
|
|
276
|
-
void routeRequest(socket, buffer.subarray(0, newline).toString("utf8"), token, dispatcher, stop, status, telemetry, handover, home, homeId, homeFilesystemId, controllerInstanceId, getPrimaryIdentity);
|
|
277
|
+
void routeRequest(socket, buffer.subarray(0, newline).toString("utf8"), token, dispatcher, stop, status, telemetry, handover, home, homeId, homeFilesystemId, controllerInstanceId, getPrimaryIdentity, { acceptedAtMs, receivedAtMs: monotonicMilliseconds() });
|
|
277
278
|
});
|
|
278
279
|
socket.on("end", () => {
|
|
279
280
|
if (!complete)
|
|
@@ -281,7 +282,7 @@ function receiveRequest(socket, token, dispatcher, stop, status, telemetry, hand
|
|
|
281
282
|
});
|
|
282
283
|
socket.on("error", () => undefined);
|
|
283
284
|
}
|
|
284
|
-
async function routeRequest(socket, line, token, dispatcher, stop, status, telemetry, handover, home, homeId, homeFilesystemId, controllerInstanceId, getPrimaryIdentity) {
|
|
285
|
+
async function routeRequest(socket, line, token, dispatcher, stop, status, telemetry, handover, home, homeId, homeFilesystemId, controllerInstanceId, getPrimaryIdentity, socketTiming) {
|
|
285
286
|
let request;
|
|
286
287
|
try {
|
|
287
288
|
request = parseControllerRequest(line);
|
|
@@ -311,9 +312,17 @@ async function routeRequest(socket, line, token, dispatcher, stop, status, telem
|
|
|
311
312
|
// observation completes when the response is handed to the socket, so a
|
|
312
313
|
// later status snapshot sees prior built-in completions and the dispatcher
|
|
313
314
|
// service-time metric stays a distinct observation.
|
|
314
|
-
const observation = telemetry.commandObserver.start(request.method, request.id);
|
|
315
|
+
const observation = telemetry.commandObserver.start(request.method, request.id, socketTiming);
|
|
316
|
+
// A queued response is not a local flush, and even a local flush is not a
|
|
317
|
+
// client receipt. Close before finish retains a failed transport observation.
|
|
318
|
+
socket.once("close", () => observation.flushed(true));
|
|
319
|
+
if (socket.destroyed)
|
|
320
|
+
observation.flushed(true);
|
|
315
321
|
const send = (response, onFlushed) => {
|
|
316
322
|
observation.complete(isBuiltinControllerMethod(request.method) ? "builtin" : "dispatched", response.ok ? "success" : "failure");
|
|
323
|
+
// Install only for our response: a peer FIN can auto-end an otherwise
|
|
324
|
+
// empty socket while its asynchronous handler is still running.
|
|
325
|
+
socket.once("finish", () => observation.flushed());
|
|
317
326
|
sendResponse(socket, response, onFlushed);
|
|
318
327
|
};
|
|
319
328
|
// Issue 02: a fenced Controller is mid-handover. It stops accepting new
|
|
@@ -13,10 +13,10 @@ export class ControllerWorkObserver {
|
|
|
13
13
|
constructor(clock = monotonicMilliseconds) {
|
|
14
14
|
this.clock = clock;
|
|
15
15
|
}
|
|
16
|
-
start(phase, target) {
|
|
16
|
+
start(phase, target, startedAtMs = this.clock()) {
|
|
17
17
|
const token = {};
|
|
18
18
|
const entry = { phase, target: target === undefined ? null
|
|
19
|
-
: createHash("sha256").update(target).digest("hex").slice(0, 16), startedAt:
|
|
19
|
+
: createHash("sha256").update(target).digest("hex").slice(0, 16), startedAt: startedAtMs };
|
|
20
20
|
this.#active.set(token, entry);
|
|
21
21
|
return (failed = false) => {
|
|
22
22
|
if (!this.#active.delete(token))
|
|
@@ -76,6 +76,7 @@ const BUILTIN_METHODS = new Set([
|
|
|
76
76
|
"controller.handover-state"
|
|
77
77
|
]);
|
|
78
78
|
const OBSERVED_METHODS = new Set([...BUILTIN_METHODS,
|
|
79
|
+
"runtime.host-observation-apply", "runtime.observation-apply",
|
|
79
80
|
"scheduler.signal", "scheduler.scan", "scheduler.configure",
|
|
80
81
|
"runtime.ensure-role-session", "runtime.stop-role-session",
|
|
81
82
|
"capability.search", "capability.describe", "capability.call",
|
|
@@ -104,6 +105,7 @@ export const productionTelemetryScheduler = {
|
|
|
104
105
|
* for end-to-end latency and is never double-counted.
|
|
105
106
|
*/
|
|
106
107
|
export class ControllerCommandObserver {
|
|
108
|
+
onRequestDiagnostic;
|
|
107
109
|
#clock;
|
|
108
110
|
#received = 0;
|
|
109
111
|
#completed = 0;
|
|
@@ -116,22 +118,64 @@ export class ControllerCommandObserver {
|
|
|
116
118
|
#dispatchedCompleted = 0;
|
|
117
119
|
#dispatchedFailed = 0;
|
|
118
120
|
#observations;
|
|
119
|
-
|
|
121
|
+
#transport;
|
|
122
|
+
constructor(clock = monotonicMilliseconds, onRequestDiagnostic) {
|
|
123
|
+
this.onRequestDiagnostic = onRequestDiagnostic;
|
|
120
124
|
this.#clock = clock;
|
|
121
125
|
this.#observations = new ControllerWorkObserver(clock);
|
|
126
|
+
this.#transport = new ControllerWorkObserver(clock);
|
|
122
127
|
}
|
|
123
|
-
start(method = "unknown", requestId) {
|
|
128
|
+
start(method = "unknown", requestId, socketTiming) {
|
|
124
129
|
this.#received += 1;
|
|
125
130
|
this.#inFlight += 1;
|
|
126
131
|
const token = {};
|
|
127
132
|
this.#inFlightStarted.set(token, this.#clock());
|
|
128
133
|
const finish = this.#observations.start(OBSERVED_METHODS.has(method) ? method : "other", requestId);
|
|
134
|
+
// Attach socket timestamps only after authentication, using the same hash
|
|
135
|
+
// as route accounting. No raw frames, params or tokens enter observations.
|
|
136
|
+
if (socketTiming !== undefined) {
|
|
137
|
+
this.#transport.start("socket.accept-to-route", requestId, socketTiming.acceptedAtMs)();
|
|
138
|
+
this.#transport.start("socket.receive-to-route", requestId, socketTiming.receivedAtMs)();
|
|
139
|
+
}
|
|
140
|
+
const finishFlush = socketTiming === undefined ? () => undefined
|
|
141
|
+
: this.#transport.start("socket.route-to-flush", requestId);
|
|
142
|
+
const routedAtMs = this.#clock();
|
|
143
|
+
let handlerCompletedAtMs, flushCompletedAtMs;
|
|
144
|
+
let handlerFailed = false, flushFailed = false;
|
|
145
|
+
const emit = () => {
|
|
146
|
+
if (socketTiming === undefined || handlerCompletedAtMs === undefined
|
|
147
|
+
|| flushCompletedAtMs === undefined)
|
|
148
|
+
return;
|
|
149
|
+
const elapsedMs = Math.max(0, Math.max(handlerCompletedAtMs, flushCompletedAtMs) - socketTiming.acceptedAtMs);
|
|
150
|
+
if (!handlerFailed && !flushFailed && elapsedMs < 1000)
|
|
151
|
+
return;
|
|
152
|
+
try {
|
|
153
|
+
this.onRequestDiagnostic?.({
|
|
154
|
+
target: requestId === undefined ? null
|
|
155
|
+
: createHash("sha256").update(requestId).digest("hex").slice(0, 16),
|
|
156
|
+
phase: OBSERVED_METHODS.has(method) ? method : "other",
|
|
157
|
+
...socketTiming, routedAtMs, handlerCompletedAtMs, flushCompletedAtMs,
|
|
158
|
+
failed: handlerFailed || flushFailed, handlerFailed, flushFailed, elapsedMs
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch { /* Diagnostic sinks cannot change command effects. */ }
|
|
162
|
+
};
|
|
129
163
|
let completed = false;
|
|
130
164
|
return {
|
|
165
|
+
flushed: (failed = false) => {
|
|
166
|
+
if (flushCompletedAtMs !== undefined)
|
|
167
|
+
return;
|
|
168
|
+
flushCompletedAtMs = this.#clock();
|
|
169
|
+
flushFailed = failed;
|
|
170
|
+
finishFlush(failed);
|
|
171
|
+
emit();
|
|
172
|
+
},
|
|
131
173
|
complete: (kind, outcome) => {
|
|
132
174
|
if (completed)
|
|
133
175
|
return;
|
|
134
176
|
completed = true;
|
|
177
|
+
handlerCompletedAtMs = this.#clock();
|
|
178
|
+
handlerFailed = outcome === "failure";
|
|
135
179
|
this.#inFlight = Math.max(0, this.#inFlight - 1);
|
|
136
180
|
this.#inFlightStarted.delete(token);
|
|
137
181
|
finish(outcome === "failure");
|
|
@@ -150,6 +194,7 @@ export class ControllerCommandObserver {
|
|
|
150
194
|
else {
|
|
151
195
|
this.#dispatchedCompleted += 1;
|
|
152
196
|
}
|
|
197
|
+
emit();
|
|
153
198
|
}
|
|
154
199
|
};
|
|
155
200
|
}
|
|
@@ -171,7 +216,8 @@ export class ControllerCommandObserver {
|
|
|
171
216
|
completed: this.#dispatchedCompleted,
|
|
172
217
|
failed: this.#dispatchedFailed
|
|
173
218
|
},
|
|
174
|
-
observations: this.#observations.snapshot()
|
|
219
|
+
observations: this.#observations.snapshot(),
|
|
220
|
+
transport: this.#transport.snapshot()
|
|
175
221
|
};
|
|
176
222
|
}
|
|
177
223
|
}
|