@love-moon/conductor-cli 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/bin/conductor-config.js +4 -2
- package/package.json +5 -5
- package/src/daemon.js +158 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @love-moon/conductor-cli
|
|
2
2
|
|
|
3
|
+
## 0.11.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 938efe7: Fix "New task from this" when the successor runs on a different daemon.
|
|
8
|
+
|
|
9
|
+
Picking another daemon in the dialog created the successor row and then killed
|
|
10
|
+
it one second later with `new task failed: Could not resolve resume cwd`. The
|
|
11
|
+
server intentionally drops the source machine's paths for a cross-daemon fork,
|
|
12
|
+
and the target daemon had nothing left to resolve: no launch-config `cwd`, no
|
|
13
|
+
worktree, no local project path (the default project has none, and a project
|
|
14
|
+
bound to another daemon is refused by design), and no source session in its own
|
|
15
|
+
session store.
|
|
16
|
+
|
|
17
|
+
Fork modes now fall back to a fresh `<workspace>/<date>/<run>` directory — the
|
|
18
|
+
same fallback `create_task` already uses — so the successor starts like a
|
|
19
|
+
brand-new task on that machine and picks up the conversation through the
|
|
20
|
+
resume-context URL. In-place restarts still fail loudly, because they must reuse
|
|
21
|
+
the original working directory.
|
|
22
|
+
|
|
23
|
+
- @love-moon/conductor-sdk@0.11.2
|
|
24
|
+
- @love-moon/ai-sdk@0.11.2
|
|
25
|
+
|
|
3
26
|
## 0.11.1
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/bin/conductor-config.js
CHANGED
|
@@ -415,8 +415,10 @@ async function main() {
|
|
|
415
415
|
"# Optional features",
|
|
416
416
|
"# ---------------------------------------------------------------------------",
|
|
417
417
|
"",
|
|
418
|
-
"#
|
|
419
|
-
"# picker
|
|
418
|
+
"# Optional: extra Codex auth.json files to switch between from the app's",
|
|
419
|
+
"# account picker (shown only when more than one is listed). The file name",
|
|
420
|
+
"# (without .json) becomes the account name. Without this, quota comes from",
|
|
421
|
+
"# $CODEX_HOME/auth.json (default ~/.codex/auth.json).",
|
|
420
422
|
"# ai_manager:",
|
|
421
423
|
"# codex:",
|
|
422
424
|
"# auth_json:",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.11.
|
|
4
|
-
"gitCommitId": "
|
|
3
|
+
"version": "0.11.2",
|
|
4
|
+
"gitCommitId": "067735d",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/lovemoon-ai/conductor.git"
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"test": "node --test test/*.test.js"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@love-moon/ai-sdk": "0.11.
|
|
28
|
-
"@love-moon/conductor-sdk": "0.11.
|
|
27
|
+
"@love-moon/ai-sdk": "0.11.2",
|
|
28
|
+
"@love-moon/conductor-sdk": "0.11.2",
|
|
29
29
|
"@github/copilot-sdk": "^0.3.0",
|
|
30
30
|
"chrome-launcher": "^1.2.1",
|
|
31
31
|
"chrome-remote-interface": "^0.33.0",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
"optionalDependencies": {
|
|
40
40
|
"@roamhq/wrtc": "^0.10.0",
|
|
41
|
-
"@love-moon/chat-web": "0.11.
|
|
41
|
+
"@love-moon/chat-web": "0.11.2"
|
|
42
42
|
},
|
|
43
43
|
"pnpm": {
|
|
44
44
|
"onlyBuiltDependencies": [
|
package/src/daemon.js
CHANGED
|
@@ -672,10 +672,102 @@ export function buildFireSpawnArgs({ selectedBackend, initialContent, launchConf
|
|
|
672
672
|
if (effectivePrefill) {
|
|
673
673
|
args.push("--prefill", effectivePrefill);
|
|
674
674
|
}
|
|
675
|
+
const resumeSessionId =
|
|
676
|
+
normalizeOptionalString(launchConfig?.resumeSessionId) ||
|
|
677
|
+
normalizeOptionalString(launchConfig?.resume_session_id);
|
|
678
|
+
if (resumeSessionId) {
|
|
679
|
+
args.push("--resume", resumeSessionId);
|
|
680
|
+
}
|
|
675
681
|
args.push("--");
|
|
676
682
|
return args;
|
|
677
683
|
}
|
|
678
684
|
|
|
685
|
+
// Backends whose local session stores the ai-sdk resume module can enumerate.
|
|
686
|
+
// Kept in sync with the server side of the `list_backend_sessions` RPC.
|
|
687
|
+
const LISTABLE_SESSION_BACKENDS = ["claude", "codex", "kimi"];
|
|
688
|
+
const DEFAULT_BACKEND_SESSION_LIMIT = 20;
|
|
689
|
+
const MAX_BACKEND_SESSION_LIMIT = 50;
|
|
690
|
+
|
|
691
|
+
// Lazy so a daemon running against an older ai-sdk build only fails when the
|
|
692
|
+
// RPC is actually invoked (surfaced as a per-backend error), not at load time.
|
|
693
|
+
async function listSessionsForBackendFromSdk(backendType, options) {
|
|
694
|
+
let sdk = await import("@love-moon/ai-sdk");
|
|
695
|
+
if (typeof sdk.listSessionsForBackend !== "function") {
|
|
696
|
+
// The root index can lag behind the resume module the function lives in;
|
|
697
|
+
// fall back to that entry point before declaring the sdk too old.
|
|
698
|
+
sdk = await import("@love-moon/ai-sdk/dist/resume/index.js").catch(() => sdk);
|
|
699
|
+
}
|
|
700
|
+
if (typeof sdk.listSessionsForBackend !== "function") {
|
|
701
|
+
throw new Error("Session listing is not supported by the installed ai-sdk");
|
|
702
|
+
}
|
|
703
|
+
return sdk.listSessionsForBackend(backendType, options);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Pure aggregation behind the `list_backend_sessions` RPC: intersect the
|
|
708
|
+
* requested backends with what this daemon supports, list each backend's
|
|
709
|
+
* sessions, and merge them newest-first. A single backend failing lands in
|
|
710
|
+
* `errors` without affecting the others. Exported so tests can drive it with
|
|
711
|
+
* a stubbed `listSessions`.
|
|
712
|
+
*/
|
|
713
|
+
export async function collectBackendSessions({
|
|
714
|
+
requestedBackends = null,
|
|
715
|
+
supportedBackends = [],
|
|
716
|
+
limit,
|
|
717
|
+
listSessions = listSessionsForBackendFromSdk,
|
|
718
|
+
} = {}) {
|
|
719
|
+
const numericLimit = Number(limit);
|
|
720
|
+
const effectiveLimit =
|
|
721
|
+
Number.isFinite(numericLimit) && numericLimit > 0
|
|
722
|
+
? Math.min(Math.floor(numericLimit), MAX_BACKEND_SESSION_LIMIT)
|
|
723
|
+
: DEFAULT_BACKEND_SESSION_LIMIT;
|
|
724
|
+
const listableBackends = LISTABLE_SESSION_BACKENDS.filter((backend) => supportedBackends.includes(backend));
|
|
725
|
+
const errors = [];
|
|
726
|
+
let backends;
|
|
727
|
+
if (Array.isArray(requestedBackends)) {
|
|
728
|
+
backends = [];
|
|
729
|
+
for (const raw of requestedBackends) {
|
|
730
|
+
const backend = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
|
731
|
+
if (!backend || backends.includes(backend)) {
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (!listableBackends.includes(backend)) {
|
|
735
|
+
errors.push({ backend, message: `Backend "${backend}" does not support session listing on this daemon` });
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
backends.push(backend);
|
|
739
|
+
}
|
|
740
|
+
} else {
|
|
741
|
+
backends = listableBackends;
|
|
742
|
+
}
|
|
743
|
+
const sessions = [];
|
|
744
|
+
for (const backend of backends) {
|
|
745
|
+
try {
|
|
746
|
+
const listed = await listSessions(backend, { limit: effectiveLimit });
|
|
747
|
+
for (const entry of Array.isArray(listed) ? listed : []) {
|
|
748
|
+
const sessionId = normalizeOptionalString(entry?.sessionId);
|
|
749
|
+
if (!sessionId) {
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
sessions.push({
|
|
753
|
+
backend,
|
|
754
|
+
session_id: sessionId,
|
|
755
|
+
session_file_path: normalizeOptionalString(entry?.sessionFilePath),
|
|
756
|
+
cwd: normalizeOptionalString(entry?.cwd),
|
|
757
|
+
title: normalizeOptionalString(entry?.title),
|
|
758
|
+
updated_at: Number.isFinite(entry?.updatedAt) ? new Date(entry.updatedAt).toISOString() : null,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
} catch (error) {
|
|
762
|
+
errors.push({ backend, message: error?.message || String(error) });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// ISO 8601 strings compare lexicographically; entries without a timestamp
|
|
766
|
+
// sort last.
|
|
767
|
+
sessions.sort((a, b) => (b.updated_at || "").localeCompare(a.updated_at || ""));
|
|
768
|
+
return { sessions, errors };
|
|
769
|
+
}
|
|
770
|
+
|
|
679
771
|
function normalizeBooleanFlag(value) {
|
|
680
772
|
if (typeof value === "boolean") {
|
|
681
773
|
return value;
|
|
@@ -3663,6 +3755,7 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
3663
3755
|
"restart_daemon",
|
|
3664
3756
|
"refresh_session_inplace",
|
|
3665
3757
|
"task_attachments_v1",
|
|
3758
|
+
"backend_session_list",
|
|
3666
3759
|
CUSTOM_COMMANDS_CAPABILITY,
|
|
3667
3760
|
UPDATE_DAEMON_CAPABILITY,
|
|
3668
3761
|
];
|
|
@@ -6173,6 +6266,10 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
6173
6266
|
void handleGetProjectAgents(event.payload);
|
|
6174
6267
|
return;
|
|
6175
6268
|
}
|
|
6269
|
+
if (event.type === "list_backend_sessions") {
|
|
6270
|
+
void handleListBackendSessions(event.payload);
|
|
6271
|
+
return;
|
|
6272
|
+
}
|
|
6176
6273
|
if (event.type === "ai_manager_request") {
|
|
6177
6274
|
// RFC 0035: reads are fine and genuinely useful to a guest (it needs to
|
|
6178
6275
|
// know how much quota the borrowed machine has left). `switch_account` is
|
|
@@ -6551,6 +6648,42 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
6551
6648
|
}
|
|
6552
6649
|
}
|
|
6553
6650
|
|
|
6651
|
+
async function handleListBackendSessions(payload) {
|
|
6652
|
+
const requestId = payload?.request_id ? String(payload.request_id).trim() : "";
|
|
6653
|
+
if (!requestId) {
|
|
6654
|
+
logError(`Invalid list_backend_sessions payload: ${JSON.stringify(payload)}`);
|
|
6655
|
+
return;
|
|
6656
|
+
}
|
|
6657
|
+
|
|
6658
|
+
let result;
|
|
6659
|
+
try {
|
|
6660
|
+
result = await collectBackendSessions({
|
|
6661
|
+
requestedBackends: Array.isArray(payload?.backends) ? payload.backends : null,
|
|
6662
|
+
supportedBackends: SUPPORTED_BACKENDS,
|
|
6663
|
+
limit: payload?.limit,
|
|
6664
|
+
listSessions: deps.listBackendSessions,
|
|
6665
|
+
});
|
|
6666
|
+
} catch (error) {
|
|
6667
|
+
result = {
|
|
6668
|
+
sessions: [],
|
|
6669
|
+
errors: [{ backend: "*", message: `Failed to list backend sessions on daemon ${AGENT_NAME}: ${error?.message || error}` }],
|
|
6670
|
+
};
|
|
6671
|
+
}
|
|
6672
|
+
|
|
6673
|
+
try {
|
|
6674
|
+
await client.sendJson({
|
|
6675
|
+
type: "backend_sessions_listed",
|
|
6676
|
+
payload: {
|
|
6677
|
+
request_id: requestId,
|
|
6678
|
+
sessions: result.sessions,
|
|
6679
|
+
errors: result.errors,
|
|
6680
|
+
},
|
|
6681
|
+
});
|
|
6682
|
+
} catch (error) {
|
|
6683
|
+
logError(`Failed to report backend_sessions_listed for ${requestId}: ${error?.message || error}`);
|
|
6684
|
+
}
|
|
6685
|
+
}
|
|
6686
|
+
|
|
6554
6687
|
function stopActiveTaskProcess(
|
|
6555
6688
|
taskId,
|
|
6556
6689
|
{
|
|
@@ -7940,6 +8073,31 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
7940
8073
|
return;
|
|
7941
8074
|
}
|
|
7942
8075
|
|
|
8076
|
+
if (!resolvedResumeCwd && isForkMode) {
|
|
8077
|
+
// A fork starts a brand-new session and receives its context as a URL, so
|
|
8078
|
+
// it does not need the source task's files. When nothing in the chain
|
|
8079
|
+
// resolves — the normal case for a cross-daemon fork, where the server
|
|
8080
|
+
// drops the source machine's paths and this daemon holds no local path
|
|
8081
|
+
// for the project — land in a fresh workspace dir exactly like
|
|
8082
|
+
// create_task does, instead of killing the successor on arrival.
|
|
8083
|
+
const now = new Date();
|
|
8084
|
+
// Keyed off the task id, not the daemon pid: the run timestamp only has
|
|
8085
|
+
// second granularity and this path never renames the dir afterwards
|
|
8086
|
+
// (create_task does), so two forks landing in the same second would
|
|
8087
|
+
// otherwise share one workspace AND one conductor.log.
|
|
8088
|
+
const taskSuffix =
|
|
8089
|
+
normalizedTargetTaskId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || String(process.pid);
|
|
8090
|
+
resolvedResumeCwd = path.join(
|
|
8091
|
+
WORKSPACE_ROOT,
|
|
8092
|
+
formatWorkspaceDate(now),
|
|
8093
|
+
`${formatWorkspaceRunTimestamp(now)}_fork_${taskSuffix}`,
|
|
8094
|
+
);
|
|
8095
|
+
log(
|
|
8096
|
+
`No local workspace for project ${normalizedProjectId} on this daemon; ` +
|
|
8097
|
+
`forking task ${normalizedTargetTaskId} into ${resolvedResumeCwd}`,
|
|
8098
|
+
);
|
|
8099
|
+
}
|
|
8100
|
+
|
|
7943
8101
|
if (!resolvedResumeCwd) {
|
|
7944
8102
|
reportRestartFailure({
|
|
7945
8103
|
taskId: normalizedTargetTaskId,
|