@mclawnet/agent 0.6.35 → 0.6.36
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/cli.js +6 -6
- package/dist/__tests__/collect-manifest.test.d.ts +2 -0
- package/dist/__tests__/collect-manifest.test.d.ts.map +1 -0
- package/dist/__tests__/hub-connection-on-activity.test.d.ts +2 -0
- package/dist/__tests__/hub-connection-on-activity.test.d.ts.map +1 -0
- package/dist/__tests__/hub-connection-wake-watch.test.d.ts +2 -0
- package/dist/__tests__/hub-connection-wake-watch.test.d.ts.map +1 -0
- package/dist/__tests__/ideas-rest-client.test.d.ts +2 -0
- package/dist/__tests__/ideas-rest-client.test.d.ts.map +1 -0
- package/dist/__tests__/legacy-claude-execute-compat.test.d.ts +2 -0
- package/dist/__tests__/legacy-claude-execute-compat.test.d.ts.map +1 -0
- package/dist/__tests__/no-adapter-cycle.test.d.ts +2 -0
- package/dist/__tests__/no-adapter-cycle.test.d.ts.map +1 -0
- package/dist/__tests__/session-manager-merge.test.d.ts +2 -0
- package/dist/__tests__/session-manager-merge.test.d.ts.map +1 -0
- package/dist/__tests__/session-manager-sticky.test.d.ts +2 -0
- package/dist/__tests__/session-manager-sticky.test.d.ts.map +1 -0
- package/dist/__tests__/session-protocol-dispatch.test.d.ts +2 -0
- package/dist/__tests__/session-protocol-dispatch.test.d.ts.map +1 -0
- package/dist/__tests__/worktree-bridge.test.d.ts +2 -0
- package/dist/__tests__/worktree-bridge.test.d.ts.map +1 -0
- package/dist/backend-adapter.d.ts +6 -232
- package/dist/backend-adapter.d.ts.map +1 -1
- package/dist/backend-factory-AFF6I7YF.js +11 -0
- package/dist/backend-factory.d.ts +23 -1
- package/dist/backend-factory.d.ts.map +1 -1
- package/dist/bootstrap-deps.d.ts +22 -0
- package/dist/bootstrap-deps.d.ts.map +1 -1
- package/dist/bootstrap-deps.js +23 -4
- package/dist/bootstrap-deps.js.map +1 -1
- package/dist/{chunk-PJ5M6Q36.js → chunk-376QZ7JB.js} +2 -2
- package/dist/chunk-376QZ7JB.js.map +1 -0
- package/dist/{chunk-B733MQCA.js → chunk-GOCWMRBB.js} +1772 -284
- package/dist/chunk-GOCWMRBB.js.map +1 -0
- package/dist/{chunk-M2CDVPQF.js → chunk-JH6RGJBQ.js} +2 -2
- package/dist/{chunk-FYM7CXUI.js → chunk-VAEFJLPL.js} +25 -3
- package/dist/chunk-VAEFJLPL.js.map +1 -0
- package/dist/{dist-EGT2NQEW.js → dist-NWVHAP5R.js} +155 -13
- package/dist/dist-NWVHAP5R.js.map +1 -0
- package/dist/errors.d.ts +20 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/hub-connection.d.ts +25 -1
- package/dist/hub-connection.d.ts.map +1 -1
- package/dist/ideas-rest-client.d.ts +25 -0
- package/dist/ideas-rest-client.d.ts.map +1 -0
- package/dist/index.js +3 -3
- package/dist/{linux-IHA4O633.js → linux-MBU6ERXL.js} +3 -3
- package/dist/{macos-G4VK2253.js → macos-I2DUWFUH.js} +3 -3
- package/dist/projects-handler.d.ts +146 -1
- package/dist/projects-handler.d.ts.map +1 -1
- package/dist/service/index.js +5 -5
- package/dist/session-manager.d.ts +58 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +3 -2
- package/dist/{windows-P6U3JLUZ.js → windows-PEJ3KOLC.js} +3 -3
- package/dist/worktree-bridge.d.ts +51 -0
- package/dist/worktree-bridge.d.ts.map +1 -0
- package/package.json +9 -8
- package/dist/backend-factory-VRPU3534.js +0 -9
- package/dist/chunk-B733MQCA.js.map +0 -1
- package/dist/chunk-FYM7CXUI.js.map +0 -1
- package/dist/chunk-PJ5M6Q36.js.map +0 -1
- package/dist/dist-EGT2NQEW.js.map +0 -1
- /package/dist/{backend-factory-VRPU3534.js.map → backend-factory-AFF6I7YF.js.map} +0 -0
- /package/dist/{chunk-M2CDVPQF.js.map → chunk-JH6RGJBQ.js.map} +0 -0
- /package/dist/{linux-IHA4O633.js.map → linux-MBU6ERXL.js.map} +0 -0
- /package/dist/{macos-G4VK2253.js.map → macos-I2DUWFUH.js.map} +0 -0
- /package/dist/{windows-P6U3JLUZ.js.map → windows-PEJ3KOLC.js.map} +0 -0
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectAgentManifest
|
|
3
|
+
} from "./chunk-VAEFJLPL.js";
|
|
1
4
|
import {
|
|
2
5
|
loadConfig
|
|
3
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-376QZ7JB.js";
|
|
4
7
|
|
|
5
8
|
// src/start.ts
|
|
6
9
|
import { homedir as homedir5 } from "os";
|
|
7
|
-
import { join as
|
|
10
|
+
import { join as join7 } from "path";
|
|
11
|
+
import { existsSync as existsSync8 } from "fs";
|
|
8
12
|
|
|
9
13
|
// src/hub-connection.ts
|
|
10
14
|
import { hostname as osHostname } from "os";
|
|
@@ -248,24 +252,164 @@ async function handleLoadSessionHistory(workDir, backendSessionId, opts) {
|
|
|
248
252
|
|
|
249
253
|
// src/projects-handler.ts
|
|
250
254
|
import { homedir as homedir2 } from "os";
|
|
251
|
-
import { join as join2 } from "path";
|
|
252
|
-
import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
|
|
253
|
-
import { TaskStore, projectRoot } from "@mclawnet/task";
|
|
255
|
+
import { dirname, isAbsolute, join as join2, parse, resolve } from "path";
|
|
256
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
257
|
+
import { TaskStore, projectRoot, ensureProjectMeta, metaFile, encodeCwd } from "@mclawnet/task";
|
|
258
|
+
import { createLogger } from "@mclawnet/logger";
|
|
254
259
|
import {
|
|
255
260
|
listAllProjectSummaries,
|
|
256
261
|
loadProjectSummary,
|
|
257
262
|
loadSwarmSummaries,
|
|
258
263
|
resolveWorkDir,
|
|
259
264
|
loadSwarmSnapshot,
|
|
265
|
+
loadShipmentResult,
|
|
260
266
|
InboxStore,
|
|
261
267
|
listProjectFiles,
|
|
262
268
|
readProjectFile,
|
|
263
269
|
writeProjectFile,
|
|
264
|
-
ProjectFilesError
|
|
270
|
+
ProjectFilesError,
|
|
271
|
+
applyShipment,
|
|
272
|
+
mergeShipment,
|
|
273
|
+
discardShipment,
|
|
274
|
+
loadAlwaysOnConfig,
|
|
275
|
+
saveAlwaysOnConfig,
|
|
276
|
+
mergeAlwaysOnConfig
|
|
265
277
|
} from "@mclawnet/swarm";
|
|
278
|
+
var log = createLogger({ module: "projects-handler" });
|
|
279
|
+
var FORBIDDEN_WORKDIR_PREFIXES = [
|
|
280
|
+
"/etc",
|
|
281
|
+
"/usr",
|
|
282
|
+
"/bin",
|
|
283
|
+
"/sbin",
|
|
284
|
+
"/var",
|
|
285
|
+
"/private",
|
|
286
|
+
"/System",
|
|
287
|
+
"/Library",
|
|
288
|
+
"/proc",
|
|
289
|
+
"/sys",
|
|
290
|
+
"/boot"
|
|
291
|
+
];
|
|
292
|
+
function forbiddenWorkDirPrefix(workDir) {
|
|
293
|
+
for (const forbidden of FORBIDDEN_WORKDIR_PREFIXES) {
|
|
294
|
+
if (workDir === forbidden) return forbidden;
|
|
295
|
+
if (workDir.startsWith(`${forbidden}/`)) return forbidden;
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
266
299
|
function getHome() {
|
|
267
300
|
return process.env.CLAWNET_HOME ?? homedir2();
|
|
268
301
|
}
|
|
302
|
+
function ensureError(status, code, message, details) {
|
|
303
|
+
return { status, code, message, ...details ? { details } : {} };
|
|
304
|
+
}
|
|
305
|
+
function normalizeExistingWorkDir(input) {
|
|
306
|
+
const normalized = normalizeAbsoluteNonRootPath(input);
|
|
307
|
+
if (normalized.error || !normalized.workDir) return normalized;
|
|
308
|
+
let st;
|
|
309
|
+
try {
|
|
310
|
+
st = statSync2(normalized.workDir);
|
|
311
|
+
} catch (err) {
|
|
312
|
+
return {
|
|
313
|
+
error: ensureError(
|
|
314
|
+
404,
|
|
315
|
+
"workdir_not_found",
|
|
316
|
+
"workDir does not exist on disk",
|
|
317
|
+
{ workDir: normalized.workDir, cause: err instanceof Error ? err.message : String(err) }
|
|
318
|
+
)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (!st.isDirectory()) {
|
|
322
|
+
return { error: ensureError(400, "not_directory", "workDir is not a directory", { workDir: normalized.workDir }) };
|
|
323
|
+
}
|
|
324
|
+
return { workDir: normalized.workDir };
|
|
325
|
+
}
|
|
326
|
+
function normalizeAbsoluteNonRootPath(input) {
|
|
327
|
+
const trimmed = typeof input === "string" ? input.trim() : "";
|
|
328
|
+
if (!trimmed) return { error: ensureError(400, "invalid_path", "workDir is required") };
|
|
329
|
+
if (trimmed.length > 4096) {
|
|
330
|
+
return { error: ensureError(400, "invalid_path", "workDir is too long") };
|
|
331
|
+
}
|
|
332
|
+
if (trimmed.includes("\0")) {
|
|
333
|
+
return { error: ensureError(400, "invalid_path", "workDir contains NUL byte") };
|
|
334
|
+
}
|
|
335
|
+
if (!isAbsolute(trimmed)) {
|
|
336
|
+
return { error: ensureError(400, "invalid_path", "workDir must be an absolute path") };
|
|
337
|
+
}
|
|
338
|
+
const parsed = parse(trimmed);
|
|
339
|
+
const normalized = resolve(trimmed);
|
|
340
|
+
if (normalized === parsed.root) {
|
|
341
|
+
return { error: ensureError(400, "root_forbidden", "filesystem root cannot be used as a project") };
|
|
342
|
+
}
|
|
343
|
+
const forbiddenPrefix = forbiddenWorkDirPrefix(normalized);
|
|
344
|
+
if (forbiddenPrefix) {
|
|
345
|
+
return {
|
|
346
|
+
error: ensureError(
|
|
347
|
+
400,
|
|
348
|
+
"path_forbidden",
|
|
349
|
+
"workDir is inside a protected system path",
|
|
350
|
+
{ workDir: normalized, forbiddenPrefix }
|
|
351
|
+
)
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return { workDir: normalized };
|
|
355
|
+
}
|
|
356
|
+
async function handleWorkdirMkdir(workDirInput) {
|
|
357
|
+
const normalized = normalizeAbsoluteNonRootPath(workDirInput);
|
|
358
|
+
if (normalized.error || !normalized.workDir) {
|
|
359
|
+
return { ok: false, error: normalized.error ?? ensureError(400, "invalid_path", "invalid workDir") };
|
|
360
|
+
}
|
|
361
|
+
const workDir = normalized.workDir;
|
|
362
|
+
const parsed = parse(workDir);
|
|
363
|
+
if (!parsed.base || parsed.base === "." || parsed.base === "..") {
|
|
364
|
+
return { ok: false, error: ensureError(400, "invalid_path", "invalid directory name", { workDir }) };
|
|
365
|
+
}
|
|
366
|
+
let st;
|
|
367
|
+
try {
|
|
368
|
+
st = statSync2(workDir);
|
|
369
|
+
if (st.isDirectory()) {
|
|
370
|
+
return { ok: true, workDir, created: false, error: null };
|
|
371
|
+
}
|
|
372
|
+
return { ok: false, error: ensureError(400, "not_directory", "workDir exists and is not a directory", { workDir }) };
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
const parent = dirname(workDir);
|
|
376
|
+
try {
|
|
377
|
+
st = statSync2(parent);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
return {
|
|
380
|
+
ok: false,
|
|
381
|
+
error: ensureError(
|
|
382
|
+
404,
|
|
383
|
+
"parent_not_found",
|
|
384
|
+
"parent directory does not exist",
|
|
385
|
+
{ workDir, parent, cause: err instanceof Error ? err.message : String(err) }
|
|
386
|
+
)
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (!st.isDirectory()) {
|
|
390
|
+
return { ok: false, error: ensureError(400, "parent_not_directory", "parent path is not a directory", { workDir, parent }) };
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
mkdirSync(workDir);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
try {
|
|
396
|
+
if (statSync2(workDir).isDirectory()) {
|
|
397
|
+
return { ok: true, workDir, created: false, error: null };
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
error: ensureError(
|
|
404
|
+
500,
|
|
405
|
+
"mkdir_failed",
|
|
406
|
+
"failed to create directory",
|
|
407
|
+
{ workDir, cause: err instanceof Error ? err.message : String(err) }
|
|
408
|
+
)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
return { ok: true, workDir, created: true, error: null };
|
|
412
|
+
}
|
|
269
413
|
async function handleProjectsList() {
|
|
270
414
|
return { projects: listAllProjectSummaries(getHome()) };
|
|
271
415
|
}
|
|
@@ -276,6 +420,61 @@ async function handleProjectsGet(encodedCwd) {
|
|
|
276
420
|
const swarms = loadSwarmSummaries(home, summary.workDir);
|
|
277
421
|
return { project: { ...summary, swarms } };
|
|
278
422
|
}
|
|
423
|
+
async function handleProjectsEnsureProject(workDirInput, opts = {}) {
|
|
424
|
+
if (opts.createDir) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
project: null,
|
|
428
|
+
error: ensureError(
|
|
429
|
+
400,
|
|
430
|
+
"create_dir_unsupported",
|
|
431
|
+
"createDir is not supported by this agent; create/select an existing directory first"
|
|
432
|
+
)
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const normalized = normalizeExistingWorkDir(workDirInput);
|
|
436
|
+
if (normalized.error || !normalized.workDir) {
|
|
437
|
+
return { ok: false, project: null, error: normalized.error ?? ensureError(400, "invalid_path", "invalid workDir") };
|
|
438
|
+
}
|
|
439
|
+
const home = getHome();
|
|
440
|
+
const encodedCwd = encodeCwd(normalized.workDir);
|
|
441
|
+
const metaPath = metaFile(normalized.workDir, home);
|
|
442
|
+
const existedBefore = existsSync2(metaPath);
|
|
443
|
+
try {
|
|
444
|
+
ensureProjectMeta(normalized.workDir, home);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
return {
|
|
447
|
+
ok: false,
|
|
448
|
+
project: null,
|
|
449
|
+
encodedCwd,
|
|
450
|
+
workDir: normalized.workDir,
|
|
451
|
+
error: ensureError(
|
|
452
|
+
500,
|
|
453
|
+
"meta_write_failed",
|
|
454
|
+
"failed to write project meta",
|
|
455
|
+
{ cause: err instanceof Error ? err.message : String(err) }
|
|
456
|
+
)
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
const project = loadProjectSummary(home, encodedCwd);
|
|
460
|
+
if (!project) {
|
|
461
|
+
return {
|
|
462
|
+
ok: false,
|
|
463
|
+
project: null,
|
|
464
|
+
encodedCwd,
|
|
465
|
+
workDir: normalized.workDir,
|
|
466
|
+
error: ensureError(500, "project_load_failed", "project meta was written but summary could not be loaded")
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
ok: true,
|
|
471
|
+
project,
|
|
472
|
+
encodedCwd,
|
|
473
|
+
workDir: normalized.workDir,
|
|
474
|
+
createdMeta: !existedBefore,
|
|
475
|
+
error: null
|
|
476
|
+
};
|
|
477
|
+
}
|
|
279
478
|
async function handleProjectsTasks(encodedCwd, filters = {}) {
|
|
280
479
|
const home = getHome();
|
|
281
480
|
const workDir = resolveWorkDir(home, encodedCwd);
|
|
@@ -412,12 +611,247 @@ async function handleProjectsFileWrite(encodedCwd, relPath, content, ifMatchEtag
|
|
|
412
611
|
};
|
|
413
612
|
}
|
|
414
613
|
}
|
|
614
|
+
var SHIPMENT_INLINE_MAX_BYTES = 1024 * 1024;
|
|
615
|
+
function tryReadInline(path) {
|
|
616
|
+
if (!path) return void 0;
|
|
617
|
+
try {
|
|
618
|
+
const st = statSync2(path);
|
|
619
|
+
if (!st.isFile() || st.size >= SHIPMENT_INLINE_MAX_BYTES) return void 0;
|
|
620
|
+
return readFileSync2(path, "utf-8");
|
|
621
|
+
} catch {
|
|
622
|
+
return void 0;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
async function handleProjectsShipmentGet(encodedCwd, swarmId) {
|
|
626
|
+
const home = getHome();
|
|
627
|
+
const workDir = resolveWorkDir(home, encodedCwd);
|
|
628
|
+
if (!workDir) return { projectFound: false, shipment: null };
|
|
629
|
+
const shipment = loadShipmentResult(workDir, swarmId);
|
|
630
|
+
if (!shipment) {
|
|
631
|
+
return { projectFound: true, shipment: null };
|
|
632
|
+
}
|
|
633
|
+
const diffContent = tryReadInline(shipment.diffPath);
|
|
634
|
+
const reportContent = tryReadInline(shipment.reportPath);
|
|
635
|
+
return {
|
|
636
|
+
projectFound: true,
|
|
637
|
+
shipment,
|
|
638
|
+
...diffContent !== void 0 ? { diffContent } : {},
|
|
639
|
+
...reportContent !== void 0 ? { reportContent } : {}
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
async function handleProjectsShipmentDiffFull(encodedCwd, swarmId) {
|
|
643
|
+
const home = getHome();
|
|
644
|
+
const workDir = resolveWorkDir(home, encodedCwd);
|
|
645
|
+
if (!workDir) return { projectFound: false, content: null };
|
|
646
|
+
const shipment = loadShipmentResult(workDir, swarmId);
|
|
647
|
+
if (!shipment || !shipment.diffPath) {
|
|
648
|
+
return { projectFound: true, content: null };
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
const st = statSync2(shipment.diffPath);
|
|
652
|
+
if (!st.isFile()) return { projectFound: true, content: null };
|
|
653
|
+
return { projectFound: true, content: readFileSync2(shipment.diffPath, "utf-8") };
|
|
654
|
+
} catch {
|
|
655
|
+
return { projectFound: true, content: null };
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
var inflightShipmentActions = /* @__PURE__ */ new Map();
|
|
659
|
+
async function handleProjectsShipmentAction(encodedCwd, swarmId, action, force) {
|
|
660
|
+
const home = getHome();
|
|
661
|
+
const workDir = resolveWorkDir(home, encodedCwd);
|
|
662
|
+
if (!workDir) return { projectFound: false, ok: false, error: "project not found" };
|
|
663
|
+
const existing = inflightShipmentActions.get(swarmId);
|
|
664
|
+
if (existing) {
|
|
665
|
+
const r2 = await existing;
|
|
666
|
+
return { projectFound: true, ...r2 };
|
|
667
|
+
}
|
|
668
|
+
const runAction = async () => {
|
|
669
|
+
if (action === "merge") return mergeShipment({ workDir, swarmId });
|
|
670
|
+
if (action === "apply") return applyShipment({ workDir, swarmId, force });
|
|
671
|
+
if (action === "discard") return discardShipment({ workDir, swarmId });
|
|
672
|
+
return { ok: false, error: `unknown action: ${action}` };
|
|
673
|
+
};
|
|
674
|
+
const promise = runAction().finally(() => {
|
|
675
|
+
inflightShipmentActions.delete(swarmId);
|
|
676
|
+
});
|
|
677
|
+
inflightShipmentActions.set(swarmId, promise);
|
|
678
|
+
const r = await promise;
|
|
679
|
+
return { projectFound: true, ...r };
|
|
680
|
+
}
|
|
681
|
+
async function handleProjectsShipmentsList(encodedCwd) {
|
|
682
|
+
const home = getHome();
|
|
683
|
+
const workDir = resolveWorkDir(home, encodedCwd);
|
|
684
|
+
if (!workDir) return { projectFound: false, shipments: [] };
|
|
685
|
+
const swarmsDir = join2(projectRoot(workDir, home), "swarms");
|
|
686
|
+
if (!existsSync2(swarmsDir)) return { projectFound: true, shipments: [] };
|
|
687
|
+
let entries;
|
|
688
|
+
try {
|
|
689
|
+
entries = readdirSync2(swarmsDir, { withFileTypes: true });
|
|
690
|
+
} catch (err) {
|
|
691
|
+
log.warn(
|
|
692
|
+
{ err: err instanceof Error ? err.message : String(err), swarmsDir },
|
|
693
|
+
"shipments_list: readdir failed; returning empty list"
|
|
694
|
+
);
|
|
695
|
+
return { projectFound: true, shipments: [] };
|
|
696
|
+
}
|
|
697
|
+
const shipments = [];
|
|
698
|
+
for (const ent of entries) {
|
|
699
|
+
if (!ent.isDirectory()) continue;
|
|
700
|
+
const shipmentPath = join2(swarmsDir, ent.name, "shipment.json");
|
|
701
|
+
if (!existsSync2(shipmentPath)) continue;
|
|
702
|
+
try {
|
|
703
|
+
const data = JSON.parse(readFileSync2(shipmentPath, "utf-8"));
|
|
704
|
+
shipments.push({ swarmId: ent.name, shipment: data });
|
|
705
|
+
} catch (err) {
|
|
706
|
+
log.warn(
|
|
707
|
+
{ err: err instanceof Error ? err.message : String(err), swarmId: ent.name },
|
|
708
|
+
"shipments_list: parse failed for one swarm; skipping"
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
shipments.sort((a, b) => {
|
|
713
|
+
const aT = a.shipment.createdAt ?? "";
|
|
714
|
+
const bT = b.shipment.createdAt ?? "";
|
|
715
|
+
return bT.localeCompare(aT);
|
|
716
|
+
});
|
|
717
|
+
return { projectFound: true, shipments };
|
|
718
|
+
}
|
|
719
|
+
var EMPTY_ALWAYS_ON_SNAPSHOT = {
|
|
720
|
+
mode: "off",
|
|
721
|
+
ticksToday: 0,
|
|
722
|
+
lastFireAt: null,
|
|
723
|
+
nextTickAt: null,
|
|
724
|
+
recentTicks: []
|
|
725
|
+
};
|
|
726
|
+
var alwaysOnHooks = {
|
|
727
|
+
getSnapshot: () => null,
|
|
728
|
+
applyConfig: () => {
|
|
729
|
+
},
|
|
730
|
+
recordActivity: () => {
|
|
731
|
+
}
|
|
732
|
+
// forceTick left undefined: handler returns projectFound=false when null
|
|
733
|
+
};
|
|
734
|
+
function _registerAlwaysOnHooks(hooks) {
|
|
735
|
+
alwaysOnHooks = {
|
|
736
|
+
getSnapshot: hooks.getSnapshot ?? (() => null),
|
|
737
|
+
applyConfig: hooks.applyConfig ?? (() => void 0),
|
|
738
|
+
recordActivity: hooks.recordActivity ?? (() => void 0),
|
|
739
|
+
...hooks.forceTick ? { forceTick: hooks.forceTick } : {}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
function _recordAlwaysOnActivity() {
|
|
743
|
+
try {
|
|
744
|
+
alwaysOnHooks.recordActivity();
|
|
745
|
+
} catch (err) {
|
|
746
|
+
log.warn(
|
|
747
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
748
|
+
"always-on recordActivity hook threw; ignored"
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function snapshotForProject(workDir) {
|
|
753
|
+
const live = alwaysOnHooks.getSnapshot(workDir);
|
|
754
|
+
return live ?? EMPTY_ALWAYS_ON_SNAPSHOT;
|
|
755
|
+
}
|
|
756
|
+
async function handleProjectsAlwaysOnGet(encodedCwd) {
|
|
757
|
+
const home = getHome();
|
|
758
|
+
const workDir = resolveWorkDir(home, encodedCwd);
|
|
759
|
+
if (!workDir) {
|
|
760
|
+
return {
|
|
761
|
+
projectFound: false,
|
|
762
|
+
config: { mode: "off", dailyBudget: 3 },
|
|
763
|
+
snapshot: EMPTY_ALWAYS_ON_SNAPSHOT
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
const config = loadAlwaysOnConfig(workDir, home);
|
|
767
|
+
return { projectFound: true, config, snapshot: snapshotForProject(workDir) };
|
|
768
|
+
}
|
|
769
|
+
async function handleProjectsAlwaysOnSet(encodedCwd, patch) {
|
|
770
|
+
const home = getHome();
|
|
771
|
+
const workDir = resolveWorkDir(home, encodedCwd);
|
|
772
|
+
if (!workDir) {
|
|
773
|
+
return {
|
|
774
|
+
projectFound: false,
|
|
775
|
+
config: { mode: "off", dailyBudget: 3 },
|
|
776
|
+
snapshot: EMPTY_ALWAYS_ON_SNAPSHOT
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
const current = loadAlwaysOnConfig(workDir, home);
|
|
780
|
+
const next = mergeAlwaysOnConfig(current, patch);
|
|
781
|
+
try {
|
|
782
|
+
saveAlwaysOnConfig(workDir, home, next);
|
|
783
|
+
} catch (err) {
|
|
784
|
+
return {
|
|
785
|
+
projectFound: true,
|
|
786
|
+
config: current,
|
|
787
|
+
snapshot: snapshotForProject(workDir),
|
|
788
|
+
error: err instanceof Error ? err.message : String(err)
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
alwaysOnHooks.applyConfig(workDir, next);
|
|
793
|
+
} catch (err) {
|
|
794
|
+
log.warn(
|
|
795
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
796
|
+
"always-on applyConfig hook threw; config was persisted"
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
projectFound: true,
|
|
801
|
+
config: next,
|
|
802
|
+
snapshot: snapshotForProject(workDir)
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
async function handleProjectsAlwaysOnTickNow(encodedCwd) {
|
|
806
|
+
const home = getHome();
|
|
807
|
+
const workDir = resolveWorkDir(home, encodedCwd);
|
|
808
|
+
if (!workDir) return { projectFound: false };
|
|
809
|
+
if (!alwaysOnHooks.forceTick) {
|
|
810
|
+
return {
|
|
811
|
+
projectFound: true,
|
|
812
|
+
error: "always-on manager not wired (forceTick unavailable)"
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
const out = await alwaysOnHooks.forceTick(workDir);
|
|
817
|
+
if (!out) {
|
|
818
|
+
return {
|
|
819
|
+
projectFound: true,
|
|
820
|
+
error: "always-on scheduler not booted for this project (enable always-on via PATCH /api/projects/:id/always-on first)"
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
projectFound: true,
|
|
825
|
+
decision: out.record.decision,
|
|
826
|
+
gateBreakdown: out.record.gateBreakdown,
|
|
827
|
+
snapshot: out.snapshot
|
|
828
|
+
};
|
|
829
|
+
} catch (err) {
|
|
830
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
831
|
+
if (msg.includes("no scheduler for")) {
|
|
832
|
+
return {
|
|
833
|
+
projectFound: true,
|
|
834
|
+
error: "always-on scheduler not booted for this project (enable always-on via PATCH /api/projects/:id/always-on first)"
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
if (msg.includes("tick is already in progress")) {
|
|
838
|
+
return {
|
|
839
|
+
projectFound: true,
|
|
840
|
+
error: `another tick is currently in flight \u2014 wait for the runner to finish or check the agent log (underlying: ${msg})`
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
projectFound: true,
|
|
845
|
+
error: `force-tick failed: ${msg}`
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
}
|
|
415
849
|
|
|
416
850
|
// src/swarm-control-dispatch.ts
|
|
417
851
|
import { randomUUID } from "crypto";
|
|
418
|
-
import { createLogger } from "@mclawnet/logger";
|
|
852
|
+
import { createLogger as createLogger2 } from "@mclawnet/logger";
|
|
419
853
|
import { InboxStore as InboxStore2 } from "@mclawnet/swarm";
|
|
420
|
-
var
|
|
854
|
+
var log2 = createLogger2({ module: "agent/swarm-control" });
|
|
421
855
|
var MAX_ROLES_PER_SWARM = 20;
|
|
422
856
|
async function handleSwarmControl(coord, msg, opts) {
|
|
423
857
|
if (!msg || msg.type !== "swarm_spawn" && msg.type !== "swarm_resume" && msg.type !== "swarm_add_role") {
|
|
@@ -426,11 +860,11 @@ async function handleSwarmControl(coord, msg, opts) {
|
|
|
426
860
|
if (msg.type === "swarm_resume") {
|
|
427
861
|
try {
|
|
428
862
|
await coord.recover(msg.swarmId);
|
|
429
|
-
|
|
863
|
+
log2.info({ swarmId: msg.swarmId }, "swarm_resume: recovered");
|
|
430
864
|
return { ok: true, swarmId: msg.swarmId };
|
|
431
865
|
} catch (err) {
|
|
432
866
|
const error = err instanceof Error ? err.message : String(err);
|
|
433
|
-
|
|
867
|
+
log2.error({ err, swarmId: msg.swarmId }, "swarm_resume failed");
|
|
434
868
|
return { ok: false, error };
|
|
435
869
|
}
|
|
436
870
|
}
|
|
@@ -467,14 +901,14 @@ async function handleSwarmControl(coord, msg, opts) {
|
|
|
467
901
|
taskPrompt: msg.taskPrompt,
|
|
468
902
|
isDynamicAdd: true
|
|
469
903
|
});
|
|
470
|
-
|
|
904
|
+
log2.info(
|
|
471
905
|
{ swarmId: msg.swarmId, roleName: msg.roleName, instanceId: role.instanceId },
|
|
472
906
|
"swarm_add_role: spawned"
|
|
473
907
|
);
|
|
474
908
|
return { ok: true, swarmId: msg.swarmId, instanceId: role.instanceId };
|
|
475
909
|
} catch (err) {
|
|
476
910
|
const error = err instanceof Error ? err.message : String(err);
|
|
477
|
-
|
|
911
|
+
log2.error({ err, swarmId: msg.swarmId, roleName: msg.roleName }, "swarm_add_role failed");
|
|
478
912
|
return { ok: false, error };
|
|
479
913
|
}
|
|
480
914
|
}
|
|
@@ -484,7 +918,7 @@ async function handleSwarmControl(coord, msg, opts) {
|
|
|
484
918
|
await coord.create(swarmId, { workDir, templateName: msg.teamName, displayName: msg.displayName });
|
|
485
919
|
} catch (err) {
|
|
486
920
|
const error = err instanceof Error ? err.message : String(err);
|
|
487
|
-
|
|
921
|
+
log2.error({ err, teamName: msg.teamName }, "swarm_spawn: create failed");
|
|
488
922
|
return { ok: false, error };
|
|
489
923
|
}
|
|
490
924
|
if (msg.msg && msg.msg.length > 0) {
|
|
@@ -503,20 +937,28 @@ async function handleSwarmControl(coord, msg, opts) {
|
|
|
503
937
|
});
|
|
504
938
|
await coord.inboxRelay.deliver(swarmId, queen.instanceId);
|
|
505
939
|
} catch (err) {
|
|
506
|
-
|
|
940
|
+
log2.warn({ err, swarmId }, "swarm_spawn: inbox seed failed (non-fatal)");
|
|
507
941
|
}
|
|
508
942
|
} else {
|
|
509
|
-
|
|
943
|
+
log2.warn({ swarmId, hasSwarm: !!swarm, hasQueen: !!queen }, "swarm_spawn: queen instance not found, skipping inbox seed");
|
|
510
944
|
}
|
|
511
945
|
}
|
|
512
|
-
|
|
946
|
+
log2.info({ swarmId, teamName: msg.teamName }, "swarm_spawn: created");
|
|
513
947
|
return { ok: true, swarmId };
|
|
514
948
|
}
|
|
515
949
|
|
|
516
950
|
// src/hub-connection.ts
|
|
517
|
-
import { createLogger as
|
|
951
|
+
import { createLogger as createLogger3, previewFields } from "@mclawnet/logger";
|
|
518
952
|
import { DEFAULT_ASSISTANT_ROLE_ID } from "@mclawnet/memory";
|
|
519
|
-
var
|
|
953
|
+
var log3 = createLogger3({ module: "agent" });
|
|
954
|
+
var WAKE_WATCH_INTERVAL_MS = 1e3;
|
|
955
|
+
var WAKE_WATCH_JUMP_THRESHOLD_MS = 15e3;
|
|
956
|
+
function extractErrorCode(err) {
|
|
957
|
+
if (err && typeof err === "object" && "code" in err && typeof err.code === "string") {
|
|
958
|
+
return err.code;
|
|
959
|
+
}
|
|
960
|
+
return void 0;
|
|
961
|
+
}
|
|
520
962
|
var HubConnection = class {
|
|
521
963
|
ws = null;
|
|
522
964
|
heartbeatTimer = null;
|
|
@@ -556,6 +998,9 @@ var HubConnection = class {
|
|
|
556
998
|
onConnectCb;
|
|
557
999
|
onDisconnect;
|
|
558
1000
|
onError;
|
|
1001
|
+
onActivity;
|
|
1002
|
+
/** (N5) Shared WorkspaceManager for chat `useWorktree` flow. */
|
|
1003
|
+
workspaceManager;
|
|
559
1004
|
constructor(opts) {
|
|
560
1005
|
this.hubUrl = opts.hubUrl;
|
|
561
1006
|
this.token = opts.token;
|
|
@@ -569,6 +1014,8 @@ var HubConnection = class {
|
|
|
569
1014
|
this.onConnectCb = opts.onConnect;
|
|
570
1015
|
this.onDisconnect = opts.onDisconnect;
|
|
571
1016
|
this.onError = opts.onError;
|
|
1017
|
+
this.onActivity = opts.onActivity;
|
|
1018
|
+
this.workspaceManager = opts.workspaceManager;
|
|
572
1019
|
this.startWakeWatch();
|
|
573
1020
|
}
|
|
574
1021
|
setSessionManager(manager) {
|
|
@@ -620,7 +1067,7 @@ var HubConnection = class {
|
|
|
620
1067
|
return;
|
|
621
1068
|
}
|
|
622
1069
|
if (this.authState === "pending" && data.type === "auth_required") {
|
|
623
|
-
|
|
1070
|
+
log3.info("hub requires auth, sending credentials");
|
|
624
1071
|
this.authState = "authenticating";
|
|
625
1072
|
this.sendRaw({
|
|
626
1073
|
type: "auth",
|
|
@@ -632,11 +1079,17 @@ var HubConnection = class {
|
|
|
632
1079
|
return;
|
|
633
1080
|
}
|
|
634
1081
|
if (this.authState === "authenticating" && data.type === "registered") {
|
|
635
|
-
|
|
1082
|
+
log3.info({ agentId: data.agentId }, "registered with hub");
|
|
636
1083
|
this.authState = "authenticated";
|
|
637
1084
|
this.agentId = data.agentId ?? null;
|
|
638
1085
|
this.reconnectDelay = DEFAULT_RECONNECT_MS;
|
|
639
1086
|
this.startHeartbeat();
|
|
1087
|
+
collectAgentManifest().then((payload) => {
|
|
1088
|
+
log3.debug({ backends: payload.backends.length }, "sending agent.manifest");
|
|
1089
|
+
this.send({ type: "agent.manifest", payload });
|
|
1090
|
+
}).catch((err) => {
|
|
1091
|
+
log3.warn({ err }, "failed to collect agent manifest \u2014 skipping upload");
|
|
1092
|
+
});
|
|
640
1093
|
this.onConnectCb?.(this.agentId);
|
|
641
1094
|
this.tryRecoverSwarms();
|
|
642
1095
|
this.reportLiveSessions();
|
|
@@ -644,9 +1097,20 @@ var HubConnection = class {
|
|
|
644
1097
|
}
|
|
645
1098
|
if (this.authState === "authenticated") {
|
|
646
1099
|
this.lastAckAt = Date.now();
|
|
647
|
-
|
|
1100
|
+
const protocolType = data.type;
|
|
1101
|
+
if (protocolType === "heartbeat_ack") {
|
|
648
1102
|
return;
|
|
649
1103
|
}
|
|
1104
|
+
if (protocolType === "ack") {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (this.onActivity) {
|
|
1108
|
+
try {
|
|
1109
|
+
this.onActivity();
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
log3.warn({ err }, "onActivity callback threw; ignored");
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
650
1114
|
if (this.handleSessionMessage(data)) return;
|
|
651
1115
|
this.onMessage?.(data);
|
|
652
1116
|
}
|
|
@@ -655,19 +1119,19 @@ var HubConnection = class {
|
|
|
655
1119
|
this.lastAckAt = Date.now();
|
|
656
1120
|
});
|
|
657
1121
|
this.ws.on("close", (code, reason) => {
|
|
658
|
-
|
|
1122
|
+
log3.warn({ code, reason: reason.toString() }, "disconnected from hub");
|
|
659
1123
|
this.stopHeartbeat();
|
|
660
1124
|
this.ws = null;
|
|
661
1125
|
this.authState = "pending";
|
|
662
1126
|
this.onDisconnect?.(code, reason.toString());
|
|
663
1127
|
if (code === WS_CLOSE_INVALID_TOKEN) {
|
|
664
|
-
|
|
1128
|
+
log3.error("auth failed \u2014 not reconnecting, check your token");
|
|
665
1129
|
return;
|
|
666
1130
|
}
|
|
667
1131
|
this.scheduleReconnect();
|
|
668
1132
|
});
|
|
669
1133
|
this.ws.on("error", (err) => {
|
|
670
|
-
|
|
1134
|
+
log3.error({ err }, "ws connection error");
|
|
671
1135
|
this.onError?.(err);
|
|
672
1136
|
if (this.ws) {
|
|
673
1137
|
const dying = this.ws;
|
|
@@ -702,11 +1166,11 @@ var HubConnection = class {
|
|
|
702
1166
|
return true;
|
|
703
1167
|
}
|
|
704
1168
|
if (msg.type === "fs.list_dir") {
|
|
705
|
-
|
|
1169
|
+
log3.info({ path: msg.path }, "fs.list_dir");
|
|
706
1170
|
handleListDir(msg.path).then((result) => {
|
|
707
1171
|
this.send({ type: "fs.list_dir_result", requestId: msg.requestId, ...result });
|
|
708
1172
|
}).catch((err) => {
|
|
709
|
-
|
|
1173
|
+
log3.error({ err, path: msg.path }, "fs.list_dir failed");
|
|
710
1174
|
this.send({
|
|
711
1175
|
type: "fs.list_dir_result",
|
|
712
1176
|
requestId: msg.requestId,
|
|
@@ -717,21 +1181,21 @@ var HubConnection = class {
|
|
|
717
1181
|
return true;
|
|
718
1182
|
}
|
|
719
1183
|
if (msg.type === "list_folders") {
|
|
720
|
-
|
|
1184
|
+
log3.info("list_folders");
|
|
721
1185
|
handleListFolders().then((result) => {
|
|
722
1186
|
this.send({ type: "folders_list_result", requestId: msg.requestId, ...result });
|
|
723
1187
|
}).catch((err) => {
|
|
724
|
-
|
|
1188
|
+
log3.error({ err }, "list_folders failed");
|
|
725
1189
|
this.send({ type: "folders_list_result", requestId: msg.requestId, folders: [] });
|
|
726
1190
|
});
|
|
727
1191
|
return true;
|
|
728
1192
|
}
|
|
729
1193
|
if (msg.type === "list_history_sessions") {
|
|
730
|
-
|
|
1194
|
+
log3.info({ workDir: msg.workDir }, "list_history_sessions");
|
|
731
1195
|
handleListHistorySessions(msg.workDir).then((result) => {
|
|
732
1196
|
this.send({ type: "history_sessions_result", requestId: msg.requestId, ...result });
|
|
733
1197
|
}).catch((err) => {
|
|
734
|
-
|
|
1198
|
+
log3.error({ err, workDir: msg.workDir }, "list_history_sessions failed");
|
|
735
1199
|
this.send({
|
|
736
1200
|
type: "history_sessions_result",
|
|
737
1201
|
requestId: msg.requestId,
|
|
@@ -742,70 +1206,70 @@ var HubConnection = class {
|
|
|
742
1206
|
return true;
|
|
743
1207
|
}
|
|
744
1208
|
if (msg.type === "load_session_history") {
|
|
745
|
-
|
|
1209
|
+
log3.info(
|
|
746
1210
|
{ workDir: msg.workDir, backendSessionId: msg.backendSessionId, before: msg.before, limit: msg.limit },
|
|
747
1211
|
"load_session_history"
|
|
748
1212
|
);
|
|
749
1213
|
handleLoadSessionHistory(msg.workDir, msg.backendSessionId, { before: msg.before, limit: msg.limit }).then((result) => {
|
|
750
1214
|
this.send({ type: "session_history_result", requestId: msg.requestId, ...result });
|
|
751
1215
|
}).catch((err) => {
|
|
752
|
-
|
|
1216
|
+
log3.error({ err, workDir: msg.workDir }, "load_session_history failed");
|
|
753
1217
|
this.send({ type: "session_history_result", requestId: msg.requestId, messages: [], oldestSeq: 0, hasMore: false });
|
|
754
1218
|
});
|
|
755
1219
|
return true;
|
|
756
1220
|
}
|
|
757
1221
|
if (msg.type === "projects.list") {
|
|
758
|
-
|
|
1222
|
+
log3.info("projects.list");
|
|
759
1223
|
handleProjectsList().then((r) => this.send({ type: "projects.list_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
760
|
-
|
|
1224
|
+
log3.error({ err }, "projects.list failed");
|
|
761
1225
|
this.send({ type: "projects.list_result", requestId: msg.requestId, projects: [] });
|
|
762
1226
|
});
|
|
763
1227
|
return true;
|
|
764
1228
|
}
|
|
765
1229
|
if (msg.type === "projects.get") {
|
|
766
|
-
|
|
1230
|
+
log3.info({ encodedCwd: msg.encodedCwd }, "projects.get");
|
|
767
1231
|
handleProjectsGet(msg.encodedCwd).then((r) => this.send({ type: "projects.get_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
768
|
-
|
|
1232
|
+
log3.error({ err, encodedCwd: msg.encodedCwd }, "projects.get failed");
|
|
769
1233
|
this.send({ type: "projects.get_result", requestId: msg.requestId, project: null });
|
|
770
1234
|
});
|
|
771
1235
|
return true;
|
|
772
1236
|
}
|
|
773
1237
|
if (msg.type === "projects.tasks") {
|
|
774
|
-
|
|
1238
|
+
log3.info({ encodedCwd: msg.encodedCwd, status: msg.status, swarmId: msg.swarmId, owner: msg.owner }, "projects.tasks");
|
|
775
1239
|
handleProjectsTasks(msg.encodedCwd, { status: msg.status, swarmId: msg.swarmId, owner: msg.owner }).then((r) => this.send({ type: "projects.tasks_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
776
|
-
|
|
1240
|
+
log3.error({ err, encodedCwd: msg.encodedCwd, status: msg.status, swarmId: msg.swarmId, owner: msg.owner }, "projects.tasks failed");
|
|
777
1241
|
this.send({ type: "projects.tasks_result", requestId: msg.requestId, tasks: [] });
|
|
778
1242
|
});
|
|
779
1243
|
return true;
|
|
780
1244
|
}
|
|
781
1245
|
if (msg.type === "projects.task") {
|
|
782
|
-
|
|
1246
|
+
log3.info({ encodedCwd: msg.encodedCwd, taskId: msg.taskId }, "projects.task");
|
|
783
1247
|
handleProjectsTask(msg.encodedCwd, msg.taskId).then((r) => this.send({ type: "projects.task_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
784
|
-
|
|
1248
|
+
log3.error({ err, encodedCwd: msg.encodedCwd, taskId: msg.taskId }, "projects.task failed");
|
|
785
1249
|
this.send({ type: "projects.task_result", requestId: msg.requestId, task: null });
|
|
786
1250
|
});
|
|
787
1251
|
return true;
|
|
788
1252
|
}
|
|
789
1253
|
if (msg.type === "projects.swarms") {
|
|
790
|
-
|
|
1254
|
+
log3.info({ encodedCwd: msg.encodedCwd }, "projects.swarms");
|
|
791
1255
|
handleProjectsSwarms(msg.encodedCwd).then((r) => this.send({ type: "projects.swarms_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
792
|
-
|
|
1256
|
+
log3.error({ err, encodedCwd: msg.encodedCwd }, "projects.swarms failed");
|
|
793
1257
|
this.send({ type: "projects.swarms_result", requestId: msg.requestId, swarms: [] });
|
|
794
1258
|
});
|
|
795
1259
|
return true;
|
|
796
1260
|
}
|
|
797
1261
|
if (msg.type === "projects.swarm") {
|
|
798
|
-
|
|
1262
|
+
log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm");
|
|
799
1263
|
handleProjectsSwarm(msg.encodedCwd, msg.swarmId).then((r) => this.send({ type: "projects.swarm_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
800
|
-
|
|
1264
|
+
log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm failed");
|
|
801
1265
|
this.send({ type: "projects.swarm_result", requestId: msg.requestId, snapshot: null, tasks: [] });
|
|
802
1266
|
});
|
|
803
1267
|
return true;
|
|
804
1268
|
}
|
|
805
1269
|
if (msg.type === "projects.inboxes") {
|
|
806
|
-
|
|
1270
|
+
log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.inboxes");
|
|
807
1271
|
handleProjectsInboxes(msg.encodedCwd, msg.swarmId).then((r) => this.send({ type: "projects.inboxes_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
808
|
-
|
|
1272
|
+
log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.inboxes failed");
|
|
809
1273
|
this.send({
|
|
810
1274
|
type: "projects.inboxes_result",
|
|
811
1275
|
requestId: msg.requestId,
|
|
@@ -817,9 +1281,9 @@ var HubConnection = class {
|
|
|
817
1281
|
return true;
|
|
818
1282
|
}
|
|
819
1283
|
if (msg.type === "projects.inbox") {
|
|
820
|
-
|
|
1284
|
+
log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId, instanceId: msg.instanceId }, "projects.inbox");
|
|
821
1285
|
handleProjectsInbox(msg.encodedCwd, msg.swarmId, msg.instanceId).then((r) => this.send({ type: "projects.inbox_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
822
|
-
|
|
1286
|
+
log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId, instanceId: msg.instanceId }, "projects.inbox failed");
|
|
823
1287
|
this.send({
|
|
824
1288
|
type: "projects.inbox_result",
|
|
825
1289
|
requestId: msg.requestId,
|
|
@@ -831,9 +1295,9 @@ var HubConnection = class {
|
|
|
831
1295
|
return true;
|
|
832
1296
|
}
|
|
833
1297
|
if (msg.type === "projects.swarm_session_id") {
|
|
834
|
-
|
|
1298
|
+
log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm_session_id");
|
|
835
1299
|
handleProjectsSwarmSessionId(msg.encodedCwd, msg.swarmId).then((r) => this.send({ type: "projects.swarm_session_id_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
836
|
-
|
|
1300
|
+
log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.swarm_session_id failed");
|
|
837
1301
|
this.send({
|
|
838
1302
|
type: "projects.swarm_session_id_result",
|
|
839
1303
|
requestId: msg.requestId,
|
|
@@ -845,17 +1309,17 @@ var HubConnection = class {
|
|
|
845
1309
|
return true;
|
|
846
1310
|
}
|
|
847
1311
|
if (msg.type === "projects.files_list") {
|
|
848
|
-
|
|
1312
|
+
log3.info({ encodedCwd: msg.encodedCwd }, "projects.files_list");
|
|
849
1313
|
handleProjectsFilesList(msg.encodedCwd).then((r) => this.send({ type: "projects.files_list_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
850
|
-
|
|
1314
|
+
log3.error({ err, encodedCwd: msg.encodedCwd }, "projects.files_list failed");
|
|
851
1315
|
this.send({ type: "projects.files_list_result", requestId: msg.requestId, projectFound: false, files: [] });
|
|
852
1316
|
});
|
|
853
1317
|
return true;
|
|
854
1318
|
}
|
|
855
1319
|
if (msg.type === "projects.file_read") {
|
|
856
|
-
|
|
1320
|
+
log3.info({ encodedCwd: msg.encodedCwd, relPath: msg.relPath }, "projects.file_read");
|
|
857
1321
|
handleProjectsFileRead(msg.encodedCwd, msg.relPath).then((r) => this.send({ type: "projects.file_read_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
858
|
-
|
|
1322
|
+
log3.error({ err, encodedCwd: msg.encodedCwd, relPath: msg.relPath }, "projects.file_read failed");
|
|
859
1323
|
this.send({
|
|
860
1324
|
type: "projects.file_read_result",
|
|
861
1325
|
requestId: msg.requestId,
|
|
@@ -867,9 +1331,9 @@ var HubConnection = class {
|
|
|
867
1331
|
return true;
|
|
868
1332
|
}
|
|
869
1333
|
if (msg.type === "projects.file_write") {
|
|
870
|
-
|
|
1334
|
+
log3.info({ encodedCwd: msg.encodedCwd, relPath: msg.relPath, hasIfMatch: !!msg.ifMatchEtag }, "projects.file_write");
|
|
871
1335
|
handleProjectsFileWrite(msg.encodedCwd, msg.relPath, msg.content, msg.ifMatchEtag).then((r) => this.send({ type: "projects.file_write_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
872
|
-
|
|
1336
|
+
log3.error({ err, encodedCwd: msg.encodedCwd, relPath: msg.relPath }, "projects.file_write failed");
|
|
873
1337
|
this.send({
|
|
874
1338
|
type: "projects.file_write_result",
|
|
875
1339
|
requestId: msg.requestId,
|
|
@@ -880,8 +1344,193 @@ var HubConnection = class {
|
|
|
880
1344
|
});
|
|
881
1345
|
return true;
|
|
882
1346
|
}
|
|
1347
|
+
if (msg.type === "projects.ensure_project") {
|
|
1348
|
+
log3.info({ workDir: msg.workDir, createDir: msg.createDir }, "projects.ensure_project");
|
|
1349
|
+
handleProjectsEnsureProject(msg.workDir, { createDir: msg.createDir }).then((r) => this.send({ type: "projects.ensure_project_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
1350
|
+
log3.error({ err, workDir: msg.workDir }, "projects.ensure_project failed");
|
|
1351
|
+
this.send({
|
|
1352
|
+
type: "projects.ensure_project_result",
|
|
1353
|
+
requestId: msg.requestId,
|
|
1354
|
+
ok: false,
|
|
1355
|
+
project: null,
|
|
1356
|
+
error: { status: 500, code: "agent_error", message: "agent error" }
|
|
1357
|
+
});
|
|
1358
|
+
});
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
if (msg.type === "workdir.mkdir") {
|
|
1362
|
+
log3.info({ workDir: msg.workDir }, "workdir.mkdir");
|
|
1363
|
+
handleWorkdirMkdir(msg.workDir).then((r) => this.send({ type: "workdir.mkdir_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
1364
|
+
log3.error({ err, workDir: msg.workDir }, "workdir.mkdir failed");
|
|
1365
|
+
this.send({
|
|
1366
|
+
type: "workdir.mkdir_result",
|
|
1367
|
+
requestId: msg.requestId,
|
|
1368
|
+
ok: false,
|
|
1369
|
+
error: { status: 500, code: "agent_error", message: "agent error" }
|
|
1370
|
+
});
|
|
1371
|
+
});
|
|
1372
|
+
return true;
|
|
1373
|
+
}
|
|
1374
|
+
if (msg.type === "projects.shipment_get") {
|
|
1375
|
+
log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.shipment_get");
|
|
1376
|
+
handleProjectsShipmentGet(msg.encodedCwd, msg.swarmId).then((r) => this.send({ type: "projects.shipment_get_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
1377
|
+
log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.shipment_get failed");
|
|
1378
|
+
this.send({
|
|
1379
|
+
type: "projects.shipment_get_result",
|
|
1380
|
+
requestId: msg.requestId,
|
|
1381
|
+
projectFound: false,
|
|
1382
|
+
shipment: null
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
return true;
|
|
1386
|
+
}
|
|
1387
|
+
if (msg.type === "projects.shipment_diff") {
|
|
1388
|
+
log3.info({ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.shipment_diff");
|
|
1389
|
+
handleProjectsShipmentDiffFull(msg.encodedCwd, msg.swarmId).then((r) => this.send({ type: "projects.shipment_diff_result", requestId: msg.requestId, ...r })).catch((err) => {
|
|
1390
|
+
log3.error({ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId }, "projects.shipment_diff failed");
|
|
1391
|
+
this.send({
|
|
1392
|
+
type: "projects.shipment_diff_result",
|
|
1393
|
+
requestId: msg.requestId,
|
|
1394
|
+
projectFound: false,
|
|
1395
|
+
content: null
|
|
1396
|
+
});
|
|
1397
|
+
});
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
if (msg.type === "projects.shipment_action") {
|
|
1401
|
+
log3.info(
|
|
1402
|
+
{ encodedCwd: msg.encodedCwd, swarmId: msg.swarmId, action: msg.action, force: msg.force },
|
|
1403
|
+
"projects.shipment_action"
|
|
1404
|
+
);
|
|
1405
|
+
handleProjectsShipmentAction(msg.encodedCwd, msg.swarmId, msg.action, msg.force).then(
|
|
1406
|
+
(r) => this.send({
|
|
1407
|
+
type: "projects.shipment_action_result",
|
|
1408
|
+
requestId: msg.requestId,
|
|
1409
|
+
...r
|
|
1410
|
+
})
|
|
1411
|
+
).catch((err) => {
|
|
1412
|
+
log3.error(
|
|
1413
|
+
{ err, encodedCwd: msg.encodedCwd, swarmId: msg.swarmId, action: msg.action },
|
|
1414
|
+
"projects.shipment_action failed"
|
|
1415
|
+
);
|
|
1416
|
+
this.send({
|
|
1417
|
+
type: "projects.shipment_action_result",
|
|
1418
|
+
requestId: msg.requestId,
|
|
1419
|
+
projectFound: false,
|
|
1420
|
+
ok: false,
|
|
1421
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1422
|
+
});
|
|
1423
|
+
});
|
|
1424
|
+
return true;
|
|
1425
|
+
}
|
|
1426
|
+
if (msg.type === "projects.shipments_list") {
|
|
1427
|
+
log3.info({ encodedCwd: msg.encodedCwd }, "projects.shipments_list");
|
|
1428
|
+
handleProjectsShipmentsList(msg.encodedCwd).then(
|
|
1429
|
+
(r) => this.send({
|
|
1430
|
+
type: "projects.shipments_list_result",
|
|
1431
|
+
requestId: msg.requestId,
|
|
1432
|
+
...r
|
|
1433
|
+
})
|
|
1434
|
+
).catch((err) => {
|
|
1435
|
+
log3.error(
|
|
1436
|
+
{ err, encodedCwd: msg.encodedCwd },
|
|
1437
|
+
"projects.shipments_list failed"
|
|
1438
|
+
);
|
|
1439
|
+
this.send({
|
|
1440
|
+
type: "projects.shipments_list_result",
|
|
1441
|
+
requestId: msg.requestId,
|
|
1442
|
+
projectFound: false,
|
|
1443
|
+
shipments: []
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1448
|
+
if (msg.type === "projects.always_on_get") {
|
|
1449
|
+
log3.info({ encodedCwd: msg.encodedCwd }, "projects.always_on_get");
|
|
1450
|
+
handleProjectsAlwaysOnGet(msg.encodedCwd).then(
|
|
1451
|
+
(r) => this.send({
|
|
1452
|
+
type: "projects.always_on_get_result",
|
|
1453
|
+
requestId: msg.requestId,
|
|
1454
|
+
...r
|
|
1455
|
+
})
|
|
1456
|
+
).catch((err) => {
|
|
1457
|
+
log3.error(
|
|
1458
|
+
{ err, encodedCwd: msg.encodedCwd },
|
|
1459
|
+
"projects.always_on_get failed"
|
|
1460
|
+
);
|
|
1461
|
+
this.send({
|
|
1462
|
+
type: "projects.always_on_get_result",
|
|
1463
|
+
requestId: msg.requestId,
|
|
1464
|
+
projectFound: false,
|
|
1465
|
+
config: { mode: "off", dailyBudget: 3 },
|
|
1466
|
+
snapshot: {
|
|
1467
|
+
mode: "off",
|
|
1468
|
+
ticksToday: 0,
|
|
1469
|
+
lastFireAt: null,
|
|
1470
|
+
nextTickAt: null,
|
|
1471
|
+
recentTicks: []
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
});
|
|
1475
|
+
return true;
|
|
1476
|
+
}
|
|
1477
|
+
if (msg.type === "projects.always_on_set") {
|
|
1478
|
+
log3.info(
|
|
1479
|
+
{ encodedCwd: msg.encodedCwd, patch: msg.config },
|
|
1480
|
+
"projects.always_on_set"
|
|
1481
|
+
);
|
|
1482
|
+
handleProjectsAlwaysOnSet(msg.encodedCwd, msg.config).then(
|
|
1483
|
+
(r) => this.send({
|
|
1484
|
+
type: "projects.always_on_set_result",
|
|
1485
|
+
requestId: msg.requestId,
|
|
1486
|
+
...r
|
|
1487
|
+
})
|
|
1488
|
+
).catch((err) => {
|
|
1489
|
+
log3.error(
|
|
1490
|
+
{ err, encodedCwd: msg.encodedCwd },
|
|
1491
|
+
"projects.always_on_set failed"
|
|
1492
|
+
);
|
|
1493
|
+
this.send({
|
|
1494
|
+
type: "projects.always_on_set_result",
|
|
1495
|
+
requestId: msg.requestId,
|
|
1496
|
+
projectFound: false,
|
|
1497
|
+
config: { mode: "off", dailyBudget: 3 },
|
|
1498
|
+
snapshot: {
|
|
1499
|
+
mode: "off",
|
|
1500
|
+
ticksToday: 0,
|
|
1501
|
+
lastFireAt: null,
|
|
1502
|
+
nextTickAt: null,
|
|
1503
|
+
recentTicks: []
|
|
1504
|
+
},
|
|
1505
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1506
|
+
});
|
|
1507
|
+
});
|
|
1508
|
+
return true;
|
|
1509
|
+
}
|
|
1510
|
+
if (msg.type === "projects.always_on_tick_now") {
|
|
1511
|
+
log3.info({ encodedCwd: msg.encodedCwd }, "projects.always_on_tick_now");
|
|
1512
|
+
handleProjectsAlwaysOnTickNow(msg.encodedCwd).then(
|
|
1513
|
+
(r) => this.send({
|
|
1514
|
+
type: "projects.always_on_tick_now_result",
|
|
1515
|
+
requestId: msg.requestId,
|
|
1516
|
+
...r
|
|
1517
|
+
})
|
|
1518
|
+
).catch((err) => {
|
|
1519
|
+
log3.error(
|
|
1520
|
+
{ err, encodedCwd: msg.encodedCwd },
|
|
1521
|
+
"projects.always_on_tick_now failed"
|
|
1522
|
+
);
|
|
1523
|
+
this.send({
|
|
1524
|
+
type: "projects.always_on_tick_now_result",
|
|
1525
|
+
requestId: msg.requestId,
|
|
1526
|
+
projectFound: false,
|
|
1527
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1528
|
+
});
|
|
1529
|
+
});
|
|
1530
|
+
return true;
|
|
1531
|
+
}
|
|
883
1532
|
if (msg.type === "list_roles") {
|
|
884
|
-
|
|
1533
|
+
log3.info("list_roles");
|
|
885
1534
|
const roleNames = listRoles();
|
|
886
1535
|
const roles = roleNames.map((name) => {
|
|
887
1536
|
try {
|
|
@@ -907,7 +1556,7 @@ var HubConnection = class {
|
|
|
907
1556
|
return true;
|
|
908
1557
|
}
|
|
909
1558
|
if (msg.type === "list_templates") {
|
|
910
|
-
|
|
1559
|
+
log3.info("list_templates");
|
|
911
1560
|
try {
|
|
912
1561
|
const names = listTemplates();
|
|
913
1562
|
const templates = names.map((name) => {
|
|
@@ -926,7 +1575,7 @@ var HubConnection = class {
|
|
|
926
1575
|
templates
|
|
927
1576
|
});
|
|
928
1577
|
} catch (err) {
|
|
929
|
-
|
|
1578
|
+
log3.error({ err }, "list_templates failed");
|
|
930
1579
|
this.send({
|
|
931
1580
|
type: "templates_list_result",
|
|
932
1581
|
sessionId: msg.sessionId,
|
|
@@ -938,9 +1587,9 @@ var HubConnection = class {
|
|
|
938
1587
|
if (msg.type === "generic.request") {
|
|
939
1588
|
const handler = this.namespaceHandlers.get(msg.namespace);
|
|
940
1589
|
if (handler) {
|
|
941
|
-
|
|
1590
|
+
log3.info({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId }, "generic.request received");
|
|
942
1591
|
handler(msg).then((result) => {
|
|
943
|
-
|
|
1592
|
+
log3.info({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId }, "generic.request handled OK");
|
|
944
1593
|
this.send({
|
|
945
1594
|
type: "generic.response",
|
|
946
1595
|
namespace: msg.namespace,
|
|
@@ -949,7 +1598,7 @@ var HubConnection = class {
|
|
|
949
1598
|
requestId: msg.requestId
|
|
950
1599
|
});
|
|
951
1600
|
}).catch((err) => {
|
|
952
|
-
|
|
1601
|
+
log3.error({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId, err }, "generic.request handler error");
|
|
953
1602
|
this.send({
|
|
954
1603
|
type: "generic.response",
|
|
955
1604
|
namespace: msg.namespace,
|
|
@@ -961,7 +1610,7 @@ var HubConnection = class {
|
|
|
961
1610
|
});
|
|
962
1611
|
return true;
|
|
963
1612
|
}
|
|
964
|
-
|
|
1613
|
+
log3.warn({ namespace: msg.namespace, action: msg.action, requestId: msg.requestId }, "generic.request unknown namespace");
|
|
965
1614
|
this.send({
|
|
966
1615
|
type: "generic.response",
|
|
967
1616
|
namespace: msg.namespace,
|
|
@@ -978,9 +1627,9 @@ var HubConnection = class {
|
|
|
978
1627
|
defaultWorkDir: process.cwd(),
|
|
979
1628
|
home: process.env.CLAWNET_HOME
|
|
980
1629
|
}).then((result) => {
|
|
981
|
-
|
|
1630
|
+
log3.info({ type: msg.type, ok: result.ok, swarmId: result.swarmId, error: result.error }, "swarm control handled");
|
|
982
1631
|
}).catch((err) => {
|
|
983
|
-
|
|
1632
|
+
log3.error({ err, type: msg.type }, "swarm control crashed");
|
|
984
1633
|
});
|
|
985
1634
|
return true;
|
|
986
1635
|
}
|
|
@@ -1009,7 +1658,7 @@ var HubConnection = class {
|
|
|
1009
1658
|
...result.error ? { error: result.error } : {}
|
|
1010
1659
|
});
|
|
1011
1660
|
}).catch((err) => {
|
|
1012
|
-
|
|
1661
|
+
log3.error({ err, swarmId: msg.swarmId }, "projects.swarm_add_role crashed");
|
|
1013
1662
|
this.send({
|
|
1014
1663
|
type: "projects.swarm_add_role_result",
|
|
1015
1664
|
requestId,
|
|
@@ -1022,12 +1671,13 @@ var HubConnection = class {
|
|
|
1022
1671
|
if (msg.type === "swarm.execute" && this.swarmCoordinator) {
|
|
1023
1672
|
const { sessionId, content, workDir, targetInstance, crewConfig } = msg;
|
|
1024
1673
|
if (this.swarmCoordinator.hasSwarm(sessionId)) {
|
|
1025
|
-
|
|
1674
|
+
log3.info({ sessionId, targetInstance }, "swarm.execute: forwarding to existing swarm");
|
|
1026
1675
|
this.swarmCoordinator.handleUserMessage(sessionId, content, targetInstance).catch((err) => {
|
|
1027
1676
|
this.send({
|
|
1028
1677
|
type: "session.error",
|
|
1029
1678
|
sessionId,
|
|
1030
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1679
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1680
|
+
code: extractErrorCode(err)
|
|
1031
1681
|
});
|
|
1032
1682
|
});
|
|
1033
1683
|
} else if (this.finishedSwarms.has(sessionId)) {
|
|
@@ -1036,16 +1686,17 @@ var HubConnection = class {
|
|
|
1036
1686
|
const templateName = crewConfig.templateName;
|
|
1037
1687
|
const roles = crewConfig.roles;
|
|
1038
1688
|
const displayName = crewConfig.displayName;
|
|
1039
|
-
|
|
1689
|
+
log3.info({ sessionId, templateName, rolesCount: roles?.length }, "swarm.execute: continuing finished swarm");
|
|
1040
1690
|
this.swarmCoordinator.create(sessionId, { workDir, templateName, displayName, roles, task: content, isContinuation: true }).catch((err) => {
|
|
1041
1691
|
this.send({
|
|
1042
1692
|
type: "session.error",
|
|
1043
1693
|
sessionId,
|
|
1044
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1694
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1695
|
+
code: extractErrorCode(err)
|
|
1045
1696
|
});
|
|
1046
1697
|
});
|
|
1047
1698
|
} else {
|
|
1048
|
-
|
|
1699
|
+
log3.info({ sessionId }, "swarm.execute ignored: swarm finished, no config to recreate");
|
|
1049
1700
|
this.send({
|
|
1050
1701
|
type: "session.error",
|
|
1051
1702
|
sessionId,
|
|
@@ -1056,16 +1707,17 @@ var HubConnection = class {
|
|
|
1056
1707
|
const templateName = crewConfig.templateName;
|
|
1057
1708
|
const roles = crewConfig.roles;
|
|
1058
1709
|
const displayName = crewConfig.displayName;
|
|
1059
|
-
|
|
1710
|
+
log3.info({ sessionId, templateName, rolesCount: roles?.length }, "swarm.execute: creating new swarm");
|
|
1060
1711
|
this.swarmCoordinator.create(sessionId, { workDir, templateName, displayName, roles, task: content }).catch((err) => {
|
|
1061
1712
|
this.send({
|
|
1062
1713
|
type: "session.error",
|
|
1063
1714
|
sessionId,
|
|
1064
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1715
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1716
|
+
code: extractErrorCode(err)
|
|
1065
1717
|
});
|
|
1066
1718
|
});
|
|
1067
1719
|
} else {
|
|
1068
|
-
|
|
1720
|
+
log3.info({ sessionId }, "swarm.execute ignored: swarm not found, no config");
|
|
1069
1721
|
this.send({
|
|
1070
1722
|
type: "session.error",
|
|
1071
1723
|
sessionId,
|
|
@@ -1077,7 +1729,7 @@ var HubConnection = class {
|
|
|
1077
1729
|
if (!this.sessionManager) return false;
|
|
1078
1730
|
if (msg.type === "abort_execution") {
|
|
1079
1731
|
const { sessionId } = msg;
|
|
1080
|
-
|
|
1732
|
+
log3.info({ sessionId }, "abort_execution");
|
|
1081
1733
|
if (this.sessionManager?.hasSession(sessionId)) {
|
|
1082
1734
|
this.sessionManager.abortSession(sessionId).then(() => {
|
|
1083
1735
|
this.send({ type: "execution_aborted", sessionId });
|
|
@@ -1093,11 +1745,11 @@ var HubConnection = class {
|
|
|
1093
1745
|
const { sessionId, content, workDir, backendSessionId, useBrainCore } = msg;
|
|
1094
1746
|
const sm = this.sessionManager;
|
|
1095
1747
|
const spawnAndSend = (resumeId, label, maxOutputTokens) => {
|
|
1096
|
-
|
|
1748
|
+
log3.info(
|
|
1097
1749
|
{ sessionId, backendSessionId: resumeId, workDir, label, maxOutputTokens },
|
|
1098
1750
|
"claude.execute: spawning"
|
|
1099
1751
|
);
|
|
1100
|
-
|
|
1752
|
+
log3.debug({ sessionId, ...previewFields(content) }, "claude.execute: input");
|
|
1101
1753
|
sm.createSession({
|
|
1102
1754
|
sessionId,
|
|
1103
1755
|
workDir,
|
|
@@ -1107,47 +1759,260 @@ var HubConnection = class {
|
|
|
1107
1759
|
maxOutputTokens
|
|
1108
1760
|
}).then(() => {
|
|
1109
1761
|
sm.sendUserInput(sessionId, content).catch((err) => {
|
|
1110
|
-
|
|
1762
|
+
log3.warn({ err, sessionId }, "sendUserInput failed");
|
|
1111
1763
|
});
|
|
1112
1764
|
}).catch((err) => {
|
|
1113
1765
|
this.send({
|
|
1114
1766
|
type: "session.error",
|
|
1115
1767
|
sessionId,
|
|
1116
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1768
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1769
|
+
code: extractErrorCode(err)
|
|
1770
|
+
});
|
|
1771
|
+
});
|
|
1772
|
+
};
|
|
1773
|
+
if (sm.isHealthy(sessionId)) {
|
|
1774
|
+
log3.info({ sessionId }, "claude.execute: reusing healthy session");
|
|
1775
|
+
log3.debug({ sessionId, ...previewFields(content) }, "claude.execute: input");
|
|
1776
|
+
sm.sendUserInput(sessionId, content).catch((err) => {
|
|
1777
|
+
log3.warn({ err, sessionId }, "sendUserInput failed");
|
|
1778
|
+
});
|
|
1779
|
+
} else if (sm.hasSession(sessionId) && backendSessionId) {
|
|
1780
|
+
const recommendedMax = sm.getRecommendedMaxOutputTokens(sessionId);
|
|
1781
|
+
log3.warn({ sessionId, backendSessionId }, "claude.execute: session unhealthy, recreating with --resume");
|
|
1782
|
+
this.send({
|
|
1783
|
+
type: "session.died",
|
|
1784
|
+
sessionId,
|
|
1785
|
+
reason: "unhealthy_before_input"
|
|
1786
|
+
});
|
|
1787
|
+
sm.abortSession(sessionId).catch((err) => {
|
|
1788
|
+
log3.warn({ sessionId, err: err instanceof Error ? err.message : String(err) }, "abortSession failed during unhealthy fallback, proceeding to respawn");
|
|
1789
|
+
}).then(() => spawnAndSend(backendSessionId, "unhealthy_fallback_resume", recommendedMax));
|
|
1790
|
+
} else if (backendSessionId) {
|
|
1791
|
+
spawnAndSend(backendSessionId, "fresh_resume", sm.getRecommendedMaxOutputTokens(sessionId));
|
|
1792
|
+
} else {
|
|
1793
|
+
log3.warn(
|
|
1794
|
+
{ sessionId, workDir },
|
|
1795
|
+
"claude.execute: entering brand_new spawn path \u2014 no backendSessionId from hub; if this sessionId already has prior turns in db, upstream hub\u2192agent hydration is broken"
|
|
1796
|
+
);
|
|
1797
|
+
spawnAndSend(void 0, "brand_new", void 0);
|
|
1798
|
+
}
|
|
1799
|
+
return true;
|
|
1800
|
+
}
|
|
1801
|
+
if (msg.type === "session.create") {
|
|
1802
|
+
const {
|
|
1803
|
+
sessionId,
|
|
1804
|
+
workDir,
|
|
1805
|
+
backend,
|
|
1806
|
+
sticky,
|
|
1807
|
+
systemPrompt,
|
|
1808
|
+
useBrainCore,
|
|
1809
|
+
initialInput,
|
|
1810
|
+
backendSessionId,
|
|
1811
|
+
useWorktree,
|
|
1812
|
+
worktreeBranchName
|
|
1813
|
+
} = msg;
|
|
1814
|
+
const sm = this.sessionManager;
|
|
1815
|
+
log3.info(
|
|
1816
|
+
{
|
|
1817
|
+
sessionId,
|
|
1818
|
+
backend,
|
|
1819
|
+
hasSticky: !!sticky,
|
|
1820
|
+
hasInitialInput: !!initialInput,
|
|
1821
|
+
useWorktree: !!useWorktree
|
|
1822
|
+
},
|
|
1823
|
+
"session.create"
|
|
1824
|
+
);
|
|
1825
|
+
if (sm.hasSession(sessionId)) {
|
|
1826
|
+
log3.warn({ sessionId }, "session.create: session already exists, ignoring");
|
|
1827
|
+
return true;
|
|
1828
|
+
}
|
|
1829
|
+
(async () => {
|
|
1830
|
+
let effectiveWorkDir = workDir;
|
|
1831
|
+
let workspaceHandle;
|
|
1832
|
+
let workspaceEnv;
|
|
1833
|
+
if (useWorktree && workDir && this.workspaceManager) {
|
|
1834
|
+
try {
|
|
1835
|
+
const swarmId = `chat-${sessionId.slice(0, 8)}`;
|
|
1836
|
+
workspaceHandle = await this.workspaceManager.prepare({
|
|
1837
|
+
swarmId,
|
|
1838
|
+
projectRoot: workDir,
|
|
1839
|
+
// PR-A: forward user-supplied branch hint (hub already prepended
|
|
1840
|
+
// `clawnet/chat-` so this is the fully-qualified name). When
|
|
1841
|
+
// omitted, GitWorktreeProvider falls back to
|
|
1842
|
+
// `clawnet/swarm-chat-<sid8>` (its own default).
|
|
1843
|
+
...worktreeBranchName ? { branchName: worktreeBranchName } : {}
|
|
1844
|
+
});
|
|
1845
|
+
effectiveWorkDir = workspaceHandle.cwd;
|
|
1846
|
+
workspaceEnv = { CLAWNET_PROJECT_ROOT: workDir };
|
|
1847
|
+
this.send({
|
|
1848
|
+
type: "session.worktree_ready",
|
|
1849
|
+
sessionId,
|
|
1850
|
+
cwd: workspaceHandle.cwd,
|
|
1851
|
+
strategy: workspaceHandle.strategy,
|
|
1852
|
+
branchName: workspaceHandle.metadata.branchName ?? null,
|
|
1853
|
+
// PR-A review fix I3: forward the real base branch so ship
|
|
1854
|
+
// can target it instead of guessing "main".
|
|
1855
|
+
baseBranch: workspaceHandle.metadata.baseBranch ?? null,
|
|
1856
|
+
projectRoot: workDir
|
|
1857
|
+
});
|
|
1858
|
+
} catch (err) {
|
|
1859
|
+
log3.warn(
|
|
1860
|
+
{
|
|
1861
|
+
err: err instanceof Error ? err.message : String(err),
|
|
1862
|
+
sessionId,
|
|
1863
|
+
workDir
|
|
1864
|
+
},
|
|
1865
|
+
"session.create: workspace prepare failed, falling back to direct workDir"
|
|
1866
|
+
);
|
|
1867
|
+
workspaceHandle = void 0;
|
|
1868
|
+
workspaceEnv = void 0;
|
|
1869
|
+
effectiveWorkDir = workDir;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
try {
|
|
1873
|
+
await sm.createSession({
|
|
1874
|
+
sessionId,
|
|
1875
|
+
workDir: effectiveWorkDir,
|
|
1876
|
+
resumeId: backendSessionId,
|
|
1877
|
+
backend,
|
|
1878
|
+
model: sticky?.model,
|
|
1879
|
+
mode: sticky?.mode,
|
|
1880
|
+
systemPrompt,
|
|
1881
|
+
useBrainCore,
|
|
1882
|
+
roleId: DEFAULT_ASSISTANT_ROLE_ID,
|
|
1883
|
+
initialUserInput: initialInput,
|
|
1884
|
+
...workspaceEnv ? { env: workspaceEnv } : {}
|
|
1885
|
+
});
|
|
1886
|
+
if (workspaceHandle) {
|
|
1887
|
+
sm.attachWorkspace(sessionId, workspaceHandle);
|
|
1888
|
+
}
|
|
1889
|
+
if (initialInput !== void 0) {
|
|
1890
|
+
await sm.sendUserInput(sessionId, initialInput);
|
|
1891
|
+
}
|
|
1892
|
+
} catch (err) {
|
|
1893
|
+
if (workspaceHandle && this.workspaceManager && sm.getWorkspaceHandle(sessionId) === void 0) {
|
|
1894
|
+
this.workspaceManager.dispose(workspaceHandle.swarmId, { keep: false }).catch(
|
|
1895
|
+
(e) => log3.warn(
|
|
1896
|
+
{ err: e instanceof Error ? e.message : String(e), sessionId },
|
|
1897
|
+
"best-effort workspace dispose after createSession failure"
|
|
1898
|
+
)
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
this.send({
|
|
1902
|
+
type: "session.error",
|
|
1903
|
+
sessionId,
|
|
1904
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1905
|
+
code: extractErrorCode(err)
|
|
1117
1906
|
});
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1907
|
+
}
|
|
1908
|
+
})().catch((err) => {
|
|
1909
|
+
log3.error(
|
|
1910
|
+
{ sessionId, err: err instanceof Error ? err.message : String(err) },
|
|
1911
|
+
"session.create IIFE unexpectedly threw"
|
|
1912
|
+
);
|
|
1913
|
+
});
|
|
1914
|
+
return true;
|
|
1915
|
+
}
|
|
1916
|
+
if (msg.type === "session.input") {
|
|
1917
|
+
const { sessionId, content, messageMeta, backendSessionId, workDir, useBrainCore } = msg;
|
|
1918
|
+
const sm = this.sessionManager;
|
|
1919
|
+
log3.debug(
|
|
1920
|
+
{
|
|
1921
|
+
sessionId,
|
|
1922
|
+
hasMessageMeta: !!messageMeta,
|
|
1923
|
+
hasBackendSessionId: !!backendSessionId,
|
|
1924
|
+
...previewFields(content)
|
|
1925
|
+
},
|
|
1926
|
+
"session.input"
|
|
1927
|
+
);
|
|
1928
|
+
if (!sm.isHealthy(sessionId)) {
|
|
1929
|
+
if (backendSessionId) {
|
|
1930
|
+
log3.info(
|
|
1931
|
+
{ sessionId, backendSessionId, hadSession: sm.hasSession(sessionId) },
|
|
1932
|
+
"session.input: auto-recovering via --resume"
|
|
1933
|
+
);
|
|
1934
|
+
(async () => {
|
|
1935
|
+
try {
|
|
1936
|
+
if (sm.hasSession(sessionId)) {
|
|
1937
|
+
this.send({
|
|
1938
|
+
type: "session.died",
|
|
1939
|
+
sessionId,
|
|
1940
|
+
reason: "unhealthy_before_input"
|
|
1941
|
+
});
|
|
1942
|
+
await sm.abortSession(sessionId).catch((err) => {
|
|
1943
|
+
log3.warn(
|
|
1944
|
+
{ sessionId, err: err instanceof Error ? err.message : String(err) },
|
|
1945
|
+
"abortSession failed during session.input recovery, continuing"
|
|
1946
|
+
);
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
if (messageMeta) {
|
|
1950
|
+
sm.setTurnOverride(sessionId, {
|
|
1951
|
+
model: messageMeta.model,
|
|
1952
|
+
mode: messageMeta.mode
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
await sm.createSession({
|
|
1956
|
+
sessionId,
|
|
1957
|
+
resumeId: backendSessionId,
|
|
1958
|
+
roleId: DEFAULT_ASSISTANT_ROLE_ID,
|
|
1959
|
+
// (Fix Important #2) Recovery spawn needs cwd + braincore mounts.
|
|
1960
|
+
// Hub forwards these from chat_sessions.{work_dir,use_brain_core}
|
|
1961
|
+
// on the session.input frame. The healthy path doesn't need them
|
|
1962
|
+
// (existing process has its own cwd already).
|
|
1963
|
+
workDir,
|
|
1964
|
+
useBrainCore
|
|
1965
|
+
});
|
|
1966
|
+
await sm.sendUserInput(sessionId, content);
|
|
1967
|
+
} catch (err) {
|
|
1968
|
+
this.send({
|
|
1969
|
+
type: "session.error",
|
|
1970
|
+
sessionId,
|
|
1971
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1972
|
+
code: extractErrorCode(err)
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
})().catch((err) => {
|
|
1976
|
+
log3.error(
|
|
1977
|
+
{ sessionId, err: err instanceof Error ? err.message : String(err) },
|
|
1978
|
+
"session.input recovery IIFE unexpectedly threw"
|
|
1979
|
+
);
|
|
1980
|
+
});
|
|
1981
|
+
return true;
|
|
1982
|
+
}
|
|
1983
|
+
log3.warn(
|
|
1984
|
+
{ sessionId, hasSession: sm.hasSession(sessionId) },
|
|
1985
|
+
"session.input: session not healthy and no backendSessionId \u2014 cannot recover"
|
|
1986
|
+
);
|
|
1129
1987
|
this.send({
|
|
1130
|
-
type: "session.
|
|
1988
|
+
type: "session.error",
|
|
1131
1989
|
sessionId,
|
|
1132
|
-
|
|
1990
|
+
error: "session.input received but session not healthy and no backendSessionId provided for recovery"
|
|
1133
1991
|
});
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
spawnAndSend(backendSessionId, "fresh_resume", sm.getRecommendedMaxOutputTokens(sessionId));
|
|
1139
|
-
} else {
|
|
1140
|
-
log2.warn(
|
|
1141
|
-
{ sessionId, workDir },
|
|
1142
|
-
"claude.execute: entering brand_new spawn path \u2014 no backendSessionId from hub; if this sessionId already has prior turns in db, upstream hub\u2192agent hydration is broken"
|
|
1143
|
-
);
|
|
1144
|
-
spawnAndSend(void 0, "brand_new", void 0);
|
|
1992
|
+
return true;
|
|
1993
|
+
}
|
|
1994
|
+
if (messageMeta) {
|
|
1995
|
+
sm.setTurnOverride(sessionId, { model: messageMeta.model, mode: messageMeta.mode });
|
|
1145
1996
|
}
|
|
1997
|
+
sm.sendUserInput(sessionId, content).catch((err) => {
|
|
1998
|
+
this.send({
|
|
1999
|
+
type: "session.error",
|
|
2000
|
+
sessionId,
|
|
2001
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2002
|
+
code: extractErrorCode(err)
|
|
2003
|
+
});
|
|
2004
|
+
});
|
|
2005
|
+
return true;
|
|
2006
|
+
}
|
|
2007
|
+
if (msg.type === "session.update_sticky") {
|
|
2008
|
+
const { sessionId, sticky } = msg;
|
|
2009
|
+
log3.info({ sessionId, sticky }, "session.update_sticky");
|
|
2010
|
+
this.sessionManager.updateSticky(sessionId, sticky);
|
|
1146
2011
|
return true;
|
|
1147
2012
|
}
|
|
1148
2013
|
if (msg.type === "session.force_restart") {
|
|
1149
2014
|
const { sessionId, reason } = msg;
|
|
1150
|
-
|
|
2015
|
+
log3.warn({ sessionId, reason }, "session.force_restart received");
|
|
1151
2016
|
const sm = this.sessionManager;
|
|
1152
2017
|
const reply = {
|
|
1153
2018
|
type: "session.died",
|
|
@@ -1156,7 +2021,7 @@ var HubConnection = class {
|
|
|
1156
2021
|
};
|
|
1157
2022
|
if (sm.hasSession(sessionId)) {
|
|
1158
2023
|
sm.abortSession(sessionId).catch((err) => {
|
|
1159
|
-
|
|
2024
|
+
log3.warn({ sessionId, err: err instanceof Error ? err.message : String(err) }, "abortSession failed during force_restart");
|
|
1160
2025
|
}).finally(() => {
|
|
1161
2026
|
this.send(reply);
|
|
1162
2027
|
});
|
|
@@ -1165,49 +2030,27 @@ var HubConnection = class {
|
|
|
1165
2030
|
}
|
|
1166
2031
|
return true;
|
|
1167
2032
|
}
|
|
1168
|
-
if (msg.type === "session.create") {
|
|
1169
|
-
log2.info({ sessionId: msg.sessionId, roleId: DEFAULT_ASSISTANT_ROLE_ID }, "session.create with memory injection");
|
|
1170
|
-
this.sessionManager.createSession({
|
|
1171
|
-
sessionId: msg.sessionId,
|
|
1172
|
-
workDir: msg.workDir,
|
|
1173
|
-
resumeId: msg.resumeId,
|
|
1174
|
-
roleId: DEFAULT_ASSISTANT_ROLE_ID
|
|
1175
|
-
}).then((backendSessionId) => {
|
|
1176
|
-
this.send({
|
|
1177
|
-
type: "session.created",
|
|
1178
|
-
sessionId: msg.sessionId,
|
|
1179
|
-
backendSessionId
|
|
1180
|
-
});
|
|
1181
|
-
}).catch((err) => {
|
|
1182
|
-
this.send({
|
|
1183
|
-
type: "session.error",
|
|
1184
|
-
sessionId: msg.sessionId,
|
|
1185
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1186
|
-
});
|
|
1187
|
-
});
|
|
1188
|
-
return true;
|
|
1189
|
-
}
|
|
1190
2033
|
if (msg.type === "session.close") {
|
|
1191
|
-
|
|
2034
|
+
log3.info({ sessionId: msg.sessionId }, "session.close");
|
|
1192
2035
|
this.sessionManager.closeSession(msg.sessionId).catch(() => {
|
|
1193
2036
|
});
|
|
1194
2037
|
return true;
|
|
1195
2038
|
}
|
|
1196
2039
|
if (msg.type === "claude.input") {
|
|
1197
|
-
|
|
2040
|
+
log3.debug(
|
|
1198
2041
|
{ sessionId: msg.sessionId, ...previewFields(msg.content) },
|
|
1199
2042
|
"claude.input"
|
|
1200
2043
|
);
|
|
1201
2044
|
this.sessionManager.sendUserInput(msg.sessionId, msg.content).catch((err) => {
|
|
1202
|
-
|
|
2045
|
+
log3.warn({ err, sessionId: msg.sessionId }, "sendUserInput failed");
|
|
1203
2046
|
});
|
|
1204
2047
|
return true;
|
|
1205
2048
|
}
|
|
1206
2049
|
if (msg.type === "client.permission_decision") {
|
|
1207
2050
|
const { sessionId, requestId, decision, reason } = msg;
|
|
1208
|
-
|
|
2051
|
+
log3.info({ sessionId, requestId, decision }, "client.permission_decision");
|
|
1209
2052
|
this.sessionManager.respondToPermission(sessionId, { callId: requestId, decision, reason }).catch((err) => {
|
|
1210
|
-
|
|
2053
|
+
log3.warn({ err, sessionId, requestId }, "respondToPermission failed");
|
|
1211
2054
|
});
|
|
1212
2055
|
return true;
|
|
1213
2056
|
}
|
|
@@ -1222,24 +2065,24 @@ var HubConnection = class {
|
|
|
1222
2065
|
const recoverableIds = new Set(snapshots.map((s) => s.id));
|
|
1223
2066
|
for (const { swarmId } of allIds) {
|
|
1224
2067
|
if (!recoverableIds.has(swarmId)) {
|
|
1225
|
-
|
|
2068
|
+
log3.info({ swarmId }, "skipping non-recoverable swarm snapshot (kept on disk)");
|
|
1226
2069
|
}
|
|
1227
2070
|
}
|
|
1228
2071
|
for (const snap of snapshots) {
|
|
1229
|
-
|
|
2072
|
+
log3.info({ swarmId: snap.id }, "recovering swarm");
|
|
1230
2073
|
recoverSwarm(this.swarmCoordinator, snap).catch((err) => {
|
|
1231
|
-
|
|
2074
|
+
log3.error({ err, swarmId: snap.id }, "failed to recover swarm");
|
|
1232
2075
|
});
|
|
1233
2076
|
}
|
|
1234
2077
|
} catch (err) {
|
|
1235
|
-
|
|
2078
|
+
log3.error({ err }, "swarm recovery failed");
|
|
1236
2079
|
}
|
|
1237
2080
|
}
|
|
1238
2081
|
reportLiveSessions() {
|
|
1239
2082
|
if (!this.sessionManager) return;
|
|
1240
2083
|
const ids = this.sessionManager.getActiveSessionIds();
|
|
1241
2084
|
if (ids.length === 0) return;
|
|
1242
|
-
|
|
2085
|
+
log3.info({ count: ids.length }, "reporting live sessions to hub after reconnect");
|
|
1243
2086
|
for (const sessionId of ids) {
|
|
1244
2087
|
this.send({
|
|
1245
2088
|
type: "session.state",
|
|
@@ -1259,7 +2102,7 @@ var HubConnection = class {
|
|
|
1259
2102
|
this.heartbeatTimer = setInterval(() => {
|
|
1260
2103
|
const sinceAck = Date.now() - this.lastAckAt;
|
|
1261
2104
|
if (sinceAck > this.heartbeatInterval * 2.5) {
|
|
1262
|
-
|
|
2105
|
+
log3.warn(
|
|
1263
2106
|
{ sinceAckMs: sinceAck, heartbeatIntervalMs: this.heartbeatInterval },
|
|
1264
2107
|
"no ack from hub \u2014 terminating dead socket"
|
|
1265
2108
|
);
|
|
@@ -1289,15 +2132,15 @@ var HubConnection = class {
|
|
|
1289
2132
|
*/
|
|
1290
2133
|
startWakeWatch() {
|
|
1291
2134
|
this.stopWakeWatch();
|
|
1292
|
-
const intervalMs =
|
|
1293
|
-
const jumpThresholdMs =
|
|
2135
|
+
const intervalMs = WAKE_WATCH_INTERVAL_MS;
|
|
2136
|
+
const jumpThresholdMs = WAKE_WATCH_JUMP_THRESHOLD_MS;
|
|
1294
2137
|
this.lastWakeTickAt = Date.now();
|
|
1295
2138
|
this.wakeWatchTimer = setInterval(() => {
|
|
1296
2139
|
const now = Date.now();
|
|
1297
2140
|
const drift = now - this.lastWakeTickAt - intervalMs;
|
|
1298
2141
|
this.lastWakeTickAt = now;
|
|
1299
2142
|
if (drift <= jumpThresholdMs) return;
|
|
1300
|
-
|
|
2143
|
+
log3.warn(
|
|
1301
2144
|
{ driftMs: drift, hasSocket: !!this.ws },
|
|
1302
2145
|
"wake detected (clock jump) \u2014 forcing reconnect"
|
|
1303
2146
|
);
|
|
@@ -1352,7 +2195,7 @@ var HubConnection = class {
|
|
|
1352
2195
|
}
|
|
1353
2196
|
this.ws = null;
|
|
1354
2197
|
}
|
|
1355
|
-
|
|
2198
|
+
log3.warn({ reason }, "forced reconnect");
|
|
1356
2199
|
this.authState = "pending";
|
|
1357
2200
|
this.scheduleReconnect();
|
|
1358
2201
|
}
|
|
@@ -1362,7 +2205,7 @@ var HubConnection = class {
|
|
|
1362
2205
|
const cap = Math.min(this.reconnectDelay, this.maxReconnectDelay);
|
|
1363
2206
|
const base = Math.min(DEFAULT_RECONNECT_MS, cap);
|
|
1364
2207
|
const delay = Math.floor(base + Math.random() * Math.max(0, cap - base));
|
|
1365
|
-
|
|
2208
|
+
log3.warn({ delayMs: delay, capMs: cap }, "reconnecting to hub...");
|
|
1366
2209
|
this.reconnectTimer = setTimeout(() => {
|
|
1367
2210
|
this.reconnectTimer = null;
|
|
1368
2211
|
this.connect();
|
|
@@ -1401,8 +2244,8 @@ import {
|
|
|
1401
2244
|
MAX_STDOUT_BYTES,
|
|
1402
2245
|
MAX_STDERR_BYTES
|
|
1403
2246
|
} from "@mclawnet/scheduler";
|
|
1404
|
-
import { createLogger as
|
|
1405
|
-
var
|
|
2247
|
+
import { createLogger as createLogger4 } from "@mclawnet/logger";
|
|
2248
|
+
var log4 = createLogger4({ module: "agent/schedule-runtime" });
|
|
1406
2249
|
var SPAWN_STDOUT_CAP_BYTES = 1024 * 1024;
|
|
1407
2250
|
var SPAWN_STDERR_CAP_BYTES = 256 * 1024;
|
|
1408
2251
|
var ScheduleRuntime = class {
|
|
@@ -1512,7 +2355,7 @@ var ScheduleRuntime = class {
|
|
|
1512
2355
|
void this.track(this.handleLog(m, requestId));
|
|
1513
2356
|
return;
|
|
1514
2357
|
default:
|
|
1515
|
-
|
|
2358
|
+
log4.warn({ type: msg.type }, "schedule-runtime: unknown message type");
|
|
1516
2359
|
if (requestId !== void 0) {
|
|
1517
2360
|
this.safeSend({
|
|
1518
2361
|
type: "schedule:error",
|
|
@@ -1645,7 +2488,7 @@ var ScheduleRuntime = class {
|
|
|
1645
2488
|
}
|
|
1646
2489
|
sendError(requestId, err) {
|
|
1647
2490
|
const message = err instanceof Error ? err.message : String(err);
|
|
1648
|
-
|
|
2491
|
+
log4.warn({ err: message, requestId }, "schedule-runtime: branch failed");
|
|
1649
2492
|
this.safeSend({
|
|
1650
2493
|
type: "schedule:error",
|
|
1651
2494
|
requestId,
|
|
@@ -1775,7 +2618,7 @@ function makeRealOneShotSpawn() {
|
|
|
1775
2618
|
};
|
|
1776
2619
|
if (opts.signal.aborted) onAbort();
|
|
1777
2620
|
else opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
1778
|
-
return new Promise((
|
|
2621
|
+
return new Promise((resolve3, reject) => {
|
|
1779
2622
|
proc.once("error", (err) => {
|
|
1780
2623
|
opts.signal.removeEventListener("abort", onAbort);
|
|
1781
2624
|
reject(err);
|
|
@@ -1786,7 +2629,7 @@ function makeRealOneShotSpawn() {
|
|
|
1786
2629
|
tryExtractAssistant(stdoutLineResidual);
|
|
1787
2630
|
stdoutLineResidual = "";
|
|
1788
2631
|
}
|
|
1789
|
-
|
|
2632
|
+
resolve3({
|
|
1790
2633
|
exitCode: code ?? -1,
|
|
1791
2634
|
stdout: stdoutBuf.toString(),
|
|
1792
2635
|
stderr: stderrBuf.toString(),
|
|
@@ -1803,7 +2646,7 @@ function makeRealSwarmStarter(_deps) {
|
|
|
1803
2646
|
}
|
|
1804
2647
|
|
|
1805
2648
|
// src/session-manager.ts
|
|
1806
|
-
import { createLogger as
|
|
2649
|
+
import { createLogger as createLogger6, previewFields as previewFields2 } from "@mclawnet/logger";
|
|
1807
2650
|
import { existsSync as existsSync4 } from "fs";
|
|
1808
2651
|
import { homedir as homedir4 } from "os";
|
|
1809
2652
|
import { join as join4 } from "path";
|
|
@@ -1816,15 +2659,15 @@ import {
|
|
|
1816
2659
|
import { MAX_TOKENS_LADDER, clampLadderIndex } from "@mclawnet/shared";
|
|
1817
2660
|
|
|
1818
2661
|
// src/skill-loader.ts
|
|
1819
|
-
import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync3, readFileSync as
|
|
2662
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
1820
2663
|
import { createHash } from "crypto";
|
|
1821
|
-
import { join as join3, dirname } from "path";
|
|
2664
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
1822
2665
|
import { homedir as homedir3 } from "os";
|
|
1823
2666
|
import { createRequire } from "module";
|
|
1824
2667
|
import { fileURLToPath } from "url";
|
|
1825
|
-
import { createLogger as
|
|
2668
|
+
import { createLogger as createLogger5 } from "@mclawnet/logger";
|
|
1826
2669
|
import { ManifestManager, mergeSkillSections } from "@mclawnet/skill-manager";
|
|
1827
|
-
var
|
|
2670
|
+
var log5 = createLogger5({ module: "agent/skill-loader" });
|
|
1828
2671
|
var CLAWNET_DIR = join3(homedir3(), ".clawnet");
|
|
1829
2672
|
var SKILLS_DIR = join3(CLAWNET_DIR, ".claude", "skills");
|
|
1830
2673
|
var MCP_CONFIG_PATH = join3(CLAWNET_DIR, "mcp.json");
|
|
@@ -1836,7 +2679,7 @@ async function initSkills() {
|
|
|
1836
2679
|
}
|
|
1837
2680
|
function defaultBuiltinSourceDir() {
|
|
1838
2681
|
const thisFile = fileURLToPath(import.meta.url);
|
|
1839
|
-
return join3(
|
|
2682
|
+
return join3(dirname2(thisFile), "..", "skills");
|
|
1840
2683
|
}
|
|
1841
2684
|
function sha256(s) {
|
|
1842
2685
|
return createHash("sha256").update(s).digest("hex");
|
|
@@ -1847,9 +2690,9 @@ function readBuiltinVersion(content) {
|
|
|
1847
2690
|
}
|
|
1848
2691
|
function syncBuiltinSkills(rootDir, srcDir) {
|
|
1849
2692
|
const skillsDir = join3(rootDir, ".claude", "skills");
|
|
1850
|
-
if (!existsSync3(skillsDir))
|
|
2693
|
+
if (!existsSync3(skillsDir)) mkdirSync2(skillsDir, { recursive: true });
|
|
1851
2694
|
if (!existsSync3(srcDir)) {
|
|
1852
|
-
|
|
2695
|
+
log5.debug({ srcDir }, "no built-in skills directory found, skipping");
|
|
1853
2696
|
return;
|
|
1854
2697
|
}
|
|
1855
2698
|
const manifest = new ManifestManager(rootDir);
|
|
@@ -1857,7 +2700,7 @@ function syncBuiltinSkills(rootDir, srcDir) {
|
|
|
1857
2700
|
try {
|
|
1858
2701
|
entries = readdirSync3(srcDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
1859
2702
|
} catch {
|
|
1860
|
-
|
|
2703
|
+
log5.warn({ srcDir }, "failed to read built-in skills directory");
|
|
1861
2704
|
return;
|
|
1862
2705
|
}
|
|
1863
2706
|
for (const skillName of entries) {
|
|
@@ -1868,26 +2711,26 @@ function syncBuiltinSkills(rootDir, srcDir) {
|
|
|
1868
2711
|
const baseSnapshotPath = join3(destDir, ".base.md");
|
|
1869
2712
|
let officialContent;
|
|
1870
2713
|
try {
|
|
1871
|
-
officialContent =
|
|
2714
|
+
officialContent = readFileSync3(srcSkillMd, "utf-8");
|
|
1872
2715
|
} catch (err) {
|
|
1873
|
-
|
|
2716
|
+
log5.warn({ skill: skillName, err }, "failed to read built-in skill");
|
|
1874
2717
|
continue;
|
|
1875
2718
|
}
|
|
1876
2719
|
const officialHash = sha256(officialContent);
|
|
1877
2720
|
const officialVersion = readBuiltinVersion(officialContent);
|
|
1878
2721
|
if (!existsSync3(destSkillMd)) {
|
|
1879
2722
|
try {
|
|
1880
|
-
|
|
2723
|
+
mkdirSync2(destDir, { recursive: true });
|
|
1881
2724
|
writeFileSync(destSkillMd, officialContent);
|
|
1882
2725
|
writeFileSync(baseSnapshotPath, officialContent);
|
|
1883
2726
|
manifest.register(skillName, officialHash, officialVersion);
|
|
1884
|
-
|
|
2727
|
+
log5.info({ skill: skillName, version: officialVersion }, "installed built-in skill");
|
|
1885
2728
|
} catch (err) {
|
|
1886
|
-
|
|
2729
|
+
log5.warn({ skill: skillName, err }, "failed to install built-in skill");
|
|
1887
2730
|
}
|
|
1888
2731
|
continue;
|
|
1889
2732
|
}
|
|
1890
|
-
const userContent =
|
|
2733
|
+
const userContent = readFileSync3(destSkillMd, "utf-8");
|
|
1891
2734
|
const userHash = sha256(userContent);
|
|
1892
2735
|
manifest.refresh(skillName, userHash);
|
|
1893
2736
|
if (!existsSync3(baseSnapshotPath)) {
|
|
@@ -1896,27 +2739,27 @@ function syncBuiltinSkills(rootDir, srcDir) {
|
|
|
1896
2739
|
try {
|
|
1897
2740
|
writeFileSync(baseSnapshotPath, userContent);
|
|
1898
2741
|
} catch (err) {
|
|
1899
|
-
|
|
2742
|
+
log5.debug({ skill: skillName, err }, "failed to write lazy .base.md");
|
|
1900
2743
|
}
|
|
1901
2744
|
}
|
|
1902
2745
|
}
|
|
1903
2746
|
const action = manifest.determineSyncAction(skillName, officialHash, officialVersion);
|
|
1904
2747
|
switch (action) {
|
|
1905
2748
|
case "skip":
|
|
1906
|
-
|
|
2749
|
+
log5.debug({ skill: skillName }, "skill already up to date");
|
|
1907
2750
|
break;
|
|
1908
2751
|
case "direct-overwrite":
|
|
1909
2752
|
try {
|
|
1910
2753
|
writeFileSync(destSkillMd, officialContent);
|
|
1911
2754
|
writeFileSync(baseSnapshotPath, officialContent);
|
|
1912
2755
|
manifest.markSynced(skillName, officialHash, officialHash, officialVersion);
|
|
1913
|
-
|
|
2756
|
+
log5.info({ skill: skillName, version: officialVersion }, "upgraded built-in skill");
|
|
1914
2757
|
} catch (err) {
|
|
1915
|
-
|
|
2758
|
+
log5.warn({ skill: skillName, err }, "failed to overwrite skill");
|
|
1916
2759
|
}
|
|
1917
2760
|
break;
|
|
1918
2761
|
case "keep-user":
|
|
1919
|
-
|
|
2762
|
+
log5.debug({ skill: skillName }, "keeping user-modified skill");
|
|
1920
2763
|
break;
|
|
1921
2764
|
case "needs-merge": {
|
|
1922
2765
|
if (!existsSync3(baseSnapshotPath)) {
|
|
@@ -1931,16 +2774,16 @@ function syncBuiltinSkills(rootDir, srcDir) {
|
|
|
1931
2774
|
}
|
|
1932
2775
|
])
|
|
1933
2776
|
);
|
|
1934
|
-
|
|
2777
|
+
log5.warn(
|
|
1935
2778
|
{ skill: skillName },
|
|
1936
2779
|
"missing .base.md snapshot \u2014 skipping auto-merge, wrote .conflict advisory"
|
|
1937
2780
|
);
|
|
1938
2781
|
} catch (err) {
|
|
1939
|
-
|
|
2782
|
+
log5.warn({ skill: skillName, err }, "failed to write .conflict advisory");
|
|
1940
2783
|
}
|
|
1941
2784
|
break;
|
|
1942
2785
|
}
|
|
1943
|
-
const baseContent =
|
|
2786
|
+
const baseContent = readFileSync3(baseSnapshotPath, "utf-8");
|
|
1944
2787
|
const merged = mergeSkillSections(baseContent, officialContent, userContent);
|
|
1945
2788
|
if (merged.success && merged.content) {
|
|
1946
2789
|
try {
|
|
@@ -1948,9 +2791,9 @@ function syncBuiltinSkills(rootDir, srcDir) {
|
|
|
1948
2791
|
writeFileSync(baseSnapshotPath, officialContent);
|
|
1949
2792
|
const newCurrentHash = sha256(merged.content);
|
|
1950
2793
|
manifest.markSynced(skillName, officialHash, newCurrentHash, officialVersion);
|
|
1951
|
-
|
|
2794
|
+
log5.info({ skill: skillName }, "auto-merged skill upgrade");
|
|
1952
2795
|
} catch (err) {
|
|
1953
|
-
|
|
2796
|
+
log5.warn({ skill: skillName, err }, "failed to write merged skill");
|
|
1954
2797
|
}
|
|
1955
2798
|
} else {
|
|
1956
2799
|
try {
|
|
@@ -1958,12 +2801,12 @@ function syncBuiltinSkills(rootDir, srcDir) {
|
|
|
1958
2801
|
destSkillMd + ".conflict",
|
|
1959
2802
|
renderConflictFile(skillName, officialContent, merged.conflicts ?? [])
|
|
1960
2803
|
);
|
|
1961
|
-
|
|
2804
|
+
log5.warn(
|
|
1962
2805
|
{ skill: skillName, conflicts: merged.conflicts?.length },
|
|
1963
2806
|
"skill merge conflict \u2014 user version kept, .conflict file written"
|
|
1964
2807
|
);
|
|
1965
2808
|
} catch (err) {
|
|
1966
|
-
|
|
2809
|
+
log5.warn({ skill: skillName, err }, "failed to write .conflict file");
|
|
1967
2810
|
}
|
|
1968
2811
|
}
|
|
1969
2812
|
break;
|
|
@@ -2003,8 +2846,8 @@ function renderConflictFile(skillName, officialContent, conflicts) {
|
|
|
2003
2846
|
}
|
|
2004
2847
|
function ensureSkillsDir() {
|
|
2005
2848
|
if (!existsSync3(SKILLS_DIR)) {
|
|
2006
|
-
|
|
2007
|
-
|
|
2849
|
+
mkdirSync2(SKILLS_DIR, { recursive: true });
|
|
2850
|
+
log5.info({ dir: SKILLS_DIR }, "created skills directory");
|
|
2008
2851
|
}
|
|
2009
2852
|
}
|
|
2010
2853
|
var cachedSkills = [];
|
|
@@ -2014,7 +2857,7 @@ function scanSkills() {
|
|
|
2014
2857
|
try {
|
|
2015
2858
|
dirs = readdirSync3(SKILLS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
2016
2859
|
} catch {
|
|
2017
|
-
|
|
2860
|
+
log5.warn("failed to read skills directory for scanning");
|
|
2018
2861
|
return [];
|
|
2019
2862
|
}
|
|
2020
2863
|
const skills = [];
|
|
@@ -2022,7 +2865,7 @@ function scanSkills() {
|
|
|
2022
2865
|
const skillMd = join3(SKILLS_DIR, dirName, "SKILL.md");
|
|
2023
2866
|
if (!existsSync3(skillMd)) continue;
|
|
2024
2867
|
try {
|
|
2025
|
-
const content =
|
|
2868
|
+
const content = readFileSync3(skillMd, "utf-8");
|
|
2026
2869
|
const parsed = parseFrontmatter(content);
|
|
2027
2870
|
if (parsed.name) {
|
|
2028
2871
|
skills.push({
|
|
@@ -2031,11 +2874,11 @@ function scanSkills() {
|
|
|
2031
2874
|
});
|
|
2032
2875
|
}
|
|
2033
2876
|
} catch (err) {
|
|
2034
|
-
|
|
2877
|
+
log5.debug({ skill: dirName, err }, "failed to parse SKILL.md frontmatter");
|
|
2035
2878
|
}
|
|
2036
2879
|
}
|
|
2037
2880
|
cachedSkills = skills;
|
|
2038
|
-
|
|
2881
|
+
log5.info({ count: skills.length }, "scanned skills");
|
|
2039
2882
|
return skills;
|
|
2040
2883
|
}
|
|
2041
2884
|
function getSkillList() {
|
|
@@ -2045,7 +2888,7 @@ function getPendingNotification() {
|
|
|
2045
2888
|
const pendingPath = join3(CLAWNET_DIR, ".claude", "pending-evolutions.json");
|
|
2046
2889
|
if (!existsSync3(pendingPath)) return null;
|
|
2047
2890
|
try {
|
|
2048
|
-
const raw = JSON.parse(
|
|
2891
|
+
const raw = JSON.parse(readFileSync3(pendingPath, "utf-8"));
|
|
2049
2892
|
const pending = Array.isArray(raw?.pending) ? raw.pending : [];
|
|
2050
2893
|
if (pending.length === 0) return null;
|
|
2051
2894
|
const lines = pending.map(
|
|
@@ -2056,7 +2899,7 @@ ${lines.join("\n")}
|
|
|
2056
2899
|
\u4F7F\u7528 skill_pending_list \u67E5\u770B\u8BE6\u60C5\uFF0Cskill_pending_approve / skill_pending_reject \u5904\u7406\u3002`;
|
|
2057
2900
|
return { count: pending.length, text };
|
|
2058
2901
|
} catch (err) {
|
|
2059
|
-
|
|
2902
|
+
log5.debug({ err }, "failed to read pending-evolutions.json");
|
|
2060
2903
|
return null;
|
|
2061
2904
|
}
|
|
2062
2905
|
}
|
|
@@ -2076,10 +2919,10 @@ function ensureMcpConfig() {
|
|
|
2076
2919
|
let mcpServerPath;
|
|
2077
2920
|
try {
|
|
2078
2921
|
const req = createRequire(import.meta.url);
|
|
2079
|
-
const mcpPkgDir =
|
|
2922
|
+
const mcpPkgDir = dirname2(req.resolve("@mclawnet/mcp-server/package.json"));
|
|
2080
2923
|
mcpServerPath = join3(mcpPkgDir, "dist", "server.js");
|
|
2081
2924
|
} catch {
|
|
2082
|
-
|
|
2925
|
+
log5.warn("could not resolve @mclawnet/mcp-server package path, skipping mcp.json generation");
|
|
2083
2926
|
return;
|
|
2084
2927
|
}
|
|
2085
2928
|
const desired = { command: "node", args: [mcpServerPath] };
|
|
@@ -2088,9 +2931,9 @@ function ensureMcpConfig() {
|
|
|
2088
2931
|
if (existsSync3(MCP_CONFIG_PATH)) {
|
|
2089
2932
|
fileExists = true;
|
|
2090
2933
|
try {
|
|
2091
|
-
config = JSON.parse(
|
|
2934
|
+
config = JSON.parse(readFileSync3(MCP_CONFIG_PATH, "utf-8"));
|
|
2092
2935
|
} catch (err) {
|
|
2093
|
-
|
|
2936
|
+
log5.warn({ err, path: MCP_CONFIG_PATH }, "mcp.json malformed \u2014 leaving alone");
|
|
2094
2937
|
return;
|
|
2095
2938
|
}
|
|
2096
2939
|
if (!config.mcpServers || typeof config.mcpServers !== "object") {
|
|
@@ -2115,18 +2958,18 @@ function ensureMcpConfig() {
|
|
|
2115
2958
|
changed = true;
|
|
2116
2959
|
}
|
|
2117
2960
|
if (!changed) {
|
|
2118
|
-
|
|
2961
|
+
log5.debug("mcp.json already up-to-date \u2014 no write");
|
|
2119
2962
|
return;
|
|
2120
2963
|
}
|
|
2121
2964
|
try {
|
|
2122
|
-
|
|
2965
|
+
mkdirSync2(CLAWNET_DIR, { recursive: true });
|
|
2123
2966
|
writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
2124
|
-
|
|
2967
|
+
log5.info(
|
|
2125
2968
|
{ path: MCP_CONFIG_PATH, fresh: !fileExists },
|
|
2126
2969
|
fileExists ? "updated mcp.json (clawnet-mcp ensured)" : "generated default mcp.json"
|
|
2127
2970
|
);
|
|
2128
2971
|
} catch (err) {
|
|
2129
|
-
|
|
2972
|
+
log5.warn({ err }, "failed to write mcp.json");
|
|
2130
2973
|
}
|
|
2131
2974
|
}
|
|
2132
2975
|
|
|
@@ -2139,6 +2982,23 @@ var SessionLimitReachedError = class extends Error {
|
|
|
2139
2982
|
}
|
|
2140
2983
|
code = "SESSION_LIMIT_REACHED";
|
|
2141
2984
|
};
|
|
2985
|
+
var RespawnRequiredError = class extends Error {
|
|
2986
|
+
code = "RESPAWN_REQUIRED";
|
|
2987
|
+
sessionId;
|
|
2988
|
+
newModel;
|
|
2989
|
+
currentModel;
|
|
2990
|
+
backendSessionId;
|
|
2991
|
+
constructor(args) {
|
|
2992
|
+
super(
|
|
2993
|
+
`per-turn model change to "${args.newModel}" requires session respawn; caller should retry as session.create with resumeId=${args.backendSessionId ?? "<unknown>"}`
|
|
2994
|
+
);
|
|
2995
|
+
this.name = "RespawnRequiredError";
|
|
2996
|
+
this.sessionId = args.sessionId;
|
|
2997
|
+
this.newModel = args.newModel;
|
|
2998
|
+
this.currentModel = args.currentModel;
|
|
2999
|
+
this.backendSessionId = args.backendSessionId;
|
|
3000
|
+
}
|
|
3001
|
+
};
|
|
2142
3002
|
function buildMessage(d) {
|
|
2143
3003
|
const kindParts = Object.entries(d.byKind).map(([k, n]) => `${k}=${n}`).join(", ");
|
|
2144
3004
|
const oldest = d.oldestIdleSessionId ? `Oldest idle: ${d.oldestIdleSessionId} (${d.oldestIdleKind}, idle ${Math.round(
|
|
@@ -2243,7 +3103,7 @@ function getSharedEmbeddingService() {
|
|
|
2243
3103
|
sharedEmbeddingService = new EmbeddingService(db, createEmbeddingProviders());
|
|
2244
3104
|
return sharedEmbeddingService;
|
|
2245
3105
|
}
|
|
2246
|
-
var
|
|
3106
|
+
var log6 = createLogger6({ module: "agent/session-manager" });
|
|
2247
3107
|
var DEFAULT_MAX_PROCESSES = 30;
|
|
2248
3108
|
var MAX_PROCESSES = Number(process.env.CLAWNET_MAX_PROCESSES) || DEFAULT_MAX_PROCESSES;
|
|
2249
3109
|
var DEFAULT_CHAT_IDLE_TTL_MS = 60 * 60 * 1e3;
|
|
@@ -2265,6 +3125,9 @@ var SessionManager = class {
|
|
|
2265
3125
|
conversationBuffer = /* @__PURE__ */ new Map();
|
|
2266
3126
|
// PR-A: parallel metadata map (kind, timestamps). See SessionMeta jsdoc.
|
|
2267
3127
|
sessionMeta = /* @__PURE__ */ new Map();
|
|
3128
|
+
// (N3) Sticky/turn-override state keyed by sessionId. See StickyState jsdoc
|
|
3129
|
+
// for the lifecycle rationale (it's intentionally NOT cleared by abort).
|
|
3130
|
+
stickyState = /* @__PURE__ */ new Map();
|
|
2268
3131
|
// PR-A: sessions currently inside a sendInput→turn_complete window. Idle
|
|
2269
3132
|
// sweeper must skip these even if lastActivityAt looks ancient — a
|
|
2270
3133
|
// long-running deep-research turn would otherwise be killed mid-flight.
|
|
@@ -2348,6 +3211,14 @@ var SessionManager = class {
|
|
|
2348
3211
|
*/
|
|
2349
3212
|
onSessionExit;
|
|
2350
3213
|
onPermissionRequest;
|
|
3214
|
+
/**
|
|
3215
|
+
* (N5) Fires when `closeSession` runs against a session that had a
|
|
3216
|
+
* workspace handle attached via {@link SessionManager.attachWorkspace}.
|
|
3217
|
+
* Receives the original handle so the host can dispose it (keep=true) and
|
|
3218
|
+
* notify hub via `session.worktree_closed`. Never blocks the close path —
|
|
3219
|
+
* exceptions are caught and logged.
|
|
3220
|
+
*/
|
|
3221
|
+
onWorkspaceClose;
|
|
2351
3222
|
// PR-A: classifies a sessionId as 'chat' or 'swarm-role'. Injected by
|
|
2352
3223
|
// start.ts via SwarmCoordinator.isSwarmSession to keep SessionManager
|
|
2353
3224
|
// independent of the swarm package. Defaults to 'chat' if absent — safe
|
|
@@ -2364,6 +3235,7 @@ var SessionManager = class {
|
|
|
2364
3235
|
this.onBeforeClose = options.onBeforeClose;
|
|
2365
3236
|
this.onSessionExit = options.onSessionExit;
|
|
2366
3237
|
this.onPermissionRequest = options.onPermissionRequest;
|
|
3238
|
+
this.onWorkspaceClose = options.onWorkspaceClose;
|
|
2367
3239
|
this.classify = options.classify ?? (() => "chat");
|
|
2368
3240
|
this.checkpointPath = options.checkpointPath ?? null;
|
|
2369
3241
|
this.checkpointDebounceMs = options.checkpointDebounceMs ?? 5e3;
|
|
@@ -2386,12 +3258,12 @@ var SessionManager = class {
|
|
|
2386
3258
|
options.systemPrompt = options.systemPrompt ? `${memorySection}
|
|
2387
3259
|
|
|
2388
3260
|
${options.systemPrompt}` : memorySection;
|
|
2389
|
-
|
|
3261
|
+
log6.debug({ workDir: options.workDir, sessionId: options.sessionId }, "memory prompt injected");
|
|
2390
3262
|
} catch (err) {
|
|
2391
|
-
|
|
3263
|
+
log6.warn({ err, workDir: options.workDir }, "failed to build memory section, proceeding without");
|
|
2392
3264
|
}
|
|
2393
3265
|
} else {
|
|
2394
|
-
|
|
3266
|
+
log6.warn(
|
|
2395
3267
|
{ sessionId: options.sessionId, roleId: options.roleId },
|
|
2396
3268
|
"session created without workDir \u2014 memory injection skipped (all memories would be cross-workdir; pass workDir to enable scoped retrieval)"
|
|
2397
3269
|
);
|
|
@@ -2404,13 +3276,13 @@ ${options.systemPrompt}` : memorySection;
|
|
|
2404
3276
|
[Skill \u8FDB\u5316\u63D0\u6848]
|
|
2405
3277
|
${notice.text}`;
|
|
2406
3278
|
options.systemPrompt = options.systemPrompt ? `${options.systemPrompt}${noticeBlock}` : noticeBlock.trimStart();
|
|
2407
|
-
|
|
3279
|
+
log6.debug(
|
|
2408
3280
|
{ sessionId: options.sessionId, pending: notice.count },
|
|
2409
3281
|
"pending skill evolution notice injected"
|
|
2410
3282
|
);
|
|
2411
3283
|
}
|
|
2412
3284
|
} catch (err) {
|
|
2413
|
-
|
|
3285
|
+
log6.debug({ err }, "failed to inject pending notification");
|
|
2414
3286
|
}
|
|
2415
3287
|
if (this.classify(options.sessionId) === "chat") {
|
|
2416
3288
|
const ideaHint = `
|
|
@@ -2422,7 +3294,7 @@ ${notice.text}`;
|
|
|
2422
3294
|
let spawnAdapter = this.adapter;
|
|
2423
3295
|
if (options.backend && this.resolveAdapter) {
|
|
2424
3296
|
const resolved = await this.resolveAdapter(options.backend);
|
|
2425
|
-
|
|
3297
|
+
log6.info(
|
|
2426
3298
|
{
|
|
2427
3299
|
sessionId: options.sessionId,
|
|
2428
3300
|
requested: options.backend,
|
|
@@ -2436,7 +3308,7 @@ ${notice.text}`;
|
|
|
2436
3308
|
spawnAdapter = resolved;
|
|
2437
3309
|
}
|
|
2438
3310
|
} else {
|
|
2439
|
-
|
|
3311
|
+
log6.info(
|
|
2440
3312
|
{
|
|
2441
3313
|
sessionId: options.sessionId,
|
|
2442
3314
|
requestedBackend: options.backend,
|
|
@@ -2446,7 +3318,24 @@ ${notice.text}`;
|
|
|
2446
3318
|
"createSession: using default adapter"
|
|
2447
3319
|
);
|
|
2448
3320
|
}
|
|
2449
|
-
const
|
|
3321
|
+
const sticky = this.stickyState.get(options.sessionId);
|
|
3322
|
+
const effectiveModel = sticky?.pendingTurnOverride?.model ?? sticky?.model ?? options.model;
|
|
3323
|
+
const effectiveMode = sticky?.pendingTurnOverride?.mode ?? sticky?.mode ?? options.mode;
|
|
3324
|
+
const effectiveOptions = {
|
|
3325
|
+
...options,
|
|
3326
|
+
model: effectiveModel,
|
|
3327
|
+
mode: effectiveMode
|
|
3328
|
+
};
|
|
3329
|
+
if (sticky?.pendingTurnOverride) {
|
|
3330
|
+
log6.debug(
|
|
3331
|
+
{ sessionId: options.sessionId, override: sticky.pendingTurnOverride },
|
|
3332
|
+
"N3: consuming pendingTurnOverride for spawn"
|
|
3333
|
+
);
|
|
3334
|
+
delete sticky.pendingTurnOverride;
|
|
3335
|
+
const prevMeta = this.sessionMeta.get(options.sessionId);
|
|
3336
|
+
if (prevMeta) delete prevMeta.pendingTurnOverride;
|
|
3337
|
+
}
|
|
3338
|
+
const process2 = await spawnAdapter.spawn(effectiveOptions);
|
|
2450
3339
|
if (spawnAdapter !== this.adapter) {
|
|
2451
3340
|
this.processAdapters.set(process2, spawnAdapter);
|
|
2452
3341
|
}
|
|
@@ -2464,12 +3353,22 @@ ${notice.text}`;
|
|
|
2464
3353
|
// Restore ladder index if this sessionId was carried over from the
|
|
2465
3354
|
// previous run's checkpoint. drain-on-read so a future re-creation of
|
|
2466
3355
|
// a different session with the same id starts fresh.
|
|
2467
|
-
currentLadderIndex: this.recoveredLadderIndex.get(options.sessionId)
|
|
3356
|
+
currentLadderIndex: this.recoveredLadderIndex.get(options.sessionId),
|
|
3357
|
+
// (N3) Mirror sticky model/mode onto meta so existing meta-readers
|
|
3358
|
+
// (debug snapshots, future PR consumers) see the resolved value.
|
|
3359
|
+
stickyModel: sticky?.model,
|
|
3360
|
+
stickyMode: sticky?.mode
|
|
2468
3361
|
});
|
|
2469
3362
|
this.recoveredLadderIndex.delete(options.sessionId);
|
|
3363
|
+
let postSpawnSticky = this.stickyState.get(options.sessionId);
|
|
3364
|
+
if (!postSpawnSticky) {
|
|
3365
|
+
postSpawnSticky = {};
|
|
3366
|
+
this.stickyState.set(options.sessionId, postSpawnSticky);
|
|
3367
|
+
}
|
|
3368
|
+
postSpawnSticky.lastSpawnModel = effectiveModel;
|
|
2470
3369
|
this.scheduleCheckpoint();
|
|
2471
3370
|
spawnAdapter.onOutput(process2, (rawData) => {
|
|
2472
|
-
const data = normalizeBackendOutput(rawData,
|
|
3371
|
+
const data = normalizeBackendOutput(rawData, log6);
|
|
2473
3372
|
if (data === null) return;
|
|
2474
3373
|
const msg = data;
|
|
2475
3374
|
if (msg?.type === "assistant" && msg?.message?.content) {
|
|
@@ -2502,7 +3401,7 @@ ${notice.text}`;
|
|
|
2502
3401
|
const currentIndex = clampLadderIndex(meta.currentLadderIndex);
|
|
2503
3402
|
const atFloor = currentIndex >= MAX_TOKENS_LADDER.length - 1;
|
|
2504
3403
|
if (atFloor) {
|
|
2505
|
-
|
|
3404
|
+
log6.warn(
|
|
2506
3405
|
{
|
|
2507
3406
|
sessionId: options.sessionId,
|
|
2508
3407
|
used: info.used,
|
|
@@ -2515,7 +3414,7 @@ ${notice.text}`;
|
|
|
2515
3414
|
}
|
|
2516
3415
|
const nextIndex = currentIndex + 1;
|
|
2517
3416
|
meta.currentLadderIndex = nextIndex;
|
|
2518
|
-
|
|
3417
|
+
log6.warn(
|
|
2519
3418
|
{
|
|
2520
3419
|
sessionId: options.sessionId,
|
|
2521
3420
|
used: info.used,
|
|
@@ -2531,7 +3430,7 @@ ${notice.text}`;
|
|
|
2531
3430
|
if (this.onSessionStarted) {
|
|
2532
3431
|
spawnAdapter.onSessionStarted?.(process2, (info) => {
|
|
2533
3432
|
if (this.aborting.has(options.sessionId)) {
|
|
2534
|
-
|
|
3433
|
+
log6.debug(
|
|
2535
3434
|
{ sessionId: options.sessionId },
|
|
2536
3435
|
"suppressing late session_started \u2014 session is aborting"
|
|
2537
3436
|
);
|
|
@@ -2555,7 +3454,7 @@ ${notice.text}`;
|
|
|
2555
3454
|
`${sid}.jsonl`
|
|
2556
3455
|
);
|
|
2557
3456
|
if (!existsSync4(jsonlPath)) {
|
|
2558
|
-
|
|
3457
|
+
log6.warn(
|
|
2559
3458
|
{
|
|
2560
3459
|
sessionId: options.sessionId,
|
|
2561
3460
|
backendSessionId: sid,
|
|
@@ -2592,10 +3491,10 @@ ${notice.text}`;
|
|
|
2592
3491
|
this.conversationBuffer.delete(options.sessionId);
|
|
2593
3492
|
this.scheduleCheckpoint();
|
|
2594
3493
|
if (!expected) {
|
|
2595
|
-
|
|
3494
|
+
log6.warn({ sessionId: options.sessionId, exitCode: code, reason: enriched }, "backend process exited unexpectedly, evicted from session map");
|
|
2596
3495
|
this.onSessionError(options.sessionId, `backend process exited (code=${code ?? "null"})`);
|
|
2597
3496
|
} else {
|
|
2598
|
-
|
|
3497
|
+
log6.debug({ sessionId: options.sessionId, exitCode: code }, "backend process exited as expected");
|
|
2599
3498
|
}
|
|
2600
3499
|
try {
|
|
2601
3500
|
this.onSessionExit?.(options.sessionId, {
|
|
@@ -2604,7 +3503,7 @@ ${notice.text}`;
|
|
|
2604
3503
|
...expected ? {} : { reason: reasonText }
|
|
2605
3504
|
});
|
|
2606
3505
|
} catch (err) {
|
|
2607
|
-
|
|
3506
|
+
log6.warn({ err, sessionId: options.sessionId }, "onSessionExit listener threw");
|
|
2608
3507
|
}
|
|
2609
3508
|
} else {
|
|
2610
3509
|
const expected = this.expectedExits.delete(options.sessionId);
|
|
@@ -2614,7 +3513,7 @@ ${notice.text}`;
|
|
|
2614
3513
|
expected
|
|
2615
3514
|
});
|
|
2616
3515
|
} catch (err) {
|
|
2617
|
-
|
|
3516
|
+
log6.warn({ err, sessionId: options.sessionId }, "onSessionExit listener threw");
|
|
2618
3517
|
}
|
|
2619
3518
|
}
|
|
2620
3519
|
});
|
|
@@ -2634,7 +3533,7 @@ ${notice.text}`;
|
|
|
2634
3533
|
this.activelyExecuting.add(sessionId);
|
|
2635
3534
|
const meta = this.sessionMeta.get(sessionId);
|
|
2636
3535
|
if (meta) meta.lastActivityAt = Date.now();
|
|
2637
|
-
|
|
3536
|
+
log6.debug(
|
|
2638
3537
|
{ sessionId, ...previewFields2(input) },
|
|
2639
3538
|
"sendInput \u2192 backend stdin"
|
|
2640
3539
|
);
|
|
@@ -2649,6 +3548,17 @@ ${notice.text}`;
|
|
|
2649
3548
|
async sendUserInput(sessionId, content) {
|
|
2650
3549
|
const meta = this.sessionMeta.get(sessionId);
|
|
2651
3550
|
const workDir = meta?.workDir;
|
|
3551
|
+
const st = this.stickyState.get(sessionId);
|
|
3552
|
+
const effectiveModel = st?.pendingTurnOverride?.model ?? st?.model;
|
|
3553
|
+
if (effectiveModel !== void 0 && st?.lastSpawnModel !== void 0 && effectiveModel !== st.lastSpawnModel) {
|
|
3554
|
+
const backendSessionId = meta?.backendSessionId;
|
|
3555
|
+
throw new RespawnRequiredError({
|
|
3556
|
+
sessionId,
|
|
3557
|
+
newModel: effectiveModel,
|
|
3558
|
+
currentModel: st.lastSpawnModel,
|
|
3559
|
+
backendSessionId
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
2652
3562
|
let prefixed = content;
|
|
2653
3563
|
try {
|
|
2654
3564
|
const section = await Promise.race([
|
|
@@ -2658,7 +3568,7 @@ ${notice.text}`;
|
|
|
2658
3568
|
embeddingService: getSharedEmbeddingService()
|
|
2659
3569
|
}),
|
|
2660
3570
|
new Promise(
|
|
2661
|
-
(
|
|
3571
|
+
(resolve3) => setTimeout(() => resolve3(""), 200)
|
|
2662
3572
|
)
|
|
2663
3573
|
]);
|
|
2664
3574
|
if (section && section.trim().length > 0) {
|
|
@@ -2669,10 +3579,84 @@ ${section}
|
|
|
2669
3579
|
${content}`;
|
|
2670
3580
|
}
|
|
2671
3581
|
} catch (err) {
|
|
2672
|
-
|
|
3582
|
+
log6.debug({ err, sessionId }, "sendUserInput: memory retrieval skipped");
|
|
2673
3583
|
}
|
|
2674
3584
|
this.sendInput(sessionId, prefixed);
|
|
2675
3585
|
}
|
|
3586
|
+
/**
|
|
3587
|
+
* (N2/N3) Update sticky model/mode on a session. Survives abort/respawn
|
|
3588
|
+
* because it writes to `stickyState`, which is independent of `sessionMeta`
|
|
3589
|
+
* (cleared on abort). Partial: fields with `undefined` are not touched.
|
|
3590
|
+
*
|
|
3591
|
+
* Valid use cases:
|
|
3592
|
+
* - patch sticky on a live session (mutates meta + stickyState)
|
|
3593
|
+
* - stage sticky BEFORE the first session.create lands (writes only to
|
|
3594
|
+
* stickyState — meta is created later by createSession, which mirrors
|
|
3595
|
+
* from stickyState)
|
|
3596
|
+
*
|
|
3597
|
+
* No-throw on missing session; this method is best-effort state staging.
|
|
3598
|
+
*/
|
|
3599
|
+
updateSticky(sessionId, sticky) {
|
|
3600
|
+
const meta = this.sessionMeta.get(sessionId);
|
|
3601
|
+
if (meta) {
|
|
3602
|
+
if (sticky.model !== void 0) meta.stickyModel = sticky.model;
|
|
3603
|
+
if (sticky.mode !== void 0) meta.stickyMode = sticky.mode;
|
|
3604
|
+
}
|
|
3605
|
+
let st = this.stickyState.get(sessionId);
|
|
3606
|
+
if (!st) {
|
|
3607
|
+
st = {};
|
|
3608
|
+
this.stickyState.set(sessionId, st);
|
|
3609
|
+
}
|
|
3610
|
+
if (sticky.model !== void 0) st.model = sticky.model;
|
|
3611
|
+
if (sticky.mode !== void 0) st.mode = sticky.mode;
|
|
3612
|
+
log6.debug(
|
|
3613
|
+
{ sessionId, stickyModel: st.model, stickyMode: st.mode, hadMeta: !!meta },
|
|
3614
|
+
"updateSticky applied"
|
|
3615
|
+
);
|
|
3616
|
+
}
|
|
3617
|
+
/**
|
|
3618
|
+
* (N2/N3) Stage a per-turn override for the very next spawn. Consumed
|
|
3619
|
+
* (and deleted from stickyState) by the next `createSession` for this
|
|
3620
|
+
* sessionId. Survives abort/respawn until consumed — so a hub-side
|
|
3621
|
+
* abort+recreate driven by a model switch still carries the override.
|
|
3622
|
+
*
|
|
3623
|
+
* No-throw on missing session: same staging-before-alive use case as
|
|
3624
|
+
* `updateSticky`.
|
|
3625
|
+
*/
|
|
3626
|
+
setTurnOverride(sessionId, override) {
|
|
3627
|
+
const meta = this.sessionMeta.get(sessionId);
|
|
3628
|
+
if (meta) {
|
|
3629
|
+
meta.pendingTurnOverride = { model: override.model, mode: override.mode };
|
|
3630
|
+
}
|
|
3631
|
+
let st = this.stickyState.get(sessionId);
|
|
3632
|
+
if (!st) {
|
|
3633
|
+
st = {};
|
|
3634
|
+
this.stickyState.set(sessionId, st);
|
|
3635
|
+
}
|
|
3636
|
+
st.pendingTurnOverride = { model: override.model, mode: override.mode };
|
|
3637
|
+
log6.debug({ sessionId, override, hadMeta: !!meta }, "setTurnOverride staged");
|
|
3638
|
+
}
|
|
3639
|
+
/**
|
|
3640
|
+
* (N5) Attach a workspace handle to an existing session's meta so that a
|
|
3641
|
+
* later `closeSession` can fire `onWorkspaceClose` with it. Idempotent: a
|
|
3642
|
+
* second call overwrites the existing handle (acceptable since the host
|
|
3643
|
+
* never adopts two workspaces for the same session in practice). No-op
|
|
3644
|
+
* with a warn when the session is unknown — the caller's prepare succeeded
|
|
3645
|
+
* before createSession failed, so the workspace is already cleaning itself
|
|
3646
|
+
* up via the error path in hub-connection.
|
|
3647
|
+
*/
|
|
3648
|
+
attachWorkspace(sessionId, handle) {
|
|
3649
|
+
const meta = this.sessionMeta.get(sessionId);
|
|
3650
|
+
if (!meta) {
|
|
3651
|
+
log6.warn({ sessionId }, "attachWorkspace: session not in meta map, ignoring");
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
meta.workspaceHandle = handle;
|
|
3655
|
+
}
|
|
3656
|
+
/** (N5) Inspect the attached workspace handle for a session, if any. */
|
|
3657
|
+
getWorkspaceHandle(sessionId) {
|
|
3658
|
+
return this.sessionMeta.get(sessionId)?.workspaceHandle;
|
|
3659
|
+
}
|
|
2676
3660
|
async abortSession(sessionId) {
|
|
2677
3661
|
const process2 = this.sessions.get(sessionId);
|
|
2678
3662
|
if (!process2) return;
|
|
@@ -2711,10 +3695,20 @@ ${content}`;
|
|
|
2711
3695
|
});
|
|
2712
3696
|
}
|
|
2713
3697
|
this.conversationBuffer.delete(sessionId);
|
|
3698
|
+
const meta = this.sessionMeta.get(sessionId);
|
|
3699
|
+
const handle = meta?.workspaceHandle;
|
|
3700
|
+
if (handle && this.onWorkspaceClose) {
|
|
3701
|
+
try {
|
|
3702
|
+
this.onWorkspaceClose(sessionId, handle);
|
|
3703
|
+
} catch (err) {
|
|
3704
|
+
log6.warn({ err, sessionId }, "onWorkspaceClose callback threw");
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
2714
3707
|
this.expectedExits.add(sessionId);
|
|
2715
3708
|
this.sessions.delete(sessionId);
|
|
2716
3709
|
this.sessionMeta.delete(sessionId);
|
|
2717
3710
|
this.activelyExecuting.delete(sessionId);
|
|
3711
|
+
this.stickyState.delete(sessionId);
|
|
2718
3712
|
this.scheduleCheckpoint();
|
|
2719
3713
|
await this.getAdapterFor(process2).stop(process2);
|
|
2720
3714
|
}
|
|
@@ -2732,6 +3726,7 @@ ${content}`;
|
|
|
2732
3726
|
this.sessions.delete(sessionId);
|
|
2733
3727
|
this.sessionMeta.delete(sessionId);
|
|
2734
3728
|
this.activelyExecuting.delete(sessionId);
|
|
3729
|
+
this.stickyState.delete(sessionId);
|
|
2735
3730
|
await this.getAdapterFor(process2).stop(process2).catch(() => {
|
|
2736
3731
|
});
|
|
2737
3732
|
}
|
|
@@ -2743,7 +3738,7 @@ ${content}`;
|
|
|
2743
3738
|
this.checkpointDebouncer = null;
|
|
2744
3739
|
}
|
|
2745
3740
|
await this.flushCheckpoint().catch(
|
|
2746
|
-
(err) =>
|
|
3741
|
+
(err) => log6.warn({ err }, "checkpoint: final flushCheckpoint failed")
|
|
2747
3742
|
);
|
|
2748
3743
|
}
|
|
2749
3744
|
}
|
|
@@ -2800,16 +3795,16 @@ ${content}`;
|
|
|
2800
3795
|
this.idleSweepIntervalMs = overrides.intervalMs;
|
|
2801
3796
|
if (this.idleSweepTimer) return;
|
|
2802
3797
|
if (this.idleTtlMs <= 0) {
|
|
2803
|
-
|
|
3798
|
+
log6.info("idleSweeper disabled (ttlMs <= 0)");
|
|
2804
3799
|
return;
|
|
2805
3800
|
}
|
|
2806
3801
|
this.idleSweepTimer = setInterval(() => {
|
|
2807
3802
|
this.sweepIdleSessions().catch(
|
|
2808
|
-
(err) =>
|
|
3803
|
+
(err) => log6.warn({ err }, "idleSweeper: tick failed")
|
|
2809
3804
|
);
|
|
2810
3805
|
}, this.idleSweepIntervalMs);
|
|
2811
3806
|
this.idleSweepTimer.unref?.();
|
|
2812
|
-
|
|
3807
|
+
log6.info(
|
|
2813
3808
|
{ ttlMs: this.idleTtlMs, intervalMs: this.idleSweepIntervalMs },
|
|
2814
3809
|
"idleSweeper started"
|
|
2815
3810
|
);
|
|
@@ -2840,12 +3835,12 @@ ${content}`;
|
|
|
2840
3835
|
}
|
|
2841
3836
|
for (const sid of victims) {
|
|
2842
3837
|
const idleMs = now - (this.sessionMeta.get(sid)?.lastActivityAt ?? now);
|
|
2843
|
-
|
|
3838
|
+
log6.info(
|
|
2844
3839
|
{ sessionId: sid, idleMs, ttlMs: ttl },
|
|
2845
3840
|
"idleSweeper: closing idle chat session"
|
|
2846
3841
|
);
|
|
2847
3842
|
await this.closeSession(sid).catch(
|
|
2848
|
-
(err) =>
|
|
3843
|
+
(err) => log6.warn({ err, sessionId: sid }, "idleSweeper: closeSession failed, continuing")
|
|
2849
3844
|
);
|
|
2850
3845
|
}
|
|
2851
3846
|
}
|
|
@@ -2899,7 +3894,7 @@ ${content}`;
|
|
|
2899
3894
|
this.checkpointDebouncer = setTimeout(() => {
|
|
2900
3895
|
this.checkpointDebouncer = null;
|
|
2901
3896
|
this.flushCheckpoint().catch(
|
|
2902
|
-
(err) =>
|
|
3897
|
+
(err) => log6.warn({ err }, "checkpoint: flush failed")
|
|
2903
3898
|
);
|
|
2904
3899
|
}, this.checkpointDebounceMs);
|
|
2905
3900
|
this.checkpointDebouncer.unref?.();
|
|
@@ -2963,7 +3958,7 @@ ${content}`;
|
|
|
2963
3958
|
}
|
|
2964
3959
|
if (isPidAlive(entry.pid)) {
|
|
2965
3960
|
orphan.push(entry);
|
|
2966
|
-
|
|
3961
|
+
log6.warn(
|
|
2967
3962
|
{ sessionId: entry.sessionId, pid: entry.pid },
|
|
2968
3963
|
"recover: pid still alive but not owned by current agent (orphan)"
|
|
2969
3964
|
);
|
|
@@ -2984,14 +3979,97 @@ function ensureRuntimeEnvDefaults() {
|
|
|
2984
3979
|
}
|
|
2985
3980
|
|
|
2986
3981
|
// src/start.ts
|
|
2987
|
-
import { SwarmCoordinator, initRoles, WakeupScheduler, listRecoverableSwarmIds as listRecoverableSwarmIds2 } from "@mclawnet/swarm";
|
|
3982
|
+
import { SwarmCoordinator, initRoles, WakeupScheduler, listRecoverableSwarmIds as listRecoverableSwarmIds2, AlwaysOnManager, listProjectDirs, resolveWorkDir as resolveWorkDir2, alwaysOnConfigPath, WorkspaceManager, createIdeaTodoSource, createIdeaResearchSource, createIntrospectionSource, loadAlwaysOnConfig as loadAlwaysOnConfig2, mergeAlwaysOnConfig as mergeAlwaysOnConfig2, saveAlwaysOnConfig as saveAlwaysOnConfig2, reconcileOrphanedResearching } from "@mclawnet/swarm";
|
|
2988
3983
|
import { TaskStore as TaskStore2 } from "@mclawnet/task";
|
|
2989
3984
|
|
|
3985
|
+
// src/ideas-rest-client.ts
|
|
3986
|
+
function normalizeHubUrlToHttp(hubUrl) {
|
|
3987
|
+
let url = hubUrl;
|
|
3988
|
+
url = url.replace(/^wss:\/\//i, "https://").replace(/^ws:\/\//i, "http://");
|
|
3989
|
+
url = url.replace(/\/ws\/agent\/?$/i, "");
|
|
3990
|
+
if (url.endsWith("/")) url = url.slice(0, -1);
|
|
3991
|
+
return url;
|
|
3992
|
+
}
|
|
3993
|
+
function createIdeasRestClient(opts) {
|
|
3994
|
+
const base = normalizeHubUrlToHttp(opts.hubUrl);
|
|
3995
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
3996
|
+
if (typeof fetchImpl !== "function") {
|
|
3997
|
+
throw new Error("createIdeasRestClient: no fetch implementation available");
|
|
3998
|
+
}
|
|
3999
|
+
async function authHeaders() {
|
|
4000
|
+
const token = await opts.getAuthToken();
|
|
4001
|
+
if (!token) {
|
|
4002
|
+
throw new Error("ideas-rest-client: no auth token available");
|
|
4003
|
+
}
|
|
4004
|
+
return { Authorization: `Bearer ${token}` };
|
|
4005
|
+
}
|
|
4006
|
+
async function request(path, init = {}) {
|
|
4007
|
+
const headers = {
|
|
4008
|
+
"Content-Type": "application/json",
|
|
4009
|
+
...await authHeaders(),
|
|
4010
|
+
...init.headers ?? {}
|
|
4011
|
+
};
|
|
4012
|
+
const res = await fetchImpl(`${base}${path}`, { ...init, headers });
|
|
4013
|
+
if (!res.ok) {
|
|
4014
|
+
let detail = "";
|
|
4015
|
+
try {
|
|
4016
|
+
const body = await res.json();
|
|
4017
|
+
if (body && typeof body.error === "string") detail = body.error;
|
|
4018
|
+
} catch {
|
|
4019
|
+
}
|
|
4020
|
+
throw new Error(
|
|
4021
|
+
`ideas-rest-client: ${init.method ?? "GET"} ${path} \u2192 ${res.status}${detail ? ` (${detail})` : ""}`
|
|
4022
|
+
);
|
|
4023
|
+
}
|
|
4024
|
+
return await res.json();
|
|
4025
|
+
}
|
|
4026
|
+
function buildListQuery(filter) {
|
|
4027
|
+
const p = new URLSearchParams();
|
|
4028
|
+
if (filter.status) p.set("status", filter.status);
|
|
4029
|
+
if (filter.priority) p.set("priority", filter.priority);
|
|
4030
|
+
if (filter.projectId) p.set("projectId", filter.projectId);
|
|
4031
|
+
if (filter.tag) p.set("tag", filter.tag);
|
|
4032
|
+
if (filter.q) p.set("q", filter.q);
|
|
4033
|
+
if (typeof filter.limit === "number") p.set("limit", String(filter.limit));
|
|
4034
|
+
const s = p.toString();
|
|
4035
|
+
return s ? `?${s}` : "";
|
|
4036
|
+
}
|
|
4037
|
+
return {
|
|
4038
|
+
async list(filter) {
|
|
4039
|
+
const res = await request(`/api/ideas${buildListQuery(filter)}`);
|
|
4040
|
+
return res.items ?? [];
|
|
4041
|
+
},
|
|
4042
|
+
async get(id) {
|
|
4043
|
+
try {
|
|
4044
|
+
const res = await request(`/api/ideas/${encodeURIComponent(id)}`);
|
|
4045
|
+
return res.idea ?? null;
|
|
4046
|
+
} catch (err) {
|
|
4047
|
+
if (err instanceof Error && /→\s*404/.test(err.message)) return null;
|
|
4048
|
+
throw err;
|
|
4049
|
+
}
|
|
4050
|
+
},
|
|
4051
|
+
async patch(id, partial) {
|
|
4052
|
+
const res = await request(`/api/ideas/${encodeURIComponent(id)}`, {
|
|
4053
|
+
method: "PATCH",
|
|
4054
|
+
body: JSON.stringify(partial)
|
|
4055
|
+
});
|
|
4056
|
+
return res.idea;
|
|
4057
|
+
},
|
|
4058
|
+
async create(input) {
|
|
4059
|
+
const res = await request(`/api/ideas`, {
|
|
4060
|
+
method: "POST",
|
|
4061
|
+
body: JSON.stringify(input)
|
|
4062
|
+
});
|
|
4063
|
+
return res.idea;
|
|
4064
|
+
}
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
|
|
2990
4068
|
// src/brain-bridge.ts
|
|
2991
|
-
import { existsSync as existsSync5, readFileSync as
|
|
4069
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4 } from "fs";
|
|
2992
4070
|
import { join as join5 } from "path";
|
|
2993
|
-
import { createLogger as
|
|
2994
|
-
var
|
|
4071
|
+
import { createLogger as createLogger7 } from "@mclawnet/logger";
|
|
4072
|
+
var log7 = createLogger7({ module: "brain-bridge" });
|
|
2995
4073
|
var BrainBridge = class {
|
|
2996
4074
|
constructor(hub, options) {
|
|
2997
4075
|
this.hub = hub;
|
|
@@ -2999,7 +4077,7 @@ var BrainBridge = class {
|
|
|
2999
4077
|
this.brainHome = options?.brainHomePath || process.env.BRAIN_HOME || join5(home, "BrainData");
|
|
3000
4078
|
this.brainCorePath = options?.brainCorePath || join5(home, ".brain", "BrainCore");
|
|
3001
4079
|
this.hub.registerNamespace("brain", (msg) => this.handleRequest(msg));
|
|
3002
|
-
|
|
4080
|
+
log7.info(
|
|
3003
4081
|
{ brainHome: this.brainHome, brainCorePath: this.brainCorePath },
|
|
3004
4082
|
"BrainBridge initialized"
|
|
3005
4083
|
);
|
|
@@ -3007,11 +4085,11 @@ var BrainBridge = class {
|
|
|
3007
4085
|
brainHome;
|
|
3008
4086
|
brainCorePath;
|
|
3009
4087
|
async handleRequest(msg) {
|
|
3010
|
-
|
|
4088
|
+
log7.info({ action: msg.action, requestId: msg.requestId }, "brain request");
|
|
3011
4089
|
switch (msg.action) {
|
|
3012
4090
|
case "setup_status": {
|
|
3013
4091
|
const status = this.checkSetup();
|
|
3014
|
-
|
|
4092
|
+
log7.info({ status }, "setup_status result");
|
|
3015
4093
|
return { status };
|
|
3016
4094
|
}
|
|
3017
4095
|
case "get_briefing":
|
|
@@ -3019,7 +4097,7 @@ var BrainBridge = class {
|
|
|
3019
4097
|
case "get_meeting_recap":
|
|
3020
4098
|
return await this.getMeetingRecap(msg.params.recapPath);
|
|
3021
4099
|
default:
|
|
3022
|
-
|
|
4100
|
+
log7.warn({ action: msg.action }, "unknown brain action");
|
|
3023
4101
|
throw new Error(`Unknown brain action: ${msg.action}`);
|
|
3024
4102
|
}
|
|
3025
4103
|
}
|
|
@@ -3042,23 +4120,23 @@ var BrainBridge = class {
|
|
|
3042
4120
|
const targetDate = date || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3043
4121
|
const reportsDir = join5(this.brainHome, "reports", "daily");
|
|
3044
4122
|
if (!existsSync5(reportsDir)) {
|
|
3045
|
-
|
|
4123
|
+
log7.info({ reportsDir }, "get_briefing: reports dir not found");
|
|
3046
4124
|
return { briefing: null, actions: [], projects: [], meetings: [], feed: [] };
|
|
3047
4125
|
}
|
|
3048
4126
|
const files = readdirSync4(reportsDir).filter((f) => f.includes(targetDate) && f.endsWith(".md")).sort().reverse();
|
|
3049
4127
|
if (files.length === 0) {
|
|
3050
|
-
|
|
4128
|
+
log7.info({ targetDate }, "get_briefing: no report for date");
|
|
3051
4129
|
return { briefing: null, actions: [], projects: [], meetings: [], feed: [] };
|
|
3052
4130
|
}
|
|
3053
|
-
|
|
3054
|
-
const content =
|
|
4131
|
+
log7.info({ targetDate, file: files[0] }, "get_briefing: reading report");
|
|
4132
|
+
const content = readFileSync4(join5(reportsDir, files[0]), "utf-8");
|
|
3055
4133
|
const tldrMatch = content.match(/## TL;DR\n([\s\S]*?)(?=\n---|\n## )/);
|
|
3056
4134
|
const tldr = tldrMatch ? tldrMatch[1].trim() : content.slice(0, 200);
|
|
3057
4135
|
const actions = this.parseActions(content);
|
|
3058
4136
|
const projects = this.parseProjects(content);
|
|
3059
4137
|
const meetings = this.parseMeetings(content);
|
|
3060
4138
|
const feed = this.parseFeed(content);
|
|
3061
|
-
|
|
4139
|
+
log7.info(
|
|
3062
4140
|
{ actions: actions.length, projects: projects.length, meetings: meetings.length, feed: feed.length },
|
|
3063
4141
|
"get_briefing: parsed"
|
|
3064
4142
|
);
|
|
@@ -3252,10 +4330,10 @@ var BrainBridge = class {
|
|
|
3252
4330
|
}
|
|
3253
4331
|
const fullPath = join5(this.brainHome, recapPath);
|
|
3254
4332
|
if (!existsSync5(fullPath)) {
|
|
3255
|
-
|
|
4333
|
+
log7.warn({ fullPath }, "meeting recap file not found");
|
|
3256
4334
|
return { error: "Recap file not found" };
|
|
3257
4335
|
}
|
|
3258
|
-
const content =
|
|
4336
|
+
const content = readFileSync4(fullPath, "utf-8");
|
|
3259
4337
|
return { recap: this.parseRecapMarkdown(content) };
|
|
3260
4338
|
}
|
|
3261
4339
|
/** Parse meeting recap markdown into structured sections */
|
|
@@ -3288,10 +4366,10 @@ var BrainBridge = class {
|
|
|
3288
4366
|
};
|
|
3289
4367
|
|
|
3290
4368
|
// src/fs-bridge.ts
|
|
3291
|
-
import { existsSync as existsSync6, readFileSync as
|
|
3292
|
-
import { extname, isAbsolute } from "path";
|
|
3293
|
-
import { createLogger as
|
|
3294
|
-
var
|
|
4369
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
4370
|
+
import { extname, isAbsolute as isAbsolute2 } from "path";
|
|
4371
|
+
import { createLogger as createLogger8 } from "@mclawnet/logger";
|
|
4372
|
+
var log8 = createLogger8({ module: "fs-bridge" });
|
|
3295
4373
|
var MAX_TEXT_SIZE = 5 * 1024 * 1024;
|
|
3296
4374
|
var MIME_MAP = {
|
|
3297
4375
|
".ts": "text/typescript",
|
|
@@ -3329,10 +4407,10 @@ var FsBridge = class {
|
|
|
3329
4407
|
constructor(hub) {
|
|
3330
4408
|
this.hub = hub;
|
|
3331
4409
|
this.hub.registerNamespace("fs", (msg) => this.handleRequest(msg));
|
|
3332
|
-
|
|
4410
|
+
log8.info("FsBridge initialized");
|
|
3333
4411
|
}
|
|
3334
4412
|
async handleRequest(msg) {
|
|
3335
|
-
|
|
4413
|
+
log8.info({ action: msg.action, requestId: msg.requestId }, "fs request");
|
|
3336
4414
|
switch (msg.action) {
|
|
3337
4415
|
case "read":
|
|
3338
4416
|
return this.readFile(msg.params);
|
|
@@ -3343,33 +4421,292 @@ var FsBridge = class {
|
|
|
3343
4421
|
readFile(params) {
|
|
3344
4422
|
const filePath = params.path;
|
|
3345
4423
|
if (!filePath) throw new Error("Missing path parameter");
|
|
3346
|
-
if (!
|
|
4424
|
+
if (!isAbsolute2(filePath)) {
|
|
3347
4425
|
throw new Error(`Access denied: only absolute paths allowed`);
|
|
3348
4426
|
}
|
|
3349
4427
|
if (!existsSync6(filePath)) {
|
|
3350
4428
|
throw new Error(`File not found: ${filePath}`);
|
|
3351
4429
|
}
|
|
3352
|
-
const stat2 =
|
|
4430
|
+
const stat2 = statSync3(filePath);
|
|
3353
4431
|
if (!stat2.isFile()) {
|
|
3354
4432
|
throw new Error(`Not a file: ${filePath}`);
|
|
3355
4433
|
}
|
|
3356
4434
|
const mimeType = getMimeType(filePath);
|
|
3357
4435
|
const totalSize = stat2.size;
|
|
3358
4436
|
if (isTextMime(mimeType)) {
|
|
3359
|
-
const raw =
|
|
4437
|
+
const raw = readFileSync5(filePath, "utf-8");
|
|
3360
4438
|
const truncated2 = raw.length > MAX_TEXT_SIZE;
|
|
3361
4439
|
const content = truncated2 ? raw.slice(0, MAX_TEXT_SIZE) : raw;
|
|
3362
4440
|
return { content, encoding: "utf-8", size: content.length, totalSize, mimeType, truncated: truncated2 };
|
|
3363
4441
|
}
|
|
3364
|
-
const buf =
|
|
4442
|
+
const buf = readFileSync5(filePath);
|
|
3365
4443
|
const truncated = buf.length > MAX_TEXT_SIZE;
|
|
3366
4444
|
const slice = truncated ? buf.subarray(0, MAX_TEXT_SIZE) : buf;
|
|
3367
4445
|
return { content: slice.toString("base64"), encoding: "base64", size: slice.length, totalSize, mimeType, truncated };
|
|
3368
4446
|
}
|
|
3369
4447
|
};
|
|
3370
4448
|
|
|
4449
|
+
// src/worktree-bridge.ts
|
|
4450
|
+
import { spawn as spawn2 } from "child_process";
|
|
4451
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4452
|
+
import { writeFile, rm } from "fs/promises";
|
|
4453
|
+
import { resolve as resolve2, isAbsolute as isAbsolute3, normalize, join as join6 } from "path";
|
|
4454
|
+
import { createLogger as createLogger9 } from "@mclawnet/logger";
|
|
4455
|
+
import { createDraftPR } from "@mclawnet/swarm";
|
|
4456
|
+
var log9 = createLogger9({ module: "worktree-bridge" });
|
|
4457
|
+
var WorktreeBridge = class {
|
|
4458
|
+
constructor(hub) {
|
|
4459
|
+
this.hub = hub;
|
|
4460
|
+
this.hub.registerNamespace("worktree", (msg) => this.handle(msg));
|
|
4461
|
+
log9.info("WorktreeBridge initialized");
|
|
4462
|
+
}
|
|
4463
|
+
async handle(msg) {
|
|
4464
|
+
log9.info({ action: msg.action, requestId: msg.requestId }, "worktree request");
|
|
4465
|
+
switch (msg.action) {
|
|
4466
|
+
case "delete":
|
|
4467
|
+
return this.deleteWorktree(msg.params);
|
|
4468
|
+
case "ship":
|
|
4469
|
+
return this.shipWorktree(msg.params);
|
|
4470
|
+
default:
|
|
4471
|
+
throw new Error(`Unknown worktree action: ${msg.action}`);
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
async deleteWorktree(params) {
|
|
4475
|
+
const worktreePath = params.worktreePath;
|
|
4476
|
+
const projectRoot2 = params.projectRoot;
|
|
4477
|
+
const branchName = params.branchName;
|
|
4478
|
+
const sessionId = params.sessionId;
|
|
4479
|
+
const force = params.force === true;
|
|
4480
|
+
if (typeof worktreePath !== "string" || !worktreePath.trim()) {
|
|
4481
|
+
throw new Error("worktreePath required");
|
|
4482
|
+
}
|
|
4483
|
+
if (typeof projectRoot2 !== "string" || !projectRoot2.trim()) {
|
|
4484
|
+
throw new Error("projectRoot required (used to find the repo root)");
|
|
4485
|
+
}
|
|
4486
|
+
if (!isAbsolute3(worktreePath) || !isAbsolute3(projectRoot2)) {
|
|
4487
|
+
throw new Error("worktreePath and projectRoot must be absolute");
|
|
4488
|
+
}
|
|
4489
|
+
const expectedPrefix = normalize(resolve2(projectRoot2, ".worktrees")) + "/";
|
|
4490
|
+
const candidate = normalize(resolve2(worktreePath));
|
|
4491
|
+
if (!candidate.startsWith(expectedPrefix)) {
|
|
4492
|
+
throw new Error(
|
|
4493
|
+
`worktreePath ${worktreePath} is not under ${projectRoot2}/.worktrees/`
|
|
4494
|
+
);
|
|
4495
|
+
}
|
|
4496
|
+
if (!existsSync7(candidate)) {
|
|
4497
|
+
log9.info({ worktreePath: candidate }, "worktree path already gone");
|
|
4498
|
+
} else {
|
|
4499
|
+
const rm1 = await runGit(projectRoot2, ["worktree", "remove", "--force", candidate]);
|
|
4500
|
+
if (rm1.exitCode !== 0) {
|
|
4501
|
+
log9.warn(
|
|
4502
|
+
{ stderr: rm1.stderr, worktreePath: candidate },
|
|
4503
|
+
"git worktree remove failed; falling back to rm -rf"
|
|
4504
|
+
);
|
|
4505
|
+
await rm(candidate, { recursive: true, force: true }).catch((err) => {
|
|
4506
|
+
log9.warn({ err, worktreePath: candidate }, "rm -rf fallback also failed");
|
|
4507
|
+
});
|
|
4508
|
+
await runGit(projectRoot2, ["worktree", "prune"]).catch(() => null);
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
let branchDeleted = false;
|
|
4512
|
+
if (typeof branchName === "string" && branchName.trim()) {
|
|
4513
|
+
const branchFlag = force ? "-D" : "-d";
|
|
4514
|
+
const br = await runGit(projectRoot2, ["branch", branchFlag, branchName]).catch(() => null);
|
|
4515
|
+
if (br && br.exitCode === 0) {
|
|
4516
|
+
branchDeleted = true;
|
|
4517
|
+
} else if (br) {
|
|
4518
|
+
log9.warn(
|
|
4519
|
+
{ branchName, stderr: br.stderr, force },
|
|
4520
|
+
"branch delete refused; caller can retry with force=true"
|
|
4521
|
+
);
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
const worktreeRemoved = !existsSync7(candidate);
|
|
4525
|
+
this.maybeEmitDeleted(sessionId, candidate);
|
|
4526
|
+
return {
|
|
4527
|
+
ok: true,
|
|
4528
|
+
worktreePath: candidate,
|
|
4529
|
+
worktreeRemoved,
|
|
4530
|
+
branchDeleted
|
|
4531
|
+
};
|
|
4532
|
+
}
|
|
4533
|
+
/**
|
|
4534
|
+
* Emit a session.worktree_deleted push frame after a successful delete so
|
|
4535
|
+
* the hub can clear the DB columns and broadcast to other tabs. Caller
|
|
4536
|
+
* passes sessionId via params; missing sessionId silently skips the push
|
|
4537
|
+
* (delete still works — just no DB sync, used by cleanup tooling that
|
|
4538
|
+
* doesn't have a session context).
|
|
4539
|
+
*/
|
|
4540
|
+
maybeEmitDeleted(sessionId, cwd) {
|
|
4541
|
+
if (typeof sessionId === "string" && sessionId) {
|
|
4542
|
+
this.hub.send({
|
|
4543
|
+
type: "session.worktree_deleted",
|
|
4544
|
+
sessionId,
|
|
4545
|
+
cwd
|
|
4546
|
+
});
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
/**
|
|
4550
|
+
* Ship a chat-session worktree: stage all changes, commit if dirty, then
|
|
4551
|
+
* either push + open draft PR (when `gh` CLI is available) or fall back to
|
|
4552
|
+
* writing diff.patch + report.md inside the worktree for manual `git apply`.
|
|
4553
|
+
*
|
|
4554
|
+
* Result shape mirrors M6 swarm ShipmentResult so the UI can render both
|
|
4555
|
+
* with the same component (Banner + future shipment list view).
|
|
4556
|
+
*
|
|
4557
|
+
* No LLM involvement (chat sessions don't have a queen role to generate
|
|
4558
|
+
* report bodies). Title/body are caller-provided (UI) or default to the
|
|
4559
|
+
* branch name + short stat summary.
|
|
4560
|
+
*/
|
|
4561
|
+
async shipWorktree(params) {
|
|
4562
|
+
const worktreePath = params.worktreePath;
|
|
4563
|
+
const projectRoot2 = params.projectRoot;
|
|
4564
|
+
const branchName = params.branchName;
|
|
4565
|
+
const sessionId = params.sessionId;
|
|
4566
|
+
const hintedBaseBranch = typeof params.baseBranch === "string" ? params.baseBranch : null;
|
|
4567
|
+
const title = params.title?.trim();
|
|
4568
|
+
const body = params.body?.trim();
|
|
4569
|
+
if (typeof worktreePath !== "string" || !isAbsolute3(worktreePath)) {
|
|
4570
|
+
throw new Error("worktreePath required (absolute)");
|
|
4571
|
+
}
|
|
4572
|
+
if (typeof projectRoot2 !== "string" || !isAbsolute3(projectRoot2)) {
|
|
4573
|
+
throw new Error("projectRoot required (absolute)");
|
|
4574
|
+
}
|
|
4575
|
+
if (typeof branchName !== "string" || !branchName.trim()) {
|
|
4576
|
+
throw new Error("branchName required");
|
|
4577
|
+
}
|
|
4578
|
+
const expectedPrefix = normalize(resolve2(projectRoot2, ".worktrees")) + "/";
|
|
4579
|
+
const cwd = normalize(resolve2(worktreePath));
|
|
4580
|
+
if (!cwd.startsWith(expectedPrefix)) {
|
|
4581
|
+
throw new Error(`worktreePath not under ${projectRoot2}/.worktrees/`);
|
|
4582
|
+
}
|
|
4583
|
+
if (!existsSync7(cwd)) {
|
|
4584
|
+
throw new Error(`Worktree no longer exists at ${cwd}`);
|
|
4585
|
+
}
|
|
4586
|
+
const add = await runGit(cwd, ["add", "-A"]);
|
|
4587
|
+
if (add.exitCode !== 0) {
|
|
4588
|
+
throw new Error(`git add failed: ${add.stderr || add.stdout}`);
|
|
4589
|
+
}
|
|
4590
|
+
const status = await runGit(cwd, ["status", "--porcelain"]);
|
|
4591
|
+
const dirty = status.exitCode === 0 && status.stdout.trim().length > 0;
|
|
4592
|
+
if (dirty) {
|
|
4593
|
+
const commitMsg = title || `chat: ${branchName.replace(/^clawnet\/chat-/, "")}`;
|
|
4594
|
+
const commit = await runGit(cwd, ["commit", "-m", commitMsg]);
|
|
4595
|
+
if (commit.exitCode !== 0) {
|
|
4596
|
+
log9.warn(
|
|
4597
|
+
{ stderr: commit.stderr },
|
|
4598
|
+
"chat ship: git commit failed (pre-commit hook?); retrying --no-verify"
|
|
4599
|
+
);
|
|
4600
|
+
const retry = await runGit(cwd, ["commit", "--no-verify", "-m", commitMsg]);
|
|
4601
|
+
if (retry.exitCode !== 0) {
|
|
4602
|
+
throw new Error(`git commit failed even with --no-verify: ${retry.stderr || retry.stdout}`);
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
let baseBranch = hintedBaseBranch ?? "main";
|
|
4607
|
+
if (!hintedBaseBranch) {
|
|
4608
|
+
const baseRes = await runGit(projectRoot2, [
|
|
4609
|
+
"symbolic-ref",
|
|
4610
|
+
"--short",
|
|
4611
|
+
"refs/remotes/origin/HEAD"
|
|
4612
|
+
]).catch(() => null);
|
|
4613
|
+
if (baseRes && baseRes.exitCode === 0) {
|
|
4614
|
+
const trimmed = baseRes.stdout.trim();
|
|
4615
|
+
const m = trimmed.match(/^origin\/(.+)$/);
|
|
4616
|
+
if (m) baseBranch = m[1];
|
|
4617
|
+
}
|
|
4618
|
+
}
|
|
4619
|
+
const ghCheck = await new Promise((res) => {
|
|
4620
|
+
const c = spawn2("gh", ["--version"], { stdio: "ignore" });
|
|
4621
|
+
c.on("error", () => res(false));
|
|
4622
|
+
c.on("close", (code) => res(code === 0));
|
|
4623
|
+
});
|
|
4624
|
+
const finalTitle = title || `Chat session: ${branchName.replace(/^clawnet\/chat-/, "")}`;
|
|
4625
|
+
const finalBody = body || `Automated ship of chat-session worktree \`${branchName}\`.
|
|
4626
|
+
|
|
4627
|
+
Review the diff before merging.
|
|
4628
|
+
`;
|
|
4629
|
+
if (ghCheck) {
|
|
4630
|
+
const pr = await createDraftPR({
|
|
4631
|
+
cwd,
|
|
4632
|
+
branchName,
|
|
4633
|
+
baseBranch,
|
|
4634
|
+
title: finalTitle,
|
|
4635
|
+
bodyMarkdown: finalBody
|
|
4636
|
+
});
|
|
4637
|
+
if (pr.ok) {
|
|
4638
|
+
const result = {
|
|
4639
|
+
ok: true,
|
|
4640
|
+
strategy: "pr",
|
|
4641
|
+
prUrl: pr.url,
|
|
4642
|
+
branchName,
|
|
4643
|
+
baseBranch,
|
|
4644
|
+
dirty,
|
|
4645
|
+
mergedAt: null
|
|
4646
|
+
};
|
|
4647
|
+
if (typeof sessionId === "string" && sessionId) {
|
|
4648
|
+
this.hub.send({
|
|
4649
|
+
type: "session.shipment_updated",
|
|
4650
|
+
sessionId,
|
|
4651
|
+
result
|
|
4652
|
+
});
|
|
4653
|
+
}
|
|
4654
|
+
return result;
|
|
4655
|
+
}
|
|
4656
|
+
log9.warn(
|
|
4657
|
+
{ step: pr.step, error: pr.error, branchName },
|
|
4658
|
+
"chat ship: PR creation failed, falling back to diff.patch"
|
|
4659
|
+
);
|
|
4660
|
+
}
|
|
4661
|
+
const diffRes = await runGit(cwd, ["diff", baseBranch + "...HEAD"]);
|
|
4662
|
+
const diff = diffRes.exitCode === 0 ? diffRes.stdout : "";
|
|
4663
|
+
const diffPath = join6(cwd, "diff.patch");
|
|
4664
|
+
const reportPath = join6(cwd, "report.md");
|
|
4665
|
+
await writeFile(diffPath, diff, "utf-8");
|
|
4666
|
+
await writeFile(reportPath, `# ${finalTitle}
|
|
4667
|
+
|
|
4668
|
+
${finalBody}
|
|
4669
|
+
|
|
4670
|
+
## Stats
|
|
4671
|
+
|
|
4672
|
+
- Branch: \`${branchName}\`
|
|
4673
|
+
- Base: \`${baseBranch}\`
|
|
4674
|
+
- Diff size: ${diff.length} bytes
|
|
4675
|
+
`, "utf-8");
|
|
4676
|
+
const fallbackResult = {
|
|
4677
|
+
ok: true,
|
|
4678
|
+
strategy: "diff-only",
|
|
4679
|
+
branchName,
|
|
4680
|
+
baseBranch,
|
|
4681
|
+
dirty,
|
|
4682
|
+
diffPath,
|
|
4683
|
+
reportPath,
|
|
4684
|
+
prUrl: null,
|
|
4685
|
+
mergedAt: null
|
|
4686
|
+
};
|
|
4687
|
+
if (typeof sessionId === "string" && sessionId) {
|
|
4688
|
+
this.hub.send({
|
|
4689
|
+
type: "session.shipment_updated",
|
|
4690
|
+
sessionId,
|
|
4691
|
+
result: fallbackResult
|
|
4692
|
+
});
|
|
4693
|
+
}
|
|
4694
|
+
return fallbackResult;
|
|
4695
|
+
}
|
|
4696
|
+
};
|
|
4697
|
+
function runGit(cwd, args) {
|
|
4698
|
+
return new Promise((resolveR) => {
|
|
4699
|
+
const child = spawn2("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
4700
|
+
let stdout = "", stderr = "";
|
|
4701
|
+
child.stdout?.on("data", (c) => stdout += c.toString("utf-8"));
|
|
4702
|
+
child.stderr?.on("data", (c) => stderr += c.toString("utf-8"));
|
|
4703
|
+
child.on("error", (e) => resolveR({ exitCode: -1, stdout, stderr: e.message }));
|
|
4704
|
+
child.on("close", (code) => resolveR({ exitCode: code ?? -1, stdout, stderr }));
|
|
4705
|
+
});
|
|
4706
|
+
}
|
|
4707
|
+
|
|
3371
4708
|
// src/templates-roles-bridge.ts
|
|
3372
|
-
import { createLogger as
|
|
4709
|
+
import { createLogger as createLogger10 } from "@mclawnet/logger";
|
|
3373
4710
|
import {
|
|
3374
4711
|
listTemplatesWithSource,
|
|
3375
4712
|
getTemplateRaw,
|
|
@@ -3384,7 +4721,7 @@ import {
|
|
|
3384
4721
|
deleteRole,
|
|
3385
4722
|
loadRole as loadRole2
|
|
3386
4723
|
} from "@mclawnet/swarm";
|
|
3387
|
-
var
|
|
4724
|
+
var log10 = createLogger10({ module: "templates-roles-bridge" });
|
|
3388
4725
|
function requireStringParam(params, key) {
|
|
3389
4726
|
const v = params[key];
|
|
3390
4727
|
if (typeof v !== "string" || !v) throw new Error(`Missing '${key}' parameter`);
|
|
@@ -3395,10 +4732,10 @@ var TemplatesRolesBridge = class {
|
|
|
3395
4732
|
this.hub = hub;
|
|
3396
4733
|
this.hub.registerNamespace("templates", (msg) => this.handleTemplates(msg));
|
|
3397
4734
|
this.hub.registerNamespace("roles", (msg) => this.handleRoles(msg));
|
|
3398
|
-
|
|
4735
|
+
log10.info("TemplatesRolesBridge initialized");
|
|
3399
4736
|
}
|
|
3400
4737
|
async handleTemplates(msg) {
|
|
3401
|
-
|
|
4738
|
+
log10.info({ action: msg.action, requestId: msg.requestId }, "templates request");
|
|
3402
4739
|
switch (msg.action) {
|
|
3403
4740
|
case "list":
|
|
3404
4741
|
return { items: listTemplatesWithSource() };
|
|
@@ -3426,7 +4763,7 @@ var TemplatesRolesBridge = class {
|
|
|
3426
4763
|
}
|
|
3427
4764
|
}
|
|
3428
4765
|
async handleRoles(msg) {
|
|
3429
|
-
|
|
4766
|
+
log10.info({ action: msg.action, requestId: msg.requestId }, "roles request");
|
|
3430
4767
|
switch (msg.action) {
|
|
3431
4768
|
case "list":
|
|
3432
4769
|
return { items: listRolesWithSource() };
|
|
@@ -3456,8 +4793,8 @@ var TemplatesRolesBridge = class {
|
|
|
3456
4793
|
};
|
|
3457
4794
|
|
|
3458
4795
|
// src/swarm-session-bridge.ts
|
|
3459
|
-
import { createLogger as
|
|
3460
|
-
var
|
|
4796
|
+
import { createLogger as createLogger11 } from "@mclawnet/logger";
|
|
4797
|
+
var log11 = createLogger11({ module: "agent:swarm-bridge" });
|
|
3461
4798
|
function createSwarmAwareSessionStartedHandler(deps) {
|
|
3462
4799
|
return (sessionId, info) => {
|
|
3463
4800
|
deps.hub.send({
|
|
@@ -3472,20 +4809,20 @@ function createSwarmAwareSessionStartedHandler(deps) {
|
|
|
3472
4809
|
info.backendSessionId
|
|
3473
4810
|
);
|
|
3474
4811
|
if (!ok) {
|
|
3475
|
-
|
|
4812
|
+
log11.debug(
|
|
3476
4813
|
{ sessionId },
|
|
3477
4814
|
"session_started for swarm-shaped sessionId, but no matching role (already destroyed?)"
|
|
3478
4815
|
);
|
|
3479
4816
|
}
|
|
3480
4817
|
} catch (err) {
|
|
3481
|
-
|
|
4818
|
+
log11.warn({ err, sessionId }, "failed to bridge session_started into swarm");
|
|
3482
4819
|
}
|
|
3483
4820
|
}
|
|
3484
4821
|
};
|
|
3485
4822
|
}
|
|
3486
4823
|
|
|
3487
4824
|
// src/start.ts
|
|
3488
|
-
import { createLogger as
|
|
4825
|
+
import { createLogger as createLogger12 } from "@mclawnet/logger";
|
|
3489
4826
|
import {
|
|
3490
4827
|
initDatabase as initDatabase2,
|
|
3491
4828
|
MemoryStore,
|
|
@@ -3500,30 +4837,31 @@ import {
|
|
|
3500
4837
|
AccumulationScanner,
|
|
3501
4838
|
triggerFromAccumulation
|
|
3502
4839
|
} from "@mclawnet/skill-manager";
|
|
3503
|
-
import { PROJECTS_RPC_CAPABILITY } from "@mclawnet/shared";
|
|
3504
|
-
var
|
|
4840
|
+
import { PROJECTS_RPC_CAPABILITY, WORKDIR_RPC_CAPABILITY } from "@mclawnet/shared";
|
|
4841
|
+
var log12 = createLogger12({ module: "agent" });
|
|
3505
4842
|
async function startAgent(options) {
|
|
3506
4843
|
ensureRuntimeEnvDefaults();
|
|
3507
4844
|
const config = loadConfig(options.config);
|
|
3508
4845
|
if (!config.token) {
|
|
3509
|
-
|
|
4846
|
+
log12.error("no token configured \u2014 set CLAWNET_TOKEN or use --token");
|
|
3510
4847
|
process.exit(1);
|
|
3511
4848
|
}
|
|
3512
|
-
|
|
3513
|
-
|
|
4849
|
+
log12.info({ backend: options.adapter.type }, "starting agent");
|
|
4850
|
+
log12.info({ hubUrl: config.hubUrl }, "connecting to hub");
|
|
4851
|
+
const workspaceManager = new WorkspaceManager();
|
|
3514
4852
|
await initRoles();
|
|
3515
4853
|
await initSkills();
|
|
3516
4854
|
const hub = new HubConnection({
|
|
3517
4855
|
hubUrl: config.hubUrl,
|
|
3518
4856
|
token: config.token,
|
|
3519
4857
|
hostname: config.name,
|
|
3520
|
-
capabilities: [PROJECTS_RPC_CAPABILITY],
|
|
4858
|
+
capabilities: [PROJECTS_RPC_CAPABILITY, WORKDIR_RPC_CAPABILITY],
|
|
3521
4859
|
onConnect: (agentId) => {
|
|
3522
|
-
|
|
4860
|
+
log12.info({ agentId }, "connected to hub");
|
|
3523
4861
|
const skills = getSkillList();
|
|
3524
4862
|
if (skills.length > 0) {
|
|
3525
4863
|
hub.sendSkillList(skills);
|
|
3526
|
-
|
|
4864
|
+
log12.info({ count: skills.length }, "pushed skill list to hub");
|
|
3527
4865
|
}
|
|
3528
4866
|
void sessionManager.recoverFromCheckpoint().then((report) => {
|
|
3529
4867
|
for (const entry of report.dead) {
|
|
@@ -3534,24 +4872,30 @@ async function startAgent(options) {
|
|
|
3534
4872
|
backendSessionId: entry.backendSessionId
|
|
3535
4873
|
});
|
|
3536
4874
|
}
|
|
3537
|
-
|
|
4875
|
+
log12.info(
|
|
3538
4876
|
{ deadCount: report.dead.length, orphanCount: report.orphan.length },
|
|
3539
4877
|
"checkpoint recover: cleanup complete"
|
|
3540
4878
|
);
|
|
3541
4879
|
}).catch((err) => {
|
|
3542
|
-
|
|
4880
|
+
log12.warn({ err }, "checkpoint recover: failed (degrading to no-op)");
|
|
3543
4881
|
});
|
|
3544
4882
|
},
|
|
3545
4883
|
onDisconnect: (code, reason) => {
|
|
3546
|
-
|
|
4884
|
+
log12.info({ code, reason }, "disconnected from hub");
|
|
3547
4885
|
},
|
|
3548
4886
|
onError: (err) => {
|
|
3549
|
-
|
|
3550
|
-
}
|
|
4887
|
+
log12.error({ err }, "hub connection error");
|
|
4888
|
+
},
|
|
4889
|
+
// M7.3 user-active signal: route every inbound hub message through the
|
|
4890
|
+
// projects-handler shim — at boot the AlwaysOnManager isn't constructed
|
|
4891
|
+
// yet, but _registerAlwaysOnHooks (called below) backs the indirection.
|
|
4892
|
+
onActivity: () => _recordAlwaysOnActivity(),
|
|
4893
|
+
// (N5) Shared workspace manager for chat `useWorktree` prepare/dispose.
|
|
4894
|
+
workspaceManager
|
|
3551
4895
|
});
|
|
3552
4896
|
let swarmCoordinator;
|
|
3553
|
-
const clawnetDir = process.env.CLAWNET_DIR ??
|
|
3554
|
-
const dbPath = process.env.CLAWNET_MEMORY_DB ??
|
|
4897
|
+
const clawnetDir = process.env.CLAWNET_DIR ?? join7(homedir5(), ".clawnet");
|
|
4898
|
+
const dbPath = process.env.CLAWNET_MEMORY_DB ?? join7(clawnetDir, "memory.db");
|
|
3555
4899
|
let memoryStore = null;
|
|
3556
4900
|
let embeddingService = null;
|
|
3557
4901
|
let evolutionPipeline = null;
|
|
@@ -3564,7 +4908,7 @@ async function startAgent(options) {
|
|
|
3564
4908
|
skillStore = new SkillStore(clawnetDir);
|
|
3565
4909
|
evolutionPipeline = new EvolutionPipeline(clawnetDir);
|
|
3566
4910
|
} catch (err) {
|
|
3567
|
-
|
|
4911
|
+
log12.warn({ err }, "failed to init memory/skill infra; distillation disabled");
|
|
3568
4912
|
}
|
|
3569
4913
|
const onBeforeClose = async (sessionId, messages) => {
|
|
3570
4914
|
if (messages.length === 0) return;
|
|
@@ -3577,7 +4921,7 @@ async function startAgent(options) {
|
|
|
3577
4921
|
embeddingService
|
|
3578
4922
|
);
|
|
3579
4923
|
} catch (err) {
|
|
3580
|
-
|
|
4924
|
+
log12.warn({ err, sessionId }, "distillation failed (non-fatal)");
|
|
3581
4925
|
}
|
|
3582
4926
|
try {
|
|
3583
4927
|
if (!skillStore || !evolutionPipeline) return;
|
|
@@ -3599,13 +4943,13 @@ async function startAgent(options) {
|
|
|
3599
4943
|
await triggerFromAccumulation(signals, evolutionPipeline);
|
|
3600
4944
|
}
|
|
3601
4945
|
} catch (err) {
|
|
3602
|
-
|
|
4946
|
+
log12.warn({ err, sessionId }, "accumulation scan failed (non-fatal)");
|
|
3603
4947
|
}
|
|
3604
4948
|
};
|
|
3605
4949
|
const sessionManager = new SessionManager({
|
|
3606
4950
|
adapter: options.adapter,
|
|
3607
4951
|
resolveAdapter: async (kind) => {
|
|
3608
|
-
const { createBackendAdapter } = await import("./backend-factory-
|
|
4952
|
+
const { createBackendAdapter } = await import("./backend-factory-AFF6I7YF.js");
|
|
3609
4953
|
return createBackendAdapter(kind);
|
|
3610
4954
|
},
|
|
3611
4955
|
onOutput: (sessionId, data) => {
|
|
@@ -3647,7 +4991,7 @@ async function startAgent(options) {
|
|
|
3647
4991
|
try {
|
|
3648
4992
|
swarmCoordinator.handleRoleCrashed(sessionId, info.reason ?? `exit code=${info.code ?? "null"}`);
|
|
3649
4993
|
} catch (err) {
|
|
3650
|
-
|
|
4994
|
+
log12.warn({ err, sessionId }, "swarmCoordinator.handleRoleCrashed threw");
|
|
3651
4995
|
}
|
|
3652
4996
|
},
|
|
3653
4997
|
onBeforeClose,
|
|
@@ -3663,6 +5007,28 @@ async function startAgent(options) {
|
|
|
3663
5007
|
meta: req.meta
|
|
3664
5008
|
});
|
|
3665
5009
|
},
|
|
5010
|
+
// (N5) Chat session close → dispose the attached workspace (keep=true)
|
|
5011
|
+
// and notify hub so the browser can render the "worktree retained" toast.
|
|
5012
|
+
// dispose accepts swarmId, not the handle itself (see
|
|
5013
|
+
// packages/@clawnet/swarm/src/workspace/workspace-manager.ts:75).
|
|
5014
|
+
onWorkspaceClose: (sessionId, handle) => {
|
|
5015
|
+
log12.info(
|
|
5016
|
+
{ sessionId, swarmId: handle.swarmId, cwd: handle.cwd, strategy: handle.strategy },
|
|
5017
|
+
"N5: disposing workspace (keep=true) for chat session close"
|
|
5018
|
+
);
|
|
5019
|
+
workspaceManager.dispose(handle.swarmId, { keep: true }).catch((err) => {
|
|
5020
|
+
log12.warn(
|
|
5021
|
+
{ err, sessionId, swarmId: handle.swarmId, cwd: handle.cwd },
|
|
5022
|
+
"workspace dispose failed (best-effort)"
|
|
5023
|
+
);
|
|
5024
|
+
});
|
|
5025
|
+
hub.send({
|
|
5026
|
+
type: "session.worktree_closed",
|
|
5027
|
+
sessionId,
|
|
5028
|
+
cwd: handle.cwd,
|
|
5029
|
+
branchName: handle.metadata.branchName ?? null
|
|
5030
|
+
});
|
|
5031
|
+
},
|
|
3666
5032
|
// PR-A: classify session kind for the idle sweeper. SessionManager stays
|
|
3667
5033
|
// independent of the swarm package; we hand it a closure that defers to
|
|
3668
5034
|
// SwarmCoordinator (created below) at call time. Lazy-read is safe because
|
|
@@ -3672,7 +5038,7 @@ async function startAgent(options) {
|
|
|
3672
5038
|
// PR-C: enable logical-state checkpoint. Lives next to memory.db under
|
|
3673
5039
|
// CLAWNET_DIR so backup/wipe affects both consistently. Per-call writes
|
|
3674
5040
|
// are debounced to 5s by SessionManager itself.
|
|
3675
|
-
checkpointPath:
|
|
5041
|
+
checkpointPath: join7(clawnetDir, "agent-sessions.json")
|
|
3676
5042
|
});
|
|
3677
5043
|
swarmCoordinator = new SwarmCoordinator(sessionManager, hub, (workDir) => {
|
|
3678
5044
|
try {
|
|
@@ -3680,10 +5046,10 @@ async function startAgent(options) {
|
|
|
3680
5046
|
const home = env ? env.replace(/\/\.clawnet\/?$/, "") : homedir5();
|
|
3681
5047
|
return new TaskStore2({ workDir, home });
|
|
3682
5048
|
} catch (err) {
|
|
3683
|
-
|
|
5049
|
+
log12.warn({ err, workDir }, "TaskStore factory failed");
|
|
3684
5050
|
return void 0;
|
|
3685
5051
|
}
|
|
3686
|
-
}, process.env.CLAWNET_HOME ?? homedir5());
|
|
5052
|
+
}, process.env.CLAWNET_HOME ?? homedir5(), workspaceManager);
|
|
3687
5053
|
hub.setSessionManager(sessionManager);
|
|
3688
5054
|
hub.setSwarmCoordinator(swarmCoordinator);
|
|
3689
5055
|
const wakeupHome = process.env.CLAWNET_HOME ?? homedir5();
|
|
@@ -3695,18 +5061,140 @@ async function startAgent(options) {
|
|
|
3695
5061
|
}));
|
|
3696
5062
|
await wakeupScheduler.restoreFromInbox(wakeupHome, knownSwarms);
|
|
3697
5063
|
} catch (err) {
|
|
3698
|
-
|
|
5064
|
+
log12.warn({ err }, "WakeupScheduler.restoreFromInbox failed (non-fatal)");
|
|
3699
5065
|
}
|
|
3700
5066
|
const scheduleRuntime = new ScheduleRuntime({ hub, sessionManager, swarmCoordinator });
|
|
3701
5067
|
hub.setScheduleRuntime(scheduleRuntime);
|
|
3702
5068
|
await scheduleRuntime.start();
|
|
5069
|
+
const alwaysOnHome = process.env.CLAWNET_HOME ?? homedir5();
|
|
5070
|
+
const taskStoreFactory = (projectRoot2) => {
|
|
5071
|
+
try {
|
|
5072
|
+
return new TaskStore2({ workDir: projectRoot2, home: alwaysOnHome });
|
|
5073
|
+
} catch (err) {
|
|
5074
|
+
log12.warn(
|
|
5075
|
+
{ err: err instanceof Error ? err.message : String(err), projectRoot: projectRoot2 },
|
|
5076
|
+
"TaskStore factory failed (always-on)"
|
|
5077
|
+
);
|
|
5078
|
+
return void 0;
|
|
5079
|
+
}
|
|
5080
|
+
};
|
|
5081
|
+
let ideasClient = null;
|
|
5082
|
+
try {
|
|
5083
|
+
ideasClient = createIdeasRestClient({
|
|
5084
|
+
hubUrl: config.hubUrl,
|
|
5085
|
+
getAuthToken: () => config.token || null
|
|
5086
|
+
});
|
|
5087
|
+
} catch (err) {
|
|
5088
|
+
log12.warn(
|
|
5089
|
+
{ err: err instanceof Error ? err.message : String(err) },
|
|
5090
|
+
"ideas REST client init failed \u2014 IdeaTodoSource will be skipped"
|
|
5091
|
+
);
|
|
5092
|
+
}
|
|
5093
|
+
const taskSources = [];
|
|
5094
|
+
if (ideasClient) {
|
|
5095
|
+
taskSources.push(
|
|
5096
|
+
createIdeaTodoSource({
|
|
5097
|
+
ideasClient,
|
|
5098
|
+
warn: (obj, msg) => log12.warn(obj, msg),
|
|
5099
|
+
info: (obj, msg) => log12.info(obj, msg)
|
|
5100
|
+
}),
|
|
5101
|
+
// M8.1 — research cycle source. Consumes status="idea" ideas, runs the
|
|
5102
|
+
// `research-only` template, and writes a ResearchRef + transitions the
|
|
5103
|
+
// idea status per the report's frontmatter `conclusion`.
|
|
5104
|
+
createIdeaResearchSource({
|
|
5105
|
+
ideasClient,
|
|
5106
|
+
warn: (obj, msg) => log12.warn(obj, msg),
|
|
5107
|
+
info: (obj, msg) => log12.info(obj, msg)
|
|
5108
|
+
}),
|
|
5109
|
+
// M8.2 — queen self-introspection (opt-in via
|
|
5110
|
+
// always-on.json `taskSources.introspection: true`). Pick() side-effects
|
|
5111
|
+
// a small introspection swarm that POSTs new ideas to /api/ideas and
|
|
5112
|
+
// always returns null — the cycle "skips" from the runner's perspective
|
|
5113
|
+
// and the user reviews the auto-discovered ideas next time. Independent
|
|
5114
|
+
// 24h cooldown; self-suppresses when the prior cohort was all archived.
|
|
5115
|
+
createIntrospectionSource({
|
|
5116
|
+
ideasClient,
|
|
5117
|
+
swarmCoordinator,
|
|
5118
|
+
getConfig: (projectRoot2) => loadAlwaysOnConfig2(projectRoot2, alwaysOnHome),
|
|
5119
|
+
updateConfig: async (projectRoot2, patch) => {
|
|
5120
|
+
const current = loadAlwaysOnConfig2(projectRoot2, alwaysOnHome);
|
|
5121
|
+
const next = mergeAlwaysOnConfig2(current, patch);
|
|
5122
|
+
saveAlwaysOnConfig2(projectRoot2, alwaysOnHome, next);
|
|
5123
|
+
},
|
|
5124
|
+
warn: (obj, msg) => log12.warn(obj, msg),
|
|
5125
|
+
info: (obj, msg) => log12.info(obj, msg)
|
|
5126
|
+
})
|
|
5127
|
+
);
|
|
5128
|
+
}
|
|
5129
|
+
const alwaysOnManager = new AlwaysOnManager({
|
|
5130
|
+
home: alwaysOnHome,
|
|
5131
|
+
swarmCoordinator,
|
|
5132
|
+
taskSources
|
|
5133
|
+
});
|
|
5134
|
+
_registerAlwaysOnHooks({
|
|
5135
|
+
getSnapshot: (workDir) => alwaysOnManager.snapshot(workDir),
|
|
5136
|
+
applyConfig: (workDir) => {
|
|
5137
|
+
void alwaysOnManager.refreshFromDisk(workDir).catch((err) => {
|
|
5138
|
+
log12.warn({ err, workDir }, "always-on refreshFromDisk failed");
|
|
5139
|
+
});
|
|
5140
|
+
},
|
|
5141
|
+
recordActivity: () => alwaysOnManager.recordActivity(),
|
|
5142
|
+
// B6 — dev-only force-tick. Hub gates the HTTP route on
|
|
5143
|
+
// NODE_ENV !== "production", so this hook is only reachable from
|
|
5144
|
+
// localhost dev tooling. Errors propagate verbatim so the handler can
|
|
5145
|
+
// classify them (no scheduler / tick already in flight / other) and
|
|
5146
|
+
// return an actionable message to the dev tool. The previous version
|
|
5147
|
+
// caught everything and returned null, which made the handler always
|
|
5148
|
+
// report "scheduler not booted" even when the real cause differed.
|
|
5149
|
+
forceTick: async (workDir) => alwaysOnManager.forceTick(workDir)
|
|
5150
|
+
});
|
|
5151
|
+
const bootedProjectRoots = [];
|
|
5152
|
+
try {
|
|
5153
|
+
const dirs = listProjectDirs(alwaysOnHome);
|
|
5154
|
+
for (const encoded of dirs) {
|
|
5155
|
+
const workDir = resolveWorkDir2(alwaysOnHome, encoded);
|
|
5156
|
+
if (!workDir) continue;
|
|
5157
|
+
const cfgPath = alwaysOnConfigPath(workDir, alwaysOnHome);
|
|
5158
|
+
if (!existsSync8(cfgPath)) continue;
|
|
5159
|
+
try {
|
|
5160
|
+
await alwaysOnManager.start(workDir);
|
|
5161
|
+
bootedProjectRoots.push(workDir);
|
|
5162
|
+
} catch (err) {
|
|
5163
|
+
log12.warn(
|
|
5164
|
+
{ err: err instanceof Error ? err.message : String(err), workDir },
|
|
5165
|
+
"always-on scheduler boot failed for project (continuing with others)"
|
|
5166
|
+
);
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
} catch (err) {
|
|
5170
|
+
log12.warn({ err }, "always-on boot scan failed (non-fatal)");
|
|
5171
|
+
}
|
|
5172
|
+
if (ideasClient && bootedProjectRoots.length > 0) {
|
|
5173
|
+
void reconcileOrphanedResearching({
|
|
5174
|
+
ideasClient,
|
|
5175
|
+
getProjectRoots: () => bootedProjectRoots,
|
|
5176
|
+
listRecoverableSwarmIds: listRecoverableSwarmIds2,
|
|
5177
|
+
log: {
|
|
5178
|
+
warn: (obj, msg) => log12.warn(obj, msg),
|
|
5179
|
+
info: (obj, msg) => log12.info(obj, msg)
|
|
5180
|
+
}
|
|
5181
|
+
}).catch((err) => {
|
|
5182
|
+
log12.warn({ err: err instanceof Error ? err.message : String(err) }, "reconcile sweep threw");
|
|
5183
|
+
});
|
|
5184
|
+
}
|
|
3703
5185
|
const brainBridge = new BrainBridge(hub);
|
|
3704
5186
|
const fsBridge = new FsBridge(hub);
|
|
5187
|
+
new WorktreeBridge(hub);
|
|
3705
5188
|
const templatesRolesBridge = new TemplatesRolesBridge(hub);
|
|
3706
5189
|
sessionManager.startIdleSweeper();
|
|
3707
5190
|
const shutdown = async () => {
|
|
3708
|
-
|
|
5191
|
+
log12.info("shutting down");
|
|
3709
5192
|
wakeupScheduler.dispose();
|
|
5193
|
+
try {
|
|
5194
|
+
await alwaysOnManager.disposeAll();
|
|
5195
|
+
} catch (err) {
|
|
5196
|
+
log12.warn({ err }, "alwaysOnManager.disposeAll failed");
|
|
5197
|
+
}
|
|
3710
5198
|
await sessionManager.closeAll();
|
|
3711
5199
|
await scheduleRuntime.stop();
|
|
3712
5200
|
hub.destroy();
|
|
@@ -3714,7 +5202,7 @@ async function startAgent(options) {
|
|
|
3714
5202
|
try {
|
|
3715
5203
|
memoryDb.close();
|
|
3716
5204
|
} catch (err) {
|
|
3717
|
-
|
|
5205
|
+
log12.warn({ err }, "failed to close memory db");
|
|
3718
5206
|
}
|
|
3719
5207
|
}
|
|
3720
5208
|
process.exit(0);
|
|
@@ -3732,4 +5220,4 @@ export {
|
|
|
3732
5220
|
FsBridge,
|
|
3733
5221
|
startAgent
|
|
3734
5222
|
};
|
|
3735
|
-
//# sourceMappingURL=chunk-
|
|
5223
|
+
//# sourceMappingURL=chunk-GOCWMRBB.js.map
|