@zq-silk/yui 0.8.2 → 0.8.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/ARCHITECTURE.md +40 -22
- package/README.md +61 -9
- package/dist/cli/commandCatalog.js +31 -14
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli.js +187 -22
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +648 -74
- package/dist/commands/taskCompletionGate.js +166 -2
- package/dist/commands/taskContextCommand.js +6 -1
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +36 -3
- package/dist/context/runContextPack.js +19 -4
- package/dist/context/sessionBootstrapManifest.js +83 -2
- package/dist/controller/clientRuntime.js +7 -7
- package/dist/controller/controller.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +64 -5
- package/dist/controller/handoverCandidate.js +10 -3
- package/dist/controller/sessionNotify.js +4 -22
- package/dist/executor/agentAdapter.js +2 -2
- package/dist/executor/agentExecutor.js +25 -5
- package/dist/executor/fileRoleLaunchPlanner.js +16 -11
- package/dist/integration/gitIntegrationService.js +50 -2
- package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
- package/dist/observability/executionAudit.js +47 -1
- package/dist/observability/faultClassification.js +6 -4
- package/dist/observability/orchestrationMetrics.js +196 -0
- package/dist/operator/operatorSessionHistory.js +36 -0
- package/dist/release/releaseHandover.js +7 -5
- package/dist/release/runtimeRelease.js +15 -0
- package/dist/repository/gitWorkspace.js +7 -4
- package/dist/repository/taskBaseFreshness.js +4 -2
- package/dist/repository/taskWorkspaceCoordinator.js +13 -10
- package/dist/review/deltaRecheck.js +3 -2
- package/dist/review/reviewFindingLedger.js +5 -4
- package/dist/review/reviewOutcomeClassifier.js +252 -54
- package/dist/review/taskFinalReviewContractEvent.js +1 -0
- package/dist/review/taskFinalReviewContractRebind.js +350 -0
- package/dist/run/agentRun.js +2 -2
- package/dist/run/runIdentity.js +10 -70
- package/dist/runtime/agentHost.js +3 -4
- package/dist/runtime/codexAppServerRuntime.js +6 -0
- package/dist/runtime/firstProgressStopLoss.js +52 -0
- package/dist/runtime/launchBroker.js +10 -2
- package/dist/runtime/runtimeDeadlines.js +14 -0
- package/dist/runtime/sessionTitle.js +24 -12
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +10 -3
- package/dist/scheduler/activeRoleRunDelivery.js +20 -18
- package/dist/scheduler/leaderWakeupProcessor.js +33 -2
- package/dist/scheduler/wakeReason.js +2 -0
- package/dist/storage/sqliteStore.js +10 -1
- package/dist/storage/taskStore.js +12 -1
- package/dist/task/completionReadiness.js +91 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +146 -51
- package/dist/task/publicationReference.js +1 -0
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/web/webSnapshot.js +7 -1
- package/dist/workItem/workItem.js +12 -0
- package/dist/workspace/workItemChangeSetManager.js +2 -1
- package/i18n/README.zh-CN.md +28 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +79 -32
- package/skills/yui-operator/SKILL.md +51 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
- package/skills/yui-runtime/SKILL.md +7 -2
|
@@ -10,6 +10,7 @@ import { processOperatorInputNotifications } from "../scheduler/operatorInputNot
|
|
|
10
10
|
import { startControllerServer } from "../core/controllerServer.js";
|
|
11
11
|
import { monotonicMilliseconds } from "../core/controllerTelemetry.js";
|
|
12
12
|
import { isProjectMaintenanceFenced } from "../repository/projectMaintenanceLock.js";
|
|
13
|
+
import { isHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
13
14
|
import { KeyedWorkQueue } from "../coordination/keyedWorkQueue.js";
|
|
14
15
|
import { MailboxScheduler } from "../coordination/mailboxScheduler.js";
|
|
15
16
|
import { nearestDeadlineBatch } from "../coordination/deadlineScheduler.js";
|
|
@@ -45,7 +46,7 @@ const ZERO_DRAIN_METRICS = Object.freeze({
|
|
|
45
46
|
* Runs one lean scheduler pass. Due native Turn completions are folded before
|
|
46
47
|
* liveness, so a valid Hook boundary fences destructive process reconciliation.
|
|
47
48
|
*/
|
|
48
|
-
export async function runControllerSchedulerPass(store, delivery, now, workspacePreparer, scope = { kind: "full" }, includeOperator = true, runtimeCleanupOutcomes = [], lifecycleHost, stallWindowMs = DEFAULT_STALL_WINDOW_MS, maintenanceFence, onMaintenanceFenceDefer, blockedTaskIds = new Set(), inputDeliveryRecoveryCutoff, diagnosticAfterMs = DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS) {
|
|
49
|
+
export async function runControllerSchedulerPass(store, delivery, now, workspacePreparer, scope = { kind: "full" }, includeOperator = true, runtimeCleanupOutcomes = [], lifecycleHost, stallWindowMs = DEFAULT_STALL_WINDOW_MS, maintenanceFence, onMaintenanceFenceDefer, blockedTaskIds = new Set(), inputDeliveryRecoveryCutoff, diagnosticAfterMs = DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS, leaderWakeFence) {
|
|
49
50
|
const compiledSelection = compileReconcileSelection(scope);
|
|
50
51
|
const selection = includeOperator
|
|
51
52
|
? { ...compiledSelection, blockedTaskIds }
|
|
@@ -58,7 +59,9 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
|
|
|
58
59
|
await controlEventLoopTurn();
|
|
59
60
|
const failedCleanupRoles = await processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost, scope, now, runtimeCleanupOutcomes, blockedTaskIds);
|
|
60
61
|
const roleSelection = selectionWithoutFailedCleanupRoles(store, selection, failedCleanupRoles);
|
|
61
|
-
const
|
|
62
|
+
const availableWakeupSelection = () => (leaderWakeFence?.() === true
|
|
63
|
+
? exactTaskSelection(new Set())
|
|
64
|
+
: selectionWithoutFailedLeaderCleanupTasks(store, selection, failedCleanupRoles));
|
|
62
65
|
if (selection.full)
|
|
63
66
|
repairOrphanedActiveTasks(store, now, selection);
|
|
64
67
|
const claimedTaskMailboxes = claimSelectedTaskMailboxes(store, selection, now);
|
|
@@ -66,7 +69,7 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
|
|
|
66
69
|
// Durable Leader work that already has a ready Task workspace belongs to
|
|
67
70
|
// the control path. Dispatch it before any unrelated Task workspace I/O;
|
|
68
71
|
// the processor itself retains the fail-closed workspace-ready guard.
|
|
69
|
-
const initialWakeups = selectedPendingWakeups(store,
|
|
72
|
+
const initialWakeups = selectedPendingWakeups(store, availableWakeupSelection());
|
|
70
73
|
const initialWakeupResults = await processLeaderWakeups(store, delivery, now, exactTaskSelection(new Set(initialWakeups.keys())));
|
|
71
74
|
// Preserve ready-Leader-first ordering, then bound the repeated state
|
|
72
75
|
// projections that follow it in this pass.
|
|
@@ -134,7 +137,8 @@ export async function runControllerSchedulerPass(store, delivery, now, workspace
|
|
|
134
137
|
&& workspacePreparation.ready.has(result.taskId)
|
|
135
138
|
? [result.taskId]
|
|
136
139
|
: [])));
|
|
137
|
-
const laterWakeupTaskIds = new Set([...selectedPendingWakeups(store,
|
|
140
|
+
const laterWakeupTaskIds = new Set([...selectedPendingWakeups(store, availableWakeupSelection())]
|
|
141
|
+
.flatMap(([taskId, wakeup]) => {
|
|
138
142
|
const initial = initialWakeups.get(taskId);
|
|
139
143
|
return initial === undefined
|
|
140
144
|
|| !pendingWakeupsMatch(initial, wakeup)
|
|
@@ -823,6 +827,7 @@ export class FileTaskController {
|
|
|
823
827
|
#onExpiredEphemeralDomain;
|
|
824
828
|
#maintenanceFence;
|
|
825
829
|
#onMaintenanceFenceDefer;
|
|
830
|
+
#leaderWakeFence;
|
|
826
831
|
#jobSupervisor;
|
|
827
832
|
#continuationReconciler;
|
|
828
833
|
#current;
|
|
@@ -867,6 +872,7 @@ export class FileTaskController {
|
|
|
867
872
|
this.#onExpiredEphemeralDomain = options.onExpiredEphemeralDomain;
|
|
868
873
|
this.#maintenanceFence = options.maintenanceFence;
|
|
869
874
|
this.#onMaintenanceFenceDefer = options.onMaintenanceFenceDefer;
|
|
875
|
+
this.#leaderWakeFence = options.leaderWakeFence;
|
|
870
876
|
this.#jobSupervisor = options.jobSupervisor;
|
|
871
877
|
this.#continuationReconciler = options.continuationReconciler;
|
|
872
878
|
this.#signalScheduler = new MailboxScheduler(async (keys) => { await this.#requestPass({ kind: "dirty", keys }); }, {
|
|
@@ -1118,7 +1124,7 @@ export class FileTaskController {
|
|
|
1118
1124
|
// processes Leader wakeups.
|
|
1119
1125
|
this.#jobSupervisor?.reconcile(this.#now());
|
|
1120
1126
|
if (scope.kind === "full") {
|
|
1121
|
-
result = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, scope, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, runtimeFailedTaskIds, this.#startedAt, this.#diagnosticAfterMs);
|
|
1127
|
+
result = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, scope, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, runtimeFailedTaskIds, this.#startedAt, this.#diagnosticAfterMs, this.#leaderWakeFence);
|
|
1122
1128
|
}
|
|
1123
1129
|
else {
|
|
1124
1130
|
const dirtyPass = await this.#runDirtySchedulerPass(scope, runtimeCleanupOutcomes, runtimeFailedTaskIds);
|
|
@@ -1216,7 +1222,7 @@ export class FileTaskController {
|
|
|
1216
1222
|
const taskScopes = partition.taskScopes.filter((taskScope) => (!blockedTaskIds.has(taskScope.taskId)));
|
|
1217
1223
|
const orderedResults = [];
|
|
1218
1224
|
if (partition.globalKeys.length > 0) {
|
|
1219
|
-
orderedResults.push(await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: partition.globalKeys }, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs));
|
|
1225
|
+
orderedResults.push(await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: partition.globalKeys }, false, runtimeCleanupOutcomes, this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs, this.#leaderWakeFence));
|
|
1220
1226
|
}
|
|
1221
1227
|
if (taskScopes.length === 0
|
|
1222
1228
|
|| this.#stopped
|
|
@@ -1249,7 +1255,7 @@ export class FileTaskController {
|
|
|
1249
1255
|
if (this.#stopped || this.#pendingFull)
|
|
1250
1256
|
continue;
|
|
1251
1257
|
try {
|
|
1252
|
-
taskResults[selected.index] = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: selected.taskScope.keys }, false, taskCleanupOutcomes[selected.index], this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs);
|
|
1258
|
+
taskResults[selected.index] = await runControllerSchedulerPass(this.store, this.delivery, this.#now(), this.#workspacePreparer, { kind: "dirty", keys: selected.taskScope.keys }, false, taskCleanupOutcomes[selected.index], this.#lifecycleHost, this.#stallWindowMs, this.#maintenanceFence, this.#onMaintenanceFenceDefer, blockedTaskIds, this.#startedAt, this.#diagnosticAfterMs, this.#leaderWakeFence);
|
|
1253
1259
|
this.#clearTaskPassRetry(selected.taskScope.taskId);
|
|
1254
1260
|
}
|
|
1255
1261
|
catch (error) {
|
|
@@ -1676,7 +1682,9 @@ export async function startFileTaskController(home, store, delivery, dispatcher,
|
|
|
1676
1682
|
const runtime = new FileTaskController(store, delivery, {
|
|
1677
1683
|
...options,
|
|
1678
1684
|
maintenanceFence: options.maintenanceFence
|
|
1679
|
-
?? ((projectId) => isProjectMaintenanceFenced(home, projectId))
|
|
1685
|
+
?? ((projectId) => isProjectMaintenanceFenced(home, projectId)),
|
|
1686
|
+
leaderWakeFence: options.leaderWakeFence
|
|
1687
|
+
?? (() => isHandoverLockHeld(home))
|
|
1680
1688
|
});
|
|
1681
1689
|
let stopping = false;
|
|
1682
1690
|
const lifecycleRequests = new Set();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
-
import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
3
|
+
import { activeLiveRoleAgentSession, bindTaskRoleProviderRuntime, bindTaskRoleRun, clearTaskRoleProviderRuntimeForCleanup, clearTaskRoleRun, createRoleSessionSet, markTaskRoleRunDelivered, markTaskRoleRunPushed, prepareTaskRoleRunRedispatch, recordRoleAgentSession, recordTaskRoleTurnBoundary, rememberRoleAgentCompletedTurn, updateRoleAgentSessionStatus, updateTaskRoleProviderRuntime } from "../executor/agentExecutor.js";
|
|
4
4
|
import { acceptProviderTurn, beginProviderTurn, createProviderRuntimeBinding, endProviderActivation, currentProviderActivation, currentProviderConversation, markProviderTurnDeliveryUnknown, rejectProviderTurn, settleProviderTurn, startProviderActivation, supersedeProviderConversation, updateProviderConversationRecoverability } from "../runtime/providerRuntimeIdentity.js";
|
|
5
5
|
import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
|
|
6
|
+
import { boundProviderRetryBeforeFirstProgress, projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
|
|
6
7
|
import { hasRecentTurnId } from "../executor/turnCompletion.js";
|
|
7
8
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
8
9
|
import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
|
|
@@ -674,6 +675,9 @@ export class FileSchedulerStoreAdapter {
|
|
|
674
675
|
: sessions?.sessions[agentId];
|
|
675
676
|
return session === undefined ? null : mapSession(session);
|
|
676
677
|
}
|
|
678
|
+
getTaskRoleSessionSet(taskId, roleName) {
|
|
679
|
+
return this.store.getTaskRoleSessionSet(taskId, roleName);
|
|
680
|
+
}
|
|
677
681
|
listEvents(taskId) {
|
|
678
682
|
return this.#taskReadProjection(taskId).events;
|
|
679
683
|
}
|
|
@@ -1222,7 +1226,14 @@ export class FileSchedulerStoreAdapter {
|
|
|
1222
1226
|
startedAt: now.toISOString()
|
|
1223
1227
|
}), batchId);
|
|
1224
1228
|
}
|
|
1225
|
-
|
|
1229
|
+
const owner = runtimeOwnerFromTarget(target);
|
|
1230
|
+
markRuntimeOwnerSessionStopped(store, owner, now);
|
|
1231
|
+
if (owner.scope === "task") {
|
|
1232
|
+
const sessions = store.getTaskRoleSessionSet(owner.taskId, owner.roleName);
|
|
1233
|
+
if (sessions !== null && sessions.providerBinding !== null) {
|
|
1234
|
+
store.saveTaskRoleSessionSet(clearTaskRoleProviderRuntimeForCleanup(sessions, now));
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1226
1237
|
saveRuntimeLifecycleMailbox(store, mailbox);
|
|
1227
1238
|
return true;
|
|
1228
1239
|
});
|
|
@@ -1288,6 +1299,40 @@ export class FileSchedulerStoreAdapter {
|
|
|
1288
1299
|
clearPendingWakeup(taskId) { this.store.clearPendingWakeup(taskId); }
|
|
1289
1300
|
getLeaderFailure(taskId) { return this.store.getLeaderFailure(taskId); }
|
|
1290
1301
|
getOperatorNotification(taskId) { return this.store.getOperatorNotification(taskId); }
|
|
1302
|
+
saveLeaderFirstProgressStopLoss(input) {
|
|
1303
|
+
return this.store.transaction((store) => {
|
|
1304
|
+
const task = store.getTask(input.taskId);
|
|
1305
|
+
const role = store.getRole(input.taskId, input.roleName);
|
|
1306
|
+
if (task === null || task.status !== "active" || role === null
|
|
1307
|
+
|| store.getLeaderFailure(input.taskId) !== null) {
|
|
1308
|
+
return "state-changed";
|
|
1309
|
+
}
|
|
1310
|
+
const stopLoss = projectFirstProgressStopLoss({
|
|
1311
|
+
sessions: store.getTaskRoleSessionSet(input.taskId, input.roleName),
|
|
1312
|
+
events: store.listEvents(input.taskId),
|
|
1313
|
+
workItems: store.listWorkItems(input.taskId),
|
|
1314
|
+
reviewRounds: store.listReviewRounds(input.taskId),
|
|
1315
|
+
integrations: store.listIntegrationAttempts(input.taskId)
|
|
1316
|
+
});
|
|
1317
|
+
if (!stopLoss.exhausted || stopLoss.fingerprint !== input.expectedFingerprint) {
|
|
1318
|
+
return "state-changed";
|
|
1319
|
+
}
|
|
1320
|
+
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
1321
|
+
const lastSession = sessions === null
|
|
1322
|
+
? undefined
|
|
1323
|
+
: [...(sessions.history ?? []), ...Object.values(sessions.sessions)]
|
|
1324
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt))
|
|
1325
|
+
.at(-1);
|
|
1326
|
+
const message = `Leader first-progress stop-loss: ${stopLoss.reason}`;
|
|
1327
|
+
store.saveRole(input.taskId, updateRoleStatus(role, "failed", input.now));
|
|
1328
|
+
store.saveLeaderFailure(recordLeaderFailure(input.taskId, lastSession?.nativeSessionId ?? "(unregistered)", message, input.now, null));
|
|
1329
|
+
store.saveOperatorNotification(createLeaderRecoveryNotification(input.taskId, message, input.now, store.getOperatorNotification(input.taskId)));
|
|
1330
|
+
enqueueWork(store, { kind: "operator" }, "leader-first-progress-stop-loss", input.now, [
|
|
1331
|
+
{ type: "task", id: input.taskId }
|
|
1332
|
+
]);
|
|
1333
|
+
return "recorded";
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1291
1336
|
saveLeaderDispatch(input) {
|
|
1292
1337
|
return this.store.transaction((store) => {
|
|
1293
1338
|
const task = store.getTask(input.task.id);
|
|
@@ -2241,7 +2286,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
2241
2286
|
failedNativeTurnId: input.nativeTurnId,
|
|
2242
2287
|
lastErrorSummary: summary,
|
|
2243
2288
|
...(input.retryAfterMs === undefined ? {} : { retryAfterMs: input.retryAfterMs })
|
|
2244
|
-
}, now, configuredProviderRetryPolicy(store));
|
|
2289
|
+
}, now, configuredProviderRetryPolicy(store, input.taskId, input.roleName));
|
|
2245
2290
|
if (retryDecision.outcome === "exhausted") {
|
|
2246
2291
|
this.recordProviderRetryClassified(store, input, classification.errorClass, {
|
|
2247
2292
|
wouldRetry: "false",
|
|
@@ -2253,7 +2298,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
2253
2298
|
if (run.providerRetry === undefined)
|
|
2254
2299
|
return null;
|
|
2255
2300
|
const finalized = finalizeProviderRetryDeadline(store, run, retryDecision.reason === "attempts"
|
|
2256
|
-
? `Provider retry failed after all ${
|
|
2301
|
+
? `Provider retry failed after all ${run.providerRetry.maxRetries} in-Session continuation attempts.`
|
|
2257
2302
|
: retryDecision.reason === "retry-after-window"
|
|
2258
2303
|
? `Provider Retry-After exceeded the bounded ${config.maxWindowMs / 1_000}-second episode window.`
|
|
2259
2304
|
: `Provider retry did not recover within the bounded ${config.maxWindowMs / 1_000}-second episode window.`, retryDecision.reason === "attempts" ? "attempts-exhausted" : "episode-window-exhausted", now);
|
|
@@ -3616,6 +3661,7 @@ function mapSession(session) {
|
|
|
3616
3661
|
adapterId: session.adapterId,
|
|
3617
3662
|
nativeSessionId: session.nativeSessionId,
|
|
3618
3663
|
...(session.launchId === undefined ? {} : { launchId: session.launchId }),
|
|
3664
|
+
...(session.title === undefined ? {} : { title: session.title }),
|
|
3619
3665
|
status: session.status,
|
|
3620
3666
|
effective: session.effective,
|
|
3621
3667
|
updatedAt: session.updatedAt
|
|
@@ -3629,6 +3675,7 @@ function saveTaskSession(store, role, session, status, now, launchId) {
|
|
|
3629
3675
|
adapterId: session.adapterId,
|
|
3630
3676
|
nativeSessionId: session.nativeSessionId,
|
|
3631
3677
|
...(launchId === undefined ? {} : { launchId }),
|
|
3678
|
+
...(session.title === undefined ? {} : { title: session.title }),
|
|
3632
3679
|
policy: "fixed",
|
|
3633
3680
|
status,
|
|
3634
3681
|
effective: session.effective
|
|
@@ -4021,7 +4068,19 @@ function compareCanonicalObservationOrder(left, right) {
|
|
|
4021
4068
|
|| (left.ordinal ?? -1) - (right.ordinal ?? -1)
|
|
4022
4069
|
|| left.eventId.localeCompare(right.eventId);
|
|
4023
4070
|
}
|
|
4024
|
-
function configuredProviderRetryPolicy(store) {
|
|
4071
|
+
function configuredProviderRetryPolicy(store, taskId, roleName) {
|
|
4025
4072
|
const config = providerRetryConfig(store.getConfig());
|
|
4073
|
+
if (taskId !== undefined && roleName === "leader") {
|
|
4074
|
+
const progress = projectFirstProgressStopLoss({
|
|
4075
|
+
sessions: store.getTaskRoleSessionSet(taskId, roleName),
|
|
4076
|
+
events: store.listEvents(taskId),
|
|
4077
|
+
workItems: store.listWorkItems(taskId),
|
|
4078
|
+
reviewRounds: store.listReviewRounds(taskId),
|
|
4079
|
+
integrations: store.listIntegrationAttempts(taskId)
|
|
4080
|
+
});
|
|
4081
|
+
if (progress.firstProgressAt === undefined) {
|
|
4082
|
+
return boundProviderRetryBeforeFirstProgress(config, progress);
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4026
4085
|
return { delaysMs: config.delaysMs, maxWindowMs: config.maxWindowMs };
|
|
4027
4086
|
}
|
|
@@ -19,6 +19,7 @@ import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
|
|
|
19
19
|
import { detectRunningRelease, isOwnerLive, readActiveReleasePointer, readHandoverFence, removeCandidateDiscovery, removeHandoverFence, writeCandidateDiscovery, writeHandoverFence, writeHandoverReceipt, writeRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
20
20
|
import { startFileTaskControllerRuntime } from "./runtime.js";
|
|
21
21
|
import { readLinuxProcessStartIdentity } from "./domainIdentity.js";
|
|
22
|
+
import { RELEASE_HANDOVER_OLD_OWNER_GRACE_MS } from "../runtime/runtimeDeadlines.js";
|
|
22
23
|
export const CONTROLLER_CANDIDATE_ENV = "YUI_CONTROLLER_CANDIDATE";
|
|
23
24
|
export const CONTROLLER_HANDOVER_ID_ENV = "YUI_CONTROLLER_HANDOVER_ID";
|
|
24
25
|
const DEFAULT_POLL_INTERVAL_MS = 100;
|
|
@@ -29,7 +30,7 @@ const DEFAULT_POLL_INTERVAL_MS = 100;
|
|
|
29
30
|
* a second independent grace (see `DEFAULT_DUAL_OWNER_GRACE_MS` in
|
|
30
31
|
* `releaseHandover.ts`, which is only an optional confirmation debounce).
|
|
31
32
|
*/
|
|
32
|
-
export const DEFAULT_DUAL_OWNER_GRACE_MS =
|
|
33
|
+
export const DEFAULT_DUAL_OWNER_GRACE_MS = RELEASE_HANDOVER_OLD_OWNER_GRACE_MS;
|
|
33
34
|
/** Reads the candidate configuration from the process environment. */
|
|
34
35
|
export function handoverCandidateFromEnvironment(environment) {
|
|
35
36
|
if (environment[CONTROLLER_CANDIDATE_ENV] !== "1")
|
|
@@ -85,7 +86,6 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
|
|
|
85
86
|
dualOwner: false
|
|
86
87
|
}));
|
|
87
88
|
advanceFence(home, fence, "candidate-ready", now().toISOString());
|
|
88
|
-
const committedAt = Date.now();
|
|
89
89
|
let dualOwnerReported = false;
|
|
90
90
|
for (;;) {
|
|
91
91
|
const current = readHandoverFence(home);
|
|
@@ -111,7 +111,7 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
|
|
|
111
111
|
}
|
|
112
112
|
if (current.phase === "committed"
|
|
113
113
|
&& !oldDead
|
|
114
|
-
&&
|
|
114
|
+
&& committedFenceAgeMs(current, now()) > dualOwnerGraceMs
|
|
115
115
|
&& !dualOwnerReported) {
|
|
116
116
|
// The old Controller was told to exit but is still live. Stay read-only
|
|
117
117
|
// and make the dual-owner condition visible; never write concurrently.
|
|
@@ -129,6 +129,13 @@ export async function runHandoverCandidate(home, handoverId, options = {}) {
|
|
|
129
129
|
await delay(pollIntervalMs);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
function committedFenceAgeMs(fence, observedAt) {
|
|
133
|
+
const committedAt = Date.parse(fence.updatedAt);
|
|
134
|
+
if (!Number.isFinite(committedAt)) {
|
|
135
|
+
throw new Error("Committed handover fence timestamp is invalid.");
|
|
136
|
+
}
|
|
137
|
+
return observedAt.getTime() - committedAt;
|
|
138
|
+
}
|
|
132
139
|
async function promote(home, fence, environment, now) {
|
|
133
140
|
removeCandidateDiscovery(home);
|
|
134
141
|
const controller = await startFileTaskControllerRuntime(home, { environment });
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { callController } from "../core/controllerClient.js";
|
|
2
2
|
import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
|
|
3
|
-
import { yuiRunBodyFromInputMessage, yuiRunIdFromInputMessages } from "../run/runIdentity.js";
|
|
4
3
|
import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
|
|
5
4
|
import { setCodexThreadName } from "../execution/codexThreadNaming.js";
|
|
6
5
|
import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
@@ -50,9 +49,11 @@ export function parseCodexSessionNotification(payloadArgument, environment) {
|
|
|
50
49
|
const nativeSessionId = requireText(payload["thread-id"], "Codex thread-id");
|
|
51
50
|
const turnId = requireText(payload["turn-id"], "Codex turn-id");
|
|
52
51
|
const lastAssistantMessage = requireAssistantMessage(payload["last-assistant-message"]);
|
|
53
|
-
const runId =
|
|
52
|
+
const runId = environment.YUI_RUN_ID === undefined
|
|
53
|
+
? undefined
|
|
54
|
+
: requireText(environment.YUI_RUN_ID, "YUI_RUN_ID");
|
|
54
55
|
const title = environment.YUI_SESSION_TITLE === undefined
|
|
55
|
-
?
|
|
56
|
+
? undefined
|
|
56
57
|
: requireText(environment.YUI_SESSION_TITLE, "YUI_SESSION_TITLE");
|
|
57
58
|
const scope = environment.YUI_SESSION_SCOPE;
|
|
58
59
|
if (scope !== "task" && scope !== "global") {
|
|
@@ -117,19 +118,6 @@ function requireAssistantMessage(value) {
|
|
|
117
118
|
}
|
|
118
119
|
return text;
|
|
119
120
|
}
|
|
120
|
-
function sessionTitleFromInputMessages(value) {
|
|
121
|
-
if (!Array.isArray(value))
|
|
122
|
-
return undefined;
|
|
123
|
-
for (const entry of value) {
|
|
124
|
-
if (typeof entry !== "string")
|
|
125
|
-
continue;
|
|
126
|
-
const body = yuiRunBodyFromInputMessage(entry);
|
|
127
|
-
const normalized = body.trim().replaceAll(/\s+/g, " ");
|
|
128
|
-
if (normalized.length > 0)
|
|
129
|
-
return truncateSessionText(normalized);
|
|
130
|
-
}
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
121
|
function shouldSetThreadName(home, params) {
|
|
134
122
|
if (params.scope !== "task"
|
|
135
123
|
|| params.title === undefined
|
|
@@ -168,12 +156,6 @@ function threadNameRequest(params, environment) {
|
|
|
168
156
|
return null;
|
|
169
157
|
}
|
|
170
158
|
}
|
|
171
|
-
function truncateSessionText(value) {
|
|
172
|
-
const truncated = value.slice(0, 1_024);
|
|
173
|
-
return /[\uD800-\uDBFF]$/.test(truncated)
|
|
174
|
-
? truncated.slice(0, -1)
|
|
175
|
-
: truncated;
|
|
176
|
-
}
|
|
177
159
|
function isObject(value) {
|
|
178
160
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
179
161
|
}
|
|
@@ -276,7 +276,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
276
276
|
return ["--append-system-prompt-file", input.managedContextFile];
|
|
277
277
|
}
|
|
278
278
|
compileResume(input) {
|
|
279
|
-
const launch =
|
|
279
|
+
const launch = this.compileNew(input);
|
|
280
280
|
return { ...launch, argv: [...launch.argv, "--resume", nativeId(input.nativeSessionId)] };
|
|
281
281
|
}
|
|
282
282
|
compileManagedControl(input, mode, nativeSessionId) {
|
|
@@ -285,7 +285,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
285
285
|
}
|
|
286
286
|
const sessionId = nativeId(nativeSessionId);
|
|
287
287
|
const launch = mode === "new"
|
|
288
|
-
?
|
|
288
|
+
? this.compileNew(input)
|
|
289
289
|
: this.compileResume({ ...input, nativeSessionId: sessionId });
|
|
290
290
|
return {
|
|
291
291
|
...launch,
|
|
@@ -173,9 +173,29 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
|
|
|
173
173
|
// receive a fresh identity after the Role workspace changes.
|
|
174
174
|
history: [...(set.history ?? []), ...Object.values(set.sessions)],
|
|
175
175
|
sessions: {},
|
|
176
|
+
providerBinding: null,
|
|
176
177
|
updatedAt: timestamp
|
|
177
178
|
});
|
|
178
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Clears the Provider transport identity after its physical runtime is proven
|
|
182
|
+
* stopped, without retiring workspace-bound Session records. Workspace
|
|
183
|
+
* retirement remains a separate, stricter transaction after every supported
|
|
184
|
+
* placeholder has been terminalized.
|
|
185
|
+
*/
|
|
186
|
+
export function clearTaskRoleProviderRuntimeForCleanup(set, now) {
|
|
187
|
+
validateRoleSessionSet(set);
|
|
188
|
+
if (set.inFlight !== null) {
|
|
189
|
+
throw new Error("Cannot clear a Task Role Provider runtime with unsettled Run state.");
|
|
190
|
+
}
|
|
191
|
+
if (set.providerBinding === null)
|
|
192
|
+
return set;
|
|
193
|
+
return validateRoleSessionSet({
|
|
194
|
+
...set,
|
|
195
|
+
providerBinding: null,
|
|
196
|
+
updatedAt: requireDate(now, "Provider Runtime cleanup timestamp")
|
|
197
|
+
});
|
|
198
|
+
}
|
|
179
199
|
/**
|
|
180
200
|
* Terminalizes only the aggregate-16 Claude placeholder shape after the
|
|
181
201
|
* caller has fenced the Task store and proved that the exact Role has no live
|
|
@@ -261,16 +281,16 @@ export function roleAgentSessionResumeMode(set, agentId, desired, workspace) {
|
|
|
261
281
|
}
|
|
262
282
|
return "new";
|
|
263
283
|
}
|
|
284
|
+
if (session.status === "stopped" || session.status === "broken") {
|
|
285
|
+
return "new";
|
|
286
|
+
}
|
|
264
287
|
const compatible = set.owner.scope === "task"
|
|
265
288
|
? effectiveLaunchSnapshotsCompatibleForTaskMain(session.effective, desired, workspace)
|
|
266
289
|
: effectiveLaunchSnapshotsCompatible(session.effective, desired);
|
|
267
290
|
if (compatible)
|
|
268
291
|
return "resume";
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
+ "Stop the existing native process before starting a fresh Session.");
|
|
272
|
-
}
|
|
273
|
-
return "new";
|
|
292
|
+
throw new Error(`Role Agent session is incompatible with the next effective launch: ${agentId}. `
|
|
293
|
+
+ "Stop the existing native process before starting a fresh Session.");
|
|
274
294
|
}
|
|
275
295
|
export function bindTaskRoleRun(set, fence, preparedAt) {
|
|
276
296
|
validateRoleSessionSet(set);
|
|
@@ -11,9 +11,10 @@ import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessi
|
|
|
11
11
|
import { materializeSessionBootstrap } from "../context/sessionBootstrapManifest.js";
|
|
12
12
|
import { serializeRunBootstrapEnvelope, serializeRunHostRecoveryEnvelope } from "../context/runContextContract.js";
|
|
13
13
|
import { serializeProviderRetryEnvelope } from "../run/providerRetry.js";
|
|
14
|
+
import { prefixYuiTitleInput } from "../run/runIdentity.js";
|
|
14
15
|
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
15
16
|
import { inspectCodexLaunchConfig } from "./codexConfigConflict.js";
|
|
16
|
-
import {
|
|
17
|
+
import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
17
18
|
import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
|
|
18
19
|
import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
|
|
19
20
|
import { activeLiveRoleAgentSession } from "./agentExecutor.js";
|
|
@@ -224,7 +225,7 @@ export class FileRoleLaunchPlanner {
|
|
|
224
225
|
if (input.mode === "resume" && !compatibleExisting) {
|
|
225
226
|
throw new Error(`Task Role resume effective snapshot drifted: ${task.id}/${role.name}.`);
|
|
226
227
|
}
|
|
227
|
-
return this.#compile(role, input, { scope: "task", taskId: task.id },
|
|
228
|
+
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, {
|
|
228
229
|
purpose: activeRun?.purpose ?? "execution"
|
|
229
230
|
});
|
|
230
231
|
}
|
|
@@ -506,12 +507,13 @@ export class FileRoleLaunchPlanner {
|
|
|
506
507
|
...(providerNativeSessionId === undefined
|
|
507
508
|
? {}
|
|
508
509
|
: { nativeSessionId: providerNativeSessionId }),
|
|
510
|
+
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
509
511
|
authority: providerAuthority,
|
|
510
512
|
...(carriesInitialTurn
|
|
511
513
|
? {
|
|
512
514
|
initialTurn: {
|
|
513
515
|
attemptId: formatAgentRunReceiptId(owner.taskId, input.runId),
|
|
514
|
-
boundedText: managedRunLaunchEnvelope(managedRun, input.mode)
|
|
516
|
+
boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionTitle)
|
|
515
517
|
}
|
|
516
518
|
}
|
|
517
519
|
: {})
|
|
@@ -578,6 +580,7 @@ export class FileRoleLaunchPlanner {
|
|
|
578
580
|
},
|
|
579
581
|
launch: scopedLaunch,
|
|
580
582
|
session,
|
|
583
|
+
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
581
584
|
...(carriesInitialTurn && input.runId !== undefined
|
|
582
585
|
? { initialTurnRunId: input.runId }
|
|
583
586
|
: {})
|
|
@@ -645,18 +648,20 @@ export class FileRoleLaunchPlanner {
|
|
|
645
648
|
return selectEnvironment(source, names);
|
|
646
649
|
}
|
|
647
650
|
}
|
|
648
|
-
function managedRunLaunchEnvelope(run, mode) {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
+
function managedRunLaunchEnvelope(run, mode, title) {
|
|
652
|
+
const body = run.providerRetry?.state === "dispatching"
|
|
653
|
+
? serializeProviderRetryEnvelope({
|
|
651
654
|
taskId: run.taskId,
|
|
652
655
|
runId: run.id,
|
|
653
656
|
roleName: run.roleName,
|
|
654
657
|
retry: run.providerRetry
|
|
655
|
-
})
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
658
|
+
})
|
|
659
|
+
: mode === "resume" && run.pushedAt !== undefined
|
|
660
|
+
? serializeRunHostRecoveryEnvelope(run.bootstrapEnvelope)
|
|
661
|
+
: serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
|
|
662
|
+
return title === undefined
|
|
663
|
+
? body
|
|
664
|
+
: prefixYuiTitleInput(body, title);
|
|
660
665
|
}
|
|
661
666
|
export function nativeAgentWorkspace(workspace) {
|
|
662
667
|
return workspace.entries.length === 1
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { lstat, mkdir, open, rm, stat } from "node:fs/promises";
|
|
4
5
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
6
|
import { promisify } from "node:util";
|
|
@@ -18,6 +19,8 @@ import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
|
18
19
|
import { recordIntegrationCheckJob, requireLeaderDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
|
|
19
20
|
import { createManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
20
21
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
22
|
+
import { readRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
23
|
+
import { findReusableIntegrationCheckEvidence, INTEGRATION_RUNTIME_RELEASE_ENV } from "./integrationCheckEvidenceReuse.js";
|
|
21
24
|
const executeFile = promisify(execFile);
|
|
22
25
|
const INTEGRATION_OPERATIONAL_ENVIRONMENT_NAMES = [
|
|
23
26
|
"PATH",
|
|
@@ -300,8 +303,6 @@ export class GitIntegrationService {
|
|
|
300
303
|
*/
|
|
301
304
|
async #startCheckJob(attempt, workspace, path, managedWorkspace, repositoryPath, gate) {
|
|
302
305
|
const runtime = this.#runtimePreparation(attempt, managedWorkspace);
|
|
303
|
-
this.runtimeIsolation.activate(runtime);
|
|
304
|
-
const environment = await integrationCheckEnvironment(this.environment, runtime);
|
|
305
306
|
const head = await gitLine(["-C", path, "rev-parse", "HEAD^{commit}"]);
|
|
306
307
|
const steps = gate === undefined
|
|
307
308
|
? attempt.checkCommands.map((command, index) => ({
|
|
@@ -319,6 +320,30 @@ export class GitIntegrationService {
|
|
|
319
320
|
timeoutMs: 30 * 60_000
|
|
320
321
|
}))
|
|
321
322
|
];
|
|
323
|
+
const releaseId = integrationRuntimeReleaseIdentity(this.home);
|
|
324
|
+
if (gate === undefined && releaseId !== null) {
|
|
325
|
+
const reusable = findReusableIntegrationCheckEvidence({
|
|
326
|
+
taskId: attempt.taskId,
|
|
327
|
+
projectId: attempt.projectId,
|
|
328
|
+
currentAttemptId: attempt.id,
|
|
329
|
+
candidateCommit: head,
|
|
330
|
+
checkCommands: attempt.checkCommands,
|
|
331
|
+
runtimeReleaseId: releaseId,
|
|
332
|
+
attempts: this.store.listIntegrationAttempts(attempt.taskId),
|
|
333
|
+
jobs: this.store.listDurableJobs(attempt.taskId),
|
|
334
|
+
logExists: (homeRelativePath) => existsSync(join(this.home, homeRelativePath)),
|
|
335
|
+
logPathFor: (job, relativeLogPath) => (join(job.artifactsLocator, "logs", relativeLogPath))
|
|
336
|
+
});
|
|
337
|
+
if (reusable !== null) {
|
|
338
|
+
return this.#finalizeGateSuccess(attempt, workspace, repositoryPath, head, [...reusable.checks]);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
this.runtimeIsolation.activate(runtime);
|
|
342
|
+
const baseEnvironment = await integrationCheckEnvironment(this.environment, runtime);
|
|
343
|
+
const environment = Object.freeze({
|
|
344
|
+
...baseEnvironment,
|
|
345
|
+
...(releaseId === null ? {} : { [INTEGRATION_RUNTIME_RELEASE_ENV]: releaseId })
|
|
346
|
+
});
|
|
322
347
|
// Persist the gate identity before starting the job so a plan edit
|
|
323
348
|
// during the gate never misattributes the evidence on resume.
|
|
324
349
|
let persisted = attempt;
|
|
@@ -906,6 +931,29 @@ function integrationRuntimeLaunchId(home, integrationId) {
|
|
|
906
931
|
.digest("hex");
|
|
907
932
|
return `${integrationId}-${homeDigest}`;
|
|
908
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* Reuse is enabled only for an immutable installed release. Development
|
|
936
|
+
* checkouts have no content-addressed release identity, so they rerun.
|
|
937
|
+
*/
|
|
938
|
+
function integrationRuntimeReleaseIdentity(home) {
|
|
939
|
+
try {
|
|
940
|
+
const receipt = readRuntimeIdentity(home);
|
|
941
|
+
const release = receipt?.activeRelease;
|
|
942
|
+
if (receipt === null
|
|
943
|
+
|| receipt.mode !== "primary"
|
|
944
|
+
|| receipt.dualOwner
|
|
945
|
+
|| release === null
|
|
946
|
+
|| release === undefined
|
|
947
|
+
|| receipt.packageDigest !== release.packageDigest
|
|
948
|
+
|| receipt.buildId !== release.buildId) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
return release.releaseId;
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
return null;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
909
957
|
function checkFailureReason(completion) {
|
|
910
958
|
if (completion.timedOut)
|
|
911
959
|
return "Command timed out after 1800 seconds.";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Internal, Controller-derived release fence carried by Integration jobs. */
|
|
2
|
+
export const INTEGRATION_RUNTIME_RELEASE_ENV = "YUI_INTEGRATION_RUNTIME_RELEASE_ID";
|
|
3
|
+
/**
|
|
4
|
+
* Find exact reusable evidence without creating cache state. Every identity
|
|
5
|
+
* field is read from the existing IntegrationAttempt and DurableJob records.
|
|
6
|
+
*/
|
|
7
|
+
export function findReusableIntegrationCheckEvidence(input) {
|
|
8
|
+
const attempts = [...input.attempts]
|
|
9
|
+
.filter((attempt) => (attempt.taskId === input.taskId
|
|
10
|
+
&& attempt.id !== input.currentAttemptId
|
|
11
|
+
&& attempt.projectId === input.projectId
|
|
12
|
+
&& sameOrderedText(attempt.checkCommands, input.checkCommands)))
|
|
13
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
14
|
+
for (const attempt of attempts) {
|
|
15
|
+
if (attempt.jobId === undefined)
|
|
16
|
+
continue;
|
|
17
|
+
const job = input.jobs.find((candidate) => (candidate.id === attempt.jobId
|
|
18
|
+
&& candidate.taskId === input.taskId
|
|
19
|
+
&& candidate.owner.kind === "integration-attempt"
|
|
20
|
+
&& candidate.owner.integrationAttemptId === attempt.id));
|
|
21
|
+
const resultSteps = job?.result?.steps ?? [];
|
|
22
|
+
const logPaths = resultSteps.map((step) => (input.logPathFor?.(job, step.logPath) ?? step.logPath));
|
|
23
|
+
if (job === undefined
|
|
24
|
+
|| job.projectId !== input.projectId
|
|
25
|
+
|| job.head !== input.candidateCommit
|
|
26
|
+
|| job.status !== "succeeded"
|
|
27
|
+
|| job.result?.outcome !== "succeeded"
|
|
28
|
+
|| job.env[INTEGRATION_RUNTIME_RELEASE_ENV] !== input.runtimeReleaseId
|
|
29
|
+
|| job.steps.length !== input.checkCommands.length
|
|
30
|
+
|| job.steps.some((step, index) => (step.name !== `check-${index + 1}`
|
|
31
|
+
|| step.command !== input.checkCommands[index]))
|
|
32
|
+
|| resultSteps.length !== input.checkCommands.length
|
|
33
|
+
|| resultSteps.some((step, index) => (step.name !== `check-${index + 1}`
|
|
34
|
+
|| step.exitCode !== 0
|
|
35
|
+
|| step.signal !== null
|
|
36
|
+
|| step.timedOut
|
|
37
|
+
|| step.head !== input.candidateCommit
|
|
38
|
+
|| (input.logExists !== undefined && !input.logExists(logPaths[index]))))) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const checks = resultSteps.map((step, index) => ({
|
|
42
|
+
name: input.checkCommands[index],
|
|
43
|
+
outcome: "passed",
|
|
44
|
+
details: `Reused successful check evidence from ${attempt.id}/${job.id}.`,
|
|
45
|
+
logPath: logPaths[index]
|
|
46
|
+
}));
|
|
47
|
+
return Object.freeze({ sourceAttempt: attempt, sourceJob: job, checks });
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function sameOrderedText(left, right) {
|
|
52
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
53
|
+
}
|