@worca/ui 0.15.2 → 0.16.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/package.json +1 -1
- package/server/process-manager.js +42 -3
- package/server/project-routes.js +24 -3
- package/server/run-dir-resolver.js +35 -1
package/package.json
CHANGED
|
@@ -422,10 +422,49 @@ export class ProcessManager {
|
|
|
422
422
|
if (opts.resume) {
|
|
423
423
|
args.push('--resume');
|
|
424
424
|
if (opts.runId) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
425
|
+
// The runner derives worca_dir from os.path.dirname(status_path) and
|
|
426
|
+
// builds the per-run dir as <worca_dir>/runs/<run_id>/. We must pass
|
|
427
|
+
// the worca root, not the per-run dir — passing <worca>/runs/<id>
|
|
428
|
+
// would make the runner compute a nested <worca>/runs/<id>/runs/<id>/
|
|
429
|
+
// and write status updates there while the UI keeps reading the
|
|
430
|
+
// original. _find_active_runs(worca_root) then locates the run.
|
|
431
|
+
const statusDir = resumeCtx ? resumeCtx.worcaDir : this.worcaDir;
|
|
428
432
|
args.push('--status-dir', statusDir);
|
|
433
|
+
|
|
434
|
+
// Worktree runs: registry lives in the parent project's .worca, not
|
|
435
|
+
// the worktree's. run_worktree.py passes --registry-base on initial
|
|
436
|
+
// launch; resume must do the same so update_pipeline() lands on the
|
|
437
|
+
// right registry entry. Without this, the runner's terminal /
|
|
438
|
+
// resume-flip-to-running registry updates silently no-op against a
|
|
439
|
+
// non-existent <worktree>/.worca/multi/pipelines.d/<id>.json.
|
|
440
|
+
if (resumeCtx && resumeCtx.worcaDir !== this.worcaDir) {
|
|
441
|
+
args.push('--registry-base', this.worcaDir);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// _find_active_runs filters out runs whose pipeline_status is in
|
|
445
|
+
// {completed, interrupted}. To resume an interrupted/failed run, flip
|
|
446
|
+
// the top-level status to "resuming" so the runner can pick it up;
|
|
447
|
+
// it'll transition to "running" once it's processing.
|
|
448
|
+
const statusPath = resumeCtx
|
|
449
|
+
? join(resumeCtx.runDir, 'status.json')
|
|
450
|
+
: join(this.worcaDir, 'runs', opts.runId, 'status.json');
|
|
451
|
+
try {
|
|
452
|
+
const s = JSON.parse(readFileSync(statusPath, 'utf8'));
|
|
453
|
+
if (
|
|
454
|
+
s.pipeline_status === 'interrupted' ||
|
|
455
|
+
s.pipeline_status === 'failed'
|
|
456
|
+
) {
|
|
457
|
+
s.pipeline_status = 'resuming';
|
|
458
|
+
delete s.stop_reason;
|
|
459
|
+
writeFileSync(
|
|
460
|
+
statusPath,
|
|
461
|
+
`${JSON.stringify(s, null, 2)}\n`,
|
|
462
|
+
'utf8',
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
/* non-fatal — runner will surface a clearer error if the file is missing */
|
|
467
|
+
}
|
|
429
468
|
}
|
|
430
469
|
} else if (opts.sourceType !== undefined) {
|
|
431
470
|
// New format: separate source and prompt args
|
package/server/project-routes.js
CHANGED
|
@@ -71,7 +71,11 @@ function validateRunId(runId) {
|
|
|
71
71
|
// Re-exported from run-dir-resolver so callers (including older tests) can
|
|
72
72
|
// continue importing from project-routes. The implementation now overlays
|
|
73
73
|
// worktree runs registered in <worcaDir>/multi/pipelines.d/.
|
|
74
|
-
import {
|
|
74
|
+
import {
|
|
75
|
+
findRunStatusPath,
|
|
76
|
+
readPipelineOverlay,
|
|
77
|
+
updatePipelineStatus,
|
|
78
|
+
} from './run-dir-resolver.js';
|
|
75
79
|
export { findRunStatusPath };
|
|
76
80
|
|
|
77
81
|
/** Validate a branch name — alphanumeric, dots, hyphens, underscores, slashes */
|
|
@@ -935,7 +939,14 @@ export function createProjectScopedRoutes({
|
|
|
935
939
|
}
|
|
936
940
|
const { worcaDir, settingsPath } = req.project;
|
|
937
941
|
try {
|
|
938
|
-
|
|
942
|
+
// Worktree runs read control.json from <worktree>/.worca/runs/<id>/.
|
|
943
|
+
// Writing it to the parent project's worcaDir leaves the runner
|
|
944
|
+
// unaware of the stop request — SIGTERM still works, but we lose
|
|
945
|
+
// graceful-shutdown semantics.
|
|
946
|
+
const overlay = readPipelineOverlay(worcaDir, runId);
|
|
947
|
+
const controlDir = overlay?.worktree_path
|
|
948
|
+
? join(overlay.worktree_path, '.worca', 'runs', runId)
|
|
949
|
+
: join(worcaDir, 'runs', runId);
|
|
939
950
|
mkdirSync(controlDir, { recursive: true });
|
|
940
951
|
writeFileSync(
|
|
941
952
|
join(controlDir, 'control.json'),
|
|
@@ -1079,6 +1090,11 @@ export function createProjectScopedRoutes({
|
|
|
1079
1090
|
st.completed_at = new Date().toISOString();
|
|
1080
1091
|
writeFileSync(statusPath, `${JSON.stringify(st, null, 2)}\n`, 'utf8');
|
|
1081
1092
|
|
|
1093
|
+
// Mirror into the multi-pipeline registry so global-mode views don't
|
|
1094
|
+
// keep reporting the run as "running". Best-effort — the registry entry
|
|
1095
|
+
// only exists for worktree runs.
|
|
1096
|
+
updatePipelineStatus(worcaDir, runId, 'cancelled');
|
|
1097
|
+
|
|
1082
1098
|
const { broadcast, scheduleRefresh } = req.app.locals;
|
|
1083
1099
|
if (broadcast) broadcast('run-cancelled', { runId });
|
|
1084
1100
|
if (scheduleRefresh) scheduleRefresh(req.project?.name);
|
|
@@ -1141,7 +1157,12 @@ export function createProjectScopedRoutes({
|
|
|
1141
1157
|
pipeline_status: st.pipeline_status,
|
|
1142
1158
|
});
|
|
1143
1159
|
}
|
|
1144
|
-
|
|
1160
|
+
// Worktree runs read control.json from <worktree>/.worca/runs/<id>/;
|
|
1161
|
+
// writing to the parent's worcaDir is invisible to the runner.
|
|
1162
|
+
const overlay = readPipelineOverlay(worcaDir, runId);
|
|
1163
|
+
const controlDir = overlay?.worktree_path
|
|
1164
|
+
? join(overlay.worktree_path, '.worca', 'runs', runId)
|
|
1165
|
+
: join(worcaDir, 'runs', runId);
|
|
1145
1166
|
mkdirSync(controlDir, { recursive: true });
|
|
1146
1167
|
writeFileSync(
|
|
1147
1168
|
join(controlDir, 'control.json'),
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* 3. `<worcaDir>/multi/pipelines.d/<runId>.json` → `<worktree_path>/.worca/runs/<runId>/`
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -77,3 +77,37 @@ export function readPipelineOverlay(worcaDir, runId) {
|
|
|
77
77
|
return null;
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Update the multi-pipeline registry entry's status field.
|
|
83
|
+
*
|
|
84
|
+
* Mirrors src/worca/orchestrator/registry.py:update_pipeline() — terminal
|
|
85
|
+
* status (cancelled/completed/failed/interrupted) only. The Python runner
|
|
86
|
+
* updates the registry on its own terminal paths; this helper exists for
|
|
87
|
+
* callers (the UI cancel route) that write status.json directly without
|
|
88
|
+
* going through the runner.
|
|
89
|
+
*
|
|
90
|
+
* Returns true when the registry entry existed and was updated, false when
|
|
91
|
+
* there's no registry entry for this runId (e.g. local in-place run).
|
|
92
|
+
*
|
|
93
|
+
* @param {string} worcaDir - the parent project's .worca directory
|
|
94
|
+
* @param {string} runId
|
|
95
|
+
* @param {string} status - new status value (e.g. 'cancelled')
|
|
96
|
+
*/
|
|
97
|
+
export function updatePipelineStatus(worcaDir, runId, status) {
|
|
98
|
+
const regPath = join(worcaDir, 'multi', 'pipelines.d', `${runId}.json`);
|
|
99
|
+
if (!existsSync(regPath)) return false;
|
|
100
|
+
try {
|
|
101
|
+
const data = JSON.parse(readFileSync(regPath, 'utf8'));
|
|
102
|
+
data.status = status;
|
|
103
|
+
data.updated_at = new Date().toISOString();
|
|
104
|
+
// Match Python's atomic write: write to .tmp + rename. Avoids partial
|
|
105
|
+
// writes if the process crashes mid-write.
|
|
106
|
+
const tmpPath = `${regPath}.tmp`;
|
|
107
|
+
writeFileSync(tmpPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
108
|
+
renameSync(tmpPath, regPath);
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|