@bastani/atomic 0.9.9-alpha.3 → 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 +6 -0
- package/dist/builtin/cursor/CHANGELOG.md +6 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/extension/startup-maintenance.ts +2 -1
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +8 -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 +2 -1
- package/docs/workflows.md +9 -1
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.9-alpha.4] - 2026-07-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed in-process workflow reload to atomically publish freshly rescanned project, user, legacy, configured, and package workflow resources across list/get/inputs/help/completion/invocation surfaces. Overlapping requests are serialized and coalesced, stale session generations cannot overwrite current state, fatal refresh failures retain the prior registry, and reload no longer blocks or changes workflows already in flight. `/workflow reload` and the workflow tool now include actionable per-resource diagnostics instead of reporting bare success when malformed or missing resources were skipped.
|
|
10
|
+
|
|
5
11
|
## [0.9.9-alpha.3] - 2026-07-14
|
|
6
12
|
|
|
7
13
|
### Added
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.9-alpha.4] - 2026-07-15
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Published a synchronized Atomic 0.9.9-alpha.4 prerelease for the Cursor provider package; no functional Cursor provider changes were made after 0.9.9-alpha.3.
|
|
10
|
+
|
|
5
11
|
## [0.9.9-alpha.3] - 2026-07-14
|
|
6
12
|
|
|
7
13
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/cursor",
|
|
3
|
-
"version": "0.9.9-alpha.
|
|
3
|
+
"version": "0.9.9-alpha.4",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Experimental first-party Atomic extension for Cursor OAuth, model discovery, and streaming provider registration.",
|
|
6
6
|
"contributors": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@bastani/atomic-natives": "0.9.9-alpha.
|
|
43
|
+
"@bastani/atomic-natives": "0.9.9-alpha.4",
|
|
44
44
|
"@bufbuild/protobuf": "^2.12.1",
|
|
45
45
|
"@earendil-works/pi-ai": "^0.80.7"
|
|
46
46
|
}
|
|
@@ -4,6 +4,12 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.9-alpha.4] - 2026-07-15
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Published a synchronized Atomic 0.9.9-alpha.4 prerelease for the intercom extension; no functional intercom changes were made after 0.9.9-alpha.3.
|
|
12
|
+
|
|
7
13
|
## [0.9.9-alpha.3] - 2026-07-14
|
|
8
14
|
|
|
9
15
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/intercom",
|
|
3
|
-
"version": "0.9.9-alpha.
|
|
3
|
+
"version": "0.9.9-alpha.4",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension providing a private coordination channel between parent and child agent sessions. Fork of: https://github.com/nicobailon/pi-intercom",
|
|
6
6
|
"contributors": [
|
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.9-alpha.4] - 2026-07-15
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Published a synchronized Atomic 0.9.9-alpha.4 prerelease for the MCP extension; no functional MCP changes were made after 0.9.9-alpha.3.
|
|
15
|
+
|
|
10
16
|
## [0.9.9-alpha.3] - 2026-07-14
|
|
11
17
|
|
|
12
18
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/mcp",
|
|
3
|
-
"version": "0.9.9-alpha.
|
|
3
|
+
"version": "0.9.9-alpha.4",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension that adapts MCP (Model Context Protocol) servers into the coding agent. Fork of: https://github.com/nicobailon/pi-mcp-adapter",
|
|
6
6
|
"contributors": [
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.9-alpha.4] - 2026-07-15
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Published a synchronized Atomic 0.9.9-alpha.4 prerelease for the subagents extension; no functional subagent changes were made after 0.9.9-alpha.3.
|
|
10
|
+
|
|
5
11
|
## [0.9.9-alpha.3] - 2026-07-14
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/subagents",
|
|
3
|
-
"version": "0.9.9-alpha.
|
|
3
|
+
"version": "0.9.9-alpha.4",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for delegating tasks to subagents with chains, parallel execution, and background runs. Fork of: https://github.com/nicobailon/pi-subagents",
|
|
6
6
|
"contributors": [
|
|
@@ -39,6 +39,7 @@ export function createSubagentStartupMaintenance(
|
|
|
39
39
|
resultsDir: string;
|
|
40
40
|
artifactCleanupDays: number;
|
|
41
41
|
resultTtlMs: number;
|
|
42
|
+
scheduleMacrotask?: (task: () => void) => () => void;
|
|
42
43
|
},
|
|
43
44
|
): SubagentStartupMaintenance {
|
|
44
45
|
const { startResultWatcher, primeExistingResults, stopResultWatcher } = createResultWatcher(
|
|
@@ -49,7 +50,7 @@ export function createSubagentStartupMaintenance(
|
|
|
49
50
|
);
|
|
50
51
|
const cancelTasks = new Set<() => void>();
|
|
51
52
|
const schedule = (task: () => void): void => {
|
|
52
|
-
const cancel = scheduleMacrotask(() => {
|
|
53
|
+
const cancel = (options.scheduleMacrotask ?? scheduleMacrotask)(() => {
|
|
53
54
|
cancelTasks.delete(cancel);
|
|
54
55
|
task();
|
|
55
56
|
});
|
|
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.9-alpha.4] - 2026-07-15
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Published a synchronized Atomic 0.9.9-alpha.4 prerelease for the web-access extension; no functional web-access changes were made after 0.9.9-alpha.3.
|
|
12
|
+
|
|
7
13
|
## [0.9.9-alpha.3] - 2026-07-14
|
|
8
14
|
|
|
9
15
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/web-access",
|
|
3
|
-
"version": "0.9.9-alpha.
|
|
3
|
+
"version": "0.9.9-alpha.4",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
|
|
6
6
|
"contributors": [
|
|
@@ -6,6 +6,14 @@ 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
|
+
|
|
9
17
|
## [0.9.9-alpha.3] - 2026-07-14
|
|
10
18
|
|
|
11
19
|
### Fixed
|
|
@@ -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
|
|
|
@@ -18,18 +18,17 @@ import {
|
|
|
18
18
|
allStageConflictMessage,
|
|
19
19
|
ambiguousRunMessage,
|
|
20
20
|
formatAlreadyEndedRetainedMessage,
|
|
21
|
-
inFlightRunCount,
|
|
22
|
-
reloadBlockedMessage,
|
|
23
21
|
reloadFailureMessage,
|
|
24
22
|
resolveToolRunTarget,
|
|
25
23
|
resolveToolStageTarget,
|
|
26
24
|
stageFailureMessage,
|
|
27
25
|
} from "./workflow-targets.js";
|
|
28
|
-
import { formatWorkflowResourceLoadWarning } from "./workflow-command-surfaces.js";
|
|
26
|
+
import { formatWorkflowReloadReport, formatWorkflowResourceLoadWarning } from "./workflow-command-surfaces.js";
|
|
27
|
+
import { normalizeWorkflowReloadReport, type WorkflowReloadReport } from "./workflow-reload-report.js";
|
|
29
28
|
|
|
30
29
|
export interface WorkflowControlActionDeps {
|
|
31
30
|
getPersistence: () => WorkflowPersistencePort | undefined;
|
|
32
|
-
reloadWorkflowResources: () => Promise<void> | void;
|
|
31
|
+
reloadWorkflowResources: () => Promise<WorkflowReloadReport | void> | void;
|
|
33
32
|
getRuntime: () => ExtensionRuntime;
|
|
34
33
|
policy: WorkflowExecutionPolicy;
|
|
35
34
|
ensureWorkflowResourcesLoaded: () => Promise<void> | void;
|
|
@@ -71,22 +70,27 @@ export async function workflowReloadAction(
|
|
|
71
70
|
args: WorkflowToolArgs,
|
|
72
71
|
deps: Pick<WorkflowControlActionDeps, "reloadWorkflowResources">,
|
|
73
72
|
): Promise<WorkflowToolResult> {
|
|
74
|
-
const activeRuns = inFlightRunCount();
|
|
75
|
-
if (activeRuns > 0) {
|
|
76
|
-
return { action: "reload", status: "noop", message: reloadBlockedMessage(activeRuns) };
|
|
77
|
-
}
|
|
78
73
|
try {
|
|
79
|
-
await deps.reloadWorkflowResources();
|
|
74
|
+
const report = normalizeWorkflowReloadReport(await deps.reloadWorkflowResources());
|
|
75
|
+
return {
|
|
76
|
+
action: "reload",
|
|
77
|
+
status: report.outcome === "applied" ? "ok" : "noop",
|
|
78
|
+
message: formatWorkflowReloadReport(report, args.reason),
|
|
79
|
+
...report,
|
|
80
|
+
};
|
|
80
81
|
} catch (error) {
|
|
81
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
action: "reload",
|
|
84
|
+
status: "noop",
|
|
85
|
+
message: reloadFailureMessage(error),
|
|
86
|
+
outcome: "failed",
|
|
87
|
+
error: error instanceof Error ? error.message : String(error),
|
|
88
|
+
generation: 0,
|
|
89
|
+
workflowCount: 0,
|
|
90
|
+
coalescedRequests: 1,
|
|
91
|
+
diagnostics: [],
|
|
92
|
+
};
|
|
82
93
|
}
|
|
83
|
-
return {
|
|
84
|
-
action: "reload",
|
|
85
|
-
status: "ok",
|
|
86
|
-
message: args.reason?.trim()
|
|
87
|
-
? `Reloaded workflow resources (${args.reason.trim()}).`
|
|
88
|
-
: "Reloaded workflow resources.",
|
|
89
|
-
};
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
export function workflowKillAction(
|
|
@@ -32,11 +32,12 @@ import {
|
|
|
32
32
|
topLevelExpandedSnapshots,
|
|
33
33
|
} from "./workflow-targets.js";
|
|
34
34
|
import { formatWorkflowResourceLoadWarning } from "./workflow-command-surfaces.js";
|
|
35
|
+
import type { WorkflowReloadReport } from "./workflow-reload-report.js";
|
|
35
36
|
|
|
36
37
|
export function makeExecuteWorkflowTool(
|
|
37
38
|
runtime: ExtensionRuntime | ((ctx: PiExecuteContext) => ExtensionRuntime),
|
|
38
39
|
getPersistence: () => WorkflowPersistencePort | undefined,
|
|
39
|
-
reloadWorkflowResources: () => Promise<void> | void,
|
|
40
|
+
reloadWorkflowResources: () => Promise<WorkflowReloadReport | void> | void,
|
|
40
41
|
ensureWorkflowResourcesLoaded: () => Promise<void> | void = () => {},
|
|
41
42
|
): (args: WorkflowToolArgs, ctx: PiExecuteContext) => Promise<WorkflowToolResult> {
|
|
42
43
|
return async function executeWorkflowTool(
|
package/docs/workflows.md
CHANGED
|
@@ -781,6 +781,14 @@ export const tableSelectorFactory: WorkflowCustomUiFactory<{ id: string; name: s
|
|
|
781
781
|
|
|
782
782
|
Workflow files are loaded via [jiti](https://github.com/unjs/jiti), so TypeScript works without compilation.
|
|
783
783
|
|
|
784
|
+
## Reloading workflow resources
|
|
785
|
+
|
|
786
|
+
Run `/workflow reload` after adding, editing, renaming, or deleting workflow modules or changing workflow config. Reload rescans project and user conventional directories, legacy `.pi` locations, configured file/directory paths, and package resources without restarting Atomic. The workflow tool's `reload` action uses the same in-process path.
|
|
787
|
+
|
|
788
|
+
Reload builds a complete replacement registry before publishing it. Concurrent requests are serialized and coalesced, stale discovery from an earlier session cannot overwrite newer state, and a fatal refresh failure retains the previous registry. Reload is safe while workflows are running: existing runs keep the definition and runtime snapshot they started with, while subsequent list/get/inputs/help/completion/invocation calls use the newly published registry.
|
|
789
|
+
|
|
790
|
+
A successful rescan may still contain per-resource diagnostics. Both reload surfaces now show `CONFIG_INVALID`, `IMPORT_FAILED`, `INVALID_DEFINITION`, `PATH_NOT_FOUND`, and duplicate-name diagnostics instead of reporting bare success while silently skipping a resource. Valid sibling workflows remain available. Fix the reported source/path and reload again; no process restart is required.
|
|
791
|
+
|
|
784
792
|
## Workflow Configuration
|
|
785
793
|
|
|
786
794
|
Configured workflow paths live in workflow extension config. Project config paths are relative to the project root. Global config paths are relative to `~/.atomic/agent`.
|
|
@@ -1107,7 +1115,7 @@ When Atomic starts with an existing session or resumes one, it shows an informat
|
|
|
1107
1115
|
/workflow resume <workflow-id-or-prefix> # Resume unfinished work or open completed detail/chat
|
|
1108
1116
|
```
|
|
1109
1117
|
|
|
1110
|
-
Explicit full IDs take precedence, while prefixes resolve across live, resumable durable, and completed targets as one namespace. Ambiguous prefixes use the existing-style ambiguity diagnostic. A completed backend row with no checkpoints or no usable retained stage conversation is hidden from the picker; an explicit target reports that it is stale or missing required durable checkpoint/session data. A completed run remains inspectable when at least one stage has a usable transcript; missing, empty, directory, context-empty, or partially malformed transcript paths are omitted from stage chat attachment. Validation uses the final retained transcript for a repeated stage replay key, so an obsolete superseded checkpoint path does not hide an otherwise valid completed run. Reopening inspection refreshes a changed authoritative retained-chat handle. Session-cache-only rows are likewise hidden because the backend is authoritative. Cancelled, killed, non-resumable failed, and other terminal non-success states are never added. Normal `/resume`, `atomic -r`, and `--continue` behavior for internal workflow stage sessions is unchanged.
|
|
1118
|
+
Explicit full IDs take precedence, while prefixes resolve across top-level live, resumable durable, and completed targets as one namespace. An exact loadable paused top-level live target resumes directly from in-session state without enumerating the durable completed-history catalog; this keeps explicit live resume responsive even when retained durable history is large and preserves live-over-durable precedence for duplicate IDs. Nested child runs remain excluded from this top-level target namespace even when addressed by an exact ID. Prefixes and other targets continue through the combined catalog so ambiguity and completed-inspection behavior remain unchanged. Ambiguous prefixes use the existing-style ambiguity diagnostic. A completed backend row with no checkpoints or no usable retained stage conversation is hidden from the picker; an explicit target reports that it is stale or missing required durable checkpoint/session data. A completed run remains inspectable when at least one stage has a usable transcript; missing, empty, directory, context-empty, or partially malformed transcript paths are omitted from stage chat attachment. Validation uses the final retained transcript for a repeated stage replay key, so an obsolete superseded checkpoint path does not hide an otherwise valid completed run. Reopening inspection refreshes a changed authoritative retained-chat handle. Session-cache-only rows are likewise hidden because the backend is authoritative. Cancelled, killed, non-resumable failed, and other terminal non-success states are never added. Normal `/resume`, `atomic -r`, and `--continue` behavior for internal workflow stage sessions is unchanged.
|
|
1111
1119
|
|
|
1112
1120
|
### Cancellation, failure, and retry semantics
|
|
1113
1121
|
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/atomic",
|
|
3
|
-
"version": "0.9.9-alpha.
|
|
3
|
+
"version": "0.9.9-alpha.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bastani/atomic",
|
|
9
|
-
"version": "0.9.9-alpha.
|
|
9
|
+
"version": "0.9.9-alpha.4",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@bastani/atomic-natives": "0.9.9-alpha.
|
|
12
|
+
"@bastani/atomic-natives": "0.9.9-alpha.4",
|
|
13
13
|
"@bufbuild/protobuf": "^2.12.1",
|
|
14
14
|
"@earendil-works/pi-agent-core": "^0.80.7",
|
|
15
15
|
"@earendil-works/pi-ai": "^0.80.7",
|
|
@@ -515,16 +515,16 @@
|
|
|
515
515
|
}
|
|
516
516
|
},
|
|
517
517
|
"node_modules/@bastani/atomic-natives": {
|
|
518
|
-
"version": "0.9.9-alpha.
|
|
519
|
-
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives/-/atomic-natives-0.9.9-alpha.
|
|
518
|
+
"version": "0.9.9-alpha.4",
|
|
519
|
+
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives/-/atomic-natives-0.9.9-alpha.4.tgz",
|
|
520
520
|
"license": "MIT",
|
|
521
521
|
"optionalDependencies": {
|
|
522
|
-
"@bastani/atomic-natives-darwin-arm64": "0.9.9-alpha.
|
|
523
|
-
"@bastani/atomic-natives-darwin-x64": "0.9.9-alpha.
|
|
524
|
-
"@bastani/atomic-natives-linux-arm64-gnu": "0.9.9-alpha.
|
|
525
|
-
"@bastani/atomic-natives-linux-x64-gnu": "0.9.9-alpha.
|
|
526
|
-
"@bastani/atomic-natives-win32-arm64-msvc": "0.9.9-alpha.
|
|
527
|
-
"@bastani/atomic-natives-win32-x64-msvc": "0.9.9-alpha.
|
|
522
|
+
"@bastani/atomic-natives-darwin-arm64": "0.9.9-alpha.4",
|
|
523
|
+
"@bastani/atomic-natives-darwin-x64": "0.9.9-alpha.4",
|
|
524
|
+
"@bastani/atomic-natives-linux-arm64-gnu": "0.9.9-alpha.4",
|
|
525
|
+
"@bastani/atomic-natives-linux-x64-gnu": "0.9.9-alpha.4",
|
|
526
|
+
"@bastani/atomic-natives-win32-arm64-msvc": "0.9.9-alpha.4",
|
|
527
|
+
"@bastani/atomic-natives-win32-x64-msvc": "0.9.9-alpha.4"
|
|
528
528
|
},
|
|
529
529
|
"engines": {
|
|
530
530
|
"bun": ">=1.3.14",
|
|
@@ -532,8 +532,8 @@
|
|
|
532
532
|
}
|
|
533
533
|
},
|
|
534
534
|
"node_modules/@bastani/atomic-natives-darwin-arm64": {
|
|
535
|
-
"version": "0.9.9-alpha.
|
|
536
|
-
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-arm64/-/atomic-natives-darwin-arm64-0.9.9-alpha.
|
|
535
|
+
"version": "0.9.9-alpha.4",
|
|
536
|
+
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-arm64/-/atomic-natives-darwin-arm64-0.9.9-alpha.4.tgz",
|
|
537
537
|
"license": "MIT",
|
|
538
538
|
"os": [
|
|
539
539
|
"darwin"
|
|
@@ -544,8 +544,8 @@
|
|
|
544
544
|
"optional": true
|
|
545
545
|
},
|
|
546
546
|
"node_modules/@bastani/atomic-natives-darwin-x64": {
|
|
547
|
-
"version": "0.9.9-alpha.
|
|
548
|
-
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-x64/-/atomic-natives-darwin-x64-0.9.9-alpha.
|
|
547
|
+
"version": "0.9.9-alpha.4",
|
|
548
|
+
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-darwin-x64/-/atomic-natives-darwin-x64-0.9.9-alpha.4.tgz",
|
|
549
549
|
"license": "MIT",
|
|
550
550
|
"os": [
|
|
551
551
|
"darwin"
|
|
@@ -556,8 +556,8 @@
|
|
|
556
556
|
"optional": true
|
|
557
557
|
},
|
|
558
558
|
"node_modules/@bastani/atomic-natives-linux-arm64-gnu": {
|
|
559
|
-
"version": "0.9.9-alpha.
|
|
560
|
-
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-gnu/-/atomic-natives-linux-arm64-gnu-0.9.9-alpha.
|
|
559
|
+
"version": "0.9.9-alpha.4",
|
|
560
|
+
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-arm64-gnu/-/atomic-natives-linux-arm64-gnu-0.9.9-alpha.4.tgz",
|
|
561
561
|
"license": "MIT",
|
|
562
562
|
"os": [
|
|
563
563
|
"linux"
|
|
@@ -571,8 +571,8 @@
|
|
|
571
571
|
"optional": true
|
|
572
572
|
},
|
|
573
573
|
"node_modules/@bastani/atomic-natives-linux-x64-gnu": {
|
|
574
|
-
"version": "0.9.9-alpha.
|
|
575
|
-
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-gnu/-/atomic-natives-linux-x64-gnu-0.9.9-alpha.
|
|
574
|
+
"version": "0.9.9-alpha.4",
|
|
575
|
+
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-linux-x64-gnu/-/atomic-natives-linux-x64-gnu-0.9.9-alpha.4.tgz",
|
|
576
576
|
"license": "MIT",
|
|
577
577
|
"os": [
|
|
578
578
|
"linux"
|
|
@@ -586,8 +586,8 @@
|
|
|
586
586
|
"optional": true
|
|
587
587
|
},
|
|
588
588
|
"node_modules/@bastani/atomic-natives-win32-arm64-msvc": {
|
|
589
|
-
"version": "0.9.9-alpha.
|
|
590
|
-
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-arm64-msvc/-/atomic-natives-win32-arm64-msvc-0.9.9-alpha.
|
|
589
|
+
"version": "0.9.9-alpha.4",
|
|
590
|
+
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-arm64-msvc/-/atomic-natives-win32-arm64-msvc-0.9.9-alpha.4.tgz",
|
|
591
591
|
"license": "MIT",
|
|
592
592
|
"os": [
|
|
593
593
|
"win32"
|
|
@@ -598,8 +598,8 @@
|
|
|
598
598
|
"optional": true
|
|
599
599
|
},
|
|
600
600
|
"node_modules/@bastani/atomic-natives-win32-x64-msvc": {
|
|
601
|
-
"version": "0.9.9-alpha.
|
|
602
|
-
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-x64-msvc/-/atomic-natives-win32-x64-msvc-0.9.9-alpha.
|
|
601
|
+
"version": "0.9.9-alpha.4",
|
|
602
|
+
"resolved": "https://registry.npmjs.org/@bastani/atomic-natives-win32-x64-msvc/-/atomic-natives-win32-x64-msvc-0.9.9-alpha.4.tgz",
|
|
603
603
|
"license": "MIT",
|
|
604
604
|
"os": [
|
|
605
605
|
"win32"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bastani/atomic",
|
|
3
|
-
"version": "0.9.9-alpha.
|
|
3
|
+
"version": "0.9.9-alpha.4",
|
|
4
4
|
"description": "Atomic coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"atomicConfig": {
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"prepublishOnly": "bun run clean && bun run build && bun run shrinkwrap"
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"@bastani/atomic-natives": "0.9.9-alpha.
|
|
80
|
+
"@bastani/atomic-natives": "0.9.9-alpha.4",
|
|
81
81
|
"@bufbuild/protobuf": "^2.12.1",
|
|
82
82
|
"@earendil-works/pi-agent-core": "^0.80.7",
|
|
83
83
|
"@earendil-works/pi-ai": "^0.80.7",
|