@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
|
@@ -14,6 +14,7 @@ import { YUI_VERSION, yuiVersionIdentity } from "../version.js";
|
|
|
14
14
|
import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
|
|
15
15
|
import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
|
|
16
16
|
import { CommandExecutionError } from "../tmux/commandExecutor.js";
|
|
17
|
+
import { redactAgentErrorText } from "../runtime/agentError.js";
|
|
17
18
|
import { detectRunningRelease, readActiveReleasePointer, readHandoverFence, verifyReleaseIntegrity, writeHandoverFence, writeRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
18
19
|
import { isBuiltinControllerMethod, ControllerCommandObserver, ControllerEventLoopDelay } from "./controllerTelemetry.js";
|
|
19
20
|
import { parseLinuxProcessStartIdentity, removeEphemeralDomainIdentity, readEphemeralDomainIdentity, writeEphemeralDomainIdentity } from "../controller/domainIdentity.js";
|
|
@@ -838,11 +839,13 @@ function safeApplicationErrorCode(code) {
|
|
|
838
839
|
}
|
|
839
840
|
}
|
|
840
841
|
function safeErrorMessage(message) {
|
|
841
|
-
const safe = message
|
|
842
|
+
const safe = redactAgentErrorText(message)
|
|
842
843
|
.replace(/[\r\n\u2028\u2029]+/gu, " ")
|
|
843
|
-
.trim()
|
|
844
|
-
|
|
845
|
-
|
|
844
|
+
.trim();
|
|
845
|
+
if (safe.length === 0)
|
|
846
|
+
return undefined;
|
|
847
|
+
const marker = "…[truncated]";
|
|
848
|
+
return safe.length <= 512 ? safe : `${safe.slice(0, 512 - marker.length)}${marker}`;
|
|
846
849
|
}
|
|
847
850
|
export async function acquireHomeLifecycleLock(home, options = {}) {
|
|
848
851
|
const lockPath = homeLifecycleLockPath(home);
|
|
@@ -17,7 +17,7 @@ export function transportAgentResult(value) {
|
|
|
17
17
|
return {
|
|
18
18
|
status: "failed",
|
|
19
19
|
diagnostic: "Provider terminal event included an Agent result with an invalid NUL byte.",
|
|
20
|
-
failureReason: "
|
|
20
|
+
failureReason: "runtime-failed"
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
23
|
if (value.trim().length === 0) {
|
|
@@ -32,7 +32,7 @@ export function transportAgentResult(value) {
|
|
|
32
32
|
return {
|
|
33
33
|
status: "failed",
|
|
34
34
|
diagnostic: `Provider Agent result is ${bytes} bytes and exceeds the ${MAX_TURN_RESULT_OUTPUT_BYTES}-byte durable result limit; the result was not stored.`,
|
|
35
|
-
failureReason: "
|
|
35
|
+
failureReason: "runtime-failed"
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
return { status: "completed", output: value };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { hasRecentTurnId, rememberRecentTurnId, validateRecentTurnIds } from "../runtime/recentTurnIds.js";
|
|
3
3
|
import { roleSessionMayContinue, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
|
|
4
|
-
import { currentProviderActivation, endProviderActivation,
|
|
4
|
+
import { currentProviderActivation, endProviderActivation, validateProviderRuntimeBinding } from "../runtime/providerRuntimeIdentity.js";
|
|
5
5
|
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
6
6
|
export function createRoleSessionSet(owner, activeAgentId, now) {
|
|
7
7
|
const base = {
|
|
@@ -264,26 +264,28 @@ export function roleAgentSessionResumeMode(set, agentId, desired) {
|
|
|
264
264
|
const session = set.sessions[requireSafeIdentity(agentId, "Agent id")];
|
|
265
265
|
if (session === undefined)
|
|
266
266
|
return "new";
|
|
267
|
-
// A
|
|
268
|
-
//
|
|
269
|
-
// without a provider-native identity; an explicit verified stop still
|
|
270
|
-
// permits the normal fresh-generation path.
|
|
267
|
+
// A Host status cannot supply a missing native Conversation identity.
|
|
268
|
+
// Replacement remains an explicit operation even after that Host ended.
|
|
271
269
|
if (typeof session.nativeSessionId !== "string"
|
|
272
270
|
|| session.nativeSessionId.trim().length === 0) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
271
|
+
throw new Error(`Role Agent session has no native Session identity: ${agentId}. `
|
|
272
|
+
+ "Restore the exact native Session or explicitly select a new Session.");
|
|
273
|
+
}
|
|
274
|
+
// An ended Host attachment is not evidence that the native Conversation
|
|
275
|
+
// vanished. Resume the exact identity; only an explicit new-Session action
|
|
276
|
+
// may replace it. A known unrecoverable Conversation needs that decision.
|
|
277
|
+
const conversation = set.owner.scope === "task"
|
|
278
|
+
? set.providerBinding?.conversations.find((entry) => entry.conversationId === session.nativeSessionId)
|
|
279
|
+
: undefined;
|
|
280
|
+
if (conversation?.recoverability === "unrecoverable") {
|
|
281
|
+
throw new Error(`Role Agent native Session is not recoverable: ${agentId}/${session.nativeSessionId}. `
|
|
282
|
+
+ "Explicitly select a new Session to continue; existing input attempts are not replayed.");
|
|
281
283
|
}
|
|
282
284
|
if (roleSessionMayContinue(session.effective, desired))
|
|
283
285
|
return "resume";
|
|
284
286
|
throw new Error(`Role Agent session cannot continue under the next launch: ${agentId}. `
|
|
285
|
-
+ "Its Agent, adapter or physical workspace changed
|
|
286
|
-
+ "
|
|
287
|
+
+ "Its Agent, adapter or physical workspace changed. Explicitly select a new Session "
|
|
288
|
+
+ "after resolving existing execution and resource ownership.");
|
|
287
289
|
}
|
|
288
290
|
export function bindTaskRoleProviderRuntime(set, binding, updatedAt) {
|
|
289
291
|
validateRoleSessionSet(set);
|
|
@@ -325,7 +327,7 @@ export function detachRoleAgentSessionHost(set, now) {
|
|
|
325
327
|
return set;
|
|
326
328
|
const timestamp = requireDate(now, "Role Host detach timestamp");
|
|
327
329
|
const { runtimeGenerationId: _runtimeGenerationId, endReason: _endReason, ...session } = active;
|
|
328
|
-
|
|
330
|
+
const updated = validateRoleSessionSet({
|
|
329
331
|
...set,
|
|
330
332
|
sessions: {
|
|
331
333
|
...set.sessions,
|
|
@@ -340,28 +342,10 @@ export function detachRoleAgentSessionHost(set, now) {
|
|
|
340
342
|
if (updated.owner.scope !== "task")
|
|
341
343
|
return updated;
|
|
342
344
|
let taskSet = updated;
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
binding = settleProviderTurn(binding, {
|
|
348
|
-
nativeTurnId: turn.nativeTurnId,
|
|
349
|
-
status: "cancelled",
|
|
350
|
-
settledAt: timestamp,
|
|
351
|
-
reason: "runtime-physical-exit"
|
|
352
|
-
});
|
|
353
|
-
taskSet = updateTaskRoleProviderRuntime(taskSet, binding, now);
|
|
354
|
-
}
|
|
355
|
-
else if (binding !== null && binding !== undefined && turn !== null && turn !== undefined
|
|
356
|
-
&& ["submitting", "delivery-unknown"].includes(turn.status)) {
|
|
357
|
-
binding = settleProviderTurnSubmission(binding, {
|
|
358
|
-
attemptId: turn.attemptId,
|
|
359
|
-
status: "rejected",
|
|
360
|
-
resolvedAt: timestamp,
|
|
361
|
-
reason: "runtime-physical-exit"
|
|
362
|
-
});
|
|
363
|
-
taskSet = updateTaskRoleProviderRuntime(taskSet, binding, now);
|
|
364
|
-
}
|
|
345
|
+
const binding = taskSet.providerBinding;
|
|
346
|
+
// Losing the attachment proves neither cancellation nor non-submission.
|
|
347
|
+
// Preserve the exact input and its acceptance/unknown facts. Only a native
|
|
348
|
+
// terminal or an explicit submission resolution may settle that attempt.
|
|
365
349
|
const activation = binding === null
|
|
366
350
|
? null
|
|
367
351
|
: currentProviderActivation(binding);
|
|
@@ -158,20 +158,20 @@ export class ExecutorRegistry {
|
|
|
158
158
|
createdAt: new Date()
|
|
159
159
|
})
|
|
160
160
|
});
|
|
161
|
-
if (outcome === "delivered") {
|
|
161
|
+
if (outcome.result === "delivered") {
|
|
162
162
|
this.#prepared.delete(input.delivery.prepared.deliveryId);
|
|
163
163
|
}
|
|
164
|
-
return outcome
|
|
164
|
+
return deliveryReport(outcome);
|
|
165
165
|
}
|
|
166
166
|
const outcome = this.tmux.sendRoleInputOnce(input.delivery.prepared.taskId, input.delivery.prepared.roleName, input.receiptId, input.text, this.readiness(input.delivery.prepared.adapterId));
|
|
167
167
|
if (outcome === "sent" || outcome === "already-sent") {
|
|
168
168
|
this.#prepared.delete(input.delivery.prepared.deliveryId);
|
|
169
169
|
}
|
|
170
|
-
return outcome;
|
|
170
|
+
return { status: outcome };
|
|
171
171
|
}
|
|
172
172
|
async steerOnce(input) {
|
|
173
173
|
if (this.runtimePorts === undefined)
|
|
174
|
-
return "unavailable";
|
|
174
|
+
return { status: "unavailable" };
|
|
175
175
|
const outcome = await this.runtimePorts.promptPush.trySteer({
|
|
176
176
|
owner: { scope: "task", taskId: input.taskId, roleName: input.roleName },
|
|
177
177
|
runtimeGenerationId: input.runtimeGenerationId,
|
|
@@ -187,7 +187,7 @@ export class ExecutorRegistry {
|
|
|
187
187
|
createdAt: new Date()
|
|
188
188
|
})
|
|
189
189
|
});
|
|
190
|
-
return outcome
|
|
190
|
+
return deliveryReport(outcome);
|
|
191
191
|
}
|
|
192
192
|
async notifyOperatorInputOnce(input) {
|
|
193
193
|
const probe = this.readiness(input.adapterId, "operator");
|
|
@@ -334,6 +334,17 @@ function activeTurnId(receiptId) {
|
|
|
334
334
|
throw new Error("Turn steer receipt is invalid.");
|
|
335
335
|
return decodeURIComponent(match[1]);
|
|
336
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Maps a runtime push outcome onto the Scheduler's delivery vocabulary while
|
|
339
|
+
* keeping the Host's structured cause attached. Only the word for success
|
|
340
|
+
* differs between the two layers; the failure record is forwarded unchanged.
|
|
341
|
+
*/
|
|
342
|
+
function deliveryReport(outcome) {
|
|
343
|
+
return {
|
|
344
|
+
status: outcome.result === "delivered" ? "sent" : outcome.result,
|
|
345
|
+
...(outcome.failure === undefined ? {} : { failure: outcome.failure })
|
|
346
|
+
};
|
|
347
|
+
}
|
|
337
348
|
export function agentProcessReadinessProbe(adapterId, _surface = "role") {
|
|
338
349
|
if (adapterId !== "codex" && adapterId !== "claude") {
|
|
339
350
|
throw new Error(`No tmux readiness probe is registered for Agent adapter: ${adapterId}.`);
|
|
@@ -16,13 +16,11 @@ import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.j
|
|
|
16
16
|
import { classifyWorkspacePreflight, formatWorkspacePreflightError } from "./workspacePreflightClassification.js";
|
|
17
17
|
import { activeLiveRoleAgentSession } from "./agentExecutor.js";
|
|
18
18
|
import { roleSessionMayContinue, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
|
|
19
|
-
import { YUI_CONTROL_PLANE_DESCRIPTOR, createExactControlPlaneDescriptor, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
|
|
20
|
-
import { detectRunningRelease } from "../release/runtimeRelease.js";
|
|
21
19
|
import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
|
|
22
20
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
23
21
|
import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
24
22
|
import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
|
|
25
|
-
import { currentProviderActivation } from "../runtime/providerRuntimeIdentity.js";
|
|
23
|
+
import { assertProviderConversationReplaceable, currentProviderActivation } from "../runtime/providerRuntimeIdentity.js";
|
|
26
24
|
import { assertCodexLaunchOverridesAvailable, inspectCodexLaunchConfig } from "./codexConfigConflict.js";
|
|
27
25
|
/** Builds managed native Agent launches from the authoritative Task records. */
|
|
28
26
|
export class FileRoleLaunchPlanner {
|
|
@@ -34,7 +32,7 @@ export class FileRoleLaunchPlanner {
|
|
|
34
32
|
#createNativeSessionId;
|
|
35
33
|
#cliPath;
|
|
36
34
|
#inspectWorkspacePhysicalState;
|
|
37
|
-
#
|
|
35
|
+
#entryPoint;
|
|
38
36
|
#resourceRegistrarValue;
|
|
39
37
|
constructor(home, store, options = {}) {
|
|
40
38
|
this.home = home;
|
|
@@ -54,21 +52,10 @@ export class FileRoleLaunchPlanner {
|
|
|
54
52
|
?? inspectWorkspacePhysicalState;
|
|
55
53
|
this.#cliPath = canonicalPath(options.cliPath
|
|
56
54
|
?? fileURLToPath(new URL("../cli.js", import.meta.url)));
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
this.#controlPlane = createExactControlPlaneDescriptor({
|
|
62
|
-
executable: process.execPath,
|
|
63
|
-
cliEntry: this.#cliPath,
|
|
64
|
-
yuiHome: this.home,
|
|
65
|
-
...(runningRelease === null
|
|
66
|
-
? {}
|
|
67
|
-
: {
|
|
68
|
-
buildId: runningRelease.manifest.buildId,
|
|
69
|
-
activeReleaseDigest: runningRelease.manifest.packageDigest
|
|
70
|
-
})
|
|
71
|
-
});
|
|
55
|
+
// Where a managed Session's commands run. Continuity is the Session
|
|
56
|
+
// Manifest plus protocol/storage and durable runtime identity, so no
|
|
57
|
+
// package or build identity belongs in this entry point.
|
|
58
|
+
this.#entryPoint = { executable: process.execPath, cliEntry: this.#cliPath };
|
|
72
59
|
}
|
|
73
60
|
#resourceRegistrar() {
|
|
74
61
|
return this.#resourceRegistrarValue ??= new ResourceRegistrar(this.home);
|
|
@@ -184,6 +171,13 @@ export class FileRoleLaunchPlanner {
|
|
|
184
171
|
&& activeLiveRoleAgentSession(sessionSet) !== null) {
|
|
185
172
|
throw new Error(`Task Role still has a live Session: ${task.id}/${role.name}.`);
|
|
186
173
|
}
|
|
174
|
+
if (input.mode === "new" && sessionSet?.providerBinding !== null
|
|
175
|
+
&& sessionSet?.providerBinding !== undefined) {
|
|
176
|
+
// Planning precedes broker tickets, Provider processes and native
|
|
177
|
+
// Conversation creation. An ended Host is not proof that its input
|
|
178
|
+
// attempt was rejected, cancelled, or completed.
|
|
179
|
+
assertProviderConversationReplaceable(sessionSet.providerBinding);
|
|
180
|
+
}
|
|
187
181
|
return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, runWorkspace, effective, {
|
|
188
182
|
purpose: activeTurn?.purpose ?? "execution"
|
|
189
183
|
});
|
|
@@ -267,7 +261,7 @@ export class FileRoleLaunchPlanner {
|
|
|
267
261
|
owner,
|
|
268
262
|
roleKind: roleSessionKind(launchRole, owner, sessionPolicy.purpose),
|
|
269
263
|
skills: baseSessionContext.skills,
|
|
270
|
-
|
|
264
|
+
entryPoint: this.#entryPoint
|
|
271
265
|
});
|
|
272
266
|
if (effective.contextProtocolVersion !== bootstrap.manifest.schemaVersion
|
|
273
267
|
|| effective.sessionManifestCompatibilityDigest
|
|
@@ -349,8 +343,7 @@ export class FileRoleLaunchPlanner {
|
|
|
349
343
|
for (const path of [
|
|
350
344
|
bootstrap.manifestPath,
|
|
351
345
|
bootstrap.sessionCliPath,
|
|
352
|
-
bootstrap.roleProfilePath
|
|
353
|
-
bootstrap.descriptorPath
|
|
346
|
+
bootstrap.roleProfilePath
|
|
354
347
|
]) {
|
|
355
348
|
this.#resourceRegistrar().registerSessionContext(path, {
|
|
356
349
|
home: resolve(this.home),
|
|
@@ -361,7 +354,11 @@ export class FileRoleLaunchPlanner {
|
|
|
361
354
|
let args = [...compiled.argv];
|
|
362
355
|
let command = configured.command;
|
|
363
356
|
let session;
|
|
364
|
-
|
|
357
|
+
// Managed Claude owns a serialized stream with exact attempt correlation.
|
|
358
|
+
// Its native Hooks carry no such request fence and must not compete with
|
|
359
|
+
// the Host for acceptance, completion, or attachment lifecycle facts.
|
|
360
|
+
// Provider tool permissions remain in compiled config, not this observer.
|
|
361
|
+
if (binding.adapterId === "claude" && owner.scope === "global") {
|
|
365
362
|
args.push("--plugin-dir", ensureClaudeLifecyclePlugin(this.home, this.#cliPath));
|
|
366
363
|
}
|
|
367
364
|
if (binding.adapterId === "codex") {
|
|
@@ -476,11 +473,6 @@ export class FileRoleLaunchPlanner {
|
|
|
476
473
|
? { YUI_AGENT_BASE_ARGS: JSON.stringify(configured.baseArgs) }
|
|
477
474
|
: {}),
|
|
478
475
|
...(jobCallerKey === undefined ? {} : { YUI_JOB_CALLER_KEY: jobCallerKey }),
|
|
479
|
-
...(owner.scope !== "task"
|
|
480
|
-
? {}
|
|
481
|
-
: {
|
|
482
|
-
[YUI_CONTROL_PLANE_DESCRIPTOR]: serializeExactDescriptor(this.#controlPlane)
|
|
483
|
-
}),
|
|
484
476
|
...(sessionTitle === undefined
|
|
485
477
|
? {}
|
|
486
478
|
: {
|
|
@@ -716,6 +708,8 @@ function managedClaudeControlPlaneConfig(config, taskId, workItemId, turnId) {
|
|
|
716
708
|
function isManagedYuiBashRule(rule) {
|
|
717
709
|
const normalized = rule.trim();
|
|
718
710
|
return /^Bash\(yui(?:\s|:\*|\*|\))/u.test(normalized)
|
|
711
|
+
// Yui no longer writes a control-plane digest into a managed rule; this
|
|
712
|
+
// shape only clears one an earlier release left in a Provider config.
|
|
719
713
|
|| /^Bash\(.*\s--yui-control\s/u.test(normalized);
|
|
720
714
|
}
|
|
721
715
|
function canonicalPath(path) {
|
|
@@ -389,9 +389,9 @@ function settleLaunchReservation(store, sessions, input) {
|
|
|
389
389
|
const mailbox = store.getWorkMailbox(target);
|
|
390
390
|
const reservation = mailbox?.processing;
|
|
391
391
|
const session = sessions?.sessions[input.agentId];
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
392
|
+
// A late result releases only its original launch, never a successor's
|
|
393
|
+
// reservation merely because both Turns reused the same native Session.
|
|
394
|
+
const runtimeGenerationId = input.runtimeGenerationId ?? session?.runtimeGenerationId;
|
|
395
395
|
if (runtimeGenerationId === undefined || !isRuntimeLaunchReservation(reservation, runtimeGenerationId))
|
|
396
396
|
return;
|
|
397
397
|
const settled = completeProcessing(mailbox, reservation.batchId);
|
|
@@ -149,6 +149,10 @@ function classifyTerminalSessionTurnRelation(turns, roleName, session, counts) {
|
|
|
149
149
|
function ok(data) {
|
|
150
150
|
return { status: "ok", data };
|
|
151
151
|
}
|
|
152
|
+
/** Keeps only the payload keys the record actually carries. */
|
|
153
|
+
function definedAuditFields(fields) {
|
|
154
|
+
return Object.fromEntries(Object.entries(fields).filter((entry) => entry[1] !== undefined));
|
|
155
|
+
}
|
|
152
156
|
function failed(error) {
|
|
153
157
|
return {
|
|
154
158
|
status: "error",
|
|
@@ -583,6 +587,14 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
583
587
|
raw: event.payload.raw ?? "",
|
|
584
588
|
inputDisposition: event.payload.inputDisposition ?? "unknown",
|
|
585
589
|
sessionDisposition: event.payload.sessionDisposition ?? "unknown",
|
|
590
|
+
...definedAuditFields({
|
|
591
|
+
registrationDisposition: event.payload.registrationDisposition,
|
|
592
|
+
errorName: event.payload.errorName,
|
|
593
|
+
causeName: event.payload.causeName,
|
|
594
|
+
expectedRuntimeGenerationId: event.payload.expectedRuntimeGenerationId,
|
|
595
|
+
observedRuntimeGenerationId: event.payload.observedRuntimeGenerationId,
|
|
596
|
+
attemptId: event.payload.attemptId
|
|
597
|
+
}),
|
|
586
598
|
createdAt: event.createdAt
|
|
587
599
|
});
|
|
588
600
|
}
|
|
@@ -2,9 +2,8 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
/**
|
|
4
4
|
* Freeze the exact committed heads of a durable managed Lane workspace at the
|
|
5
|
-
* synchronous runtime-terminalization boundary
|
|
6
|
-
*
|
|
7
|
-
* cannot use the asynchronous workspace-preparation port.
|
|
5
|
+
* synchronous runtime-terminalization boundary, before opening the result
|
|
6
|
+
* transaction. The committing fold revalidates durable workspace ownership.
|
|
8
7
|
*/
|
|
9
8
|
export function snapshotExecutionLaneWorkspaceSync(store, workspace) {
|
|
10
9
|
if (workspace.owner.type !== "execution-lane") {
|
|
@@ -71,6 +70,8 @@ function git(cwd, args) {
|
|
|
71
70
|
return execFileSync("git", args, {
|
|
72
71
|
cwd,
|
|
73
72
|
encoding: "utf8",
|
|
73
|
+
timeout: 5_000,
|
|
74
|
+
maxBuffer: 512 * 1024,
|
|
74
75
|
stdio: ["ignore", "pipe", "pipe"]
|
|
75
76
|
}).trim();
|
|
76
77
|
}
|
|
@@ -11,7 +11,7 @@ import { activateTask, bindTaskProjectCommits, bindTaskWorkspaceIdentity, synchr
|
|
|
11
11
|
import { validateDraftTaskForActivation } from "../task/draftPlan.js";
|
|
12
12
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
13
13
|
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
14
|
-
import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
|
|
14
|
+
import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, currentWorkItemExecutionGroup, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
|
|
15
15
|
import { createManagedWorkspace, isTaskOwnedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
16
16
|
import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
|
|
17
17
|
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
@@ -473,19 +473,27 @@ export class FileTaskWorkspacePreparer {
|
|
|
473
473
|
// durable WorkItem or ReviewRound workspace owns that cwd; Task-main
|
|
474
474
|
// preparation must not move a Reviewer Session out from under an
|
|
475
475
|
// active or retained ReviewRound. Other Roles use Task main.
|
|
476
|
-
// Prefer the active Turn's exact WorkItem
|
|
477
|
-
//
|
|
476
|
+
// Prefer the active Turn's exact WorkItem, then a retained direct
|
|
477
|
+
// WorkItem owning this cwd, before considering queued assignments.
|
|
478
478
|
const activeRoleTurn = tx.getActiveTurn(task.id, role.name);
|
|
479
479
|
const activeTurnItem = activeRoleTurn !== null
|
|
480
480
|
&& activeRoleTurn.purpose === "execution"
|
|
481
481
|
&& activeRoleTurn.workItemId !== undefined
|
|
482
482
|
? tx.getWorkItem(task.id, activeRoleTurn.workItemId)
|
|
483
483
|
: null;
|
|
484
|
+
// Rejection ends an execution iteration, not its workspace ownership.
|
|
485
|
+
// Preserve this direct WorkItem's existing cwd while dispatch prepares
|
|
486
|
+
// the Task before atomically reopening the failed WorkItem. Merely
|
|
487
|
+
// reading Task context must not migrate its resumable Worker either.
|
|
488
|
+
const retainedItem = tx.listWorkItems(task.id).find((candidate) => (candidate.assignee === role.name
|
|
489
|
+
&& candidate.status === "failed"
|
|
490
|
+
&& currentWorkItemExecutionGroup(candidate) === undefined
|
|
491
|
+
&& tx.getWorkItemWorkspace(task.id, candidate.id)?.root === role.workspace));
|
|
484
492
|
const assignedItem = activeTurnItem !== null
|
|
485
493
|
&& activeTurnItem.assignee === role.name
|
|
486
494
|
&& !["completed", "failed", "retired"].includes(activeTurnItem.status)
|
|
487
495
|
? activeTurnItem
|
|
488
|
-
: tx.listWorkItems(task.id).find((candidate) => (candidate.assignee === role.name
|
|
496
|
+
: retainedItem ?? tx.listWorkItems(task.id).find((candidate) => (candidate.assignee === role.name
|
|
489
497
|
&& !["completed", "failed", "retired"]
|
|
490
498
|
.includes(candidate.status)));
|
|
491
499
|
const assignedWorkspace = assignedItem === undefined
|
|
@@ -1,27 +1,21 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { requireIdentity } from "../domain/validation.js";
|
|
3
2
|
export const TASK_FINAL_REVIEW_ARGUMENT = "--yui-task-final-review";
|
|
4
3
|
export function createTaskFinalReviewContract(input) {
|
|
5
4
|
const taskId = requireIdentity(input.taskId, "Task final-review contract Task id");
|
|
6
5
|
const reviewerRoleName = requireIdentity(input.reviewerRoleName, "Task final-review contract Reviewer Role");
|
|
7
|
-
const controlPlaneDigest = requireDigest(input.controlPlaneDigest, "Task final-review contract control-plane digest");
|
|
8
6
|
return Object.freeze({
|
|
9
|
-
schemaVersion: 1,
|
|
10
7
|
taskId,
|
|
11
|
-
reviewerRoleName
|
|
12
|
-
controlPlaneDigest,
|
|
13
|
-
digest: contractDigest(taskId, reviewerRoleName, controlPlaneDigest)
|
|
8
|
+
reviewerRoleName
|
|
14
9
|
});
|
|
15
10
|
}
|
|
16
11
|
export function validateTaskFinalReviewContract(value) {
|
|
17
|
-
if (typeof value !== "object" || value === null
|
|
18
|
-
throw new Error("Task final-review contract must
|
|
19
|
-
}
|
|
20
|
-
const expected = createTaskFinalReviewContract(value);
|
|
21
|
-
const digest = requireDigest(value.digest, "Task final-review contract digest");
|
|
22
|
-
if (digest !== expected.digest) {
|
|
23
|
-
throw new Error("Task final-review contract digest does not match its immutable fields.");
|
|
12
|
+
if (typeof value !== "object" || value === null) {
|
|
13
|
+
throw new Error("Task final-review contract must be a record.");
|
|
24
14
|
}
|
|
15
|
+
// A contract recorded by an earlier release also carried a version tag and the
|
|
16
|
+
// runtime that established it. Those fields are historical evidence and are
|
|
17
|
+
// never read again; the required fields below are the whole contract.
|
|
18
|
+
createTaskFinalReviewContract(value);
|
|
25
19
|
return value;
|
|
26
20
|
}
|
|
27
21
|
export function taskFinalReviewConfig(contract) {
|
|
@@ -31,13 +25,15 @@ export function taskFinalReviewConfig(contract) {
|
|
|
31
25
|
export function sameTaskFinalReviewContract(left, right) {
|
|
32
26
|
if (left === undefined || right === undefined)
|
|
33
27
|
return left === right;
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
const first = validateTaskFinalReviewContract(left);
|
|
29
|
+
const second = validateTaskFinalReviewContract(right);
|
|
30
|
+
return first.taskId === second.taskId
|
|
31
|
+
&& first.reviewerRoleName === second.reviewerRoleName;
|
|
36
32
|
}
|
|
37
33
|
/**
|
|
38
34
|
* The contract switch is an exact CLI prefix, never an environment variable.
|
|
39
|
-
* It must
|
|
40
|
-
*
|
|
35
|
+
* It must be the first CLI argument so the preflight can bind it to the
|
|
36
|
+
* Leader's managed Session before opening mutable storage.
|
|
41
37
|
*/
|
|
42
38
|
export function extractTaskFinalReviewRequest(args) {
|
|
43
39
|
const index = args.indexOf(TASK_FINAL_REVIEW_ARGUMENT);
|
|
@@ -73,18 +69,3 @@ export function extractTaskFinalReviewRequest(args) {
|
|
|
73
69
|
};
|
|
74
70
|
}
|
|
75
71
|
}
|
|
76
|
-
function contractDigest(taskId, reviewerRoleName, controlPlaneDigest) {
|
|
77
|
-
return createHash("sha256").update(JSON.stringify([
|
|
78
|
-
"yui-task-final-review",
|
|
79
|
-
1,
|
|
80
|
-
taskId,
|
|
81
|
-
reviewerRoleName,
|
|
82
|
-
controlPlaneDigest
|
|
83
|
-
])).digest("hex");
|
|
84
|
-
}
|
|
85
|
-
function requireDigest(value, label) {
|
|
86
|
-
if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
|
|
87
|
-
throw new Error(`${label} is invalid.`);
|
|
88
|
-
}
|
|
89
|
-
return value;
|
|
90
|
-
}
|