@bastani/atomic 0.9.9-alpha.2 → 0.9.9-alpha.4
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/CHANGELOG.md +16 -0
- package/dist/builtin/cursor/CHANGELOG.md +12 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +13 -0
- package/dist/builtin/intercom/README.md +10 -3
- package/dist/builtin/intercom/broker/client.ts +2 -0
- package/dist/builtin/intercom/broker/send-handler.ts +22 -20
- package/dist/builtin/intercom/foreground-detach-handoff.ts +3 -1
- package/dist/builtin/intercom/inbound-idle-queue.ts +82 -0
- package/dist/builtin/intercom/index-heavy.ts +47 -37
- package/dist/builtin/intercom/intercom-tool.ts +6 -4
- package/dist/builtin/intercom/intercom-utils.ts +30 -0
- package/dist/builtin/intercom/lifecycle.ts +8 -5
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/intercom/reply-tracker.ts +19 -19
- package/dist/builtin/intercom/session-target.ts +68 -0
- package/dist/builtin/intercom/skills/intercom/SKILL.md +7 -4
- package/dist/builtin/intercom/source-ownership.ts +34 -0
- package/dist/builtin/intercom/subagent-relay.ts +45 -20
- package/dist/builtin/intercom/terminal-ordering-barrier.ts +111 -0
- package/dist/builtin/intercom/types.ts +5 -0
- package/dist/builtin/mcp/CHANGELOG.md +12 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +12 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/startup-maintenance.ts +2 -1
- package/dist/builtin/subagents/src/runs/background/notify.ts +53 -9
- package/dist/builtin/subagents/src/shared/types-config.ts +1 -0
- package/dist/builtin/web-access/CHANGELOG.md +12 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +14 -0
- package/dist/builtin/workflows/README.md +2 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +102 -48
- package/dist/builtin/workflows/src/extension/render-result.ts +11 -2
- package/dist/builtin/workflows/src/extension/workflow-command-registration.ts +11 -8
- package/dist/builtin/workflows/src/extension/workflow-command-surfaces.ts +23 -0
- package/dist/builtin/workflows/src/extension/workflow-reload-coordinator.ts +66 -0
- package/dist/builtin/workflows/src/extension/workflow-reload-report.ts +39 -0
- package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +47 -25
- package/dist/builtin/workflows/src/extension/workflow-targets.ts +0 -10
- package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +21 -17
- package/dist/builtin/workflows/src/extension/workflow-tool.ts +20 -3
- package/dist/core/agent-session-extension-bindings.d.ts.map +1 -1
- package/dist/core/agent-session-extension-bindings.js +9 -0
- package/dist/core/agent-session-extension-bindings.js.map +1 -1
- package/dist/core/agent-session-message-queue.d.ts +4 -1
- package/dist/core/agent-session-message-queue.d.ts.map +1 -1
- package/dist/core/agent-session-message-queue.js +35 -0
- package/dist/core/agent-session-message-queue.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +2 -1
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/extensions/api-types.d.ts +3 -1
- package/dist/core/extensions/api-types.d.ts.map +1 -1
- package/dist/core/extensions/api-types.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader-api.d.ts.map +1 -1
- package/dist/core/extensions/loader-api.js +4 -0
- package/dist/core/extensions/loader-api.js.map +1 -1
- package/dist/core/extensions/loader-runtime.d.ts.map +1 -1
- package/dist/core/extensions/loader-runtime.js +1 -0
- package/dist/core/extensions/loader-runtime.js.map +1 -1
- package/dist/core/extensions/message-types.d.ts +3 -0
- package/dist/core/extensions/message-types.d.ts.map +1 -1
- package/dist/core/extensions/message-types.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/runtime-types.d.ts +3 -1
- package/dist/core/extensions/runtime-types.d.ts.map +1 -1
- package/dist/core/extensions/runtime-types.js.map +1 -1
- package/docs/extensions.md +13 -0
- package/docs/subagents.md +2 -0
- package/docs/workflows.md +11 -3
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
|
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.9-alpha.4] - 2026-07-15
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed workflow resource reload to build and atomically publish a fresh registry for additions, edits, renames, deletions, config paths, conventional/legacy directories, and package resources without restarting Atomic. Reloads now serialize and coalesce, reject stale session generations, retain the active registry on fatal failures, remain safe during in-flight runs, and return visible config/discovery diagnostics through both slash-command and tool surfaces while preserving valid siblings.
|
|
14
|
+
- Fixed exact paused top-level live `/workflow resume <id>` targets to resume before enumerating retained completed durable history, preserving live-over-durable precedence and keeping headless command output responsive when the durable catalog is large, while exact nested child IDs remain excluded from the top-level resume namespace. Slash-dispatch tests now isolate durable state in memory and restore the backend after each test so they never read or populate a user's workflow history.
|
|
15
|
+
- Isolated lazy-startup continuation tests with fresh in-memory durable backends and a completed-catalog regression guard, preventing test runs from enumerating or populating the user's real durable workflow history while preserving failed-run resource-loading coverage.
|
|
16
|
+
|
|
17
|
+
## [0.9.9-alpha.3] - 2026-07-14
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Made `workflow({ action: "status", runId })` apply the same exact-or-unique-prefix contract as the other run actions. A prefix shared by multiple retained runs now returns the standard ambiguity diagnostic instead of silently inspecting the first match, so abbreviated run IDs printed by status surfaces remain safe and actionable.
|
|
22
|
+
|
|
9
23
|
## [0.9.9-alpha.2] - 2026-07-14
|
|
10
24
|
|
|
11
25
|
### Changed
|
|
@@ -29,6 +29,8 @@ Adding workflow files under `.atomic/workflows/` (project scope) or `~/.atomic/a
|
|
|
29
29
|
}
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
After Atomic is running, use `/workflow reload` or the workflow tool's `reload` action to rescan all workflow sources in process. Additions, edits, renames, deletions, config changes, and package-resource changes become visible immediately to list/get/inputs/help/completion/invocation surfaces. Reload requests are serialized/coalesced and publish a complete replacement registry; an in-flight workflow keeps its original definition while later calls use the new registry. Fatal refresh failures retain the prior registry, and skipped malformed or missing resources are reported with actionable diagnostics while valid siblings remain available.
|
|
33
|
+
|
|
32
34
|
### Workflow lifecycle notifications
|
|
33
35
|
|
|
34
36
|
Workflow lifecycle notices are enabled by default. They send steer prompts into the main chat/model context when a run completes, fails, or ends blocked. Awaiting-input prompts are tracked for dedupe/restore, but they do not wake the main chat agent. Configure lifecycle tracking in the same extension config file:
|
|
@@ -36,7 +36,11 @@ import {
|
|
|
36
36
|
} from "./hil-answer-notifications.js";
|
|
37
37
|
import type { ExtensionAPI, PiModelContext } from "./public-types.js";
|
|
38
38
|
import { makeMcpPort, makePersistencePort } from "./workflow-ports.js";
|
|
39
|
-
import {
|
|
39
|
+
import { createWorkflowReloadCoordinator } from "./workflow-reload-coordinator.js";
|
|
40
|
+
import {
|
|
41
|
+
workflowReloadDiagnostics,
|
|
42
|
+
type WorkflowReloadReport,
|
|
43
|
+
} from "./workflow-reload-report.js";
|
|
40
44
|
|
|
41
45
|
export interface WorkflowExtensionRuntimeState {
|
|
42
46
|
persistenceRef: { current: WorkflowPersistencePort | undefined };
|
|
@@ -49,8 +53,8 @@ export interface WorkflowExtensionRuntimeState {
|
|
|
49
53
|
runtimeForContext(ctx?: PiModelContext): ExtensionRuntime;
|
|
50
54
|
resetWorkflowDiscoveryForSession(): void;
|
|
51
55
|
ensureWorkflowConfigLoaded(): Promise<void>;
|
|
52
|
-
ensureWorkflowResourcesLoaded(
|
|
53
|
-
reloadWorkflowResources(
|
|
56
|
+
ensureWorkflowResourcesLoaded(): Promise<void>;
|
|
57
|
+
reloadWorkflowResources(): Promise<WorkflowReloadReport>;
|
|
54
58
|
startWorkflowDiscoveryWarmup(onSettled?: () => void): void;
|
|
55
59
|
runWithLifecycleSuppressedForPolicy<T>(policy: WorkflowExecutionPolicy, fn: () => Promise<T>): Promise<T>;
|
|
56
60
|
setNotificationsActive(active: boolean): void;
|
|
@@ -198,10 +202,9 @@ export function createWorkflowExtensionRuntimeState(
|
|
|
198
202
|
});
|
|
199
203
|
}
|
|
200
204
|
|
|
201
|
-
let
|
|
202
|
-
|
|
203
|
-
let lazyDiscoveryPromise: Promise<void> | null = null;
|
|
205
|
+
let lazyDiscoveryPromise: Promise<WorkflowReloadReport> | null = null;
|
|
204
206
|
let workflowDiscoveryGeneration = 0;
|
|
207
|
+
let activeResourceGeneration = 0;
|
|
205
208
|
function deferToMacrotask(): Promise<void> {
|
|
206
209
|
return new Promise((resolve) => setImmediate(resolve));
|
|
207
210
|
}
|
|
@@ -245,7 +248,6 @@ export function createWorkflowExtensionRuntimeState(
|
|
|
245
248
|
workflowDiscoveryGeneration += 1;
|
|
246
249
|
discoveryRef.current = null;
|
|
247
250
|
lazyDiscoveryPromise = null;
|
|
248
|
-
workflowReloadQueue = Promise.resolve();
|
|
249
251
|
rebuildRuntime(startupDiscovery.registry);
|
|
250
252
|
}
|
|
251
253
|
|
|
@@ -257,13 +259,36 @@ export function createWorkflowExtensionRuntimeState(
|
|
|
257
259
|
rebuildRuntime();
|
|
258
260
|
}
|
|
259
261
|
|
|
260
|
-
function
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
function supersededReloadReport(
|
|
263
|
+
coalescedRequests: number,
|
|
264
|
+
configResult?: ConfigLoadResult,
|
|
265
|
+
result?: DiscoveryResult,
|
|
266
|
+
): WorkflowReloadReport {
|
|
267
|
+
return {
|
|
268
|
+
outcome: "superseded",
|
|
269
|
+
generation: activeResourceGeneration,
|
|
270
|
+
workflowCount: runtimeRef.current.registry.names().length,
|
|
271
|
+
coalescedRequests,
|
|
272
|
+
diagnostics: workflowReloadDiagnostics(configResult?.diagnostics ?? [], result?.errors ?? []),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function failedReloadReport(
|
|
277
|
+
error: unknown,
|
|
278
|
+
coalescedRequests: number,
|
|
279
|
+
configResult?: ConfigLoadResult,
|
|
280
|
+
): WorkflowReloadReport {
|
|
281
|
+
return {
|
|
282
|
+
outcome: "failed",
|
|
283
|
+
error: error instanceof Error ? error.message : String(error),
|
|
284
|
+
generation: activeResourceGeneration,
|
|
285
|
+
workflowCount: runtimeRef.current.registry.names().length,
|
|
286
|
+
coalescedRequests,
|
|
287
|
+
diagnostics: workflowReloadDiagnostics(configResult?.diagnostics ?? [], []),
|
|
288
|
+
};
|
|
264
289
|
}
|
|
265
290
|
|
|
266
|
-
function trackLazyDiscovery(pending: Promise<
|
|
291
|
+
function trackLazyDiscovery(pending: Promise<WorkflowReloadReport>): Promise<WorkflowReloadReport> {
|
|
267
292
|
lazyDiscoveryPromise = pending;
|
|
268
293
|
void pending.finally(() => {
|
|
269
294
|
if (lazyDiscoveryPromise === pending) lazyDiscoveryPromise = null;
|
|
@@ -271,49 +296,74 @@ export function createWorkflowExtensionRuntimeState(
|
|
|
271
296
|
return pending;
|
|
272
297
|
}
|
|
273
298
|
|
|
274
|
-
function reloadWorkflowResources(
|
|
275
|
-
|
|
276
|
-
return trackLazyDiscovery(queueWorkflowResourceReload(options, generation));
|
|
299
|
+
function reloadWorkflowResources(): Promise<WorkflowReloadReport> {
|
|
300
|
+
return trackLazyDiscovery(reloadCoordinator.request(workflowDiscoveryGeneration));
|
|
277
301
|
}
|
|
278
302
|
async function loadPackageWorkflowPaths(): Promise<string[]> {
|
|
279
303
|
const packageResources = (await pi.refreshWorkflowResources?.()) ?? pi.getWorkflowResources?.() ?? [];
|
|
280
304
|
return packageResources.filter((resource) => resource.enabled !== false).map((resource) => resource.path);
|
|
281
305
|
}
|
|
282
|
-
async function reloadWorkflowResourcesNow(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
console.warn(`Workflow reload bypassed in-flight guard with ${activeRuns} active run(s).`);
|
|
306
|
+
async function reloadWorkflowResourcesNow(
|
|
307
|
+
discoveryGeneration: number,
|
|
308
|
+
coalescedRequests: number,
|
|
309
|
+
): Promise<WorkflowReloadReport> {
|
|
310
|
+
if (!isWorkflowDiscoveryCurrent(discoveryGeneration)) {
|
|
311
|
+
return supersededReloadReport(coalescedRequests);
|
|
289
312
|
}
|
|
290
313
|
const configResult = await loadWorkflowConfig();
|
|
291
|
-
if (!isWorkflowDiscoveryCurrent(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
314
|
+
if (!isWorkflowDiscoveryCurrent(discoveryGeneration)) {
|
|
315
|
+
return supersededReloadReport(coalescedRequests, configResult);
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const hasGlobal = configResult.globalConfig != null;
|
|
319
|
+
const hasProject = configResult.projectConfig != null;
|
|
320
|
+
const discoveryConfig = hasGlobal || hasProject
|
|
321
|
+
? toScopedDiscoveryConfig(configResult.globalConfig ?? null, configResult.projectConfig ?? null, { projectRoot: process.cwd() })
|
|
322
|
+
: undefined;
|
|
323
|
+
const packageWorkflowPaths = await loadPackageWorkflowPaths();
|
|
324
|
+
if (!isWorkflowDiscoveryCurrent(discoveryGeneration)) {
|
|
325
|
+
return supersededReloadReport(coalescedRequests, configResult);
|
|
326
|
+
}
|
|
327
|
+
const result = await discoverWorkflows({ config: discoveryConfig, packageWorkflowPaths });
|
|
328
|
+
if (!isWorkflowDiscoveryCurrent(discoveryGeneration)) {
|
|
329
|
+
return supersededReloadReport(coalescedRequests, configResult, result);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Commit config, diagnostics, and the replacement runtime synchronously,
|
|
333
|
+
// after every fallible/awaited discovery step has completed.
|
|
334
|
+
applyWorkflowConfig(configResult);
|
|
335
|
+
discoveryRef.current = result;
|
|
336
|
+
rebuildRuntime(result.registry);
|
|
337
|
+
activeResourceGeneration += 1;
|
|
338
|
+
return {
|
|
339
|
+
outcome: "applied",
|
|
340
|
+
generation: activeResourceGeneration,
|
|
341
|
+
workflowCount: result.registry.names().length,
|
|
342
|
+
coalescedRequests,
|
|
343
|
+
diagnostics: workflowReloadDiagnostics(configResult.diagnostics, result.errors),
|
|
344
|
+
};
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return failedReloadReport(error, coalescedRequests, configResult);
|
|
347
|
+
}
|
|
304
348
|
}
|
|
305
349
|
|
|
306
|
-
|
|
350
|
+
const reloadCoordinator = createWorkflowReloadCoordinator(async (discoveryGeneration, coalescedRequests) => {
|
|
351
|
+
try {
|
|
352
|
+
return await reloadWorkflowResourcesNow(discoveryGeneration, coalescedRequests);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
return failedReloadReport(error, coalescedRequests);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
async function ensureWorkflowResourcesLoaded(): Promise<void> {
|
|
307
359
|
while (!pi.disableAsyncDiscovery && discoveryRef.current === null) {
|
|
308
|
-
const
|
|
309
|
-
const pending = lazyDiscoveryPromise
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (!isWorkflowDiscoveryCurrent(
|
|
314
|
-
if (discoveryRef.current === null)
|
|
315
|
-
lazyDiscoveryPromise = null;
|
|
316
|
-
}
|
|
360
|
+
const discoveryGeneration = workflowDiscoveryGeneration;
|
|
361
|
+
const pending = lazyDiscoveryPromise
|
|
362
|
+
?? trackLazyDiscovery(reloadCoordinator.request(discoveryGeneration));
|
|
363
|
+
const report = await pending;
|
|
364
|
+
if (report.outcome === "failed") throw new Error(report.error);
|
|
365
|
+
if (!isWorkflowDiscoveryCurrent(discoveryGeneration)) continue;
|
|
366
|
+
if (discoveryRef.current === null) lazyDiscoveryPromise = null;
|
|
317
367
|
}
|
|
318
368
|
}
|
|
319
369
|
|
|
@@ -321,10 +371,14 @@ export function createWorkflowExtensionRuntimeState(
|
|
|
321
371
|
if (pi.disableAsyncDiscovery || discoveryRef.current !== null || lazyDiscoveryPromise !== null) return;
|
|
322
372
|
const notificationStart = notificationGeneration;
|
|
323
373
|
const discoveryStart = workflowDiscoveryGeneration;
|
|
324
|
-
const pending = (async () => {
|
|
374
|
+
const pending = (async (): Promise<WorkflowReloadReport> => {
|
|
325
375
|
await deferToMacrotask();
|
|
326
|
-
if (!isWorkflowDiscoveryCurrent(discoveryStart))
|
|
327
|
-
|
|
376
|
+
if (!isWorkflowDiscoveryCurrent(discoveryStart)) {
|
|
377
|
+
return supersededReloadReport(1);
|
|
378
|
+
}
|
|
379
|
+
const report = await reloadCoordinator.request(discoveryStart);
|
|
380
|
+
if (report.outcome === "failed") throw new Error(report.error);
|
|
381
|
+
return report;
|
|
328
382
|
})();
|
|
329
383
|
lazyDiscoveryPromise = pending;
|
|
330
384
|
const isCurrentWarmup = (): boolean => lazyDiscoveryPromise === pending
|
|
@@ -16,6 +16,7 @@ import type { WorkflowDetails } from "../shared/types.js";
|
|
|
16
16
|
import type { RunDetail } from "../runs/background/status.js";
|
|
17
17
|
import { renderInputsSchema } from "../shared/render-inputs-schema.js";
|
|
18
18
|
import { renderStatusList } from "../tui/status-list.js";
|
|
19
|
+
import type { WorkflowReloadReport } from "./workflow-reload-report.js";
|
|
19
20
|
import { renderRunDetail } from "../tui/run-detail.js";
|
|
20
21
|
import { renderWorkflowList } from "../tui/workflow-list.js";
|
|
21
22
|
import { deriveGraphTheme } from "../tui/graph-theme.js";
|
|
@@ -23,6 +24,7 @@ import { renderDispatchConfirm } from "../tui/dispatch-confirm.js";
|
|
|
23
24
|
import type { WorkflowInputValues, WorkflowOutputValues } from "../shared/types.js";
|
|
24
25
|
import { renderRoundedBox } from "../tui/chat-surface.js";
|
|
25
26
|
import { truncateToWidth } from "../tui/text-helpers.js";
|
|
27
|
+
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
26
28
|
|
|
27
29
|
// ---------------------------------------------------------------------------
|
|
28
30
|
// Result variants
|
|
@@ -138,7 +140,7 @@ type TranscriptResult = {
|
|
|
138
140
|
};
|
|
139
141
|
type SendResult = { action: "send"; runId: string; stageId: string; delivery: string; status: "ok" | "noop"; message: string };
|
|
140
142
|
type PauseResult = { action: "pause"; runId: string; status: string; message: string };
|
|
141
|
-
type ReloadResult = { action: "reload"; status: "ok" | "noop"; message: string };
|
|
143
|
+
type ReloadResult = WorkflowReloadReport & { action: "reload"; status: "ok" | "noop"; message: string };
|
|
142
144
|
type InterruptResult = { action: "interrupt"; runId: string; status: string; message: string };
|
|
143
145
|
type KillResult = { action: "kill"; runId: string; status: string; message: string };
|
|
144
146
|
type ResumeResult = { action: "resume"; runId: string; status: string; message: string };
|
|
@@ -201,6 +203,13 @@ function fitLine(line: string, width?: number): string {
|
|
|
201
203
|
return truncateToWidth(line, width, "…");
|
|
202
204
|
}
|
|
203
205
|
|
|
206
|
+
function noticeBodyLines(message: string, width?: number): string[] {
|
|
207
|
+
return message.split(/\r?\n/).flatMap((line) => {
|
|
208
|
+
if (line.length === 0 || width === undefined || width <= 0) return [line];
|
|
209
|
+
return wrapTextWithAnsi(line, width);
|
|
210
|
+
}).map((line) => ` ${line} `);
|
|
211
|
+
}
|
|
212
|
+
|
|
204
213
|
function renderNotice(
|
|
205
214
|
title: string,
|
|
206
215
|
message: string,
|
|
@@ -212,7 +221,7 @@ function renderNotice(
|
|
|
212
221
|
const contentWidth = width && width > 0 ? Math.max(1, width - 4) : undefined;
|
|
213
222
|
return renderRoundedBox({
|
|
214
223
|
title,
|
|
215
|
-
bodyLines:
|
|
224
|
+
bodyLines: noticeBodyLines(message, contentWidth),
|
|
216
225
|
theme,
|
|
217
226
|
width,
|
|
218
227
|
});
|
|
@@ -23,22 +23,25 @@ import {
|
|
|
23
23
|
type WorkflowCommandHandler,
|
|
24
24
|
type WorkflowCommandOutputDetails,
|
|
25
25
|
} from "./workflow-command-utils.js";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
emitTerminalRunDetailSurface,
|
|
28
|
+
formatWorkflowReloadReport,
|
|
29
|
+
formatWorkflowResourceLoadWarning,
|
|
30
|
+
} from "./workflow-command-surfaces.js";
|
|
27
31
|
import { handleRunControlCommand, type WorkflowRunControlDeps } from "./workflow-run-control-command.js";
|
|
28
32
|
import { workflowPolicyFromContext } from "./workflow-policy.js";
|
|
29
33
|
import {
|
|
30
|
-
inFlightRunCount,
|
|
31
|
-
reloadBlockedMessage,
|
|
32
34
|
reloadFailureMessage,
|
|
33
35
|
resolveRunIdPrefix,
|
|
34
36
|
overlaySurfaceFromContext,
|
|
35
37
|
} from "./workflow-targets.js";
|
|
38
|
+
import { normalizeWorkflowReloadReport, type WorkflowReloadReport } from "./workflow-reload-report.js";
|
|
36
39
|
|
|
37
40
|
export interface WorkflowSlashCommandDeps {
|
|
38
41
|
runtimeProxy: ExtensionRuntime;
|
|
39
42
|
runtimeForContext: (ctx?: PiCommandContext) => ExtensionRuntime;
|
|
40
43
|
overlay: GraphOverlayPort;
|
|
41
|
-
reloadWorkflowResources: () => Promise<void> | void;
|
|
44
|
+
reloadWorkflowResources: () => Promise<WorkflowReloadReport | void> | void;
|
|
42
45
|
ensureWorkflowResourcesLoaded: () => Promise<void> | void;
|
|
43
46
|
runWithLifecycleSuppressedForPolicy: <T>(
|
|
44
47
|
policy: WorkflowExecutionPolicy,
|
|
@@ -141,11 +144,11 @@ async function workflowSlashHandler(
|
|
|
141
144
|
return;
|
|
142
145
|
}
|
|
143
146
|
if (subcommand === "reload") {
|
|
144
|
-
const activeRuns = inFlightRunCount();
|
|
145
|
-
if (activeRuns > 0) return fail(reloadBlockedMessage(activeRuns));
|
|
146
147
|
try {
|
|
147
|
-
await deps.reloadWorkflowResources();
|
|
148
|
-
|
|
148
|
+
const report = normalizeWorkflowReloadReport(await deps.reloadWorkflowResources());
|
|
149
|
+
const message = formatWorkflowReloadReport(report);
|
|
150
|
+
if (report.outcome === "applied") print(message);
|
|
151
|
+
else fail(message);
|
|
149
152
|
} catch (error) {
|
|
150
153
|
fail(reloadFailureMessage(error));
|
|
151
154
|
}
|
|
@@ -7,6 +7,7 @@ import type { DiscoveryResult } from "./discovery.js";
|
|
|
7
7
|
import type { WorkflowToolResult } from "./render-result.js";
|
|
8
8
|
import type { ExtensionAPI } from "./public-types.js";
|
|
9
9
|
import { isRunStatus } from "./workflow-targets.js";
|
|
10
|
+
import type { WorkflowReloadReport } from "./workflow-reload-report.js";
|
|
10
11
|
|
|
11
12
|
function fallbackRunDetailFromResult(
|
|
12
13
|
workflowName: string,
|
|
@@ -54,6 +55,28 @@ export function formatWorkflowResourceLoadWarning(error: unknown): string {
|
|
|
54
55
|
"Using the currently loaded workflow registry; run `/workflow reload` after fixing the issue.",
|
|
55
56
|
].join("\n");
|
|
56
57
|
}
|
|
58
|
+
|
|
59
|
+
export function formatWorkflowReloadReport(report: WorkflowReloadReport, reason?: string): string {
|
|
60
|
+
const reasonSuffix = reason?.trim() ? ` (${reason.trim()})` : "";
|
|
61
|
+
const legacySuccess = report.generation === 0 && report.workflowCount === 0 && report.diagnostics.length === 0;
|
|
62
|
+
const headline = report.outcome === "applied"
|
|
63
|
+
? legacySuccess
|
|
64
|
+
? `Reloaded workflow resources${reasonSuffix}.`
|
|
65
|
+
: `Reloaded workflow resources${reasonSuffix}. ${report.workflowCount} workflow(s), generation ${report.generation}.`
|
|
66
|
+
: report.outcome === "failed"
|
|
67
|
+
? `Workflow resources could not be refreshed; the current registry was retained: ${report.error}`
|
|
68
|
+
: "Workflow reload was superseded by a newer session; no stale resources were applied.";
|
|
69
|
+
if (report.diagnostics.length === 0) return headline;
|
|
70
|
+
|
|
71
|
+
const lines = report.diagnostics.map((diagnostic) =>
|
|
72
|
+
`- [${diagnostic.level} ${diagnostic.code}] ${diagnostic.source ?? `workflow ${diagnostic.phase}`}: ${diagnostic.message}`
|
|
73
|
+
);
|
|
74
|
+
return [
|
|
75
|
+
headline,
|
|
76
|
+
`Workflow discovery diagnostics (${report.diagnostics.length}): some resources were skipped or need attention.`,
|
|
77
|
+
...lines,
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
57
80
|
export function formatStartupDiagnostics(
|
|
58
81
|
configResult: ConfigLoadResult | null,
|
|
59
82
|
discoveryResult: DiscoveryResult | null,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { WorkflowReloadReport } from "./workflow-reload-report.js";
|
|
2
|
+
|
|
3
|
+
type ReloadBatch = {
|
|
4
|
+
readonly discoveryGeneration: number;
|
|
5
|
+
requestCount: number;
|
|
6
|
+
readonly promise: Promise<WorkflowReloadReport>;
|
|
7
|
+
readonly resolve: (report: WorkflowReloadReport) => void;
|
|
8
|
+
readonly reject: (error: Error) => void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface WorkflowReloadCoordinator {
|
|
12
|
+
request(discoveryGeneration: number): Promise<WorkflowReloadReport>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createBatch(discoveryGeneration: number): ReloadBatch {
|
|
16
|
+
let resolve: (report: WorkflowReloadReport) => void = () => undefined;
|
|
17
|
+
let reject: (error: Error) => void = () => undefined;
|
|
18
|
+
const promise = new Promise<WorkflowReloadReport>((resolvePromise, rejectPromise) => {
|
|
19
|
+
resolve = resolvePromise;
|
|
20
|
+
reject = (error) => rejectPromise(error);
|
|
21
|
+
});
|
|
22
|
+
return { discoveryGeneration, requestCount: 0, promise, resolve, reject };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Runs one reload at a time. Requests coalesce only with the latest queued pass
|
|
27
|
+
* from the same session generation; generation boundaries remain ordered.
|
|
28
|
+
*/
|
|
29
|
+
export function createWorkflowReloadCoordinator(
|
|
30
|
+
reload: (discoveryGeneration: number, coalescedRequests: number) => Promise<WorkflowReloadReport>,
|
|
31
|
+
): WorkflowReloadCoordinator {
|
|
32
|
+
const pending: ReloadBatch[] = [];
|
|
33
|
+
let draining = false;
|
|
34
|
+
|
|
35
|
+
const drain = async (): Promise<void> => {
|
|
36
|
+
if (draining) return;
|
|
37
|
+
draining = true;
|
|
38
|
+
try {
|
|
39
|
+
while (pending.length > 0) {
|
|
40
|
+
const batch = pending.shift()!;
|
|
41
|
+
try {
|
|
42
|
+
const report = await reload(batch.discoveryGeneration, batch.requestCount);
|
|
43
|
+
batch.resolve(report);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
batch.reject(error instanceof Error ? error : new Error(String(error)));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} finally {
|
|
49
|
+
draining = false;
|
|
50
|
+
if (pending.length > 0) void drain();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
request(discoveryGeneration): Promise<WorkflowReloadReport> {
|
|
56
|
+
let batch = pending.at(-1);
|
|
57
|
+
if (batch?.discoveryGeneration !== discoveryGeneration) {
|
|
58
|
+
batch = createBatch(discoveryGeneration);
|
|
59
|
+
pending.push(batch);
|
|
60
|
+
}
|
|
61
|
+
batch.requestCount += 1;
|
|
62
|
+
queueMicrotask(() => void drain());
|
|
63
|
+
return batch.promise;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ConfigDiagnostic } from "./config-loader.js";
|
|
2
|
+
import type { DiscoveryDiagnostic } from "./discovery.js";
|
|
3
|
+
|
|
4
|
+
export type WorkflowReloadDiagnostic =
|
|
5
|
+
| (ConfigDiagnostic & { readonly phase: "config" })
|
|
6
|
+
| (DiscoveryDiagnostic & { readonly phase: "discovery" });
|
|
7
|
+
|
|
8
|
+
type WorkflowReloadReportState = {
|
|
9
|
+
readonly generation: number;
|
|
10
|
+
readonly workflowCount: number;
|
|
11
|
+
readonly coalescedRequests: number;
|
|
12
|
+
readonly diagnostics: WorkflowReloadDiagnostic[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type WorkflowReloadReport = WorkflowReloadReportState & (
|
|
16
|
+
| { readonly outcome: "applied" }
|
|
17
|
+
| { readonly outcome: "failed"; readonly error: string }
|
|
18
|
+
| { readonly outcome: "superseded" }
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export function normalizeWorkflowReloadReport(report: WorkflowReloadReport | void): WorkflowReloadReport {
|
|
22
|
+
return report ?? {
|
|
23
|
+
outcome: "applied",
|
|
24
|
+
generation: 0,
|
|
25
|
+
workflowCount: 0,
|
|
26
|
+
coalescedRequests: 1,
|
|
27
|
+
diagnostics: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function workflowReloadDiagnostics(
|
|
32
|
+
configDiagnostics: readonly ConfigDiagnostic[],
|
|
33
|
+
discoveryDiagnostics: readonly DiscoveryDiagnostic[],
|
|
34
|
+
): WorkflowReloadDiagnostic[] {
|
|
35
|
+
return [
|
|
36
|
+
...configDiagnostics.map((diagnostic) => ({ ...diagnostic, phase: "config" as const })),
|
|
37
|
+
...discoveryDiagnostics.map((diagnostic) => ({ ...diagnostic, phase: "discovery" as const })),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
@@ -288,35 +288,57 @@ export async function handleRunControlCommand(
|
|
|
288
288
|
} else if (action === "resume") {
|
|
289
289
|
const backend = getDurableBackend();
|
|
290
290
|
const exactBeforePreparation = store.runs().find((run) => run.id === target);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
} catch (error) {
|
|
301
|
-
preparationError = error instanceof Error ? error.message : String(error);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
const loadableRuns = topLevelWorkflowRuns(store.runs()).filter((run) => backend.isWorkflowLoadable(run.id));
|
|
305
|
-
const combined = resolveWorkflowResumeTarget(target, loadableRuns, durable, backend.listCompletedWorkflows());
|
|
306
|
-
if (combined.kind === "ambiguous") {
|
|
307
|
-
fail(`Ambiguous workflow prefix "${target}" matches: ${combined.matches.map((match) => `${match.name} (${match.workflowId.slice(0, 8)})`).join(", ")}`);
|
|
291
|
+
const exactHasPausedState = exactBeforePreparation?.status === "paused"
|
|
292
|
+
|| (exactBeforePreparation?.stages.some((stage) => stage.status === "paused") ?? false);
|
|
293
|
+
const exactIsActivelyRunning = exactBeforePreparation !== undefined
|
|
294
|
+
&& exactBeforePreparation.endedAt === undefined
|
|
295
|
+
&& exactBeforePreparation.status === "running"
|
|
296
|
+
&& !exactHasPausedState
|
|
297
|
+
&& exactBeforePreparation.exitReason !== "quit";
|
|
298
|
+
if (exactIsActivelyRunning) {
|
|
299
|
+
fail(`Workflow ${exactBeforePreparation.id.slice(0, 8)} is already running in this session. Attach with \`/workflow connect ${exactBeforePreparation.id.slice(0, 8)}\` instead of resuming.`);
|
|
308
300
|
return true;
|
|
309
301
|
}
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
302
|
+
if (exactBeforePreparation !== undefined
|
|
303
|
+
&& exactBeforePreparation.parentRunId === undefined
|
|
304
|
+
&& exactHasPausedState
|
|
305
|
+
&& backend.isWorkflowLoadable(exactBeforePreparation.id)) {
|
|
306
|
+
// Exact top-level live state is authoritative. Avoid scanning the
|
|
307
|
+
// potentially large completed catalog while preserving the established
|
|
308
|
+
// top-level target namespace and live-over-durable precedence.
|
|
309
|
+
runId = exactBeforePreparation.id;
|
|
310
|
+
} else {
|
|
311
|
+
let durable: readonly ResumableWorkflowEntry[] = [];
|
|
312
|
+
let preparationError: string | undefined;
|
|
313
|
+
const needsDurablePreparation = exactBeforePreparation === undefined
|
|
314
|
+
|| (!backend.isWorkflowLoadable(exactBeforePreparation.id)
|
|
315
|
+
&& (!exactHasPausedState || backend.hydrateResumableWorkflows !== undefined));
|
|
316
|
+
if (needsDurablePreparation) {
|
|
317
|
+
await ensureWorkflowResourcesVisible();
|
|
318
|
+
const runtime = deps.runtimeForContext(ctx);
|
|
319
|
+
try {
|
|
320
|
+
durable = await runtime.prepareDurableResumable(target);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
preparationError = error instanceof Error ? error.message : String(error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const loadableRuns = topLevelWorkflowRuns(store.runs()).filter((run) => backend.isWorkflowLoadable(run.id));
|
|
326
|
+
const combined = resolveWorkflowResumeTarget(target, loadableRuns, durable, backend.listCompletedWorkflows());
|
|
327
|
+
if (combined.kind === "ambiguous") {
|
|
328
|
+
fail(`Ambiguous workflow prefix "${target}" matches: ${combined.matches.map((match) => `${match.name} (${match.workflowId.slice(0, 8)})`).join(", ")}`);
|
|
317
329
|
return true;
|
|
318
330
|
}
|
|
319
|
-
|
|
331
|
+
if (combined.kind === "completed" || combined.kind === "durable") {
|
|
332
|
+
return await handleDurableResume(combined.workflowId, ctx, reporter, deps);
|
|
333
|
+
}
|
|
334
|
+
if (combined.kind === "live") runId = combined.workflowId;
|
|
335
|
+
else {
|
|
336
|
+
if (preparationError !== undefined) {
|
|
337
|
+
fail(`Failed to resolve workflow resume target: ${preparationError}`);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
return await handleDurableResume(target, ctx, reporter, deps);
|
|
341
|
+
}
|
|
320
342
|
}
|
|
321
343
|
} else {
|
|
322
344
|
const resolved = resolveRunIdPrefix(target);
|
|
@@ -41,23 +41,13 @@ export function topLevelExpandedSnapshots() {
|
|
|
41
41
|
}));
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export function reloadBlockedMessage(count = inFlightRunCount()): string {
|
|
45
|
-
return `Reload skipped: ${count} workflow run(s) still in flight. Wait for them to finish, or pause/kill them before reloading workflow resources.`;
|
|
46
|
-
}
|
|
47
44
|
|
|
48
45
|
export function allStageConflictMessage(action: "pause" | "interrupt" | "kill"): string {
|
|
49
46
|
return `Cannot ${action} --all with a stageId; omit stageId or target a single run.`;
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
export class WorkflowReloadBlockedError extends Error {
|
|
53
|
-
constructor(message: string) {
|
|
54
|
-
super(message);
|
|
55
|
-
this.name = "WorkflowReloadBlockedError";
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
49
|
|
|
59
50
|
export function reloadFailureMessage(error: unknown): string {
|
|
60
|
-
if (error instanceof WorkflowReloadBlockedError) return error.message;
|
|
61
51
|
return `Reload failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
62
52
|
}
|
|
63
53
|
|