@zq-silk/yui 0.8.3 → 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 +46 -16
- package/dist/cli/commandCatalog.js +23 -11
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli.js +154 -16
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +386 -138
- package/dist/commands/taskCompletionGate.js +36 -24
- package/dist/commands/taskContextCommand.js +6 -1
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +36 -3
- package/dist/context/sessionBootstrapManifest.js +82 -1
- 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/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/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 +1 -0
- package/dist/storage/sqliteStore.js +8 -1
- package/dist/storage/taskStore.js +10 -1
- package/dist/task/completionReadiness.js +48 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +145 -52
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/web/webSnapshot.js +7 -1
- package/i18n/README.zh-CN.md +28 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +73 -31
- package/skills/yui-operator/SKILL.md +51 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
|
@@ -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
|
+
}
|
|
@@ -18,6 +18,7 @@ import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.j
|
|
|
18
18
|
import { classifyAgentRunFailure, classifyIntegrationAttempt, classifyReviewRound, classifyWakeReasons, countFaultClasses } from "./faultClassification.js";
|
|
19
19
|
import { RUNTIME_LAUNCH_KINDS, RUNTIME_LAUNCH_PHASES } from "../runtime/launchDiagnostics.js";
|
|
20
20
|
import { UNSUPPORTED } from "./runtimeIdentity.js";
|
|
21
|
+
import { projectTaskOrchestration } from "./orchestrationMetrics.js";
|
|
21
22
|
export function createProductionExecutionAuditPorts() {
|
|
22
23
|
return {
|
|
23
24
|
openStore: (home) => openCompatibleFileTaskStore(home),
|
|
@@ -206,6 +207,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
206
207
|
events: section,
|
|
207
208
|
providerRetries: section,
|
|
208
209
|
workItems: section,
|
|
210
|
+
orchestration: section,
|
|
209
211
|
storage: section,
|
|
210
212
|
runtimeProtocol: section,
|
|
211
213
|
topLongRunning: section
|
|
@@ -443,7 +445,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
443
445
|
else if (round.deltaRecheck.disposition === "requires-full-review")
|
|
444
446
|
deltaEscalated += 1;
|
|
445
447
|
}
|
|
446
|
-
const classification = classifyReviewRound(round);
|
|
448
|
+
const classification = classifyReviewRound(round, store);
|
|
447
449
|
if (classification.faultClass === "review-infra")
|
|
448
450
|
infraFailed += 1;
|
|
449
451
|
else if (classification.faultClass === "review-semantic-negative") {
|
|
@@ -674,6 +676,37 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
674
676
|
return failed(error);
|
|
675
677
|
}
|
|
676
678
|
})();
|
|
679
|
+
const orchestration = (() => {
|
|
680
|
+
try {
|
|
681
|
+
const metrics = taskIds.flatMap((taskId) => {
|
|
682
|
+
const task = store.getTask(taskId);
|
|
683
|
+
if (task === null)
|
|
684
|
+
return [];
|
|
685
|
+
return [projectTaskOrchestration({
|
|
686
|
+
task,
|
|
687
|
+
runs: withinWindow(store.listAgentRuns(taskId), options),
|
|
688
|
+
roleSessionSets: sessionSetsWithinWindow(store.listRoleSessionSets(taskId), options),
|
|
689
|
+
workItems: withinWindow(store.listWorkItems(taskId), options),
|
|
690
|
+
changeSets: withinWindow(store.listChangeSets(taskId), options),
|
|
691
|
+
reviewRounds: withinWindow(store.listReviewRounds(taskId), options),
|
|
692
|
+
reviewFindings: withinWindow(store.listReviewFindings(taskId), options),
|
|
693
|
+
integrations: withinWindow(store.listIntegrationAttempts(taskId), options),
|
|
694
|
+
durableJobs: withinWindow(store.listDurableJobs(taskId), options),
|
|
695
|
+
publications: withinWindow(store.listPublicationReferences(taskId), options),
|
|
696
|
+
decisions: withinWindow(store.listDecisions(taskId), options),
|
|
697
|
+
events: withinWindow(store.listEvents(taskId), options),
|
|
698
|
+
managedWorkspaces: withinWindow(store.listManagedWorkspaces(taskId), options)
|
|
699
|
+
})];
|
|
700
|
+
});
|
|
701
|
+
return ok({
|
|
702
|
+
tasks: metrics,
|
|
703
|
+
advisoryCount: metrics.reduce((total, task) => total + task.advisories.length, 0)
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
return failed(error);
|
|
708
|
+
}
|
|
709
|
+
})();
|
|
677
710
|
const storage = (() => {
|
|
678
711
|
try {
|
|
679
712
|
let stateJsonBytes = UNSUPPORTED;
|
|
@@ -834,8 +867,21 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
834
867
|
events,
|
|
835
868
|
providerRetries,
|
|
836
869
|
workItems,
|
|
870
|
+
orchestration,
|
|
837
871
|
storage,
|
|
838
872
|
runtimeProtocol,
|
|
839
873
|
topLongRunning
|
|
840
874
|
};
|
|
841
875
|
}
|
|
876
|
+
function withinWindow(records, options) {
|
|
877
|
+
return records.filter((record) => inWindow(record.createdAt, options));
|
|
878
|
+
}
|
|
879
|
+
function sessionSetsWithinWindow(sets, options) {
|
|
880
|
+
if (options.since === undefined && options.until === undefined)
|
|
881
|
+
return sets;
|
|
882
|
+
return sets.map((set) => ({
|
|
883
|
+
...set,
|
|
884
|
+
sessions: Object.fromEntries(Object.entries(set.sessions).filter(([, session]) => (inWindow(session.createdAt, options)))),
|
|
885
|
+
history: (set.history ?? []).filter((session) => inWindow(session.createdAt, options))
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyReviewRoundOutcome } from "../review/reviewOutcomeClassifier.js";
|
|
1
2
|
export const FAULT_CLASSES = [
|
|
2
3
|
"provider-transient",
|
|
3
4
|
"policy-denied",
|
|
@@ -73,15 +74,16 @@ export function classifyAgentRunFailure(run, structured) {
|
|
|
73
74
|
* Review execution failure (the Round itself failed to execute/deliver) is
|
|
74
75
|
* `review-infra`; a completed Round with failed checks is a semantic negative.
|
|
75
76
|
*/
|
|
76
|
-
export function classifyReviewRound(round) {
|
|
77
|
-
|
|
77
|
+
export function classifyReviewRound(round, evidence) {
|
|
78
|
+
const outcome = classifyReviewRoundOutcome(round, evidence);
|
|
79
|
+
if (outcome?.kind === "non-semantic") {
|
|
78
80
|
return {
|
|
79
81
|
faultClass: "review-infra",
|
|
80
82
|
basis: "structured",
|
|
81
|
-
evidence:
|
|
83
|
+
evidence: outcome.reason
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
|
-
if (
|
|
86
|
+
if (outcome?.kind === "semantic" && (round.checks ?? []).some((c) => c.outcome === "failed")) {
|
|
85
87
|
const failed = (round.checks ?? [])
|
|
86
88
|
.filter((c) => c.outcome === "failed")
|
|
87
89
|
.map((c) => c.name)
|