@mingchuno/agent-workflows 0.4.0 → 0.5.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/src/adapters/agents.js +9 -0
- package/dist/src/cli.js +14 -1
- package/dist/src/config.d.ts +10 -0
- package/dist/src/config.js +9 -11
- package/dist/src/domain.d.ts +6 -0
- package/dist/src/invocation.d.ts +2 -4
- package/dist/src/invocation.js +204 -107
- package/dist/src/operations.d.ts +1 -2
- package/dist/src/operations.js +18 -33
- package/dist/src/recovery.d.ts +20 -2
- package/dist/src/recovery.js +49 -1
- package/dist/src/runner.d.ts +6 -2
- package/dist/src/runner.js +81 -83
- package/dist/src/runtime/process.d.ts +1 -0
- package/dist/src/runtime/process.js +6 -2
- package/dist/src/store.d.ts +4 -2
- package/dist/src/store.js +69 -51
- package/dist/src/tui/data.d.ts +1 -1
- package/dist/src/tui/dialogs.js +1 -0
- package/dist/src/tui/monitor-navigation.d.ts +76 -0
- package/dist/src/tui/monitor-navigation.js +187 -0
- package/dist/src/tui/monitor.js +70 -183
- package/dist/src/tui/projection.d.ts +22 -0
- package/dist/src/tui/projection.js +49 -0
- package/dist/src/validation-selection.d.ts +6 -0
- package/dist/src/validation-selection.js +29 -0
- package/dist/src/workspace.js +3 -2
- package/docs/api.md +1 -1
- package/docs/configuration.md +33 -0
- package/docs/operations.md +4 -3
- package/package.json +1 -1
- package/dist/src/tui/actions.d.ts +0 -16
- package/dist/src/tui/actions.js +0 -23
package/dist/src/recovery.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ import type { Project } from "./config.js";
|
|
|
2
2
|
import { type HostingAdapter, type RunRecord, type Workspace } from "./domain.js";
|
|
3
3
|
import type { Store } from "./store.js";
|
|
4
4
|
export declare const publicationSteps: readonly string[];
|
|
5
|
+
interface WorkflowStep {
|
|
6
|
+
functionID: number;
|
|
7
|
+
name?: string;
|
|
8
|
+
error?: unknown;
|
|
9
|
+
}
|
|
5
10
|
export declare function recoveryUnavailable(run: RunRecord): string | undefined;
|
|
6
11
|
/** Hash execution inputs without reading credential environment values. */
|
|
7
12
|
export declare function executionFingerprint(project: Project, workflowVersion: string, signal?: AbortSignal): Promise<string>;
|
|
@@ -14,6 +19,19 @@ interface RecoveryDependencies {
|
|
|
14
19
|
workflowVersion: string;
|
|
15
20
|
signal?: AbortSignal;
|
|
16
21
|
}
|
|
17
|
-
/**
|
|
18
|
-
export declare function
|
|
22
|
+
/** Verify checkpoint history and live state before persisting recovery intent. */
|
|
23
|
+
export declare function verifyPublicationRecoveryAdmission(run: RunRecord, checkpoints: {
|
|
24
|
+
status: {
|
|
25
|
+
status: string;
|
|
26
|
+
applicationVersion?: string;
|
|
27
|
+
} | null | undefined;
|
|
28
|
+
steps: readonly WorkflowStep[] | undefined;
|
|
29
|
+
applicationVersion: string;
|
|
30
|
+
}, dependencies: RecoveryDependencies): Promise<string[]>;
|
|
31
|
+
interface RecoveryStartDependencies extends RecoveryDependencies {
|
|
32
|
+
store: Pick<Store, "invocations" | "project" | "patchRun">;
|
|
33
|
+
signal: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
/** Revalidate a recovered Execution at its first non-replayed step. */
|
|
36
|
+
export declare function resumePublicationExecution(run: RunRecord, workflowId: string, stepId: number, dependencies: RecoveryStartDependencies): Promise<void>;
|
|
19
37
|
export {};
|
package/dist/src/recovery.js
CHANGED
|
@@ -6,11 +6,38 @@ import { verifyEvidence } from "./evidence.js";
|
|
|
6
6
|
import { projectPrompts } from "./prompts.js";
|
|
7
7
|
import { assertProcessesStopped } from "./runtime/ownership.js";
|
|
8
8
|
import { command } from "./runtime/process.js";
|
|
9
|
+
const recoveryGatePollIntervalMs = 100;
|
|
9
10
|
export const publicationSteps = [
|
|
10
11
|
"push",
|
|
11
12
|
"change-request",
|
|
12
13
|
"review-publication",
|
|
13
14
|
];
|
|
15
|
+
/** Prove that a fork will replay only completed publication checkpoints. */
|
|
16
|
+
function completedPublicationCheckpoints(run, status, steps, applicationVersion) {
|
|
17
|
+
if (!status || !["SUCCESS", "ERROR"].includes(status.status))
|
|
18
|
+
throw new Error("Source execution has not finished");
|
|
19
|
+
if (status.applicationVersion !== applicationVersion)
|
|
20
|
+
throw new Error("Workflow version changed; use retry");
|
|
21
|
+
const failed = steps?.find((step) => step.functionID === run.executions?.at(-1)?.failedStep);
|
|
22
|
+
if (!failed?.error || failed.name !== run.phase)
|
|
23
|
+
throw new Error("Failed publication checkpoint is unavailable");
|
|
24
|
+
const prefix = steps.filter((step) => step.functionID < failed.functionID);
|
|
25
|
+
if (prefix.length !== failed.functionID ||
|
|
26
|
+
prefix.some((step, index) => step.error || step.functionID !== index) ||
|
|
27
|
+
!prefix.some((step) => step.name === "commit"))
|
|
28
|
+
throw new Error("Completed publication checkpoints are unavailable");
|
|
29
|
+
return prefix.map((step) => step.name);
|
|
30
|
+
}
|
|
31
|
+
/** Check the identity of the first step that DBOS did not replay. */
|
|
32
|
+
function verifyPublicationExecutionStart(run, workflowId, stepId) {
|
|
33
|
+
const execution = run.executions?.at(-1);
|
|
34
|
+
if (!execution?.recoveryOf)
|
|
35
|
+
return;
|
|
36
|
+
if (execution.id !== workflowId)
|
|
37
|
+
throw new BlockedError("Execution has been superseded");
|
|
38
|
+
if (stepId < execution.startStep)
|
|
39
|
+
throw new BlockedError("A reused checkpoint is missing; recovery refused");
|
|
40
|
+
}
|
|
14
41
|
export function recoveryUnavailable(run) {
|
|
15
42
|
if (run.outcome !== "failed")
|
|
16
43
|
return "Only failed publication runs can be recovered";
|
|
@@ -47,8 +74,29 @@ export async function executionFingerprint(project, workflowVersion, signal) {
|
|
|
47
74
|
}))
|
|
48
75
|
.digest("hex");
|
|
49
76
|
}
|
|
77
|
+
/** Verify checkpoint history and live state before persisting recovery intent. */
|
|
78
|
+
export async function verifyPublicationRecoveryAdmission(run, checkpoints, dependencies) {
|
|
79
|
+
const completedSteps = completedPublicationCheckpoints(run, checkpoints.status, checkpoints.steps, checkpoints.applicationVersion);
|
|
80
|
+
await verifyPublicationRecovery(run, dependencies);
|
|
81
|
+
return completedSteps;
|
|
82
|
+
}
|
|
83
|
+
/** Revalidate a recovered Execution at its first non-replayed step. */
|
|
84
|
+
export async function resumePublicationExecution(run, workflowId, stepId, dependencies) {
|
|
85
|
+
verifyPublicationExecutionStart(run, workflowId, stepId);
|
|
86
|
+
while (true) {
|
|
87
|
+
dependencies.signal.throwIfAborted();
|
|
88
|
+
const state = await dependencies.store.project(dependencies.project.id);
|
|
89
|
+
if (state.blocked)
|
|
90
|
+
throw new BlockedError(state.blocked);
|
|
91
|
+
if (!state.paused)
|
|
92
|
+
break;
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, recoveryGatePollIntervalMs));
|
|
94
|
+
}
|
|
95
|
+
await dependencies.store.patchRun(run.id, { outcome: "running" });
|
|
96
|
+
await verifyPublicationRecovery(run, dependencies);
|
|
97
|
+
}
|
|
50
98
|
/** Read-only checks shared by admission and the first non-replayed operation. */
|
|
51
|
-
|
|
99
|
+
async function verifyPublicationRecovery(run, dependencies) {
|
|
52
100
|
const { project, workspace, hosting, store, stateDirectory, workflowVersion, signal, } = dependencies;
|
|
53
101
|
if (run.checkout !== project.checkout)
|
|
54
102
|
throw new BlockedError("Configured checkout differs from the recorded run checkout");
|
package/dist/src/runner.d.ts
CHANGED
|
@@ -43,12 +43,16 @@ export declare class Runner {
|
|
|
43
43
|
private recordWorkflowFailure;
|
|
44
44
|
private execute;
|
|
45
45
|
private executeOwned;
|
|
46
|
+
private checkExecutionStart;
|
|
47
|
+
private recordExecutionFailure;
|
|
46
48
|
pause(projectId: string): Promise<void>;
|
|
47
49
|
private initializeExecution;
|
|
48
50
|
resume(projectId: string): Promise<void>;
|
|
49
51
|
stop(runId: string): Promise<void>;
|
|
50
|
-
retry(runId: string, commandId?: string
|
|
52
|
+
retry(runId: string, commandId?: string, options?: {
|
|
53
|
+
refreshIssue?: boolean;
|
|
54
|
+
}): Promise<string>;
|
|
51
55
|
recover(runId: string, commandId?: string): Promise<string>;
|
|
52
|
-
private
|
|
56
|
+
private projectForRun;
|
|
53
57
|
shutdown(): Promise<void>;
|
|
54
58
|
}
|
package/dist/src/runner.js
CHANGED
|
@@ -8,11 +8,12 @@ import { BlockedError, isBlockedError, } from "./domain.js";
|
|
|
8
8
|
import { assertEvidenceDirectory } from "./evidence.js";
|
|
9
9
|
import { defaultWorkflow, Operations } from "./operations.js";
|
|
10
10
|
import { projectPrompts } from "./prompts.js";
|
|
11
|
-
import { executionFingerprint,
|
|
11
|
+
import { executionFingerprint, resumePublicationExecution, verifyPublicationRecoveryAdmission, } from "./recovery.js";
|
|
12
12
|
import { createQueuedRun } from "./run-record.js";
|
|
13
13
|
import { assertProcessesStopped, CheckoutOwnership, } from "./runtime/ownership.js";
|
|
14
14
|
import { createRedactor, runtimeLogger } from "./runtime/redaction.js";
|
|
15
15
|
import { Store } from "./store.js";
|
|
16
|
+
import { selectValidation } from "./validation-selection.js";
|
|
16
17
|
import { ExistingCheckout } from "./workspace.js";
|
|
17
18
|
const runnerPollIntervalMs = 100;
|
|
18
19
|
const shutdownPollIntervalMs = 20;
|
|
@@ -180,6 +181,8 @@ export class Runner {
|
|
|
180
181
|
await this.stop(request.target);
|
|
181
182
|
else if (request.kind === "retry")
|
|
182
183
|
await this.retry(request.target, request.id);
|
|
184
|
+
else if (request.kind === "retry-refresh")
|
|
185
|
+
await this.retry(request.target, request.id, { refreshIssue: true });
|
|
183
186
|
else if (request.kind === "recover")
|
|
184
187
|
await this.recover(request.target, request.id);
|
|
185
188
|
else
|
|
@@ -281,9 +284,7 @@ export class Runner {
|
|
|
281
284
|
const run = await DBOS.runStep(() => this.initializeExecution(runId), {
|
|
282
285
|
name: "load-run",
|
|
283
286
|
});
|
|
284
|
-
const project = this.
|
|
285
|
-
if (!project)
|
|
286
|
-
throw new Error("Project removed from configuration");
|
|
287
|
+
const project = this.projectForRun(run);
|
|
287
288
|
const workspace = this.options.workspace ?? new ExistingCheckout();
|
|
288
289
|
let recoveryChecked = false;
|
|
289
290
|
const operations = new Operations(runId, {
|
|
@@ -300,30 +301,7 @@ export class Runner {
|
|
|
300
301
|
beforeStep: async () => {
|
|
301
302
|
if (recoveryChecked)
|
|
302
303
|
return;
|
|
303
|
-
|
|
304
|
-
const execution = current.executions?.at(-1);
|
|
305
|
-
if (!execution?.recoveryOf) {
|
|
306
|
-
await this.store.patchRun(runId, { outcome: "running" });
|
|
307
|
-
recoveryChecked = true;
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
if (execution.id !== DBOS.workflowID)
|
|
311
|
-
throw new BlockedError("Execution has been superseded");
|
|
312
|
-
if (DBOS.stepID < execution.startStep)
|
|
313
|
-
throw new BlockedError("A reused checkpoint is missing; recovery refused");
|
|
314
|
-
// This runs inside the first non-replayed step, so copied start-gate
|
|
315
|
-
// checkpoints cannot bypass today's pause or checkout checks.
|
|
316
|
-
while (true) {
|
|
317
|
-
controller.signal.throwIfAborted();
|
|
318
|
-
const state = await this.store.project(project.id);
|
|
319
|
-
if (state.blocked)
|
|
320
|
-
throw new BlockedError(state.blocked);
|
|
321
|
-
if (!state.paused)
|
|
322
|
-
break;
|
|
323
|
-
await new Promise((resolve) => setTimeout(resolve, runnerPollIntervalMs));
|
|
324
|
-
}
|
|
325
|
-
await this.store.patchRun(runId, { outcome: "running" });
|
|
326
|
-
await this.checkRecoveryState(current, project, controller.signal);
|
|
304
|
+
await this.checkExecutionStart(runId, project, controller.signal);
|
|
327
305
|
recoveryChecked = true;
|
|
328
306
|
},
|
|
329
307
|
});
|
|
@@ -348,31 +326,50 @@ export class Runner {
|
|
|
348
326
|
}, { name: "terminal-check" });
|
|
349
327
|
}
|
|
350
328
|
catch (error) {
|
|
351
|
-
await DBOS.runStep(
|
|
352
|
-
let unsafe = false;
|
|
353
|
-
try {
|
|
354
|
-
await workspace.check(project);
|
|
355
|
-
}
|
|
356
|
-
catch {
|
|
357
|
-
unsafe = true;
|
|
358
|
-
}
|
|
359
|
-
const outcome = controller.signal.aborted
|
|
360
|
-
? "cancelled"
|
|
361
|
-
: isBlockedError(error)
|
|
362
|
-
? "blocked"
|
|
363
|
-
: "failed";
|
|
364
|
-
await this.store.patchRun(runId, {
|
|
365
|
-
outcome,
|
|
366
|
-
error: this.redact(String(error)),
|
|
367
|
-
});
|
|
368
|
-
if (unsafe || isBlockedError(error))
|
|
369
|
-
await this.store.blockProject(project.id, `Run ${runId} requires recovery: ${this.redact(String(error))}`);
|
|
370
|
-
}, { name: "record-failure", retriesAllowed: false });
|
|
329
|
+
await DBOS.runStep(() => this.recordExecutionFailure(runId, project, workspace, controller, error), { name: "record-failure", retriesAllowed: false });
|
|
371
330
|
}
|
|
372
331
|
finally {
|
|
373
332
|
this.controllers.delete(runId);
|
|
374
333
|
}
|
|
375
334
|
}
|
|
335
|
+
async checkExecutionStart(runId, project, signal) {
|
|
336
|
+
const current = await this.store.run(runId);
|
|
337
|
+
const execution = current.executions?.at(-1);
|
|
338
|
+
if (!execution?.recoveryOf) {
|
|
339
|
+
await this.store.patchRun(runId, { outcome: "running" });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// The first non-replayed step must check today's pause and checkout state.
|
|
343
|
+
await resumePublicationExecution(current, DBOS.workflowID, DBOS.stepID, {
|
|
344
|
+
project,
|
|
345
|
+
signal,
|
|
346
|
+
store: this.store,
|
|
347
|
+
workspace: this.options.workspace ?? new ExistingCheckout(),
|
|
348
|
+
hosting: this.hosting.get(project.id),
|
|
349
|
+
stateDirectory: this.config.stateDirectory,
|
|
350
|
+
workflowVersion: this.options.workflowVersion ?? "phase1-v2",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async recordExecutionFailure(runId, project, workspace, controller, error) {
|
|
354
|
+
let unsafe = false;
|
|
355
|
+
try {
|
|
356
|
+
await workspace.check(project);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
unsafe = true;
|
|
360
|
+
}
|
|
361
|
+
const outcome = controller.signal.aborted
|
|
362
|
+
? "cancelled"
|
|
363
|
+
: isBlockedError(error)
|
|
364
|
+
? "blocked"
|
|
365
|
+
: "failed";
|
|
366
|
+
await this.store.patchRun(runId, {
|
|
367
|
+
outcome,
|
|
368
|
+
error: this.redact(String(error)),
|
|
369
|
+
});
|
|
370
|
+
if (unsafe || isBlockedError(error))
|
|
371
|
+
await this.store.blockProject(project.id, `Run ${runId} requires recovery: ${this.redact(String(error))}`);
|
|
372
|
+
}
|
|
376
373
|
async pause(projectId) {
|
|
377
374
|
await this.store.project(projectId);
|
|
378
375
|
await this.store.setProject(projectId, { paused: true });
|
|
@@ -381,9 +378,7 @@ export class Runner {
|
|
|
381
378
|
const run = await this.store.run(runId);
|
|
382
379
|
if (run.executions?.length)
|
|
383
380
|
return run;
|
|
384
|
-
|
|
385
|
-
if (!project)
|
|
386
|
-
throw new Error("Project removed from configuration");
|
|
381
|
+
this.projectForRun(run);
|
|
387
382
|
return this.store.patchRun(runId, {
|
|
388
383
|
executions: [
|
|
389
384
|
{
|
|
@@ -421,20 +416,33 @@ export class Runner {
|
|
|
421
416
|
if (run.outcome === "running")
|
|
422
417
|
throw new BlockedError("No local process ownership for this running workflow");
|
|
423
418
|
}
|
|
424
|
-
async retry(runId, commandId) {
|
|
419
|
+
async retry(runId, commandId, options = {}) {
|
|
425
420
|
return this.store.admitRetry(runId, {
|
|
426
421
|
commandId,
|
|
427
422
|
checkSafety: async (previous) => {
|
|
428
423
|
if (this.controllers.has(runId))
|
|
429
424
|
throw new Error("Work has not stopped");
|
|
430
|
-
const project = this.
|
|
431
|
-
if (!project)
|
|
432
|
-
throw new Error("Project removed from configuration");
|
|
425
|
+
const project = this.projectForRun(previous);
|
|
433
426
|
await assertProcessesStopped(resolve(this.config.stateDirectory, runId));
|
|
434
427
|
await (this.options.workspace ?? new ExistingCheckout()).check(project);
|
|
428
|
+
let issue;
|
|
429
|
+
if (options.refreshIssue) {
|
|
430
|
+
const hosting = this.hosting.get(project.id) ?? this.options.hosting(project);
|
|
431
|
+
const current = await hosting.getIssue(previous.issue.number);
|
|
432
|
+
if (current.id !== previous.issue.id ||
|
|
433
|
+
current.number !== previous.issue.number ||
|
|
434
|
+
current.url !== previous.issue.url)
|
|
435
|
+
throw new Error("Refreshed issue identity differs from the run");
|
|
436
|
+
if (!current.open ||
|
|
437
|
+
!project.labels.every((label) => current.labels.includes(label)))
|
|
438
|
+
throw new Error("Refreshed issue is closed or missing required labels");
|
|
439
|
+
selectValidation(current.body, project);
|
|
440
|
+
issue = current;
|
|
441
|
+
}
|
|
435
442
|
return {
|
|
436
443
|
checkout: project.checkout,
|
|
437
444
|
branchTemplate: project.branchTemplate,
|
|
445
|
+
issue,
|
|
438
446
|
};
|
|
439
447
|
},
|
|
440
448
|
});
|
|
@@ -449,40 +457,30 @@ export class Runner {
|
|
|
449
457
|
throw new Error("Publication recovery currently supports the default workflow only");
|
|
450
458
|
if (this.controllers.has(runId))
|
|
451
459
|
throw new Error("Work has not stopped");
|
|
452
|
-
const project = this.
|
|
453
|
-
if (!project)
|
|
454
|
-
throw new Error("Project removed from configuration");
|
|
460
|
+
const project = this.projectForRun(run);
|
|
455
461
|
const execution = run.executions.at(-1);
|
|
456
462
|
const status = await DBOS.getWorkflowStatus(execution.id);
|
|
457
|
-
if (!status || !["SUCCESS", "ERROR"].includes(status.status))
|
|
458
|
-
throw new Error("Source execution has not finished");
|
|
459
|
-
if (status.applicationVersion !==
|
|
460
|
-
`${this.config.id}-${this.options.workflowVersion ?? "phase1-v2"}`)
|
|
461
|
-
throw new Error("Workflow version changed; use retry");
|
|
462
463
|
const steps = await DBOS.listWorkflowSteps(execution.id);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
464
|
+
return verifyPublicationRecoveryAdmission(run, {
|
|
465
|
+
status,
|
|
466
|
+
steps,
|
|
467
|
+
applicationVersion: `${this.config.id}-${this.options.workflowVersion ?? "phase1-v2"}`,
|
|
468
|
+
}, {
|
|
469
|
+
project,
|
|
470
|
+
store: this.store,
|
|
471
|
+
workspace: this.options.workspace ?? new ExistingCheckout(),
|
|
472
|
+
hosting: this.hosting.get(project.id),
|
|
473
|
+
stateDirectory: this.config.stateDirectory,
|
|
474
|
+
workflowVersion: this.options.workflowVersion ?? "phase1-v2",
|
|
475
|
+
});
|
|
473
476
|
},
|
|
474
477
|
});
|
|
475
478
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
workspace: this.options.workspace ?? new ExistingCheckout(),
|
|
482
|
-
hosting: this.hosting.get(project.id),
|
|
483
|
-
stateDirectory: this.config.stateDirectory,
|
|
484
|
-
workflowVersion: this.options.workflowVersion ?? "phase1-v2",
|
|
485
|
-
});
|
|
479
|
+
projectForRun(run) {
|
|
480
|
+
const project = this.config.projects.find((item) => item.id === run.projectId);
|
|
481
|
+
if (!project)
|
|
482
|
+
throw new Error("Project removed from configuration");
|
|
483
|
+
return project;
|
|
486
484
|
}
|
|
487
485
|
async shutdown() {
|
|
488
486
|
this.stopping = true;
|
|
@@ -64,8 +64,12 @@ export function command(executable, args, options) {
|
|
|
64
64
|
const value = decoders[target].decode(chunk, {
|
|
65
65
|
stream: chunk !== undefined,
|
|
66
66
|
});
|
|
67
|
-
if (value)
|
|
68
|
-
options.
|
|
67
|
+
if (value) {
|
|
68
|
+
if (target === "stderr" && options.onStderr)
|
|
69
|
+
options.onStderr(value);
|
|
70
|
+
else
|
|
71
|
+
options.onOutput?.(value);
|
|
72
|
+
}
|
|
69
73
|
if (options.captureOutput !== false) {
|
|
70
74
|
if (target === "stdout")
|
|
71
75
|
stdout += value;
|
package/dist/src/store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Pool } from "pg";
|
|
2
|
-
import type { RunRecord } from "./domain.js";
|
|
2
|
+
import type { Issue, RunRecord } from "./domain.js";
|
|
3
3
|
export interface EventRecord {
|
|
4
4
|
sequence: number;
|
|
5
5
|
runId: string | null;
|
|
@@ -41,6 +41,7 @@ interface RetryAdmission {
|
|
|
41
41
|
checkSafety: (previous: RunRecord) => Promise<{
|
|
42
42
|
checkout: string;
|
|
43
43
|
branchTemplate: string;
|
|
44
|
+
issue?: Issue;
|
|
44
45
|
}>;
|
|
45
46
|
}
|
|
46
47
|
interface RecoveryAdmission {
|
|
@@ -88,12 +89,13 @@ export declare class Store {
|
|
|
88
89
|
saveInvocation(record: InvocationRecord): Promise<void>;
|
|
89
90
|
invocations(runId: string): Promise<InvocationRecord[]>;
|
|
90
91
|
emit(runId: string | null, kind: string, payload: unknown): Promise<void>;
|
|
92
|
+
private event;
|
|
91
93
|
events(after?: number, runId?: string): Promise<EventRecord[]>;
|
|
92
94
|
subscribe(listener: (event: EventRecord) => void, options?: {
|
|
93
95
|
after?: number;
|
|
94
96
|
intervalMs?: number;
|
|
95
97
|
}): () => void;
|
|
96
|
-
request(kind: "pause" | "resume" | "stop" | "retry" | "recover", target: string): Promise<string>;
|
|
98
|
+
request(kind: "pause" | "resume" | "stop" | "retry" | "retry-refresh" | "recover", target: string): Promise<string>;
|
|
97
99
|
commands(): Promise<Array<{
|
|
98
100
|
id: string;
|
|
99
101
|
kind: string;
|
package/dist/src/store.js
CHANGED
|
@@ -88,41 +88,51 @@ export class Store {
|
|
|
88
88
|
return state;
|
|
89
89
|
}
|
|
90
90
|
async setProject(id, change) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
await this.db.transaction(async (tx) => {
|
|
92
|
+
const { projects } = tables;
|
|
93
|
+
if (change.paused !== undefined || change.blocked !== undefined) {
|
|
94
|
+
await tx
|
|
95
|
+
.update(projects)
|
|
96
|
+
.set(change)
|
|
97
|
+
.where(and(eq(projects.scope, this.scope), eq(projects.id, id)));
|
|
98
|
+
}
|
|
99
|
+
await tx
|
|
100
|
+
.insert(tables.events)
|
|
101
|
+
.values(this.event(null, "project", { id, ...change }));
|
|
102
|
+
});
|
|
99
103
|
}
|
|
100
104
|
async insertRun(run) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
return this.db.transaction(async (tx) => {
|
|
106
|
+
const { runs } = tables;
|
|
107
|
+
const inserted = await tx
|
|
108
|
+
.insert(runs)
|
|
109
|
+
.values({
|
|
110
|
+
scope: this.scope,
|
|
111
|
+
id: run.id,
|
|
112
|
+
taskKey: run.taskKey,
|
|
113
|
+
attempt: run.attempt,
|
|
114
|
+
record: redactValue(run, this.redact),
|
|
115
|
+
})
|
|
116
|
+
.onConflictDoNothing()
|
|
117
|
+
.returning({ id: runs.id });
|
|
118
|
+
if (inserted.length)
|
|
119
|
+
await tx.insert(tables.events).values(this.event(run.id, "run", run));
|
|
120
|
+
return inserted.length > 0;
|
|
121
|
+
});
|
|
116
122
|
}
|
|
117
123
|
async blockProject(id, reason) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
await this.db.transaction(async (tx) => {
|
|
125
|
+
const { projects } = tables;
|
|
126
|
+
const updated = await tx
|
|
127
|
+
.update(projects)
|
|
128
|
+
.set({ blocked: this.redact(reason) })
|
|
129
|
+
.where(and(eq(projects.scope, this.scope), eq(projects.id, id), isNull(projects.blocked)))
|
|
130
|
+
.returning({ id: projects.id });
|
|
131
|
+
if (updated.length)
|
|
132
|
+
await tx
|
|
133
|
+
.insert(tables.events)
|
|
134
|
+
.values(this.event(null, "project", { id, blocked: reason }));
|
|
135
|
+
});
|
|
126
136
|
}
|
|
127
137
|
/**
|
|
128
138
|
* Admit a retry and its events atomically. Safety checks run under the project
|
|
@@ -177,7 +187,7 @@ export class Store {
|
|
|
177
187
|
taskKey: previous.taskKey,
|
|
178
188
|
attempt,
|
|
179
189
|
retryOf: previous.id,
|
|
180
|
-
issue: previous.issue,
|
|
190
|
+
issue: target.issue ?? previous.issue,
|
|
181
191
|
now,
|
|
182
192
|
branchTemplate: target.branchTemplate,
|
|
183
193
|
});
|
|
@@ -329,7 +339,7 @@ export class Store {
|
|
|
329
339
|
const { runs } = tables;
|
|
330
340
|
const change = { ...patch, updatedAt: new Date().toISOString() };
|
|
331
341
|
const predicate = and(eq(runs.scope, this.scope), eq(runs.id, id));
|
|
332
|
-
|
|
342
|
+
return this.db.transaction(async (tx) => {
|
|
333
343
|
// Serialize read/merge/write so concurrent patches cannot lose fields.
|
|
334
344
|
const [row] = await tx
|
|
335
345
|
.select({ record: runs.record })
|
|
@@ -354,27 +364,30 @@ export class Store {
|
|
|
354
364
|
execution.finishedAt = change.updatedAt;
|
|
355
365
|
}
|
|
356
366
|
await tx.update(runs).set({ record: merged }).where(predicate);
|
|
367
|
+
await tx.insert(tables.events).values(this.event(id, "run", change));
|
|
357
368
|
return merged;
|
|
358
369
|
});
|
|
359
|
-
await this.emit(id, "run", change);
|
|
360
|
-
return record;
|
|
361
370
|
}
|
|
362
371
|
async saveInvocation(record) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
372
|
+
await this.db.transaction(async (tx) => {
|
|
373
|
+
const { invocations } = tables;
|
|
374
|
+
const persisted = redactValue(record, this.redact);
|
|
375
|
+
await tx
|
|
376
|
+
.insert(invocations)
|
|
377
|
+
.values({
|
|
378
|
+
scope: this.scope,
|
|
379
|
+
id: record.id,
|
|
380
|
+
runId: record.runId,
|
|
381
|
+
record: persisted,
|
|
382
|
+
})
|
|
383
|
+
.onConflictDoUpdate({
|
|
384
|
+
target: invocations.id,
|
|
385
|
+
set: { record: persisted },
|
|
386
|
+
});
|
|
387
|
+
await tx
|
|
388
|
+
.insert(tables.events)
|
|
389
|
+
.values(this.event(record.runId, "invocation", record));
|
|
376
390
|
});
|
|
377
|
-
await this.emit(record.runId, "invocation", record);
|
|
378
391
|
}
|
|
379
392
|
async invocations(runId) {
|
|
380
393
|
const { invocations } = tables;
|
|
@@ -387,7 +400,12 @@ export class Store {
|
|
|
387
400
|
.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
388
401
|
}
|
|
389
402
|
async emit(runId, kind, payload) {
|
|
390
|
-
await this.db
|
|
403
|
+
await this.db
|
|
404
|
+
.insert(tables.events)
|
|
405
|
+
.values(this.event(runId, kind, payload));
|
|
406
|
+
}
|
|
407
|
+
event(runId, kind, payload) {
|
|
408
|
+
return {
|
|
391
409
|
scope: this.scope,
|
|
392
410
|
runId,
|
|
393
411
|
kind,
|
|
@@ -395,7 +413,7 @@ export class Store {
|
|
|
395
413
|
payload: new SQL([
|
|
396
414
|
new Param(JSON.stringify(redactValue(payload, this.redact))),
|
|
397
415
|
]),
|
|
398
|
-
}
|
|
416
|
+
};
|
|
399
417
|
}
|
|
400
418
|
async events(after = 0, runId) {
|
|
401
419
|
const { events } = tables;
|
package/dist/src/tui/data.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface MonitorSource {
|
|
|
9
9
|
request: Store["request"];
|
|
10
10
|
commands: Store["commands"];
|
|
11
11
|
}
|
|
12
|
-
export type MonitorAction = "pause" | "resume" | "stop" | "retry" | "recover";
|
|
12
|
+
export type MonitorAction = "pause" | "resume" | "stop" | "retry" | "retry-refresh" | "recover";
|
|
13
13
|
export declare function useMonitorData(source: MonitorSource, selection: {
|
|
14
14
|
projectId?: string;
|
|
15
15
|
runId?: string;
|
package/dist/src/tui/dialogs.js
CHANGED
|
@@ -32,6 +32,7 @@ const helpPages = [
|
|
|
32
32
|
["p", "Pause or resume project intake"],
|
|
33
33
|
["s", "Stop selected run"],
|
|
34
34
|
["r", "Retry as a new run"],
|
|
35
|
+
["R", "Retry with the current hosted issue"],
|
|
35
36
|
["c", "Recover failed publication"],
|
|
36
37
|
["[ / ]", "Inspect previous or next step event"],
|
|
37
38
|
["End", "Follow latest step event"],
|