@zq-silk/yui 0.2.0 → 0.4.2
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 +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import { createAgentRun } from "../run/agentRun.js";
|
|
2
|
+
import { markYuiRunInput } from "../run/runIdentity.js";
|
|
3
|
+
import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
4
|
+
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
5
|
+
import { effectiveLaunchSnapshotsCompatible } from "../executor/effectiveLaunch.js";
|
|
6
|
+
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
2
7
|
import { recordLeaderFailure } from "./leaderFailure.js";
|
|
3
8
|
import { createLeaderRecoveryNotification } from "./operatorNotification.js";
|
|
4
|
-
|
|
9
|
+
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
10
|
+
export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
5
11
|
const results = [];
|
|
6
|
-
|
|
12
|
+
const wakeups = selection === undefined || selection.full
|
|
13
|
+
? store.listPendingWakeups()
|
|
14
|
+
: [...selection.taskIds].flatMap((taskId) => {
|
|
15
|
+
const wakeup = store.getPendingWakeup(taskId);
|
|
16
|
+
return wakeup === null ? [] : [wakeup];
|
|
17
|
+
});
|
|
18
|
+
for (const wakeup of wakeups) {
|
|
7
19
|
const task = store.getTask(wakeup.taskId);
|
|
8
20
|
const role = store.getRole(wakeup.taskId, "leader");
|
|
9
21
|
if (task === null || task.status !== "active" || role === null) {
|
|
10
22
|
results.push({ taskId: wakeup.taskId, status: "skipped", reason: "unavailable" });
|
|
11
23
|
continue;
|
|
12
24
|
}
|
|
13
|
-
|
|
25
|
+
const taskWorkspace = store.getTaskWorkspace(task.id);
|
|
26
|
+
if (!isSchedulerTaskWorkspaceReady(task, taskWorkspace)) {
|
|
14
27
|
results.push({ taskId: task.id, status: "skipped", reason: "workspace-not-ready" });
|
|
15
28
|
continue;
|
|
16
29
|
}
|
|
@@ -23,73 +36,290 @@ export async function processLeaderWakeups(store, delivery, now) {
|
|
|
23
36
|
continue;
|
|
24
37
|
}
|
|
25
38
|
// This check deliberately precedes every tmux operation. A pending wake is
|
|
26
|
-
// durable state, not text that may be injected into a busy Agent
|
|
39
|
+
// durable state, not text that may be injected into a busy Agent process.
|
|
27
40
|
if (store.getActiveAgentRun(task.id, role.name) !== null) {
|
|
28
41
|
results.push({ taskId: task.id, status: "skipped", reason: "busy" });
|
|
29
42
|
continue;
|
|
30
43
|
}
|
|
31
|
-
|
|
44
|
+
if (typeof store.hasInFlightTurn === "function"
|
|
45
|
+
&& store.hasInFlightTurn(task.id, role.name)) {
|
|
46
|
+
results.push({ taskId: task.id, status: "skipped", reason: "busy" });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const reopening = wakeup.reasons.includes("task-reopened");
|
|
50
|
+
const existingSession = store.getRoleSession(task.id, role.name, reopening ? undefined : role.effective.agentId);
|
|
32
51
|
let effectiveSession = existingSession;
|
|
33
52
|
let claimed = false;
|
|
53
|
+
let deliveryAttempted = false;
|
|
34
54
|
let run = null;
|
|
55
|
+
let prepared;
|
|
56
|
+
let preStartFencePersisted = false;
|
|
35
57
|
try {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
if (existingSession !== null && !hasNativeSession(existingSession)) {
|
|
59
|
+
const sessionIsTerminal = existingSession.status === "stopped"
|
|
60
|
+
|| existingSession.status === "broken";
|
|
61
|
+
if (!sessionIsTerminal) {
|
|
62
|
+
// An opaque live Session has no provider identity that can be safely
|
|
63
|
+
// rebound. A host absence observation is not a verified stop; keep
|
|
64
|
+
// the wake durable until an explicit exact cleanup/reset settles it.
|
|
65
|
+
if (existingSession.launchId !== undefined) {
|
|
66
|
+
await delivery.inspectRole({
|
|
67
|
+
taskId: task.id,
|
|
68
|
+
roleName: role.name,
|
|
69
|
+
agentId: existingSession.agentId,
|
|
70
|
+
adapterId: existingSession.adapterId
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
results.push({
|
|
74
|
+
taskId: task.id,
|
|
75
|
+
status: "skipped",
|
|
76
|
+
reason: "recovery-blocked"
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
|
|
81
|
+
scope: "task",
|
|
82
|
+
taskId: task.id,
|
|
83
|
+
roleName: role.name
|
|
84
|
+
})))) {
|
|
85
|
+
// A stopped/broken Session is eligible for a fresh mode only after
|
|
86
|
+
// its exact runtime cleanup/reservation lane has settled.
|
|
87
|
+
results.push({
|
|
88
|
+
taskId: task.id,
|
|
89
|
+
status: "skipped",
|
|
90
|
+
reason: "recovery-blocked"
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const compatibleSession = existingSession !== null
|
|
96
|
+
&& effectiveLaunchSnapshotsCompatible(existingSession.effective, role.effective);
|
|
97
|
+
const reopenIdentityDrift = reopening
|
|
98
|
+
&& hasNativeSession(existingSession)
|
|
99
|
+
&& !compatibleSession;
|
|
100
|
+
if (hasNativeSession(existingSession) && !compatibleSession
|
|
101
|
+
&& !reopenIdentityDrift
|
|
102
|
+
&& existingSession.status !== "stopped" && existingSession.status !== "broken") {
|
|
103
|
+
throw new Error(`Leader Session is incompatible with desired effective launch: ${task.id}/${role.name}.`);
|
|
104
|
+
}
|
|
105
|
+
const mode = hasNativeSession(existingSession) && compatibleSession ? "resume" : "new";
|
|
106
|
+
const runId = store.peekNextAgentRunId(task.id);
|
|
107
|
+
const input = markYuiRunInput(leaderWakeupInput(task.id, runId, wakeup.reasons, task.projectBindings), runId, taskRoleSessionTitle(task, role.name));
|
|
108
|
+
run = createAgentRun(runId, task.id, role.name, mode, input, now, {
|
|
109
|
+
...(role.managedWorkspace === undefined
|
|
110
|
+
? {}
|
|
111
|
+
: { workspace: role.managedWorkspace }),
|
|
112
|
+
effective: role.effective
|
|
113
|
+
});
|
|
114
|
+
const claim = store.saveLeaderDispatch({
|
|
115
|
+
task,
|
|
116
|
+
role,
|
|
117
|
+
run,
|
|
118
|
+
// An incompatible reopened generation is intentionally not rebound to
|
|
119
|
+
// a new native host. Keep the old fixed Session as evidence while
|
|
120
|
+
// claiming a short-lived Run so the existing failure path can record a
|
|
121
|
+
// durable, retryable recovery obligation before any provider call.
|
|
122
|
+
session: reopenIdentityDrift ? null : existingSession,
|
|
123
|
+
wakeup,
|
|
124
|
+
now
|
|
125
|
+
});
|
|
126
|
+
if (claim !== "claimed") {
|
|
127
|
+
results.push({ taskId: task.id, status: "skipped", reason: claim });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
claimed = true;
|
|
131
|
+
if (reopenIdentityDrift) {
|
|
132
|
+
throw new Error(`Leader reopen refused after effective launch identity drift: ${task.id}/${role.name}.`);
|
|
133
|
+
}
|
|
134
|
+
prepared = await delivery.prepareRoleSession({
|
|
40
135
|
taskId: task.id,
|
|
41
136
|
roleName: role.name,
|
|
42
|
-
agentId: role.
|
|
43
|
-
adapterId: role.adapterId,
|
|
137
|
+
agentId: role.effective.agentId,
|
|
138
|
+
adapterId: role.effective.adapterId,
|
|
139
|
+
effective: role.effective,
|
|
140
|
+
workspace: role.effective.workspace.root,
|
|
141
|
+
...(run.workspace === undefined
|
|
142
|
+
? {}
|
|
143
|
+
: { managedWorkspace: run.workspace }),
|
|
44
144
|
mode,
|
|
45
|
-
|
|
145
|
+
runId: run.id,
|
|
146
|
+
...(mode === "resume" ? { nativeSessionId: existingSession.nativeSessionId } : {}),
|
|
147
|
+
beforeHostStart: (preflight) => {
|
|
148
|
+
persistPreStartFence(store, task, role, run, existingSession, mode, preflight, now);
|
|
149
|
+
effectiveSession = preflightSession(run.effective, existingSession, mode, preflight);
|
|
150
|
+
preStartFencePersisted = true;
|
|
151
|
+
}
|
|
46
152
|
});
|
|
153
|
+
// A fresh Codex host may already carry the exact Run prompt in its
|
|
154
|
+
// launch argv. Once preparation returns that transport fact, any
|
|
155
|
+
// later readiness or aggregate-write failure is delivery uncertainty,
|
|
156
|
+
// not a launch failure: preserve the Run and its reservation for the
|
|
157
|
+
// matching provider Hook instead of terminalizing it.
|
|
158
|
+
deliveryAttempted = prepared.inputSubmittedAtLaunch === true;
|
|
159
|
+
// Persist the exact preparation fence before waiting on provider
|
|
160
|
+
// readiness. A pre-input lifecycle Hook may fire during that wait; it
|
|
161
|
+
// must be able to resolve this Run/Session/launch generation from durable
|
|
162
|
+
// state rather than an in-memory delivery object. Fresh providers that
|
|
163
|
+
// discover their native Session later intentionally persist `null`.
|
|
164
|
+
if (!preStartFencePersisted) {
|
|
165
|
+
const preparedFenceSession = prepared.session === undefined
|
|
166
|
+
? existingSession
|
|
167
|
+
: validateReadySession(run.effective, existingSession, mode, prepared.session);
|
|
168
|
+
effectiveSession = preparedFenceSession;
|
|
169
|
+
store.saveRoleRunPrepared({
|
|
170
|
+
task,
|
|
171
|
+
role,
|
|
172
|
+
run,
|
|
173
|
+
session: preparedFenceSession,
|
|
174
|
+
...(prepared.launchId === undefined ? {} : { launchId: prepared.launchId }),
|
|
175
|
+
now
|
|
176
|
+
});
|
|
177
|
+
}
|
|
47
178
|
const ready = await delivery.waitUntilReady(prepared);
|
|
179
|
+
deliveryAttempted = deliveryAttempted
|
|
180
|
+
|| ready.prepared.inputSubmittedAtLaunch === true;
|
|
48
181
|
const latestTask = store.getTask(task.id);
|
|
49
182
|
if (latestTask === null || latestTask.status !== "active") {
|
|
183
|
+
delivery.forgetPrepared?.({
|
|
184
|
+
taskId: task.id,
|
|
185
|
+
roleName: role.name,
|
|
186
|
+
runId: run.id,
|
|
187
|
+
...(prepared.launchId === undefined
|
|
188
|
+
? {}
|
|
189
|
+
: { launchId: prepared.launchId })
|
|
190
|
+
});
|
|
50
191
|
results.push({ taskId: task.id, status: "skipped", reason: "unavailable" });
|
|
51
192
|
continue;
|
|
52
193
|
}
|
|
53
|
-
effectiveSession = validateReadySession(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
194
|
+
effectiveSession = validateReadySession(run.effective, existingSession, mode, ready.session);
|
|
195
|
+
store.saveRoleRunPrepared({
|
|
196
|
+
task,
|
|
197
|
+
role,
|
|
198
|
+
run,
|
|
199
|
+
session: effectiveSession,
|
|
200
|
+
...(ready.prepared.launchId === undefined
|
|
201
|
+
? {}
|
|
202
|
+
: { launchId: ready.prepared.launchId }),
|
|
203
|
+
now
|
|
204
|
+
});
|
|
205
|
+
if (ready.prepared.inputSubmittedAtLaunch === true) {
|
|
206
|
+
// A fresh Codex command may carry the exact first prompt in its launch
|
|
207
|
+
// argv. That is transport evidence only: the matching Provider Hook
|
|
208
|
+
// still owns Run acceptance. Persist the push fence without writing a
|
|
209
|
+
// second terminal prompt, then leave the reservation for the async
|
|
210
|
+
// SessionStart/UserPromptSubmit fold.
|
|
211
|
+
store.saveRoleRunDelivery({
|
|
212
|
+
task,
|
|
213
|
+
role,
|
|
214
|
+
run,
|
|
215
|
+
session: effectiveSession,
|
|
216
|
+
...(ready.prepared.launchId === undefined
|
|
217
|
+
? {}
|
|
218
|
+
: { launchId: ready.prepared.launchId }),
|
|
219
|
+
now
|
|
220
|
+
});
|
|
221
|
+
delivery.forgetPrepared?.({
|
|
222
|
+
taskId: task.id,
|
|
223
|
+
roleName: role.name,
|
|
224
|
+
runId: run.id,
|
|
225
|
+
...(ready.prepared.launchId === undefined
|
|
226
|
+
? {}
|
|
227
|
+
: { launchId: ready.prepared.launchId })
|
|
228
|
+
});
|
|
229
|
+
results.push({ taskId: task.id, runId: run.id, status: "dispatched" });
|
|
57
230
|
continue;
|
|
58
231
|
}
|
|
59
|
-
|
|
60
|
-
await delivery.sendOnce({
|
|
232
|
+
deliveryAttempted = true;
|
|
233
|
+
const outcome = await delivery.sendOnce({
|
|
61
234
|
delivery: ready,
|
|
62
|
-
receiptId:
|
|
235
|
+
receiptId: formatAgentRunReceiptId(task.id, run.id),
|
|
63
236
|
text: input
|
|
64
237
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
238
|
+
if (outcome === "busy" || outcome === "unavailable") {
|
|
239
|
+
results.push({
|
|
240
|
+
taskId: task.id,
|
|
241
|
+
runId: run.id,
|
|
242
|
+
status: "skipped",
|
|
243
|
+
reason: "not-ready"
|
|
244
|
+
});
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
store.saveRoleRunDelivery({
|
|
72
248
|
task,
|
|
73
249
|
role,
|
|
250
|
+
run,
|
|
74
251
|
session: effectiveSession,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
252
|
+
...(ready.prepared.launchId === undefined
|
|
253
|
+
? {}
|
|
254
|
+
: { launchId: ready.prepared.launchId }),
|
|
78
255
|
now
|
|
79
256
|
});
|
|
80
|
-
results.push({ taskId: task.id,
|
|
257
|
+
results.push({ taskId: task.id, runId: run.id, status: "dispatched" });
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
261
|
+
const message = `Leader dispatch failed: ${detail}`;
|
|
262
|
+
if (claimed && run !== null) {
|
|
263
|
+
// Once delivery begins, a send may have succeeded even when receipt
|
|
264
|
+
// observation or the aggregate write failed. Preserve the exact
|
|
265
|
+
// durable Run and let receipt-backed active delivery recover it.
|
|
266
|
+
if (deliveryAttempted) {
|
|
267
|
+
results.push({
|
|
268
|
+
taskId: task.id,
|
|
269
|
+
runId: run.id,
|
|
270
|
+
status: "failed",
|
|
271
|
+
reason: "delivery-uncertain",
|
|
272
|
+
error: message
|
|
273
|
+
});
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
delivery.forgetPrepared?.({
|
|
277
|
+
taskId: task.id,
|
|
278
|
+
roleName: role.name,
|
|
279
|
+
runId: run.id,
|
|
280
|
+
...(prepared?.launchId === undefined
|
|
281
|
+
? {}
|
|
282
|
+
: { launchId: prepared.launchId })
|
|
283
|
+
});
|
|
284
|
+
const failureResult = store.saveLeaderDispatchFailure({
|
|
285
|
+
task,
|
|
286
|
+
role,
|
|
287
|
+
session: effectiveSession,
|
|
288
|
+
claimed: { run, wakeup },
|
|
289
|
+
failure: recordLeaderFailure(task.id, effectiveSession?.nativeSessionId ?? "(unregistered)", message, now, store.getLeaderFailure(task.id)),
|
|
290
|
+
notification: createLeaderRecoveryNotification(task.id, message, now, store.getOperatorNotification(task.id)),
|
|
291
|
+
now
|
|
292
|
+
});
|
|
293
|
+
if (failureResult === "state-changed") {
|
|
294
|
+
results.push({ taskId: task.id, status: "skipped", reason: "state-changed" });
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
results.push({ taskId: task.id, runId: run.id, status: "failed", error: message });
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
results.push({
|
|
302
|
+
taskId: task.id,
|
|
303
|
+
runId: run?.id,
|
|
304
|
+
status: "failed",
|
|
305
|
+
reason: "not-ready",
|
|
306
|
+
error: message
|
|
307
|
+
});
|
|
81
308
|
}
|
|
82
309
|
}
|
|
83
310
|
return results;
|
|
84
311
|
}
|
|
85
|
-
function validateReadySession(
|
|
312
|
+
function validateReadySession(effective, existing, mode, session) {
|
|
86
313
|
if (mode === "new" && session === null)
|
|
87
314
|
return null;
|
|
88
315
|
if (session === null)
|
|
89
316
|
throw new Error("Leader resume returned no fixed native session.");
|
|
90
|
-
if (session.agentId !==
|
|
317
|
+
if (session.agentId !== effective.agentId || session.adapterId !== effective.adapterId) {
|
|
91
318
|
throw new Error(`Ready session belongs to another Agent: ${session.agentId}.`);
|
|
92
319
|
}
|
|
320
|
+
if (!effectiveLaunchSnapshotsCompatible(session.effective, effective)) {
|
|
321
|
+
throw new Error("Ready Leader session effective snapshot changed.");
|
|
322
|
+
}
|
|
93
323
|
if (!hasNativeSession(session)) {
|
|
94
324
|
throw new Error("Ready Leader session has no native session id.");
|
|
95
325
|
}
|
|
@@ -98,46 +328,57 @@ function validateReadySession(activeAgentId, existing, mode, session) {
|
|
|
98
328
|
}
|
|
99
329
|
return { ...session, status: "running" };
|
|
100
330
|
}
|
|
331
|
+
function persistPreStartFence(store, task, role, run, existingSession, mode, preflight, now) {
|
|
332
|
+
if (preflight.owner.scope !== "task"
|
|
333
|
+
|| preflight.owner.taskId !== task.id
|
|
334
|
+
|| preflight.owner.roleName !== role.name
|
|
335
|
+
|| preflight.runId !== run.id
|
|
336
|
+
|| preflight.launchId.trim().length === 0) {
|
|
337
|
+
throw new Error(`Pre-start launch fence changed the Leader Run: ${task.id}/${role.name}.`);
|
|
338
|
+
}
|
|
339
|
+
const session = preflightSession(run.effective, existingSession, mode, preflight);
|
|
340
|
+
store.saveRoleRunPrepared({
|
|
341
|
+
task,
|
|
342
|
+
role,
|
|
343
|
+
run,
|
|
344
|
+
session,
|
|
345
|
+
launchId: preflight.launchId,
|
|
346
|
+
now
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function preflightSession(effective, existing, mode, preflight) {
|
|
350
|
+
const session = preflight.nativeSessionId === undefined
|
|
351
|
+
? null
|
|
352
|
+
: {
|
|
353
|
+
agentId: preflight.agentId,
|
|
354
|
+
adapterId: preflight.adapterId,
|
|
355
|
+
nativeSessionId: preflight.nativeSessionId,
|
|
356
|
+
launchId: preflight.launchId,
|
|
357
|
+
status: "ready",
|
|
358
|
+
effective: preflight.effective
|
|
359
|
+
};
|
|
360
|
+
return validateReadySession(effective, existing, mode, session);
|
|
361
|
+
}
|
|
101
362
|
function hasNativeSession(session) {
|
|
102
363
|
return session !== null &&
|
|
103
364
|
typeof session.nativeSessionId === "string" &&
|
|
104
365
|
session.nativeSessionId.trim().length > 0;
|
|
105
366
|
}
|
|
106
|
-
function leaderWakeupInput(taskId,
|
|
367
|
+
function leaderWakeupInput(taskId, runId, reasons, projectBindings) {
|
|
107
368
|
const lines = [
|
|
108
|
-
|
|
369
|
+
"Follow the injected yui-leader Skill for this Yui wakeup.",
|
|
370
|
+
`Current Leader Run: ${runId}.`,
|
|
371
|
+
`For every Leader decision, milestone, or Work Item lifecycle command that is meaningful progress, carry this exact current-turn assertion on that command: YUI_LEADER_ACTION_RUN_ID=${runId} YUI_LEADER_ACTION_RECEIPT_ID=${formatAgentRunReceiptId(taskId, runId)}. The native Session environment may retain an older YUI_RUN_ID/launch; never copy those values, and never reuse this assertion after the turn changes.`,
|
|
372
|
+
`Yui wakeup reasons: ${reasons.join(", ")}.`,
|
|
373
|
+
`Read the authoritative context with yui task context ${taskId}.`,
|
|
374
|
+
"Keep the context layers separate: Yui Core owns durable identity, lifecycle, access, workspace, and exact-yield safety; the generic role Skill owns portable collaboration behavior; Project Policy/Knowledge owns project-specific build, test, migration, release, and review rules; the Task Contract owns this Task's objective, scope, acceptance, and evidence.",
|
|
375
|
+
"For role-run-stalled or runtime-health attention, diagnose from the exact Run/Event/Session and related WorkItem/Review/Integration records. Preserve the current fence and write a Task Message only for a new root cause, impact, recovery action, acceptance decision, or user-relevant conclusion; an unchanged healthy wait is zero Message.",
|
|
376
|
+
projectBindings.length === 0
|
|
377
|
+
? "This Task has no bound Project Policy; do not invent repository-specific rules."
|
|
378
|
+
: `Project Policy references: ${projectBindings.map((binding) => `${binding.directory} (${binding.projectId})`).join(", ")}. Read each with yui project show <project>, then yui project knowledge list <project> and yui project knowledge show <project> <knowledge>.`,
|
|
379
|
+
"Use narrower Task message, WorkItem, decision, milestone, and input commands only when a specific record needs closer inspection.",
|
|
380
|
+
`When the requested outcome is finished and there are no active Worker Runs or unresolved inputs, complete the Task with yui task complete ${taskId} --summary-file - and a quoted heredoc containing the final outcome and evidence.`,
|
|
381
|
+
`Before ending this turn, if the Task was not completed and no InputRequest terminalized this Run, release the active fence with yui task run yield ${runId} --summary-file - and a quoted heredoc containing the current result or waiting state. In particular, yield before waiting for Worker results; do not end the native turn while this Run remains active. The yield command must be the final tool action: after it succeeds, stop immediately and do not inspect, poll, accept, or perform further work in the same native turn.`
|
|
109
382
|
];
|
|
110
|
-
if (brief !== null) {
|
|
111
|
-
lines.push(`Objective: ${brief.objective}`);
|
|
112
|
-
if (brief.boundaries.length > 0) {
|
|
113
|
-
lines.push("Boundaries:");
|
|
114
|
-
for (const boundary of brief.boundaries) {
|
|
115
|
-
lines.push(` - ${boundary}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (brief.currentFocus.trim().length > 0) {
|
|
119
|
-
lines.push(`Current focus: ${brief.currentFocus}`);
|
|
120
|
-
}
|
|
121
|
-
if (brief.leaderSummary.trim().length > 0) {
|
|
122
|
-
lines.push(`Leader summary: ${brief.leaderSummary}`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const activeDecisions = decisions.filter((d) => d.status === "active").slice(-3);
|
|
126
|
-
if (activeDecisions.length > 0) {
|
|
127
|
-
lines.push("Active decisions:");
|
|
128
|
-
for (const decision of activeDecisions) {
|
|
129
|
-
lines.push(` - ${decision.title}: ${decision.rationale}`);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
const recentMilestones = [...milestones]
|
|
133
|
-
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
134
|
-
.slice(0, 3);
|
|
135
|
-
if (recentMilestones.length > 0) {
|
|
136
|
-
lines.push("Recent milestones:");
|
|
137
|
-
for (const milestone of recentMilestones) {
|
|
138
|
-
lines.push(` - ${milestone.title}`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
lines.push(`Inspect yui task context ${taskId}, which includes open and recently resolved input requests; then continue Leader stewardship. Use narrower show/list commands only when one record needs closer inspection.`);
|
|
142
383
|
return lines.join("\n");
|
|
143
384
|
}
|
|
@@ -1,85 +1,148 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
if (
|
|
1
|
+
import { createInputRequestOperatorPresentation, createLeaderRecoveryOperatorPresentation, createLeaderStallOperatorPresentation, createTaskTerminalOperatorPresentation } from "../interaction/operatorPresentation.js";
|
|
2
|
+
export async function processOperatorInputNotifications(store, delivery, selection, now = new Date()) {
|
|
3
|
+
if (selection !== undefined && !selection.full && !selection.operator)
|
|
4
4
|
return [];
|
|
5
|
+
const targetMailbox = { kind: "operator" };
|
|
6
|
+
const mailbox = store.getWorkMailbox(targetMailbox);
|
|
7
|
+
if (mailbox === null || (mailbox.pending === null && mailbox.processing === null))
|
|
8
|
+
return [];
|
|
9
|
+
const pending = mailbox.pending;
|
|
10
|
+
const claim = store.claimWorkMailbox({
|
|
11
|
+
target: targetMailbox,
|
|
12
|
+
batchId: pending === null
|
|
13
|
+
? "operator-recovery"
|
|
14
|
+
: `operator:${pending.fromSequence}-${pending.toSequence}`,
|
|
15
|
+
owner: "controller",
|
|
16
|
+
now
|
|
17
|
+
});
|
|
18
|
+
if (claim.status === "empty")
|
|
19
|
+
return [];
|
|
20
|
+
const processing = claim.processing;
|
|
21
|
+
const requests = processing.batch.refs
|
|
22
|
+
.flatMap((ref) => ref.type === "input"
|
|
23
|
+
? [store.getInputRequest(ref.taskId, ref.id)]
|
|
24
|
+
: [])
|
|
25
|
+
.filter((request) => request !== null && request.status === "open")
|
|
26
|
+
.map((request) => ({ kind: "input", request }));
|
|
27
|
+
const notifications = processing.batch.refs
|
|
28
|
+
.filter((ref) => ref.type === "task")
|
|
29
|
+
.map((ref) => store.getOperatorNotification(ref.id))
|
|
30
|
+
.filter((notification) => notification !== null)
|
|
31
|
+
.map((notification) => ({ kind: "notification", notification }));
|
|
32
|
+
const attentions = deduplicateAttention([...requests, ...notifications]);
|
|
33
|
+
if (attentions.length === 0) {
|
|
34
|
+
store.completeWorkMailbox(targetMailbox, processing.batchId);
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
5
37
|
const target = store.getOperatorDeliveryTarget();
|
|
6
38
|
if (target === null || delivery.notifyOperatorInputOnce === undefined) {
|
|
7
39
|
const reason = target === null ? "operator-unavailable" : "delivery-unsupported";
|
|
8
|
-
|
|
40
|
+
store.releaseWorkMailbox(targetMailbox, processing.batchId);
|
|
41
|
+
return attentions.map((attention) => skipped(attention, reason));
|
|
9
42
|
}
|
|
10
43
|
const results = [];
|
|
11
|
-
for (const [index,
|
|
44
|
+
for (const [index, attention] of attentions.entries()) {
|
|
12
45
|
try {
|
|
46
|
+
const presentation = createAttentionPresentation(attention, store);
|
|
13
47
|
const outcome = await delivery.notifyOperatorInputOnce({
|
|
14
48
|
...target,
|
|
15
|
-
receiptId:
|
|
16
|
-
text:
|
|
49
|
+
receiptId: presentation.receiptId,
|
|
50
|
+
text: presentation.text
|
|
17
51
|
});
|
|
18
52
|
if (outcome === "unavailable") {
|
|
19
|
-
|
|
20
|
-
results.push(
|
|
53
|
+
store.releaseWorkMailbox(targetMailbox, processing.batchId);
|
|
54
|
+
results.push(skipped(attention, "operator-unavailable"));
|
|
55
|
+
results.push(...attentions.slice(index + 1).map((pending) => (skipped(pending, "operator-unavailable"))));
|
|
21
56
|
break;
|
|
22
57
|
}
|
|
23
58
|
else if (outcome === "not-ready") {
|
|
24
|
-
|
|
25
|
-
results.push(
|
|
59
|
+
store.releaseWorkMailbox(targetMailbox, processing.batchId);
|
|
60
|
+
results.push(skipped(attention, "operator-not-ready"));
|
|
61
|
+
results.push(...attentions.slice(index + 1).map((pending) => (skipped(pending, "operator-not-ready"))));
|
|
26
62
|
break;
|
|
27
63
|
}
|
|
28
64
|
else {
|
|
29
65
|
results.push({
|
|
30
|
-
|
|
31
|
-
taskId: request.taskId,
|
|
66
|
+
...attentionIdentity(attention),
|
|
32
67
|
status: outcome
|
|
33
68
|
});
|
|
34
69
|
if (outcome === "sent") {
|
|
35
|
-
|
|
70
|
+
if (index === attentions.length - 1) {
|
|
71
|
+
store.completeWorkMailbox(targetMailbox, processing.batchId);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
store.releaseWorkMailbox(targetMailbox, processing.batchId);
|
|
75
|
+
}
|
|
76
|
+
results.push(...attentions.slice(index + 1).map((pending) => (skipped(pending, "operator-not-ready"))));
|
|
36
77
|
break;
|
|
37
78
|
}
|
|
38
79
|
}
|
|
39
80
|
}
|
|
40
81
|
catch (error) {
|
|
82
|
+
store.releaseWorkMailbox(targetMailbox, processing.batchId);
|
|
41
83
|
results.push({
|
|
42
|
-
|
|
43
|
-
taskId: request.taskId,
|
|
84
|
+
...attentionIdentity(attention),
|
|
44
85
|
status: "failed",
|
|
45
86
|
error: error instanceof Error ? error.message : String(error)
|
|
46
87
|
});
|
|
88
|
+
break;
|
|
47
89
|
}
|
|
48
90
|
}
|
|
91
|
+
if (results.length > 0 && results.every((result) => result.status === "already-sent")) {
|
|
92
|
+
store.completeWorkMailbox(targetMailbox, processing.batchId);
|
|
93
|
+
}
|
|
49
94
|
return results;
|
|
50
95
|
}
|
|
51
|
-
function
|
|
52
|
-
const recommendedChoiceKey = request.policy.kind === "recommended"
|
|
53
|
-
? request.policy.recommendedChoiceKey
|
|
54
|
-
: undefined;
|
|
55
|
-
const recommendedChoice = recommendedChoiceKey === undefined
|
|
56
|
-
? undefined
|
|
57
|
-
: request.choices.find((choice) => choice.key === recommendedChoiceKey);
|
|
58
|
-
return [
|
|
59
|
-
"A Task Leader is waiting for user input. Present this question to the user; do not answer it yourself.",
|
|
60
|
-
`Task: ${request.taskId}`,
|
|
61
|
-
`Input: ${request.id}`,
|
|
62
|
-
`Question: ${request.question}`,
|
|
63
|
-
...(request.choices.length === 0
|
|
64
|
-
? ["Answer type: free text"]
|
|
65
|
-
: ["Choices:", ...request.choices.map((choice) => ` ${choice.key}: ${choice.label}`)]),
|
|
66
|
-
...(request.policy.kind === "required"
|
|
67
|
-
? ["Decision policy: user response required; there is no automatic fallback."]
|
|
68
|
-
: [
|
|
69
|
-
`Agent recommendation: ${recommendedChoice.key}: ${recommendedChoice.label}`,
|
|
70
|
-
`Automatic fallback after: ${request.policy.timeoutAt}`
|
|
71
|
-
]),
|
|
72
|
-
`Inspect: yui task input show ${request.id}`,
|
|
73
|
-
request.choices.length === 0
|
|
74
|
-
? `After the user replies: yui task input answer ${request.id} --text "<answer>"`
|
|
75
|
-
: `After the user chooses: yui task input answer ${request.id} --choice <key>`
|
|
76
|
-
].join("\n");
|
|
77
|
-
}
|
|
78
|
-
function skipped(request, reason) {
|
|
96
|
+
function skipped(attention, reason) {
|
|
79
97
|
return {
|
|
80
|
-
|
|
81
|
-
taskId: request.taskId,
|
|
98
|
+
...attentionIdentity(attention),
|
|
82
99
|
status: "skipped",
|
|
83
100
|
reason
|
|
84
101
|
};
|
|
85
102
|
}
|
|
103
|
+
function attentionIdentity(attention) {
|
|
104
|
+
if (attention.kind === "input") {
|
|
105
|
+
return { inputRequestId: attention.request.id, taskId: attention.request.taskId };
|
|
106
|
+
}
|
|
107
|
+
if (attention.notification.type === "leader-recovery-failed") {
|
|
108
|
+
return {
|
|
109
|
+
recoveryTaskId: attention.notification.taskId,
|
|
110
|
+
taskId: attention.notification.taskId
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (attention.notification.type === "leader-stalled") {
|
|
114
|
+
return {
|
|
115
|
+
stallTaskId: attention.notification.taskId,
|
|
116
|
+
taskId: attention.notification.taskId
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
terminalTaskId: attention.notification.taskId,
|
|
121
|
+
taskId: attention.notification.taskId
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function createAttentionPresentation(attention, store) {
|
|
125
|
+
if (attention.kind === "input") {
|
|
126
|
+
return createInputRequestOperatorPresentation(attention.request, store.getPresentationContext());
|
|
127
|
+
}
|
|
128
|
+
if (attention.notification.type === "leader-recovery-failed") {
|
|
129
|
+
return createLeaderRecoveryOperatorPresentation(attention.notification);
|
|
130
|
+
}
|
|
131
|
+
return attention.notification.type === "leader-stalled"
|
|
132
|
+
? createLeaderStallOperatorPresentation(attention.notification)
|
|
133
|
+
: createTaskTerminalOperatorPresentation(attention.notification);
|
|
134
|
+
}
|
|
135
|
+
function deduplicateAttention(attentions) {
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
return attentions.filter((attention) => {
|
|
138
|
+
const key = attention.kind === "input"
|
|
139
|
+
? `input:${attention.request.taskId}:${attention.request.id}`
|
|
140
|
+
: attention.notification.type === "leader-stalled"
|
|
141
|
+
? `stall:${attention.notification.taskId}:${attention.notification.runId}:${attention.notification.progressAt}`
|
|
142
|
+
: `recovery:${attention.notification.taskId}:${attention.notification.createdAt}`;
|
|
143
|
+
if (seen.has(key))
|
|
144
|
+
return false;
|
|
145
|
+
seen.add(key);
|
|
146
|
+
return true;
|
|
147
|
+
});
|
|
148
|
+
}
|