@enderfga/claw-orchestrator 3.7.1 → 4.0.3
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/README.md +45 -3
- package/configs/autoloop-coder-prompt.md +65 -28
- package/configs/autoloop-planner-prompt.md +103 -42
- package/configs/autoloop-reviewer-prompt.md +58 -30
- package/dist/bin/cli.js +30 -7
- package/dist/bin/cli.js.map +1 -1
- package/dist/src/autoloop/dispatcher.js +16 -2
- package/dist/src/autoloop/dispatcher.js.map +1 -1
- package/dist/src/autoloop/messages.d.ts +0 -2
- package/dist/src/autoloop/messages.js +0 -2
- package/dist/src/autoloop/messages.js.map +1 -1
- package/dist/src/autoloop/planner-tools.d.ts +11 -6
- package/dist/src/autoloop/planner-tools.js +24 -7
- package/dist/src/autoloop/planner-tools.js.map +1 -1
- package/dist/src/autoloop/runner.d.ts +1 -2
- package/dist/src/autoloop/runner.js +1 -2
- package/dist/src/autoloop/runner.js.map +1 -1
- package/dist/src/autoloop/types.d.ts +0 -2
- package/dist/src/autoloop/types.js +0 -2
- package/dist/src/autoloop/types.js.map +1 -1
- package/dist/src/council.js +1 -0
- package/dist/src/council.js.map +1 -1
- package/dist/src/dashboard/index.html +1066 -12
- package/dist/src/embedded-server.d.ts +7 -0
- package/dist/src/embedded-server.js +359 -17
- package/dist/src/embedded-server.js.map +1 -1
- package/dist/src/index.js +222 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/session-manager.d.ts +98 -1
- package/dist/src/session-manager.js +340 -5
- package/dist/src/session-manager.js.map +1 -1
- package/dist/src/ultraapp/build-events.d.ts +46 -0
- package/dist/src/ultraapp/build-events.js +6 -0
- package/dist/src/ultraapp/build-events.js.map +1 -0
- package/dist/src/ultraapp/build.d.ts +39 -0
- package/dist/src/ultraapp/build.js +111 -0
- package/dist/src/ultraapp/build.js.map +1 -0
- package/dist/src/ultraapp/conventions.d.ts +8 -0
- package/dist/src/ultraapp/conventions.js +248 -0
- package/dist/src/ultraapp/conventions.js.map +1 -0
- package/dist/src/ultraapp/council-adapter.d.ts +49 -0
- package/dist/src/ultraapp/council-adapter.js +152 -0
- package/dist/src/ultraapp/council-adapter.js.map +1 -0
- package/dist/src/ultraapp/deploy.d.ts +45 -0
- package/dist/src/ultraapp/deploy.js +82 -0
- package/dist/src/ultraapp/deploy.js.map +1 -0
- package/dist/src/ultraapp/diff-apply.d.ts +15 -0
- package/dist/src/ultraapp/diff-apply.js +48 -0
- package/dist/src/ultraapp/diff-apply.js.map +1 -0
- package/dist/src/ultraapp/docker.d.ts +57 -0
- package/dist/src/ultraapp/docker.js +83 -0
- package/dist/src/ultraapp/docker.js.map +1 -0
- package/dist/src/ultraapp/feedback-classifier.d.ts +24 -0
- package/dist/src/ultraapp/feedback-classifier.js +85 -0
- package/dist/src/ultraapp/feedback-classifier.js.map +1 -0
- package/dist/src/ultraapp/files.d.ts +24 -0
- package/dist/src/ultraapp/files.js +79 -0
- package/dist/src/ultraapp/files.js.map +1 -0
- package/dist/src/ultraapp/fix-on-failure-session.d.ts +23 -0
- package/dist/src/ultraapp/fix-on-failure-session.js +48 -0
- package/dist/src/ultraapp/fix-on-failure-session.js.map +1 -0
- package/dist/src/ultraapp/fix-on-failure.d.ts +39 -0
- package/dist/src/ultraapp/fix-on-failure.js +69 -0
- package/dist/src/ultraapp/fix-on-failure.js.map +1 -0
- package/dist/src/ultraapp/host-strategy.d.ts +57 -0
- package/dist/src/ultraapp/host-strategy.js +205 -0
- package/dist/src/ultraapp/host-strategy.js.map +1 -0
- package/dist/src/ultraapp/interview-parser.d.ts +41 -0
- package/dist/src/ultraapp/interview-parser.js +83 -0
- package/dist/src/ultraapp/interview-parser.js.map +1 -0
- package/dist/src/ultraapp/interview-tools.d.ts +16 -0
- package/dist/src/ultraapp/interview-tools.js +36 -0
- package/dist/src/ultraapp/interview-tools.js.map +1 -0
- package/dist/src/ultraapp/json-patch.d.ts +6 -0
- package/dist/src/ultraapp/json-patch.js +78 -0
- package/dist/src/ultraapp/json-patch.js.map +1 -0
- package/dist/src/ultraapp/lifecycle.d.ts +38 -0
- package/dist/src/ultraapp/lifecycle.js +31 -0
- package/dist/src/ultraapp/lifecycle.js.map +1 -0
- package/dist/src/ultraapp/manager.d.ts +150 -0
- package/dist/src/ultraapp/manager.js +684 -0
- package/dist/src/ultraapp/manager.js.map +1 -0
- package/dist/src/ultraapp/narrator-prompt.d.ts +10 -0
- package/dist/src/ultraapp/narrator-prompt.js +54 -0
- package/dist/src/ultraapp/narrator-prompt.js.map +1 -0
- package/dist/src/ultraapp/narrator.d.ts +49 -0
- package/dist/src/ultraapp/narrator.js +83 -0
- package/dist/src/ultraapp/narrator.js.map +1 -0
- package/dist/src/ultraapp/patcher.d.ts +35 -0
- package/dist/src/ultraapp/patcher.js +159 -0
- package/dist/src/ultraapp/patcher.js.map +1 -0
- package/dist/src/ultraapp/router.d.ts +39 -0
- package/dist/src/ultraapp/router.js +139 -0
- package/dist/src/ultraapp/router.js.map +1 -0
- package/dist/src/ultraapp/spec-delta.d.ts +18 -0
- package/dist/src/ultraapp/spec-delta.js +35 -0
- package/dist/src/ultraapp/spec-delta.js.map +1 -0
- package/dist/src/ultraapp/spec.d.ts +82 -0
- package/dist/src/ultraapp/spec.js +125 -0
- package/dist/src/ultraapp/spec.js.map +1 -0
- package/dist/src/ultraapp/store.d.ts +67 -0
- package/dist/src/ultraapp/store.js +177 -0
- package/dist/src/ultraapp/store.js.map +1 -0
- package/dist/src/ultraapp/versions.d.ts +50 -0
- package/dist/src/ultraapp/versions.js +65 -0
- package/dist/src/ultraapp/versions.js.map +1 -0
- package/openclaw.plugin.json +15 -1
- package/package.json +3 -1
- package/skills/SKILL.md +2 -2
- package/skills/references/autoloop.md +7 -4
- package/skills/references/dashboard.md +153 -0
- package/skills/references/mcp.md +2 -2
- package/skills/references/tools.md +183 -0
- package/skills/references/ultraapp.md +203 -0
- package/skills/ultraapp/SKILL.md +112 -0
|
@@ -120,7 +120,124 @@ import { ClaudeAgentDispatcher } from './autoloop/dispatcher.js';
|
|
|
120
120
|
import { DEFAULT_PUSH_POLICY } from './autoloop/types.js';
|
|
121
121
|
import { Msg as AutoloopMsg } from './autoloop/messages.js';
|
|
122
122
|
import { appendPushLog, notifyUserFallbackChain } from './autoloop/notify.js';
|
|
123
|
+
import { UltraappManager } from './ultraapp/manager.js';
|
|
124
|
+
import { UltraappStore, defaultStoreRoot } from './ultraapp/store.js';
|
|
123
125
|
import { PERSIST_DISK_TTL_MS, DEBOUNCED_SAVE_MS, CLEANUP_INTERVAL_MS, TURN_TIMEOUT_MS, GREP_HISTORY_FETCH, RESULT_TTL_MS, ULTRAPLAN_TIMEOUT_MS, ULTRAREVIEW_POLL_INTERVAL_MS, STOP_SIGKILL_DELAY_MS, SESSION_EVENT, DEFAULT_HISTORY_LIMIT, } from './constants.js';
|
|
126
|
+
// ─── Disk enumeration (cross-process visibility) ────────────────────────────
|
|
127
|
+
//
|
|
128
|
+
// When the dashboard's standalone clawo-serve and the OpenClaw plugin run as
|
|
129
|
+
// separate processes, each has its own in-memory map of active runs. To make
|
|
130
|
+
// past runs visible across processes we read what's persisted on disk:
|
|
131
|
+
// - Council: transcripts at ~/.openclaw/council-logs/council-*.md
|
|
132
|
+
// - Autoloop: registry at ~/.claw-orchestrator/autoloop-registry.jsonl
|
|
133
|
+
const DEFAULT_COUNCIL_LOG_DIR = path.join(os.homedir(), '.openclaw', 'council-logs');
|
|
134
|
+
const DEFAULT_AUTOLOOP_REGISTRY = path.join(os.homedir(), '.claw-orchestrator', 'autoloop-registry.jsonl');
|
|
135
|
+
/** Append-only registry write. Safe under concurrent writers — append is atomic for short lines. */
|
|
136
|
+
export function appendAutoloopRegistry(file, entry) {
|
|
137
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
138
|
+
fs.appendFileSync(file, JSON.stringify(entry) + '\n');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Read the registry, dedup by run_id (newest entry wins), drop entries whose
|
|
142
|
+
* ledger_dir no longer exists on disk (cleanup of moved/deleted workspaces).
|
|
143
|
+
* Returns entries newest-first.
|
|
144
|
+
*/
|
|
145
|
+
export function listAutoloopsFromRegistry(file = DEFAULT_AUTOLOOP_REGISTRY) {
|
|
146
|
+
if (!fs.existsSync(file))
|
|
147
|
+
return [];
|
|
148
|
+
const lines = fs.readFileSync(file, 'utf-8').split('\n').filter(Boolean);
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
const out = [];
|
|
151
|
+
// Walk in reverse so the latest entry for a given run_id wins.
|
|
152
|
+
for (const line of [...lines].reverse()) {
|
|
153
|
+
try {
|
|
154
|
+
const e = JSON.parse(line);
|
|
155
|
+
if (seen.has(e.run_id))
|
|
156
|
+
continue;
|
|
157
|
+
seen.add(e.run_id);
|
|
158
|
+
if (!fs.existsSync(e.ledger_dir))
|
|
159
|
+
continue; // stale entry, ledger gone
|
|
160
|
+
out.push(e);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// malformed line; skip
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out; // already newest-first because we reversed
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Rewrite the registry with every line for the given run_id filtered out.
|
|
170
|
+
* Used by autoloopDelete to scrub a run from cross-process visibility.
|
|
171
|
+
* No-op if the file does not exist. Returns the number of lines removed.
|
|
172
|
+
*/
|
|
173
|
+
export function removeAutoloopFromRegistry(file, runId) {
|
|
174
|
+
if (!fs.existsSync(file))
|
|
175
|
+
return 0;
|
|
176
|
+
const lines = fs.readFileSync(file, 'utf-8').split('\n');
|
|
177
|
+
let removed = 0;
|
|
178
|
+
const kept = [];
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
if (!line) {
|
|
181
|
+
kept.push(line);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const e = JSON.parse(line);
|
|
186
|
+
if (e.run_id === runId) {
|
|
187
|
+
removed += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
// malformed line — keep it, we only filter recognizable entries
|
|
193
|
+
}
|
|
194
|
+
kept.push(line);
|
|
195
|
+
}
|
|
196
|
+
if (removed === 0)
|
|
197
|
+
return 0;
|
|
198
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
199
|
+
fs.writeFileSync(tmp, kept.join('\n'));
|
|
200
|
+
fs.renameSync(tmp, file);
|
|
201
|
+
return removed;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Enumerate council sessions from on-disk transcripts. Called by
|
|
205
|
+
* SessionManager.councilList() to surface runs that the current process didn't
|
|
206
|
+
* spawn itself (e.g. runs started in another process whose transcripts have
|
|
207
|
+
* already been flushed to ~/.openclaw/council-logs/).
|
|
208
|
+
*
|
|
209
|
+
* Format parsed (matches src/council.ts saveTranscript):
|
|
210
|
+
* - **ID**: <session.id>
|
|
211
|
+
* - **Time**: <iso>
|
|
212
|
+
* - **Task**: <text>
|
|
213
|
+
* - **Status**: <consensus|max_rounds|...>
|
|
214
|
+
*
|
|
215
|
+
* Legacy transcripts written before the ID field was added fall back to a
|
|
216
|
+
* filename-derived id (basename without .md). That's stable across reruns
|
|
217
|
+
* even if uncomfortable as a display id.
|
|
218
|
+
*/
|
|
219
|
+
export function listCouncilsFromDisk(logDir = DEFAULT_COUNCIL_LOG_DIR) {
|
|
220
|
+
if (!fs.existsSync(logDir))
|
|
221
|
+
return [];
|
|
222
|
+
const out = [];
|
|
223
|
+
for (const entry of fs.readdirSync(logDir)) {
|
|
224
|
+
if (!entry.startsWith('council-') || !entry.endsWith('.md'))
|
|
225
|
+
continue;
|
|
226
|
+
let head;
|
|
227
|
+
try {
|
|
228
|
+
head = fs.readFileSync(path.join(logDir, entry), 'utf-8').slice(0, 2000);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const id = /^-\s+\*\*ID\*\*:\s*([^\n]+)/m.exec(head)?.[1]?.trim() || entry.replace(/\.md$/, '');
|
|
234
|
+
const task = /^-\s+\*\*Task\*\*:\s*([^\n]+)/m.exec(head)?.[1]?.trim() || '(no task recorded)';
|
|
235
|
+
const startTime = /^-\s+\*\*Time\*\*:\s*([^\n]+)/m.exec(head)?.[1]?.trim() || '';
|
|
236
|
+
const status = /^-\s+\*\*Status\*\*:\s*([^\n]+)/m.exec(head)?.[1]?.trim() || 'unknown';
|
|
237
|
+
out.push({ id, task, status, startTime });
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
124
241
|
// ─── SessionManager ──────────────────────────────────────────────────────────
|
|
125
242
|
export class SessionManager {
|
|
126
243
|
sessions = new Map();
|
|
@@ -135,6 +252,9 @@ export class SessionManager {
|
|
|
135
252
|
_circuitBreaker = new CircuitBreaker();
|
|
136
253
|
_inbox = new InboxManager();
|
|
137
254
|
logger;
|
|
255
|
+
_ultraappManager = null;
|
|
256
|
+
_ultraappRouter = null;
|
|
257
|
+
_ultraappRuntimeMode = 'host';
|
|
138
258
|
constructor(config, logger) {
|
|
139
259
|
this.logger = logger || createConsoleLogger('SessionManager');
|
|
140
260
|
this.pluginConfig = {
|
|
@@ -158,6 +278,48 @@ export class SessionManager {
|
|
|
158
278
|
// Start TTL cleanup timer
|
|
159
279
|
this.cleanupTimer = setInterval(() => this._cleanupIdleSessions(), CLEANUP_INTERVAL_MS);
|
|
160
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Lazily-constructed ultraapp manager. The ultraapp manager itself uses
|
|
283
|
+
* `this` as its session-manager dependency; building it lazily avoids any
|
|
284
|
+
* circular initialisation concerns.
|
|
285
|
+
*/
|
|
286
|
+
getUltraappManager() {
|
|
287
|
+
if (!this._ultraappManager) {
|
|
288
|
+
this._ultraappManager = new UltraappManager({
|
|
289
|
+
store: new UltraappStore(defaultStoreRoot()),
|
|
290
|
+
sessionManager: this,
|
|
291
|
+
router: this._ultraappRouter ?? undefined,
|
|
292
|
+
runtimeMode: this._ultraappRuntimeMode,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return this._ultraappManager;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Inject a started UltraappRouter so deploy + lifecycle wiring becomes
|
|
299
|
+
* available. Must be called BEFORE the first `getUltraappManager()` call —
|
|
300
|
+
* the manager is constructed lazily and reads the router reference at that
|
|
301
|
+
* point. Production: bin/cli.ts wires this. Tests: leave unset to keep
|
|
302
|
+
* v0.2-style "build-complete is resting state" behaviour.
|
|
303
|
+
*/
|
|
304
|
+
setUltraappRouter(router) {
|
|
305
|
+
if (this._ultraappManager) {
|
|
306
|
+
throw new Error('setUltraappRouter must be called before getUltraappManager');
|
|
307
|
+
}
|
|
308
|
+
this._ultraappRouter = router;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Pick the ultraapp runtime mode. 'host' (default) spawns the generated
|
|
312
|
+
* app as a regular Node process — works anywhere Node works, no Docker
|
|
313
|
+
* required. 'docker' uses `docker build` + `docker run` for isolation,
|
|
314
|
+
* intended for shared production hosts. Must be called before the first
|
|
315
|
+
* `getUltraappManager()` call.
|
|
316
|
+
*/
|
|
317
|
+
setUltraappRuntimeMode(mode) {
|
|
318
|
+
if (this._ultraappManager) {
|
|
319
|
+
throw new Error('setUltraappRuntimeMode must be called before getUltraappManager');
|
|
320
|
+
}
|
|
321
|
+
this._ultraappRuntimeMode = mode;
|
|
322
|
+
}
|
|
161
323
|
// ─── Session Lifecycle ─────────────────────────────────────────────────
|
|
162
324
|
async startSession(config) {
|
|
163
325
|
const name = config.name || `session-${Date.now()}`;
|
|
@@ -1040,7 +1202,51 @@ export class SessionManager {
|
|
|
1040
1202
|
const dir = path.dirname(SessionManager.PID_FILE);
|
|
1041
1203
|
if (!fs.existsSync(dir))
|
|
1042
1204
|
fs.mkdirSync(dir, { recursive: true });
|
|
1043
|
-
|
|
1205
|
+
// The PID file is host-shared: any SessionManager (gateway, dashboard,
|
|
1206
|
+
// standalone test runner) writes here. We MUST NOT overwrite entries
|
|
1207
|
+
// owned by another live SessionManager process — that would erase its
|
|
1208
|
+
// record of pids it spawned, and its next cleanup pass might decide
|
|
1209
|
+
// they're orphans and kill them. Read-merge-write keyed by ownerPid.
|
|
1210
|
+
let existing = {};
|
|
1211
|
+
try {
|
|
1212
|
+
existing = JSON.parse(fs.readFileSync(SessionManager.PID_FILE, 'utf8'));
|
|
1213
|
+
}
|
|
1214
|
+
catch {
|
|
1215
|
+
/* missing or malformed — start fresh */
|
|
1216
|
+
}
|
|
1217
|
+
const merged = {};
|
|
1218
|
+
const now = new Date().toISOString();
|
|
1219
|
+
// Keep entries from OTHER LIVE owners untouched. Entries whose
|
|
1220
|
+
// ownerPid is a dead process are stale bookkeeping — drop them so
|
|
1221
|
+
// the file doesn't grow unboundedly across server restarts. The
|
|
1222
|
+
// child processes those entries used to track were already reaped
|
|
1223
|
+
// by _cleanupOrphanedPids (which runs at SessionManager init,
|
|
1224
|
+
// before the first save).
|
|
1225
|
+
for (const [name, raw] of Object.entries(existing)) {
|
|
1226
|
+
if (typeof raw === 'number')
|
|
1227
|
+
continue; // legacy format — drop on first save
|
|
1228
|
+
const entry = raw;
|
|
1229
|
+
if (typeof entry.pid !== 'number' || typeof entry.ownerPid !== 'number')
|
|
1230
|
+
continue;
|
|
1231
|
+
if (entry.ownerPid === process.pid)
|
|
1232
|
+
continue; // ours; we're about to rewrite
|
|
1233
|
+
try {
|
|
1234
|
+
process.kill(entry.ownerPid, 0);
|
|
1235
|
+
}
|
|
1236
|
+
catch {
|
|
1237
|
+
continue; // owner dead — stale entry, drop it
|
|
1238
|
+
}
|
|
1239
|
+
merged[name] = {
|
|
1240
|
+
pid: entry.pid,
|
|
1241
|
+
ownerPid: entry.ownerPid,
|
|
1242
|
+
since: entry.since ?? now,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
// Add OUR current entries
|
|
1246
|
+
for (const [name, pid] of this._activePids) {
|
|
1247
|
+
merged[name] = { pid, ownerPid: process.pid, since: now };
|
|
1248
|
+
}
|
|
1249
|
+
fs.writeFileSync(SessionManager.PID_FILE, JSON.stringify(merged));
|
|
1044
1250
|
}
|
|
1045
1251
|
catch {
|
|
1046
1252
|
/* best effort */
|
|
@@ -1077,7 +1283,49 @@ export class SessionManager {
|
|
|
1077
1283
|
if (!fs.existsSync(SessionManager.PID_FILE))
|
|
1078
1284
|
return;
|
|
1079
1285
|
const data = JSON.parse(fs.readFileSync(SessionManager.PID_FILE, 'utf8'));
|
|
1080
|
-
for (const [name,
|
|
1286
|
+
for (const [name, raw] of Object.entries(data)) {
|
|
1287
|
+
// Resolve entry shape: legacy = number; current = { pid, ownerPid, since }.
|
|
1288
|
+
let pid;
|
|
1289
|
+
let ownerPid;
|
|
1290
|
+
if (typeof raw === 'number') {
|
|
1291
|
+
pid = raw;
|
|
1292
|
+
ownerPid = null; // unknown owner — treat conservatively (skip kill)
|
|
1293
|
+
}
|
|
1294
|
+
else if (raw && typeof raw === 'object') {
|
|
1295
|
+
const e = raw;
|
|
1296
|
+
if (typeof e.pid !== 'number')
|
|
1297
|
+
continue;
|
|
1298
|
+
pid = e.pid;
|
|
1299
|
+
ownerPid = typeof e.ownerPid === 'number' ? e.ownerPid : null;
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
// Cross-process safety: if this PID has a known owner SessionManager
|
|
1305
|
+
// and that owner is still alive, the child is NOT an orphan — it's
|
|
1306
|
+
// owned by another live manager. Only kill if owner is dead or unknown
|
|
1307
|
+
// AND the conservative legacy-format path has been ruled out.
|
|
1308
|
+
if (ownerPid !== null && ownerPid !== process.pid) {
|
|
1309
|
+
let ownerAlive = false;
|
|
1310
|
+
try {
|
|
1311
|
+
process.kill(ownerPid, 0);
|
|
1312
|
+
ownerAlive = true;
|
|
1313
|
+
}
|
|
1314
|
+
catch {
|
|
1315
|
+
/* owner dead */
|
|
1316
|
+
}
|
|
1317
|
+
if (ownerAlive) {
|
|
1318
|
+
this.logger.info(`PID ${pid} (session: ${name}) owned by live SessionManager pid=${ownerPid} — skipping`);
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
else if (ownerPid === null) {
|
|
1323
|
+
// Legacy format with no owner info — too risky to kill if a host
|
|
1324
|
+
// shares the file across managers. Skip; the entry will be cleaned
|
|
1325
|
+
// up on the next save (read-merge-write drops legacy format).
|
|
1326
|
+
this.logger.info(`PID ${pid} (session: ${name}) is legacy-format (no ownerPid) — skipping kill`);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1081
1329
|
try {
|
|
1082
1330
|
process.kill(pid, 0); // check if alive
|
|
1083
1331
|
// Alive — but verify it's actually a coding CLI, not a recycled PID
|
|
@@ -1230,11 +1478,32 @@ export class SessionManager {
|
|
|
1230
1478
|
const council = this.councils.get(id);
|
|
1231
1479
|
return council?.getSession();
|
|
1232
1480
|
}
|
|
1233
|
-
/**
|
|
1481
|
+
/**
|
|
1482
|
+
* List all council sessions visible to this process.
|
|
1483
|
+
*
|
|
1484
|
+
* Includes (a) in-memory sessions managed by this SessionManager and (b)
|
|
1485
|
+
* sessions reconstructed from on-disk transcripts at ~/.openclaw/council-logs/.
|
|
1486
|
+
* The disk path lets the dashboard see runs started in OTHER processes
|
|
1487
|
+
* (e.g. plugin-managed runs visible to a standalone clawo-serve dashboard).
|
|
1488
|
+
* Dedup by id; in-memory wins. Sorted by startTime descending so the newest
|
|
1489
|
+
* appears at the top of the sidebar.
|
|
1490
|
+
*/
|
|
1234
1491
|
councilList() {
|
|
1235
|
-
|
|
1492
|
+
const inMemory = Array.from(this.councils.values())
|
|
1236
1493
|
.map((c) => c.getSession())
|
|
1237
1494
|
.filter((s) => s !== null && s !== undefined);
|
|
1495
|
+
const inMemIds = new Set(inMemory.map((s) => s.id));
|
|
1496
|
+
const fromDisk = listCouncilsFromDisk()
|
|
1497
|
+
.filter((r) => !inMemIds.has(r.id))
|
|
1498
|
+
.map((r) => ({
|
|
1499
|
+
id: r.id,
|
|
1500
|
+
task: r.task,
|
|
1501
|
+
status: r.status,
|
|
1502
|
+
startTime: r.startTime,
|
|
1503
|
+
responses: [],
|
|
1504
|
+
config: { agents: [], maxRounds: 0, projectDir: '' },
|
|
1505
|
+
}));
|
|
1506
|
+
return [...inMemory, ...fromDisk].sort((a, b) => (b.startTime || '').localeCompare(a.startTime || ''));
|
|
1238
1507
|
}
|
|
1239
1508
|
/** Used by embedded-server to subscribe to a council's event stream. */
|
|
1240
1509
|
getCouncil(id) {
|
|
@@ -1598,6 +1867,21 @@ export class SessionManager {
|
|
|
1598
1867
|
pushPolicy,
|
|
1599
1868
|
});
|
|
1600
1869
|
await runner.start();
|
|
1870
|
+
// Record into the cross-process registry so the dashboard / another
|
|
1871
|
+
// SessionManager instance can list this run even after it ends. Best
|
|
1872
|
+
// effort — registry failure should not block the run.
|
|
1873
|
+
try {
|
|
1874
|
+
appendAutoloopRegistry(DEFAULT_AUTOLOOP_REGISTRY, {
|
|
1875
|
+
run_id: opts.runId,
|
|
1876
|
+
workspace: opts.workspace,
|
|
1877
|
+
ledger_dir: ledgerDir,
|
|
1878
|
+
started_at: runner.state.started_at,
|
|
1879
|
+
planner_session: dispatcher.sessionNames.planner,
|
|
1880
|
+
});
|
|
1881
|
+
}
|
|
1882
|
+
catch (err) {
|
|
1883
|
+
this.logger.warn?.(`[autoloop/${runId}] registry append failed: ${err.message}`);
|
|
1884
|
+
}
|
|
1601
1885
|
return {
|
|
1602
1886
|
runId: opts.runId,
|
|
1603
1887
|
plannerSession: dispatcher.sessionNames.planner,
|
|
@@ -1631,7 +1915,26 @@ export class SessionManager {
|
|
|
1631
1915
|
return this.autoloops.get(runId)?.runner.state;
|
|
1632
1916
|
}
|
|
1633
1917
|
autoloopList() {
|
|
1634
|
-
|
|
1918
|
+
const inMemory = Array.from(this.autoloops.values()).map((c) => c.runner.state);
|
|
1919
|
+
const inMemIds = new Set(inMemory.map((s) => s.run_id));
|
|
1920
|
+
const fromDisk = listAutoloopsFromRegistry()
|
|
1921
|
+
.filter((e) => !inMemIds.has(e.run_id))
|
|
1922
|
+
.map((e) => ({
|
|
1923
|
+
run_id: e.run_id,
|
|
1924
|
+
status: 'terminated',
|
|
1925
|
+
iter: 0,
|
|
1926
|
+
subagents_spawned: false,
|
|
1927
|
+
started_at: e.started_at,
|
|
1928
|
+
workspace: e.workspace,
|
|
1929
|
+
ledger_dir: e.ledger_dir,
|
|
1930
|
+
push_log_count: 0,
|
|
1931
|
+
status_reason: 'reconstructed from registry — not in current process memory',
|
|
1932
|
+
consecutive_phase_errors: 0,
|
|
1933
|
+
recent_phase_errors: [],
|
|
1934
|
+
metric_history: [],
|
|
1935
|
+
last_activity_at: 0,
|
|
1936
|
+
}));
|
|
1937
|
+
return [...inMemory, ...fromDisk].sort((a, b) => (b.started_at || '').localeCompare(a.started_at || ''));
|
|
1635
1938
|
}
|
|
1636
1939
|
/**
|
|
1637
1940
|
* Reset a single subagent on a v2 run. Useful when an agent has drifted
|
|
@@ -1655,6 +1958,38 @@ export class SessionManager {
|
|
|
1655
1958
|
this.autoloops.delete(runId);
|
|
1656
1959
|
return true;
|
|
1657
1960
|
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Delete a run from the system: stop the runner if it's still alive in this
|
|
1963
|
+
* process, then scrub the row from the cross-process registry so it stops
|
|
1964
|
+
* appearing in `autoloop_list` / the dashboard. The ledger directory on disk
|
|
1965
|
+
* is NOT removed — postmortem artifacts (chat history, push log, plan.md)
|
|
1966
|
+
* are kept for the user to inspect or `rm` manually.
|
|
1967
|
+
*
|
|
1968
|
+
* Returns true if anything was removed (in-memory entry OR registry row).
|
|
1969
|
+
*/
|
|
1970
|
+
async autoloopDelete(runId) {
|
|
1971
|
+
const ctx = this.autoloops.get(runId);
|
|
1972
|
+
let touched = false;
|
|
1973
|
+
if (ctx) {
|
|
1974
|
+
try {
|
|
1975
|
+
await ctx.runner.send(AutoloopMsg.terminate(ctx.runner.state.iter, { reason: 'user-delete' }));
|
|
1976
|
+
}
|
|
1977
|
+
catch (err) {
|
|
1978
|
+
this.logger.warn?.(`[autoloop/${runId}] terminate during delete failed: ${err.message}`);
|
|
1979
|
+
}
|
|
1980
|
+
this.autoloops.delete(runId);
|
|
1981
|
+
touched = true;
|
|
1982
|
+
}
|
|
1983
|
+
try {
|
|
1984
|
+
const removed = removeAutoloopFromRegistry(DEFAULT_AUTOLOOP_REGISTRY, runId);
|
|
1985
|
+
if (removed > 0)
|
|
1986
|
+
touched = true;
|
|
1987
|
+
}
|
|
1988
|
+
catch (err) {
|
|
1989
|
+
this.logger.warn?.(`[autoloop/${runId}] registry scrub failed: ${err.message}`);
|
|
1990
|
+
}
|
|
1991
|
+
return touched;
|
|
1992
|
+
}
|
|
1658
1993
|
/** Used by embedded-server to attach SSE listeners. */
|
|
1659
1994
|
getAutoloop(runId) {
|
|
1660
1995
|
const ctx = this.autoloops.get(runId);
|