@zq-silk/yui 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commandCatalog.js +35 -12
- package/dist/cli/interactionPolicy.js +2 -2
- package/dist/cli/updatePorts.js +20 -0
- package/dist/commands/durableJobCommands.js +50 -0
- package/dist/commands/projectCommands.js +4 -33
- package/dist/commands/scheduleCommands.js +45 -0
- package/dist/commands/taskActivationCommands.js +19 -5
- package/dist/commands/taskCommands.js +2 -1
- package/dist/commands/taskFactCommands.js +24 -12
- package/dist/commands/taskPublicationCommands.js +6 -3
- package/dist/commands/taskPublicationVerifyCommand.js +74 -28
- package/dist/context/runContextPack.js +24 -4
- package/dist/context/taskContext.js +52 -15
- package/dist/context/taskPhase.js +99 -0
- package/dist/controlPlaneCli.js +24 -5
- package/dist/controller/controller.js +35 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +16 -1
- package/dist/controller/jobControl.js +42 -12
- package/dist/controller/jobDefinitionControl.js +304 -0
- package/dist/controller/jobSupervisor.js +47 -32
- package/dist/controller/runtime.js +11 -2
- package/dist/controller/scheduleControl.js +176 -0
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/external/githubPublicationVerifier.js +14 -5
- package/dist/external/gitlabPublicationVerifier.js +2 -1
- package/dist/interaction/notificationPresentation.js +3 -3
- package/dist/interaction/operatorPresentation.js +5 -0
- package/dist/job/builtinJob.js +58 -0
- package/dist/job/durableJob.js +72 -49
- package/dist/job/jobDefinition.js +68 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/projectRefreshRunner.js +48 -0
- package/dist/repository/projectRefresh.js +58 -0
- package/dist/repository/taskWorkspacePreparer.js +2 -1
- package/dist/schedule/schedule.js +108 -0
- package/dist/scheduler/operatorEvent.js +2 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +8 -0
- package/dist/storage/contextRecords.js +8 -0
- package/dist/storage/jobSchema.js +15 -0
- package/dist/storage/maintenanceCaller.js +69 -0
- package/dist/storage/recordValidation.js +5 -0
- package/dist/storage/scheduleSchema.js +12 -0
- package/dist/storage/sqliteSchema.js +75 -25
- package/dist/storage/sqliteStore.js +110 -9
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +5 -0
- package/dist/storage/upgrade/jobDefinitionMigration.js +22 -0
- package/dist/task/publicationToolObservation.js +128 -0
- package/dist/task/taskActivationService.js +35 -0
- package/dist/web/assets/client/app/settings.js +7 -1
- package/dist/web/assets/client/i18n/en.js +61 -0
- package/dist/web/assets/client/i18n/zh-CN.js +61 -0
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/views/jobs.js +222 -0
- package/dist/web/assets/client/views/task/page.js +11 -1
- package/dist/web/assets/shared/manageNav.js +1 -0
- package/dist/web/assets/shared/taskTabs.js +2 -2
- package/dist/web/assets/shell/settingsContent.js +3 -0
- package/dist/web/assets/styles/index.js +2 -0
- package/dist/web/assets/styles/views/jobs.js +19 -0
- package/dist/web/webJobs.js +52 -0
- package/dist/web/webServer.js +31 -0
- package/docs/cli-information-contract.md +43 -2
- package/docs/cli-information-contract.zh-CN.md +32 -1
- package/docs/job-runs.md +224 -0
- package/docs/release-workflow.md +65 -0
- package/docs/release-workflow.zh-CN.md +49 -0
- package/docs/schedules.md +132 -0
- package/docs/storage-baseline.md +18 -1
- package/docs/storage-baseline.zh-CN.md +15 -1
- package/docs/task-delivery.md +68 -2
- package/docs/task-delivery.zh-CN.md +33 -1
- package/docs/testing/verification-levels.md +6 -2
- package/docs/testing/verification-levels.zh-CN.md +6 -3
- package/package.json +4 -3
- package/skills/yui-leader/SKILL.md +18 -1
- package/skills/yui-leader/VERSION +1 -1
- package/skills/yui-leader/references/planning.md +9 -1
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-reviewer/VERSION +1 -1
- package/skills/yui-runtime/SKILL.md +9 -3
- package/skills/yui-runtime/VERSION +1 -1
- package/skills/yui-runtime/references/context-ordering.md +103 -0
- package/skills/yui-runtime/references/publication.md +44 -4
- package/skills/yui-worker/SKILL.md +5 -0
- package/skills/yui-worker/VERSION +1 -1
|
@@ -15,7 +15,7 @@ import { spawn } from "node:child_process";
|
|
|
15
15
|
import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, statSync } from "node:fs";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { cancelQueuedDurableJob, completeDurableJob, isDurableJobTerminal, markDurableJobUnknown, markDurableJobWakeupNotified, rejectQueuedDurableJob, startDurableJob, touchDurableJobHeartbeat } from "../job/durableJob.js";
|
|
18
|
+
import { cancelQueuedDurableJob, completeDurableJob, isDurableJobTerminal, markDurableJobUnknown, markDurableJobWakeupNotified, rejectQueuedDurableJob, startDurableJob, touchDurableJobHeartbeat, executionNamespace } from "../job/durableJob.js";
|
|
19
19
|
import { recordOperationEvidence } from "../kernel/operationFacts.js";
|
|
20
20
|
import { wakeReason } from "../scheduler/wakeReason.js";
|
|
21
21
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
@@ -45,12 +45,23 @@ export class DurableJobSupervisor {
|
|
|
45
45
|
}
|
|
46
46
|
reconcile(now) {
|
|
47
47
|
const jobs = this.#store.listActiveDurableJobs();
|
|
48
|
-
|
|
48
|
+
const refresh = (job) => !("taskId" in job) && job.action === "project.refresh";
|
|
49
|
+
const waiting = (job) => refresh(job) && job.status === "queued"
|
|
50
|
+
&& job.operation.effect === "none" && !job.cancelRequestedAt;
|
|
51
|
+
// Harvest/recover existing attempts first. Network operations remain in
|
|
52
|
+
// detached runners, with at most four outstanding Project refreshes.
|
|
53
|
+
const ordered = [...jobs.filter(job => !waiting(job)), ...jobs.filter(waiting)];
|
|
54
|
+
for (const job of ordered) {
|
|
49
55
|
try {
|
|
56
|
+
if (waiting(job)) {
|
|
57
|
+
const occupied = this.#store.listActiveDurableJobs().filter(candidate => refresh(candidate) && (candidate.status === "running" || candidate.operation.effect !== "none"));
|
|
58
|
+
if (occupied.length >= 4)
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
50
61
|
this.#reconcileJob(job, now);
|
|
51
62
|
}
|
|
52
63
|
catch (error) {
|
|
53
|
-
this.#onError(new Error(`DurableJob supervision failed for ${job
|
|
64
|
+
this.#onError(new Error(`DurableJob supervision failed for ${executionNamespace(job)}/${job.id}: `
|
|
54
65
|
+ `${error instanceof Error ? error.message : String(error)}. `
|
|
55
66
|
+ "Inspect retained process/artifact evidence before retry or cancellation; effects remain unconfirmed.", { cause: error }));
|
|
56
67
|
}
|
|
@@ -68,14 +79,14 @@ export class DurableJobSupervisor {
|
|
|
68
79
|
}
|
|
69
80
|
/** Composite key for the SIGKILL deadline map. */
|
|
70
81
|
#sigkillKey(job) {
|
|
71
|
-
return `${job
|
|
82
|
+
return `${executionNamespace(job)}/${job.id}`;
|
|
72
83
|
}
|
|
73
84
|
/** Adopt observed execution, preserve unknown, or start an unattempted request. */
|
|
74
85
|
#reconcileQueued(job, now) {
|
|
75
|
-
const marker = this.#artifacts.readStartMarker(job
|
|
86
|
+
const marker = this.#artifacts.readStartMarker(executionNamespace(job), job.id);
|
|
76
87
|
const spawned = this.#spawnedProcessFromEvidence(job, marker);
|
|
77
88
|
if (job.cancelRequestedAt !== undefined) {
|
|
78
|
-
this.#artifacts.writeCancelFence(job
|
|
89
|
+
this.#artifacts.writeCancelFence(executionNamespace(job), job.id);
|
|
79
90
|
if (spawned !== null) {
|
|
80
91
|
this.#process.signalIfOwned(spawned.pid, spawned.startIdentity, "SIGTERM");
|
|
81
92
|
}
|
|
@@ -87,13 +98,13 @@ export class DurableJobSupervisor {
|
|
|
87
98
|
if (job.operation.effect !== "none") {
|
|
88
99
|
// A send intent was committed but no acceptance can be proved. Absence
|
|
89
100
|
// of a file is not proof of no external effect. Never respawn unknown.
|
|
90
|
-
const terminal = this.#store.transitionDurableJob(job
|
|
101
|
+
const terminal = this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => markDurableJobWakeupNotified(markDurableJobUnknown(current, "runner acceptance is unknown; inspect the original request", current.checkpoint?.completedSteps ?? [], now), now), now, { reason: wakeReason("job-finished"), refs: wakeupRefs(job) });
|
|
91
102
|
this.#deliverTerminalEvent(terminal);
|
|
92
103
|
return;
|
|
93
104
|
}
|
|
94
105
|
if (job.cancelRequestedAt !== undefined) {
|
|
95
106
|
// No effect attempted. Cancellation and its wake commit atomically.
|
|
96
|
-
const terminal = this.#store.transitionDurableJob(job
|
|
107
|
+
const terminal = this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => markDurableJobWakeupNotified(cancelQueuedDurableJob(current, now), now), now, { reason: wakeReason("job-finished"), refs: wakeupRefs(job) });
|
|
97
108
|
this.#deliverTerminalEvent(terminal);
|
|
98
109
|
return;
|
|
99
110
|
}
|
|
@@ -112,7 +123,7 @@ export class DurableJobSupervisor {
|
|
|
112
123
|
// around the spawn), while ready.json is written by the runner itself
|
|
113
124
|
// before any side effect. A disagreement (PID reuse, spawn retry, stale
|
|
114
125
|
// /proc read) is resolved in favor of ready.json.
|
|
115
|
-
const ready = this.#artifacts.readReadyFile(job
|
|
126
|
+
const ready = this.#artifacts.readReadyFile(executionNamespace(job), job.id);
|
|
116
127
|
if (ready !== null) {
|
|
117
128
|
return { pid: ready.pid, startIdentity: ready.startIdentity };
|
|
118
129
|
}
|
|
@@ -128,9 +139,9 @@ export class DurableJobSupervisor {
|
|
|
128
139
|
* process. A request with unobserved acceptance instead stays unknown.
|
|
129
140
|
*/
|
|
130
141
|
#adoptAndContinue(job, process, now) {
|
|
131
|
-
this.#store.transitionDurableJob(job
|
|
142
|
+
this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => startDurableJob(current, process, now), now);
|
|
132
143
|
// The runner may already have finished while the Controller was down.
|
|
133
|
-
const exit = this.#artifacts.readExitJson(job
|
|
144
|
+
const exit = this.#artifacts.readExitJson(executionNamespace(job), job.id);
|
|
134
145
|
if (exit !== null) {
|
|
135
146
|
this.#harvestExit(job, exit, now);
|
|
136
147
|
return;
|
|
@@ -153,18 +164,18 @@ export class DurableJobSupervisor {
|
|
|
153
164
|
}
|
|
154
165
|
const spec = {
|
|
155
166
|
jobId: job.id,
|
|
156
|
-
taskId: job.taskId,
|
|
167
|
+
...("taskId" in job ? { taskId: job.taskId } : { scope: "global" }),
|
|
157
168
|
workspace: job.workspace,
|
|
158
169
|
env: job.env,
|
|
159
170
|
steps: job.steps,
|
|
160
171
|
defaultStepTimeoutMs: DEFAULT_STEP_TIMEOUT_MS,
|
|
161
|
-
artifactDir: this.#artifacts.artifactDir(job
|
|
172
|
+
artifactDir: this.#artifacts.artifactDir(executionNamespace(job), job.id),
|
|
162
173
|
head: job.head
|
|
163
174
|
};
|
|
164
|
-
const specPath = this.#artifacts.writeSpec(job
|
|
175
|
+
const specPath = this.#artifacts.writeSpec(executionNamespace(job), job.id, spec);
|
|
165
176
|
let attempted;
|
|
166
177
|
try {
|
|
167
|
-
attempted = this.#store.transitionDurableJob(job
|
|
178
|
+
attempted = this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => {
|
|
168
179
|
if (current.status !== "queued" || current.operation.effect !== "none") {
|
|
169
180
|
throw new Error("Job already attempted; inspect the original request.");
|
|
170
181
|
}
|
|
@@ -184,7 +195,7 @@ export class DurableJobSupervisor {
|
|
|
184
195
|
return;
|
|
185
196
|
// Both the request and possible-effect boundary precede spawn. A missing
|
|
186
197
|
// acceptance after this point is unknown, never permission to respawn.
|
|
187
|
-
this.#artifacts.writeStartMarker(job
|
|
198
|
+
this.#artifacts.writeStartMarker(executionNamespace(job), job.id, {
|
|
188
199
|
pid: 0,
|
|
189
200
|
startIdentity: "pending",
|
|
190
201
|
spawnedAt: now.toISOString()
|
|
@@ -194,7 +205,7 @@ export class DurableJobSupervisor {
|
|
|
194
205
|
// the runner's own ready.json is the fallback: the next reconcile reads
|
|
195
206
|
// ready and adopts. The runner checks ready with O_EXCL and exits if it
|
|
196
207
|
// already exists, preventing duplicate execution.
|
|
197
|
-
this.#artifacts.writeStartMarker(job
|
|
208
|
+
this.#artifacts.writeStartMarker(executionNamespace(job), job.id, {
|
|
198
209
|
pid: spawned.pid,
|
|
199
210
|
startIdentity: spawned.startIdentity ?? String(spawned.pid),
|
|
200
211
|
spawnedAt: now.toISOString()
|
|
@@ -206,27 +217,27 @@ export class DurableJobSupervisor {
|
|
|
206
217
|
// terminalized or no longer exists; the recovery interval stays the
|
|
207
218
|
// cross-restart fallback. No new timer is pinned while the Controller is
|
|
208
219
|
// idle — each wake is a single event-driven signal.
|
|
209
|
-
spawned.onExit?.(() => this.#wake(job
|
|
210
|
-
this.#wake(job
|
|
220
|
+
spawned.onExit?.(() => this.#wake(executionNamespace(job)));
|
|
221
|
+
this.#wake(executionNamespace(job));
|
|
211
222
|
}
|
|
212
223
|
#rejectStart(job, error, now) {
|
|
213
224
|
// Only an explicit domain refusal is a known failure. Storage/CAS/runtime
|
|
214
225
|
// errors still propagate; never infer rejection from an unavailable read.
|
|
215
226
|
if (!(error instanceof Error) || error.name !== "CoreJobError")
|
|
216
227
|
throw error;
|
|
217
|
-
const terminal = this.#store.transitionDurableJob(job
|
|
228
|
+
const terminal = this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => markDurableJobWakeupNotified(rejectQueuedDurableJob(current, error.message, now), now), now, { reason: wakeReason("job-finished"), refs: wakeupRefs(job) });
|
|
218
229
|
this.#deliverTerminalEvent(terminal);
|
|
219
230
|
}
|
|
220
231
|
#superviseRunning(job, now) {
|
|
221
232
|
// 1. Harvest exit.json if present.
|
|
222
|
-
const exit = this.#artifacts.readExitJson(job
|
|
233
|
+
const exit = this.#artifacts.readExitJson(executionNamespace(job), job.id);
|
|
223
234
|
if (exit !== null) {
|
|
224
235
|
this.#harvestExit(job, exit, now);
|
|
225
236
|
return;
|
|
226
237
|
}
|
|
227
238
|
// 2. Cancel requested: write fence + SIGTERM.
|
|
228
239
|
if (job.cancelRequestedAt !== undefined && job.process !== undefined) {
|
|
229
|
-
this.#artifacts.writeCancelFence(job
|
|
240
|
+
this.#artifacts.writeCancelFence(executionNamespace(job), job.id);
|
|
230
241
|
this.#process.signalIfOwned(job.process.pid, job.process.startIdentity, "SIGTERM");
|
|
231
242
|
}
|
|
232
243
|
// 3. Process liveness via pid + startIdentity.
|
|
@@ -241,7 +252,7 @@ export class DurableJobSupervisor {
|
|
|
241
252
|
}
|
|
242
253
|
}
|
|
243
254
|
// 4. Heartbeat freshness.
|
|
244
|
-
const heartbeatMtime = this.#artifacts.heartbeatMtime(job
|
|
255
|
+
const heartbeatMtime = this.#artifacts.heartbeatMtime(executionNamespace(job), job.id);
|
|
245
256
|
if (heartbeatMtime !== null) {
|
|
246
257
|
const staleMs = now.getTime() - heartbeatMtime;
|
|
247
258
|
if (staleMs > HEARTBEAT_STALE_MS && job.process !== undefined) {
|
|
@@ -255,7 +266,7 @@ export class DurableJobSupervisor {
|
|
|
255
266
|
? Date.parse(job.heartbeatAt)
|
|
256
267
|
: 0;
|
|
257
268
|
if (heartbeatMtime > recordHeartbeat) {
|
|
258
|
-
this.#store.transitionDurableJob(job
|
|
269
|
+
this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => touchDurableJobHeartbeat(current, new Date(heartbeatMtime)), now);
|
|
259
270
|
}
|
|
260
271
|
}
|
|
261
272
|
// 5. SIGKILL escalation.
|
|
@@ -268,7 +279,7 @@ export class DurableJobSupervisor {
|
|
|
268
279
|
#harvestExit(job, exit, now) {
|
|
269
280
|
// Persist the original receipt locator before interpreting its output.
|
|
270
281
|
// A bad schema cannot erase evidence of an already executed runner.
|
|
271
|
-
this.#store.transitionDurableJob(job
|
|
282
|
+
this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => ({
|
|
272
283
|
...current,
|
|
273
284
|
operation: recordOperationEvidence(current.operation, {
|
|
274
285
|
effect: "confirmed",
|
|
@@ -292,7 +303,7 @@ export class DurableJobSupervisor {
|
|
|
292
303
|
// the adapter enqueues the Leader mailbox entry in the same transaction.
|
|
293
304
|
// A separate #notifyWakeup pass would lose the wakeup if the Controller
|
|
294
305
|
// died between the terminal write and the flag flip.
|
|
295
|
-
const terminal = this.#store.transitionDurableJob(job
|
|
306
|
+
const terminal = this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => {
|
|
296
307
|
let completed;
|
|
297
308
|
try {
|
|
298
309
|
completed = completeDurableJob(current, result, now);
|
|
@@ -310,7 +321,7 @@ export class DurableJobSupervisor {
|
|
|
310
321
|
this.#sigkillAt.delete(this.#sigkillKey(job));
|
|
311
322
|
}
|
|
312
323
|
#handleDeadProcess(job, now) {
|
|
313
|
-
const checkpoint = this.#artifacts.readCheckpoint(job
|
|
324
|
+
const checkpoint = this.#artifacts.readCheckpoint(executionNamespace(job), job.id);
|
|
314
325
|
const proven = checkpoint === null ? null : proveOutcomeFromCheckpoint(job, checkpoint);
|
|
315
326
|
// rr4/finding-7: Same atomic wakeup composition as #harvestExit — the
|
|
316
327
|
// terminal transition and the Leader wakeup commit together.
|
|
@@ -318,12 +329,12 @@ export class DurableJobSupervisor {
|
|
|
318
329
|
// The runner died without exit.json, but its step checkpoint covered
|
|
319
330
|
// every planned step: the outcome is still provable from durable
|
|
320
331
|
// evidence. No false pass — the checkpoint is the runner's own record.
|
|
321
|
-
const terminal = this.#store.transitionDurableJob(job
|
|
332
|
+
const terminal = this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => markDurableJobWakeupNotified(completeDurableJob(current, { ...proven, evidenceSource: "checkpoint" }, now), now), now, { reason: wakeReason("job-finished"), refs: wakeupRefs(job) });
|
|
322
333
|
this.#deliverTerminalEvent(terminal);
|
|
323
334
|
}
|
|
324
335
|
else {
|
|
325
336
|
const completedSteps = checkpoint?.completedSteps ?? [];
|
|
326
|
-
const terminal = this.#store.transitionDurableJob(job
|
|
337
|
+
const terminal = this.#store.transitionDurableJob(executionNamespace(job), job.id, (current) => markDurableJobWakeupNotified(markDurableJobUnknown(current, "runner process exited without writing exit.json", completedSteps, now), now), now, { reason: wakeReason("job-finished"), refs: wakeupRefs(job) });
|
|
327
338
|
this.#deliverTerminalEvent(terminal);
|
|
328
339
|
}
|
|
329
340
|
this.#sigkillAt.delete(this.#sigkillKey(job));
|
|
@@ -339,8 +350,10 @@ export class DurableJobSupervisor {
|
|
|
339
350
|
#deliverTerminalEvent(job) {
|
|
340
351
|
if (job === null || !isDurableJobTerminal(job.status))
|
|
341
352
|
return;
|
|
353
|
+
if (!("taskId" in job))
|
|
354
|
+
return; // The durable Operator mailbox owns global notices.
|
|
342
355
|
this.#terminalEvents?.deliverTerminalEvent({
|
|
343
|
-
taskId: job
|
|
356
|
+
taskId: executionNamespace(job),
|
|
344
357
|
jobId: job.id,
|
|
345
358
|
status: job.status,
|
|
346
359
|
outcome: job.result?.outcome ?? job.status
|
|
@@ -348,9 +361,11 @@ export class DurableJobSupervisor {
|
|
|
348
361
|
}
|
|
349
362
|
}
|
|
350
363
|
function wakeupRefs(job) {
|
|
351
|
-
|
|
364
|
+
if (!("taskId" in job))
|
|
365
|
+
return [];
|
|
366
|
+
const refs = [{ type: "task", id: executionNamespace(job) }];
|
|
352
367
|
if (job.owner.kind === "work-item") {
|
|
353
|
-
refs.push({ type: "work-item", taskId: job
|
|
368
|
+
refs.push({ type: "work-item", taskId: executionNamespace(job), id: job.owner.workItemId });
|
|
354
369
|
}
|
|
355
370
|
return refs;
|
|
356
371
|
}
|
|
@@ -45,6 +45,7 @@ import { archiveOrdinaryTask } from "../task/ordinaryArchive.js";
|
|
|
45
45
|
import { createWebSettings } from "../web/webSettings.js";
|
|
46
46
|
import { createModelManagement } from "../models/modelManagement.js";
|
|
47
47
|
import { createWebSkills } from "../web/webSkills.js";
|
|
48
|
+
import { createWebJobs } from "../web/webJobs.js";
|
|
48
49
|
import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
49
50
|
import { AgentRuntimeObserver } from "./agentRuntimeObserver.js";
|
|
50
51
|
import { createCapabilityDispatcher } from "./capabilityBridge.js";
|
|
@@ -55,6 +56,8 @@ import { createEphemeralResourceReaper } from "./ephemeralResourceReaper.js";
|
|
|
55
56
|
import { AgentHostProviderSessionBusyError, AgentHostProviderTurnFenceError, FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
|
|
56
57
|
import { deliverGlobalInputs } from "./globalInputDelivery.js";
|
|
57
58
|
import { authorizeJobStart } from "./jobControl.js";
|
|
59
|
+
import { authorizeGlobalJobStart, createJobDefinitionControl } from "./jobDefinitionControl.js";
|
|
60
|
+
import { createScheduleControl, reconcileSchedules } from "./scheduleControl.js";
|
|
58
61
|
import { createFileArtifactPort, createLinuxProcessPort, DurableJobSupervisor } from "./jobSupervisor.js";
|
|
59
62
|
import { createProviderRetryHooks } from "./providerRetryDelivery.js";
|
|
60
63
|
import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
|
|
@@ -412,6 +415,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
412
415
|
const web = createControllerWeb(store, {
|
|
413
416
|
previews: () => createTaskPreviews(store, home),
|
|
414
417
|
skills: createWebSkills(store),
|
|
418
|
+
jobs: createWebJobs(store, home),
|
|
415
419
|
models: createModelManagement(store),
|
|
416
420
|
settings: createWebSettings(store, {
|
|
417
421
|
environment: options.environment ?? process.env, catalogs,
|
|
@@ -483,7 +487,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
483
487
|
store: schedulerStore,
|
|
484
488
|
process: kernel.runner,
|
|
485
489
|
artifacts: createFileArtifactPort(home),
|
|
486
|
-
authorizeStart: (job) => authorizeJobStart(store, job),
|
|
490
|
+
authorizeStart: (job) => "taskId" in job ? authorizeJobStart(store, job) : authorizeGlobalJobStart(store, job),
|
|
487
491
|
// rr6/f1: Bounded supervision wake. The supervisor signals the Controller
|
|
488
492
|
// after spawning a runner (queued→running adoption) and when a runner
|
|
489
493
|
// exits (terminal harvest), so a quick job converges without waiting for
|
|
@@ -492,7 +496,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
492
496
|
// no-op. The recovery interval stays the cross-restart fallback.
|
|
493
497
|
wake: (taskId) => {
|
|
494
498
|
try {
|
|
495
|
-
runningRuntime?.signal(`task:${taskId}`);
|
|
499
|
+
runningRuntime?.signal(taskId === "global" ? "operator" : `task:${taskId}`);
|
|
496
500
|
}
|
|
497
501
|
catch {
|
|
498
502
|
// Controller stopped; the recovery interval remains the fallback.
|
|
@@ -568,6 +572,11 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
568
572
|
await lifecycleDispatcher("runtime.ensure-role-session", { scope: "global", roleName });
|
|
569
573
|
}, options.onError ?? (() => undefined), deliveryOptions),
|
|
570
574
|
jobControl,
|
|
575
|
+
jobDefinitions: createJobDefinitionControl(store, home),
|
|
576
|
+
schedules: {
|
|
577
|
+
control: createScheduleControl(store, home),
|
|
578
|
+
reconcile: now => reconcileSchedules(store, home, now, options.onError)
|
|
579
|
+
},
|
|
571
580
|
capabilityDispatcher: createCapabilityDispatcher(kernel.capabilities),
|
|
572
581
|
...(continuationReconciler === undefined ? {} : { continuationReconciler }),
|
|
573
582
|
...(resourceReaper === undefined ? {} : { resourceReaper }),
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { requireKnownFields } from "../domain/validation.js";
|
|
2
|
+
import { nextScheduleDue, scheduleId, validateSchedule } from "../schedule/schedule.js";
|
|
3
|
+
import { admitJobRun, authorizeDefinition, authorizeScope, createJobDefinitionControl } from "./jobDefinitionControl.js";
|
|
4
|
+
import { jobAuthorityBinding } from "./jobControl.js";
|
|
5
|
+
import { createGlobalRoleMessage } from "../message/message.js";
|
|
6
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
7
|
+
import { enqueueSupervisorEvent } from "../scheduler/operatorEvent.js";
|
|
8
|
+
import { redactLaunchText } from "../runtime/launchDiagnostics.js";
|
|
9
|
+
function binding(store, caller, scope) {
|
|
10
|
+
return jobAuthorityBinding(store, caller.scope, caller.role, scope.kind === "task" ? scope.taskId : "");
|
|
11
|
+
}
|
|
12
|
+
function authorizeSchedule(store, schedule, caller, write) {
|
|
13
|
+
const job = store.getJobDefinition(schedule.scope, schedule.jobId);
|
|
14
|
+
if (job === null)
|
|
15
|
+
throw new Error("Schedule Job definition is missing.");
|
|
16
|
+
authorizeDefinition(store, job, caller, write);
|
|
17
|
+
return job;
|
|
18
|
+
}
|
|
19
|
+
function fence(schedule, now) {
|
|
20
|
+
return new Date(Math.max(now.getTime(), Date.parse(schedule?.processedThrough ?? "") || 0));
|
|
21
|
+
}
|
|
22
|
+
export function createScheduleControl(store, home) {
|
|
23
|
+
return (value, now) => {
|
|
24
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
25
|
+
throw new Error("Invalid Schedule request.");
|
|
26
|
+
requireKnownFields(value, ["op", "scope", "caller", "id", "jobId", "rule", "expectedRevision"], "Schedule request");
|
|
27
|
+
const input = value;
|
|
28
|
+
authorizeScope(store, input.scope, input.caller);
|
|
29
|
+
return store.transaction(tx => {
|
|
30
|
+
if (input.op === "list") {
|
|
31
|
+
return tx.listSchedules(input.scope).filter(s => {
|
|
32
|
+
try {
|
|
33
|
+
authorizeSchedule(tx, s, input.caller, false);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
scheduleId(input.id);
|
|
42
|
+
const previous = tx.getSchedule(input.scope, input.id);
|
|
43
|
+
if (input.op === "create") {
|
|
44
|
+
scheduleId(input.jobId);
|
|
45
|
+
const job = tx.getJobDefinition(input.scope, input.jobId);
|
|
46
|
+
if (job === null)
|
|
47
|
+
throw new Error("Schedule Job definition is missing.");
|
|
48
|
+
if (previous !== null) {
|
|
49
|
+
authorizeSchedule(tx, previous, input.caller, false);
|
|
50
|
+
if (previous.jobId !== input.jobId || JSON.stringify(previous.rule) !== JSON.stringify(input.rule)) {
|
|
51
|
+
throw new Error("Schedule id already exists; use update with its revision.");
|
|
52
|
+
}
|
|
53
|
+
return previous;
|
|
54
|
+
}
|
|
55
|
+
authorizeDefinition(tx, job, input.caller, true);
|
|
56
|
+
const due = nextScheduleDue(input.rule, now);
|
|
57
|
+
if (due === null)
|
|
58
|
+
throw new Error("A new once Schedule must be in the future.");
|
|
59
|
+
const schedule = validateSchedule({
|
|
60
|
+
schemaVersion: 1, id: input.id, scope: structuredClone(input.scope), jobId: input.jobId,
|
|
61
|
+
rule: structuredClone(input.rule), state: "enabled", revision: 1, generation: 1, nextDue: due,
|
|
62
|
+
authorization: { caller: structuredClone(input.caller), binding: binding(tx, input.caller, input.scope), jobRevision: job.revision },
|
|
63
|
+
createdAt: now.toISOString(), updatedAt: now.toISOString()
|
|
64
|
+
});
|
|
65
|
+
tx.saveSchedule(schedule);
|
|
66
|
+
return schedule;
|
|
67
|
+
}
|
|
68
|
+
if (previous === null)
|
|
69
|
+
throw new Error("Schedule not found.");
|
|
70
|
+
authorizeSchedule(tx, previous, input.caller, false);
|
|
71
|
+
if (input.op === "show") {
|
|
72
|
+
const runs = createJobDefinitionControl(tx, home)({
|
|
73
|
+
op: "runs", scope: input.scope, id: previous.jobId, caller: input.caller
|
|
74
|
+
}, now);
|
|
75
|
+
const ownRuns = runs.filter(r => r.trigger.source === "schedule" && r.trigger.key.startsWith(`${previous.id}:`));
|
|
76
|
+
const lastSuccess = ownRuns.filter(r => r.status === "succeeded")
|
|
77
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))[0] ?? null;
|
|
78
|
+
return { schedule: previous, lastSuccess };
|
|
79
|
+
}
|
|
80
|
+
if (previous.revision !== input.expectedRevision)
|
|
81
|
+
throw new Error("Schedule revision changed; read it before modifying.");
|
|
82
|
+
let next;
|
|
83
|
+
if (input.op === "disable") {
|
|
84
|
+
next = { ...previous, state: "disabled", reason: "Explicitly disabled; no future admission. Existing Runs are unchanged." };
|
|
85
|
+
}
|
|
86
|
+
else if (input.op === "enable" || input.op === "update") {
|
|
87
|
+
if (input.op === "enable" && ["completed", "retired"].includes(previous.state)) {
|
|
88
|
+
throw new Error("Completed/retired Schedule requires an explicit future rule update.");
|
|
89
|
+
}
|
|
90
|
+
const jobId = input.op === "update" ? input.jobId ?? previous.jobId : previous.jobId;
|
|
91
|
+
scheduleId(jobId);
|
|
92
|
+
const rule = input.op === "update" ? input.rule : previous.rule;
|
|
93
|
+
const job = authorizeSchedule(tx, { ...previous, jobId }, input.caller, true);
|
|
94
|
+
const after = fence(previous, now);
|
|
95
|
+
// Once overlap/disabled intent keeps its original due instant on enable.
|
|
96
|
+
const nextDue = input.op === "enable" && rule.kind === "once"
|
|
97
|
+
? previous.nextDue : nextScheduleDue(rule, after);
|
|
98
|
+
if (nextDue === null)
|
|
99
|
+
throw new Error("Rule must have a future occurrence.");
|
|
100
|
+
next = {
|
|
101
|
+
...previous, jobId, rule: structuredClone(rule), nextDue,
|
|
102
|
+
state: input.op === "enable" || previous.state === "enabled" ? "enabled" : "disabled",
|
|
103
|
+
generation: previous.generation + (input.op === "update" ? 1 : 0),
|
|
104
|
+
processedThrough: after.toISOString(),
|
|
105
|
+
authorization: { caller: structuredClone(input.caller), binding: binding(tx, input.caller, input.scope), jobRevision: job.revision }
|
|
106
|
+
};
|
|
107
|
+
const { reason: _reason, ...withoutReason } = next;
|
|
108
|
+
next = withoutReason;
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
throw new Error(`Unknown Schedule operation: ${input.op}.`);
|
|
112
|
+
next = { ...next, revision: previous.revision + 1, updatedAt: now.toISOString() };
|
|
113
|
+
tx.saveSchedule(next);
|
|
114
|
+
return next;
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** One bounded pass in the existing Controller. Transactions never spawn. */
|
|
119
|
+
export function reconcileSchedules(store, home, now, onError = () => { }, limit = 100) {
|
|
120
|
+
for (const candidate of store.listDueSchedules(now.toISOString(), limit)) {
|
|
121
|
+
try {
|
|
122
|
+
store.transaction(tx => {
|
|
123
|
+
const schedule = tx.getSchedule(candidate.scope, candidate.id);
|
|
124
|
+
if (schedule?.state !== "enabled" || schedule.nextDue === null || Date.parse(schedule.nextDue) > now.getTime())
|
|
125
|
+
return;
|
|
126
|
+
const dueAt = schedule.nextDue;
|
|
127
|
+
const after = fence(schedule, now);
|
|
128
|
+
if (schedule.scope.kind === "task") {
|
|
129
|
+
const task = tx.getTask(schedule.scope.taskId);
|
|
130
|
+
if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
131
|
+
tx.saveSchedule({ ...schedule, state: "needs-attention", revision: schedule.revision + 1,
|
|
132
|
+
reason: "Task execution unavailable; pending intent retained.", updatedAt: now.toISOString() });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const job = authorizeSchedule(tx, schedule, schedule.authorization.caller, true);
|
|
137
|
+
if (job.revision !== schedule.authorization.jobRevision
|
|
138
|
+
|| binding(tx, schedule.authorization.caller, schedule.scope) !== schedule.authorization.binding) {
|
|
139
|
+
throw new Error("Schedule authority or Job revision changed; inspect and explicitly enable.");
|
|
140
|
+
}
|
|
141
|
+
const result = admitJobRun(tx, home, schedule.scope, schedule.jobId, schedule.authorization.caller, { source: "schedule", key: `${schedule.id}:${schedule.generation}:${dueAt}` }, now);
|
|
142
|
+
const overlap = result.reason === "overlap";
|
|
143
|
+
const nextDue = overlap && schedule.rule.kind === "once" ? dueAt : nextScheduleDue(schedule.rule, after);
|
|
144
|
+
tx.saveSchedule({
|
|
145
|
+
...schedule, nextDue, state: nextDue === null ? "completed" : "enabled",
|
|
146
|
+
processedThrough: after.toISOString(), revision: schedule.revision + 1,
|
|
147
|
+
lastAttempt: { at: now.toISOString(), dueAt, outcome: overlap ? "overlap" : "admitted", runId: result.run.id },
|
|
148
|
+
updatedAt: now.toISOString()
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
const reason = redactLaunchText(error instanceof Error ? error.message : String(error));
|
|
154
|
+
// Admission/advancement rolled back together. Do not retry an unknown error.
|
|
155
|
+
store.transaction(tx => {
|
|
156
|
+
const current = tx.getSchedule(candidate.scope, candidate.id);
|
|
157
|
+
if (current?.state !== "enabled" || current.revision !== candidate.revision)
|
|
158
|
+
return;
|
|
159
|
+
tx.saveSchedule({ ...current, state: "needs-attention", reason, revision: current.revision + 1,
|
|
160
|
+
lastAttempt: { at: now.toISOString(), dueAt: current.nextDue, outcome: "blocked", reason },
|
|
161
|
+
updatedAt: now.toISOString() });
|
|
162
|
+
if (current.scope.kind === "task") {
|
|
163
|
+
const event = createTaskEvent(tx.nextEventId(current.scope.taskId), current.scope.taskId, "schedule.needs-attention", { scheduleId: current.id, reason }, now);
|
|
164
|
+
tx.saveEvent(current.scope.taskId, event);
|
|
165
|
+
enqueueSupervisorEvent(tx, event, "leader", "schedule-needs-attention", now);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
tx.saveGlobalRoleMessage(createGlobalRoleMessage(tx.nextGlobalRoleMessageId(), "operator", `Yui-Summary: Schedule ${current.id} needs attention; no Run was admitted.\n`
|
|
169
|
+
+ `Reason: ${reason}\nRead: yui schedule show --global --schedule ${current.id}\n`
|
|
170
|
+
+ "Inspect the original intent before explicitly enabling; no new authority is implied.", "system", { type: "system" }, now, { inputControl: { action: "queue", requestId: `schedule-attention:${current.id}:${current.revision}` } }));
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
onError(new Error(`Schedule ${candidate.id} needs attention: ${reason}`, { cause: error }));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -315,11 +315,11 @@ function parseTarget(value) {
|
|
|
315
315
|
function parseRef(value, label) {
|
|
316
316
|
const ref = record(value, label);
|
|
317
317
|
const types = [
|
|
318
|
-
"task", "run", "work-item", "input", "session", "message", "event"
|
|
318
|
+
"task", "run", "work-item", "input", "session", "message", "event", "global-job-run"
|
|
319
319
|
];
|
|
320
320
|
if (!types.includes(ref.type))
|
|
321
321
|
throw new Error(`${label} type is invalid`);
|
|
322
|
-
if (ref.type === "task" || ref.type === "session") {
|
|
322
|
+
if (ref.type === "task" || ref.type === "session" || ref.type === "global-job-run") {
|
|
323
323
|
exact(ref, ["type", "id"], label);
|
|
324
324
|
return { type: ref.type, id: requireString(ref.id, `${label} id`) };
|
|
325
325
|
}
|
|
@@ -360,7 +360,7 @@ function copyTarget(target) {
|
|
|
360
360
|
}
|
|
361
361
|
export function copyRef(ref) {
|
|
362
362
|
if (!("taskId" in ref)) {
|
|
363
|
-
if (ref.type !== "task" && ref.type !== "session") {
|
|
363
|
+
if (ref.type !== "task" && ref.type !== "session" && ref.type !== "global-job-run") {
|
|
364
364
|
throw new Error(`entity reference taskId is required for ${ref.type}`);
|
|
365
365
|
}
|
|
366
366
|
return { type: ref.type, id: requireText(ref.id, "entity reference id") };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createExecFileCommandRunner, createPinnedCommandRunner } from "./pinnedCommandRunner.js";
|
|
2
2
|
import { redactLaunchText } from "../runtime/launchDiagnostics.js";
|
|
3
|
+
import { canonicalPublicationUrl } from "../task/publicationToolObservation.js";
|
|
3
4
|
const GIT_COMMIT = /^[0-9a-f]{40}$/iu;
|
|
4
5
|
const GITHUB_REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
|
|
5
6
|
const MAX_FAILURE_DETAIL_CHARS = 4_000;
|
|
@@ -20,22 +21,24 @@ async function inspectGitHubPullRequest(run, input) {
|
|
|
20
21
|
if (!PULL_REQUEST_NUMBER.test(input.externalId)) {
|
|
21
22
|
throw new Error(`GitHub pull request id must be numeric: ${input.externalId}.`);
|
|
22
23
|
}
|
|
24
|
+
const hostname = input.externalUrl === undefined
|
|
25
|
+
? "github.com" : new URL(canonicalPublicationUrl(input.externalUrl)).host;
|
|
23
26
|
const result = await run("gh", [
|
|
24
27
|
"pr", "view", input.externalId,
|
|
25
|
-
"--repo", input.repository
|
|
26
|
-
"--json", "number,state,headRefOid,mergeCommit,mergedAt,url"
|
|
28
|
+
"--repo", `${hostname}/${input.repository}`,
|
|
29
|
+
"--json", "number,state,headRefOid,mergeCommit,mergedAt,url,baseRefName"
|
|
27
30
|
]);
|
|
28
31
|
if (result.code !== 0) {
|
|
29
32
|
if (result.code === 127) {
|
|
30
|
-
throw new Error("
|
|
33
|
+
throw new Error("The built-in GitHub adapter needs gh on PATH; alternatively record authorized tool evidence with task publication verify --observation.");
|
|
31
34
|
}
|
|
32
35
|
const detail = boundedFailureDetail(redactLaunchText(result.stderr || result.stdout).trim());
|
|
33
36
|
throw new Error(`gh pr view failed for ${input.repository}#${input.externalId} `
|
|
34
37
|
+ `(exit ${result.code})${detail.length === 0 ? "" : `: ${detail}`}`);
|
|
35
38
|
}
|
|
36
|
-
return parseGitHubPullRequest(result.stdout, input.repository, input.externalId);
|
|
39
|
+
return parseGitHubPullRequest(result.stdout, input.repository, input.externalId, hostname);
|
|
37
40
|
}
|
|
38
|
-
export function parseGitHubPullRequest(stdout, repository, externalId) {
|
|
41
|
+
export function parseGitHubPullRequest(stdout, repository, externalId, expectedHost = "github.com") {
|
|
39
42
|
let parsed;
|
|
40
43
|
try {
|
|
41
44
|
parsed = JSON.parse(stdout.trim());
|
|
@@ -60,6 +63,11 @@ export function parseGitHubPullRequest(stdout, repository, externalId) {
|
|
|
60
63
|
throw new Error("Merged GitHub PR did not expose a merge commit.");
|
|
61
64
|
}
|
|
62
65
|
const externalUrl = optionalText(record.url);
|
|
66
|
+
if (externalUrl === undefined
|
|
67
|
+
|| canonicalPublicationUrl(externalUrl, "github")
|
|
68
|
+
!== canonicalPublicationUrl(`https://${expectedHost}/${repository}/pull/${externalId}`, "github")) {
|
|
69
|
+
throw new Error("GitHub PR URL does not match the expected service, repository and PR.");
|
|
70
|
+
}
|
|
63
71
|
const mergedAt = optionalText(record.mergedAt);
|
|
64
72
|
return {
|
|
65
73
|
provider: "github",
|
|
@@ -67,6 +75,7 @@ export function parseGitHubPullRequest(stdout, repository, externalId) {
|
|
|
67
75
|
externalKind: "pull-request",
|
|
68
76
|
externalId,
|
|
69
77
|
...(externalUrl === undefined ? {} : { externalUrl }),
|
|
78
|
+
...(optionalText(record.baseRefName) === undefined ? {} : { targetBranch: optionalText(record.baseRefName) }),
|
|
70
79
|
state,
|
|
71
80
|
headCommit,
|
|
72
81
|
...(remoteCommit === undefined ? {} : { remoteCommit }),
|
|
@@ -30,7 +30,7 @@ async function inspectGitLabMergeRequest(run, input) {
|
|
|
30
30
|
]);
|
|
31
31
|
if (result.code !== 0) {
|
|
32
32
|
if (result.code === 127) {
|
|
33
|
-
throw new Error("
|
|
33
|
+
throw new Error("The built-in GitLab adapter needs glab on PATH; alternatively record authorized tool evidence with task publication verify --observation.");
|
|
34
34
|
}
|
|
35
35
|
const detail = boundedFailureDetail(redactLaunchText(result.stderr || result.stdout).trim());
|
|
36
36
|
throw new Error(`glab api failed for ${input.repository}!${input.externalId} `
|
|
@@ -74,6 +74,7 @@ export function parseGitLabMergeRequest(stdout, repository, externalId, expected
|
|
|
74
74
|
externalKind: "merge-request",
|
|
75
75
|
externalId,
|
|
76
76
|
...(externalUrl === undefined ? {} : { externalUrl }),
|
|
77
|
+
...(optionalText(record.target_branch) === undefined ? {} : { targetBranch: optionalText(record.target_branch) }),
|
|
77
78
|
state,
|
|
78
79
|
headCommit,
|
|
79
80
|
...(remoteCommit === undefined ? {} : { remoteCommit }),
|
|
@@ -49,11 +49,11 @@ export function wakeChangeLabels(reasons) {
|
|
|
49
49
|
}
|
|
50
50
|
}))];
|
|
51
51
|
}
|
|
52
|
-
export function wakeMessages(wake, messages) {
|
|
52
|
+
export function wakeMessages(wake, messages, history = false) {
|
|
53
53
|
const ids = new Set(wake.refs?.filter(ref => ref.type === "message" && ref.taskId === wake.taskId).map(ref => ref.id));
|
|
54
54
|
const from = Date.parse(wake.fromCursor), to = Date.parse(wake.toCursor);
|
|
55
|
-
return messages.filter(message => ids.has(message.id)
|
|
56
|
-
|| Date.parse(message.createdAt) > from && Date.parse(message.createdAt) <= to);
|
|
55
|
+
return messages.filter(message => message.taskId === wake.taskId && (ids.has(message.id)
|
|
56
|
+
|| (history || wake.refs === undefined) && Date.parse(message.createdAt) > from && Date.parse(message.createdAt) <= to));
|
|
57
57
|
}
|
|
58
58
|
export function taskNotificationPresentation(wake, store) {
|
|
59
59
|
const messages = wakeMessages(wake, store.listMessages(wake.taskId));
|
|
@@ -16,6 +16,11 @@ export function createOperatorBatchPresentation(batchId, items) {
|
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
18
|
function renderItem(item) {
|
|
19
|
+
if (item.kind === "global-job-run")
|
|
20
|
+
return [
|
|
21
|
+
`- Global Job Run ${item.run.status}: ${item.run.definitionId}/${item.run.id}`,
|
|
22
|
+
` Inspect: yui job run-get --global --job ${item.run.definitionId} --run ${item.run.id}`
|
|
23
|
+
];
|
|
19
24
|
if (item.kind === "input-request") {
|
|
20
25
|
const { request } = item;
|
|
21
26
|
return [
|