@zq-silk/yui 0.15.3 → 0.15.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/managedRuntimeEnvironment.js +3 -0
- package/dist/cli.js +35 -127
- package/dist/commands/executionAuditCommands.js +6 -0
- package/dist/commands/taskContextCommand.js +4 -2
- package/dist/context/sessionBootstrapManifest.js +12 -21
- package/dist/controller/clientRuntime.js +1 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +270 -162
- package/dist/controller/runtimeEventInbox.js +8 -0
- package/dist/controller/runtimeEventProcessor.js +31 -4
- package/dist/controller/runtimeHookTurnFence.js +101 -62
- package/dist/controller/runtimeLaunchCoordinator.js +38 -12
- package/dist/controller/runtimeObservationHook.js +17 -1
- package/dist/controller/structuredProviderObservation.js +39 -27
- package/dist/core/controllerClient.js +5 -0
- package/dist/core/controllerServer.js +7 -4
- package/dist/domain/agentResultTransport.js +2 -2
- package/dist/executor/agentExecutor.js +22 -38
- package/dist/executor/executorRegistry.js +16 -5
- package/dist/executor/fileRoleLaunchPlanner.js +22 -28
- package/dist/lifecycle/exactTurnTerminalization.js +3 -3
- package/dist/observability/executionAudit.js +12 -0
- package/dist/repository/executionLaneGitSnapshot.js +4 -3
- package/dist/repository/taskWorkspacePreparer.js +12 -4
- package/dist/review/taskFinalReviewContract.js +13 -32
- package/dist/runtime/agentError.js +299 -12
- package/dist/runtime/agentHost.js +419 -41
- package/dist/runtime/builtinAgentDrivers.js +5 -0
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/ports.js +16 -2
- package/dist/runtime/providerRuntimeIdentity.js +34 -28
- package/dist/runtime/runtimeCoherence.js +91 -0
- package/dist/runtime/runtimeObservation.js +8 -5
- package/dist/runtime/structuredProviderHost.js +53 -44
- package/dist/runtime/tmuxAdapters.js +72 -43
- package/dist/scheduler/activeRoleTurnDelivery.js +59 -11
- package/dist/scheduler/leaderWakeupProcessor.js +61 -7
- package/dist/storage/sqliteSchema.js +9 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/turn/turn.js +7 -1
- package/package.json +1 -1
- package/dist/runtime/exactControlPlane.js +0 -232
|
@@ -3,10 +3,10 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { createRuntimeBinding } from "./runtimeBinding.js";
|
|
5
5
|
import { normalizeRuntimeOwner } from "./runtimeOwner.js";
|
|
6
|
-
import { RuntimeGenerationMismatchError, RuntimeHostContentionError } from "./ports.js";
|
|
6
|
+
import { RuntimeGenerationMismatchError, RuntimeHostContentionError, RuntimeHostUnavailableError, promptPushOutcome } from "./ports.js";
|
|
7
|
+
import { providerDeliveryFailureFrom } from "./agentError.js";
|
|
7
8
|
import { toRuntimeLaunchFailure } from "./launchDiagnostics.js";
|
|
8
9
|
import { requireSafeIdentity } from "./validation.js";
|
|
9
|
-
import { YUI_CONTROL_PLANE_DESCRIPTOR } from "./exactControlPlane.js";
|
|
10
10
|
import { launchBrokerForHome } from "./launchBroker.js";
|
|
11
11
|
import { AGENT_HOST_CONTROL_PROTOCOL, sendAgentHostLaunchControl, sendAgentHostTurnControl, sendAgentHostSteerControl, waitForAgentHostLaunchAck } from "./agentHost.js";
|
|
12
12
|
const DEFAULT_INACTIVITY_TIMEOUT_MS = 300_000;
|
|
@@ -307,11 +307,9 @@ export class TmuxSessionHost {
|
|
|
307
307
|
}
|
|
308
308
|
const broker = launchBrokerForHome(yuiHome);
|
|
309
309
|
const sessionManifest = planned.launch.env.YUI_SESSION_MANIFEST;
|
|
310
|
-
const frozenControlPlane = planned.launch.env[YUI_CONTROL_PLANE_DESCRIPTOR];
|
|
311
310
|
if (request.owner.scope === "task" && request.turnId !== undefined
|
|
312
|
-
&&
|
|
313
|
-
|
|
314
|
-
throw new Error("Managed Task Agent Host launch is missing its Session Manifest or frozen control descriptor.");
|
|
311
|
+
&& sessionManifest === undefined) {
|
|
312
|
+
throw new Error("Managed Task Agent Host launch is missing its Session Manifest.");
|
|
315
313
|
}
|
|
316
314
|
const reservation = broker.reserve(Object.freeze({
|
|
317
315
|
schemaVersion: 2,
|
|
@@ -352,10 +350,7 @@ export class TmuxSessionHost {
|
|
|
352
350
|
: { YUI_WORKSPACE: planned.launch.env.YUI_WORKSPACE }),
|
|
353
351
|
...(sessionManifest === undefined
|
|
354
352
|
? {}
|
|
355
|
-
: { YUI_SESSION_MANIFEST: sessionManifest })
|
|
356
|
-
...(frozenControlPlane === undefined
|
|
357
|
-
? {}
|
|
358
|
-
: { [YUI_CONTROL_PLANE_DESCRIPTOR]: frozenControlPlane })
|
|
353
|
+
: { YUI_SESSION_MANIFEST: sessionManifest })
|
|
359
354
|
}
|
|
360
355
|
};
|
|
361
356
|
let hostCreated = false;
|
|
@@ -413,18 +408,28 @@ export class TmuxSessionHost {
|
|
|
413
408
|
if (controlResult.outcome === "active-same-generation") {
|
|
414
409
|
broker.revoke(request.runtimeGenerationId);
|
|
415
410
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
411
|
+
// Identity and readiness are separate facts. Only a genuinely
|
|
412
|
+
// different generation is a conflict that must fail closed, not
|
|
413
|
+
// permission to stop the Host; a matching unsettled generation is this
|
|
414
|
+
// exact activation still coming up, and stopping it would destroy a
|
|
415
|
+
// healthy Session (and any Turn it is carrying).
|
|
416
|
+
const observedGeneration = controlResult.snapshot.runtimeGenerationId;
|
|
417
|
+
if (observedGeneration !== reservation.runtimeGenerationId) {
|
|
419
418
|
broker.revoke(request.runtimeGenerationId);
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
419
|
+
throw new RuntimeGenerationMismatchError(reservation.runtimeGenerationId, observedGeneration, controlResult.snapshot.state, `Agent Host acknowledgement generation mismatch for ${reservation.runtimeGenerationId}; observed=${observedGeneration ?? "none"}; `
|
|
420
|
+
+ `state=${controlResult.snapshot.state}${describeHostFailure(controlResult)}.`);
|
|
421
|
+
}
|
|
422
|
+
if (!["idle", "ready", "busy"].includes(controlResult.snapshot.state)) {
|
|
423
|
+
broker.revoke(request.runtimeGenerationId);
|
|
424
|
+
if (["starting", "settling", "delivery-unknown"].includes(controlResult.snapshot.state)) {
|
|
425
|
+
// The right generation is present but not yet deliverable. This
|
|
426
|
+
// is transient backpressure, so leave the Host running and let
|
|
427
|
+
// the caller retry rather than terminalizing the Turn.
|
|
428
|
+
throw new RuntimeHostContentionError("provider-child-active", `The Agent Host for ${request.owner.roleName} is still ${controlResult.snapshot.state} on this exact generation${describeHostFailure(controlResult)}.`);
|
|
425
429
|
}
|
|
426
|
-
|
|
427
|
-
|
|
430
|
+
// This launch is unusable. It did not create this Host, so failure
|
|
431
|
+
// is not authority to clean its resources or unknown execution.
|
|
432
|
+
throw new RuntimeHostUnavailableError(reservation.runtimeGenerationId, controlResult.snapshot.state, `Agent Host reached ${controlResult.snapshot.state} for ${reservation.runtimeGenerationId}${describeHostFailure(controlResult)}.`, { cause: controlResult.failure ?? controlResult.snapshot });
|
|
428
433
|
}
|
|
429
434
|
providerSnapshot = controlResult.snapshot;
|
|
430
435
|
providerDispatchObserved = true;
|
|
@@ -551,6 +556,15 @@ async function deadHostLaunchFailure(tmux, hostId, roleName, pane, context) {
|
|
|
551
556
|
...(stderrTail === undefined || stderrTail.length === 0 ? {} : { stderrTail })
|
|
552
557
|
});
|
|
553
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* Appends the Host's own cause to a launch diagnostic. Without this the
|
|
561
|
+
* caller only learns the state name and the real reason stays trapped in the
|
|
562
|
+
* Host process.
|
|
563
|
+
*/
|
|
564
|
+
function describeHostFailure(result) {
|
|
565
|
+
const detail = result.failure?.detail ?? result.snapshot.detail;
|
|
566
|
+
return detail === undefined ? "" : `; detail=${detail}`;
|
|
567
|
+
}
|
|
554
568
|
/** Structured managed-Turn input; tmux remains presentation/liveness only. */
|
|
555
569
|
export class AgentHostPromptPushAdapter {
|
|
556
570
|
home;
|
|
@@ -562,7 +576,7 @@ export class AgentHostPromptPushAdapter {
|
|
|
562
576
|
if (ref.scope !== "task" || request.binding.nativeSessionId === undefined
|
|
563
577
|
|| request.binding.providerAuthority === undefined
|
|
564
578
|
|| request.binding.providerAuthority.owner !== "controller") {
|
|
565
|
-
return "unavailable";
|
|
579
|
+
return promptPushOutcome("unavailable");
|
|
566
580
|
}
|
|
567
581
|
try {
|
|
568
582
|
const result = await sendAgentHostTurnControl({
|
|
@@ -583,28 +597,28 @@ export class AgentHostPromptPushAdapter {
|
|
|
583
597
|
}
|
|
584
598
|
}
|
|
585
599
|
});
|
|
586
|
-
|
|
587
|
-
|
|
600
|
+
// The Host attaches its own structured cause to every non-delivery.
|
|
601
|
+
const failure = result.failure;
|
|
602
|
+
if (result.snapshot.state === "delivery-unknown") {
|
|
603
|
+
return promptPushOutcome("delivery-unknown", failure);
|
|
604
|
+
}
|
|
588
605
|
if (result.snapshot.state === "busy")
|
|
589
|
-
return "busy";
|
|
606
|
+
return promptPushOutcome("busy", failure);
|
|
590
607
|
if (result.outcome === "rejected")
|
|
591
|
-
return "rejected";
|
|
608
|
+
return promptPushOutcome("rejected", failure);
|
|
592
609
|
if (result.snapshot.attemptId !== request.envelope.id) {
|
|
593
610
|
return result.snapshot.state === "starting" || result.snapshot.state === "settling"
|
|
594
|
-
? "busy"
|
|
595
|
-
: "unavailable";
|
|
611
|
+
? promptPushOutcome("busy", failure)
|
|
612
|
+
: promptPushOutcome("unavailable", failure);
|
|
596
613
|
}
|
|
597
614
|
if (result.snapshot.state === "ready")
|
|
598
|
-
return "delivered";
|
|
615
|
+
return promptPushOutcome("delivered");
|
|
599
616
|
return result.snapshot.state === "starting" || result.snapshot.state === "settling"
|
|
600
|
-
? "busy"
|
|
601
|
-
: "unavailable";
|
|
617
|
+
? promptPushOutcome("busy", failure)
|
|
618
|
+
: promptPushOutcome("unavailable", failure);
|
|
602
619
|
}
|
|
603
620
|
catch (error) {
|
|
604
|
-
|
|
605
|
-
return code === "ENOENT" || code === "ECONNREFUSED"
|
|
606
|
-
? "unavailable"
|
|
607
|
-
: "delivery-unknown";
|
|
621
|
+
return transportFailureOutcome(error, "turn-submit", request.envelope.id);
|
|
608
622
|
}
|
|
609
623
|
}
|
|
610
624
|
async trySteer(request) {
|
|
@@ -628,21 +642,36 @@ export class AgentHostPromptPushAdapter {
|
|
|
628
642
|
}
|
|
629
643
|
});
|
|
630
644
|
if (result.outcome === "accepted")
|
|
631
|
-
return "delivered";
|
|
632
|
-
|
|
633
|
-
|
|
645
|
+
return promptPushOutcome("delivered");
|
|
646
|
+
const failure = result.failure;
|
|
647
|
+
if (result.snapshot.state === "delivery-unknown") {
|
|
648
|
+
return promptPushOutcome("delivery-unknown", failure);
|
|
649
|
+
}
|
|
634
650
|
if (result.snapshot.state === "busy")
|
|
635
|
-
return "busy";
|
|
636
|
-
return result.outcome === "rejected"
|
|
651
|
+
return promptPushOutcome("busy", failure);
|
|
652
|
+
return result.outcome === "rejected"
|
|
653
|
+
? promptPushOutcome("rejected", failure)
|
|
654
|
+
: promptPushOutcome("unavailable", failure);
|
|
637
655
|
}
|
|
638
656
|
catch (error) {
|
|
639
|
-
|
|
640
|
-
return code === "ENOENT" || code === "ECONNREFUSED"
|
|
641
|
-
? "unavailable"
|
|
642
|
-
: "delivery-unknown";
|
|
657
|
+
return transportFailureOutcome(error, "turn-submit", request.envelope.id);
|
|
643
658
|
}
|
|
644
659
|
}
|
|
645
660
|
}
|
|
661
|
+
/**
|
|
662
|
+
* A transport failure decides the input disposition. A refused or absent
|
|
663
|
+
* socket proves the Host never received the request; anything else leaves
|
|
664
|
+
* delivery genuinely ambiguous and must not be replayed automatically.
|
|
665
|
+
*/
|
|
666
|
+
function transportFailureOutcome(error, phase, attemptId) {
|
|
667
|
+
const code = error.code;
|
|
668
|
+
const unreachable = code === "ENOENT" || code === "ECONNREFUSED";
|
|
669
|
+
return promptPushOutcome(unreachable ? "unavailable" : "delivery-unknown", providerDeliveryFailureFrom(error, {
|
|
670
|
+
phase,
|
|
671
|
+
attemptId,
|
|
672
|
+
inputDisposition: unreachable ? "not-accepted" : "unknown"
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
646
675
|
async function ensureRoleWindow(tmux, hostId, role, launch) {
|
|
647
676
|
return tmux.ensureRoleWindowAsync === undefined
|
|
648
677
|
? tmux.ensureRoleWindow(hostId, role, launch)
|
|
@@ -2,9 +2,9 @@ import { serializeTurnInputEnvelope } from "../context/turnInputContract.js";
|
|
|
2
2
|
import { roleSessionMayContinue, sameEffectiveLaunch } from "../executor/effectiveLaunch.js";
|
|
3
3
|
import { RuntimeLifecycleBusyError } from "../runtime/lifecycleReservation.js";
|
|
4
4
|
import { managedProviderTurnId } from "../runtime/providerRuntimeIdentity.js";
|
|
5
|
-
import { serializeAgentErrorRaw } from "../runtime/agentError.js";
|
|
5
|
+
import { formatProviderDeliveryFailure, providerDeliveryFailureFacts, innermostCauseName, serializeAgentErrorRaw } from "../runtime/agentError.js";
|
|
6
6
|
import { RuntimeLaunchFailure } from "../runtime/launchDiagnostics.js";
|
|
7
|
-
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
7
|
+
import { RuntimeGenerationMismatchError, RuntimeLaunchError } from "../runtime/ports.js";
|
|
8
8
|
import { formatTurnReceiptId } from "../task/taskRecordReference.js";
|
|
9
9
|
import { turnInputEnvelope } from "../turn/turn.js";
|
|
10
10
|
import { captureRoleTurnDispatch } from "../coordination/workMailboxQueue.js";
|
|
@@ -63,7 +63,10 @@ async function deliverActiveTurn(store, delivery, task, role, turn, now) {
|
|
|
63
63
|
if (currentProviderTurn !== null) {
|
|
64
64
|
const reason = currentProviderTurn.terminalReason
|
|
65
65
|
?? `Provider Turn ended with status ${currentProviderTurn.status} without recording its Turn result.`;
|
|
66
|
-
|
|
66
|
+
// A terminal Provider projection without an application result is a
|
|
67
|
+
// framework consistency failure, not evidence that the Agent omitted its
|
|
68
|
+
// report. Keep the exact input fenced; never submit it again.
|
|
69
|
+
return { ...base, status: "skipped", reason: "delivery-uncertain", error: reason };
|
|
67
70
|
}
|
|
68
71
|
const attemptId = initialAttemptId;
|
|
69
72
|
const mode = turn.mode;
|
|
@@ -117,29 +120,59 @@ async function deliverActiveTurn(store, delivery, task, role, turn, now) {
|
|
|
117
120
|
receiptId: attemptId,
|
|
118
121
|
text: serializeTurnInputEnvelope(turnInputEnvelope(turn))
|
|
119
122
|
});
|
|
120
|
-
if (outcome === "busy" || outcome === "unavailable") {
|
|
123
|
+
if (outcome.status === "busy" || outcome.status === "unavailable") {
|
|
121
124
|
forget(delivery, task.id, role.name, turn.id, ready.prepared.runtimeGenerationId);
|
|
122
125
|
return {
|
|
123
126
|
...base,
|
|
124
127
|
status: "skipped",
|
|
125
|
-
reason: outcome === "busy" ? "not-ready" : "runtime-unavailable"
|
|
128
|
+
reason: outcome.status === "busy" ? "not-ready" : "runtime-unavailable"
|
|
126
129
|
};
|
|
127
130
|
}
|
|
128
|
-
if (outcome === "rejected" || outcome === "delivery-unknown") {
|
|
131
|
+
if (outcome.status === "rejected" || outcome.status === "delivery-unknown") {
|
|
129
132
|
forget(delivery, task.id, role.name, turn.id, ready.prepared.runtimeGenerationId);
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
const unknown = outcome.status === "delivery-unknown";
|
|
134
|
+
const failure = outcome.failure;
|
|
135
|
+
// The Host's own account of the failure. Without it the only honest
|
|
136
|
+
// statement is that delivery did not complete — never that the Provider
|
|
137
|
+
// rejected the input, which is one specific cause among many.
|
|
138
|
+
const cause = failure === undefined
|
|
139
|
+
? `The Agent Host did not deliver the managed Turn (${outcome.status}) and reported no cause.`
|
|
140
|
+
: formatProviderDeliveryFailure(failure);
|
|
141
|
+
// This path is the common Provider write failure and previously left no
|
|
142
|
+
// durable fact at all, so the cause was unrecoverable after the fact.
|
|
143
|
+
store.recordAgentError?.({
|
|
144
|
+
taskId: task.id,
|
|
145
|
+
roleName: role.name,
|
|
146
|
+
turnId: turn.id,
|
|
147
|
+
source: "host",
|
|
148
|
+
phase: failure?.phase ?? "turn-submit",
|
|
149
|
+
message: cause,
|
|
150
|
+
// The Host already serialized the complete redacted chain. Re-serializing
|
|
151
|
+
// the failure object here would persist this layer's wrapper instead of
|
|
152
|
+
// the original cause, which is the detail an authorized reader needs.
|
|
153
|
+
raw: failure?.raw ?? serializeAgentErrorRaw(failure ?? cause),
|
|
154
|
+
inputDisposition: failure?.inputDisposition ?? (unknown ? "unknown" : "not-accepted"),
|
|
155
|
+
...providerDeliveryFailureFacts(failure)
|
|
156
|
+
}, now);
|
|
157
|
+
return failTurnDelivery(store, turn, now, unknown ? "delivery-unknown" : "runtime-failed", unknown
|
|
158
|
+
? `Provider Turn delivery is ambiguous; Yui will not replay it automatically. ${cause}`
|
|
159
|
+
: cause);
|
|
133
160
|
}
|
|
134
161
|
forget(delivery, task.id, role.name, turn.id, ready.prepared.runtimeGenerationId);
|
|
135
162
|
settleAcceptedRoleTurnDispatch(store, turn, dispatchToken);
|
|
136
163
|
return {
|
|
137
164
|
...base,
|
|
138
|
-
status: outcome === "sent" ? "delivered" : "already-delivered"
|
|
165
|
+
status: outcome.status === "sent" ? "delivered" : "already-delivered"
|
|
139
166
|
};
|
|
140
167
|
}
|
|
141
168
|
catch (error) {
|
|
142
169
|
const message = error instanceof Error ? error.message : String(error);
|
|
170
|
+
// A launch failure carries its own structure — the class that threw, the
|
|
171
|
+
// innermost cause, and for a generation mismatch both generations. Recording
|
|
172
|
+
// only `message` flattened all of it into prose that no reader could
|
|
173
|
+
// reliably parse back.
|
|
174
|
+
const causeName = innermostCauseName(error);
|
|
175
|
+
const mismatch = error instanceof RuntimeGenerationMismatchError ? error : undefined;
|
|
143
176
|
store.recordAgentError?.({
|
|
144
177
|
taskId: task.id,
|
|
145
178
|
roleName: role.name,
|
|
@@ -148,7 +181,22 @@ async function deliverActiveTurn(store, delivery, task, role, turn, now) {
|
|
|
148
181
|
phase: submitted ? "turn-submit" : mode === "new" ? "session-start" : "session-restore",
|
|
149
182
|
message,
|
|
150
183
|
raw: serializeAgentErrorRaw(error),
|
|
151
|
-
inputDisposition: submitted ? "unknown" : "not-accepted"
|
|
184
|
+
inputDisposition: submitted ? "unknown" : "not-accepted",
|
|
185
|
+
// Nothing was submitted, so the Provider provably holds no registration
|
|
186
|
+
// for this attempt; after a submit the Host owns that fact, not this layer.
|
|
187
|
+
...(submitted ? {} : { registrationDisposition: "not-committed" }),
|
|
188
|
+
...(error instanceof Error ? { errorName: error.name } : {}),
|
|
189
|
+
...(causeName === undefined ? {} : { causeName }),
|
|
190
|
+
...(mismatch === undefined ? {} : {
|
|
191
|
+
expectedRuntimeGenerationId: mismatch.expectedRuntimeGenerationId,
|
|
192
|
+
...(mismatch.observedRuntimeGenerationId === undefined
|
|
193
|
+
? {}
|
|
194
|
+
: { observedRuntimeGenerationId: mismatch.observedRuntimeGenerationId })
|
|
195
|
+
}),
|
|
196
|
+
...(error instanceof RuntimeLaunchError
|
|
197
|
+
? { expectedRuntimeGenerationId: error.runtimeGenerationId }
|
|
198
|
+
: {}),
|
|
199
|
+
attemptId
|
|
152
200
|
}, now);
|
|
153
201
|
if (error instanceof RuntimeLifecycleBusyError
|
|
154
202
|
|| (error instanceof RuntimeLaunchError && error.retryable)) {
|
|
@@ -3,6 +3,7 @@ import { roleSessionMayContinue } from "../executor/effectiveLaunch.js";
|
|
|
3
3
|
import { roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
|
|
4
4
|
import { createTurn } from "../turn/turn.js";
|
|
5
5
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
6
|
+
import { formatProviderDeliveryFailure, providerDeliveryFailureFacts, serializeAgentErrorRaw, innermostCauseName } from "../runtime/agentError.js";
|
|
6
7
|
export const LEADER_WAKE_AGGREGATION_MS = 60_000;
|
|
7
8
|
export const LEADER_WAKE_FORCE_MS = 10 * 60_000;
|
|
8
9
|
/**
|
|
@@ -146,6 +147,15 @@ async function forceLeaderSteer(store, delivery, taskId, roleName, active, now)
|
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
149
|
const owner = `leader-steer:${active.id}`;
|
|
150
|
+
if (existing?.owner === owner) {
|
|
151
|
+
// A previous pass already claimed this input. If it could not settle the
|
|
152
|
+
// claim, delivery may have happened even when the Host has since restarted.
|
|
153
|
+
// Do not turn a mailbox read into an automatic Provider retry.
|
|
154
|
+
return {
|
|
155
|
+
taskId, turnId: active.id, status: "skipped", reason: "not-ready",
|
|
156
|
+
error: "The existing Leader steer attempt is unresolved; its mailbox claim and error evidence are preserved."
|
|
157
|
+
};
|
|
158
|
+
}
|
|
149
159
|
const batchId = existing?.batchId
|
|
150
160
|
?? `leader-steer:${encodeURIComponent(taskId)}:${encodeURIComponent(active.id)}:${pending.fromSequence}-${pending.toSequence}`;
|
|
151
161
|
const claim = store.claimWorkMailbox({ target, batchId, owner, now });
|
|
@@ -156,6 +166,11 @@ async function forceLeaderSteer(store, delivery, taskId, roleName, active, now)
|
|
|
156
166
|
if (processing.batchId !== batchId || processing.owner !== owner) {
|
|
157
167
|
return { taskId, turnId: active.id, status: "skipped", reason: "busy" };
|
|
158
168
|
}
|
|
169
|
+
const receiptId = `turn-input:${taskId}/${active.id}/${batchId}`;
|
|
170
|
+
let inputMayBeAccepted = false;
|
|
171
|
+
let inputAccepted = false;
|
|
172
|
+
let deliveryReturned = false;
|
|
173
|
+
let runtimeGenerationId;
|
|
159
174
|
try {
|
|
160
175
|
const sessions = store.getTaskRoleSessionSet?.(taskId, roleName) ?? null;
|
|
161
176
|
const session = sessions?.sessions[active.effective.agentId];
|
|
@@ -192,6 +207,8 @@ async function forceLeaderSteer(store, delivery, taskId, roleName, active, now)
|
|
|
192
207
|
directive,
|
|
193
208
|
deltaRefIds: []
|
|
194
209
|
});
|
|
210
|
+
runtimeGenerationId = session.runtimeGenerationId;
|
|
211
|
+
inputMayBeAccepted = true;
|
|
195
212
|
const outcome = await delivery.steerOnce({
|
|
196
213
|
taskId,
|
|
197
214
|
roleName,
|
|
@@ -205,18 +222,40 @@ async function forceLeaderSteer(store, delivery, taskId, roleName, active, now)
|
|
|
205
222
|
owner: "controller",
|
|
206
223
|
holderId: authority.holderId
|
|
207
224
|
},
|
|
208
|
-
receiptId
|
|
225
|
+
receiptId,
|
|
209
226
|
text: directive
|
|
210
227
|
});
|
|
211
|
-
|
|
212
|
-
|
|
228
|
+
deliveryReturned = true;
|
|
229
|
+
inputAccepted = outcome.status === "sent" || outcome.status === "already-sent"
|
|
230
|
+
|| outcome.failure?.inputDisposition === "accepted";
|
|
231
|
+
inputMayBeAccepted = outcome.status === "delivery-unknown" || inputAccepted;
|
|
232
|
+
if (outcome.status !== "sent" && outcome.status !== "already-sent") {
|
|
233
|
+
if (!inputMayBeAccepted)
|
|
213
234
|
store.releaseWorkMailbox(target, batchId);
|
|
235
|
+
const failure = outcome.failure;
|
|
236
|
+
if (failure !== undefined || outcome.status === "rejected" || outcome.status === "delivery-unknown") {
|
|
237
|
+
store.recordAgentError?.({
|
|
238
|
+
taskId, roleName, turnId: active.id,
|
|
239
|
+
source: "host",
|
|
240
|
+
phase: failure?.phase ?? "turn-submit",
|
|
241
|
+
message: failure === undefined ? `Leader steer ${outcome.status}.` : formatProviderDeliveryFailure(failure),
|
|
242
|
+
raw: failure?.raw ?? serializeAgentErrorRaw(failure ?? outcome.status),
|
|
243
|
+
inputDisposition: failure?.inputDisposition
|
|
244
|
+
?? (outcome.status === "delivery-unknown" ? "unknown" : "not-accepted"),
|
|
245
|
+
...providerDeliveryFailureFacts(failure),
|
|
246
|
+
attemptId: receiptId
|
|
247
|
+
}, now);
|
|
248
|
+
}
|
|
214
249
|
return {
|
|
215
250
|
taskId,
|
|
216
251
|
turnId: active.id,
|
|
217
|
-
status: outcome === "busy" ? "skipped" : "failed",
|
|
218
|
-
reason: outcome === "busy" ? "busy" : "not-ready",
|
|
219
|
-
|
|
252
|
+
status: outcome.status === "busy" ? "skipped" : "failed",
|
|
253
|
+
reason: outcome.status === "busy" ? "busy" : "not-ready",
|
|
254
|
+
// Report the Host's cause when it supplied one; the bare status word
|
|
255
|
+
// alone cannot distinguish a rejection from a lost transport.
|
|
256
|
+
error: outcome.failure === undefined
|
|
257
|
+
? outcome.status
|
|
258
|
+
: `${outcome.status}: ${formatProviderDeliveryFailure(outcome.failure)}`
|
|
220
259
|
};
|
|
221
260
|
}
|
|
222
261
|
const saved = store.saveLeaderSteer({ taskId, turnId: active.id, batchId, input, now });
|
|
@@ -227,7 +266,22 @@ async function forceLeaderSteer(store, delivery, taskId, roleName, active, now)
|
|
|
227
266
|
: { taskId, turnId: active.id, status: "skipped", reason: saved };
|
|
228
267
|
}
|
|
229
268
|
catch (error) {
|
|
230
|
-
|
|
269
|
+
// A transport exception (or failure after delivery) is not evidence that
|
|
270
|
+
// the input was unsubmitted. Preserve its exact mailbox attempt.
|
|
271
|
+
if (!inputMayBeAccepted)
|
|
272
|
+
store.releaseWorkMailbox(target, batchId);
|
|
273
|
+
store.recordAgentError?.({
|
|
274
|
+
taskId, roleName, turnId: active.id,
|
|
275
|
+
source: inputMayBeAccepted && !deliveryReturned ? "host" : "yui",
|
|
276
|
+
phase: "turn-submit",
|
|
277
|
+
message: error instanceof Error ? error.message : String(error),
|
|
278
|
+
raw: serializeAgentErrorRaw(error),
|
|
279
|
+
inputDisposition: inputAccepted ? "accepted" : inputMayBeAccepted ? "unknown" : "not-accepted",
|
|
280
|
+
...(error instanceof Error ? { errorName: error.name } : {}),
|
|
281
|
+
causeName: innermostCauseName(error),
|
|
282
|
+
expectedRuntimeGenerationId: runtimeGenerationId,
|
|
283
|
+
attemptId: receiptId
|
|
284
|
+
}, now);
|
|
231
285
|
return {
|
|
232
286
|
taskId,
|
|
233
287
|
turnId: active.id,
|
|
@@ -688,6 +688,15 @@ CREATE UNIQUE INDEX idx_durable_jobs_request
|
|
|
688
688
|
ON durable_jobs(task_id, json_extract(payload, '$.operation.actorId'),
|
|
689
689
|
json_extract(payload, '$.operation.requestId'));
|
|
690
690
|
`
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
version: 3,
|
|
694
|
+
name: "exact-attempt-result-identity",
|
|
695
|
+
introducedIn: "0.15.5",
|
|
696
|
+
// Append after the released T01 migration without changing its checksum.
|
|
697
|
+
// Accepted inputs/results can use an exact attempt without a native Turn id.
|
|
698
|
+
// Preserve all valid historical records; never repair failed Turns or logs.
|
|
699
|
+
sql: "SELECT 1; -- exact attempt identity without a fabricated native Turn id"
|
|
691
700
|
}
|
|
692
701
|
]);
|
|
693
702
|
for (let index = 0; index < MIGRATIONS.length; index += 1) {
|
package/dist/turn/turn.js
CHANGED
|
@@ -354,7 +354,13 @@ function validateTurnProviderResult(provider) {
|
|
|
354
354
|
requireText(provider.accountScope, "Provider account scope");
|
|
355
355
|
requireText(provider.conversationId, "Provider Conversation id");
|
|
356
356
|
requireText(provider.activationId, "Provider Activation id");
|
|
357
|
-
|
|
357
|
+
if (provider.nativeTurnId !== undefined)
|
|
358
|
+
requireText(provider.nativeTurnId, "Provider native Turn id");
|
|
359
|
+
if (provider.attemptId !== undefined)
|
|
360
|
+
requireText(provider.attemptId, "Provider attempt id");
|
|
361
|
+
if (provider.nativeTurnId === undefined && provider.attemptId === undefined) {
|
|
362
|
+
throw new Error("Provider result requires a native Turn or exact attempt identity.");
|
|
363
|
+
}
|
|
358
364
|
if (!["completed", "failed", "cancelled"].includes(provider.status)) {
|
|
359
365
|
throw new Error(`Provider Turn result status is invalid: ${String(provider.status)}.`);
|
|
360
366
|
}
|