@botcord/daemon 0.2.66 → 0.2.67
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/dist/working-memory.d.ts +2 -4
- package/dist/working-memory.js +48 -38
- package/package.json +1 -1
- package/src/__tests__/working-memory.test.ts +46 -18
- package/src/working-memory.ts +52 -39
package/dist/working-memory.d.ts
CHANGED
|
@@ -10,10 +10,8 @@ export declare const MAX_GOAL_CHARS = 500;
|
|
|
10
10
|
export declare const MAX_TOTAL_CHARS = 20000;
|
|
11
11
|
export declare const DEFAULT_SECTION = "notes";
|
|
12
12
|
/**
|
|
13
|
-
* Canonical per-agent
|
|
14
|
-
*
|
|
15
|
-
* `~/.botcord/daemon/memory/{agentId}` is migrated lazily on first read —
|
|
16
|
-
* see §8 of the daemon-agent-workspace plan.
|
|
13
|
+
* Canonical per-agent memory directory. This intentionally matches
|
|
14
|
+
* @botcord/cli's `botcord memory` path so the CLI and daemon share one store.
|
|
17
15
|
*/
|
|
18
16
|
export declare function resolveMemoryDir(agentId: string): string;
|
|
19
17
|
export declare function readWorkingMemory(agentId: string): WorkingMemory | null;
|
package/dist/working-memory.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Working memory — persistent, account-scoped notes injected into every turn.
|
|
3
3
|
*
|
|
4
|
-
* Stored at `~/.botcord/
|
|
5
|
-
*
|
|
4
|
+
* Stored at `~/.botcord/memory/{agentId}/working-memory.json`, matching the
|
|
5
|
+
* @botcord/cli `botcord memory` command so writes made by an agent are visible
|
|
6
|
+
* to daemon context injection on the next turn.
|
|
6
7
|
*
|
|
7
8
|
* Ported from plugin/src/memory.ts (dropping workspace + OpenClaw runtime
|
|
8
9
|
* branches) and plugin/src/memory-protocol.ts (prompt builder).
|
|
9
10
|
*/
|
|
10
11
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
11
13
|
import path from "node:path";
|
|
12
14
|
import { agentStateDir } from "./agent-workspace.js";
|
|
13
15
|
import { DAEMON_DIR_PATH } from "./config.js";
|
|
@@ -23,86 +25,94 @@ const MEMORY_SIZE_WARN_CHARS = 2_000;
|
|
|
23
25
|
const RESERVED_TAGS_RE = /<\/?(?:current_memory|section_\w+)\b[^>]*>/gi;
|
|
24
26
|
// ── Path resolution ────────────────────────────────────────────────
|
|
25
27
|
/**
|
|
26
|
-
* Canonical per-agent
|
|
27
|
-
*
|
|
28
|
-
* `~/.botcord/daemon/memory/{agentId}` is migrated lazily on first read —
|
|
29
|
-
* see §8 of the daemon-agent-workspace plan.
|
|
28
|
+
* Canonical per-agent memory directory. This intentionally matches
|
|
29
|
+
* @botcord/cli's `botcord memory` path so the CLI and daemon share one store.
|
|
30
30
|
*/
|
|
31
31
|
export function resolveMemoryDir(agentId) {
|
|
32
32
|
if (!agentId)
|
|
33
33
|
throw new Error("resolveMemoryDir: agentId is required");
|
|
34
|
+
return path.join(homedir(), ".botcord", "memory", agentId);
|
|
35
|
+
}
|
|
36
|
+
/** Previous daemon-owned location retained for one-shot migration on read. */
|
|
37
|
+
function daemonStateMemoryDir(agentId) {
|
|
34
38
|
return agentStateDir(agentId);
|
|
35
39
|
}
|
|
36
|
-
/**
|
|
37
|
-
function
|
|
40
|
+
/** Older daemon location retained for one-shot migration on read. */
|
|
41
|
+
function daemonLegacyMemoryDir(agentId) {
|
|
38
42
|
return path.join(DAEMON_DIR_PATH, "memory", agentId);
|
|
39
43
|
}
|
|
40
44
|
function workingMemoryPath(agentId) {
|
|
41
45
|
return path.join(resolveMemoryDir(agentId), "working-memory.json");
|
|
42
46
|
}
|
|
43
|
-
function
|
|
44
|
-
return path.join(
|
|
47
|
+
function daemonStateWorkingMemoryPath(agentId) {
|
|
48
|
+
return path.join(daemonStateMemoryDir(agentId), "working-memory.json");
|
|
49
|
+
}
|
|
50
|
+
function daemonLegacyWorkingMemoryPath(agentId) {
|
|
51
|
+
return path.join(daemonLegacyMemoryDir(agentId), "working-memory.json");
|
|
45
52
|
}
|
|
46
53
|
// Migration conflict warnings are emitted at most once per agent per
|
|
47
54
|
// process. Reset only by daemon restart — good enough for a one-release
|
|
48
55
|
// transitional branch that gets removed later.
|
|
49
56
|
const warnedMigrationConflict = new Set();
|
|
50
57
|
/**
|
|
51
|
-
* Resolve the path to read from, migrating from
|
|
58
|
+
* Resolve the path to read from, migrating from daemon-only locations if
|
|
52
59
|
* necessary. Returns the path the caller should read, or `null` when no
|
|
53
60
|
* memory file exists anywhere.
|
|
54
|
-
*
|
|
55
|
-
* Migration branch (the `else if` on `legacyExists` below) is meant to be
|
|
56
|
-
* deleted one release after this change ships; see plan §8 step 6.
|
|
57
61
|
*/
|
|
58
62
|
function resolveReadPath(agentId) {
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
const cliPath = workingMemoryPath(agentId);
|
|
64
|
+
const daemonStatePath = daemonStateWorkingMemoryPath(agentId);
|
|
65
|
+
const daemonLegacyPath = daemonLegacyWorkingMemoryPath(agentId);
|
|
66
|
+
const cliExists = existsSync(cliPath);
|
|
67
|
+
const daemonStateExists = existsSync(daemonStatePath);
|
|
68
|
+
const daemonLegacyExists = existsSync(daemonLegacyPath);
|
|
69
|
+
if (cliExists) {
|
|
70
|
+
if ((daemonStateExists || daemonLegacyExists) && !warnedMigrationConflict.has(agentId)) {
|
|
65
71
|
warnedMigrationConflict.add(agentId);
|
|
66
|
-
daemonLog.warn("working-memory: both
|
|
72
|
+
daemonLog.warn("working-memory: both cli and daemon paths exist; using cli", {
|
|
67
73
|
agentId,
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
cliPath,
|
|
75
|
+
daemonStatePath,
|
|
76
|
+
daemonLegacyPath,
|
|
70
77
|
});
|
|
71
78
|
}
|
|
72
|
-
return
|
|
79
|
+
return cliPath;
|
|
73
80
|
}
|
|
74
|
-
|
|
81
|
+
const migrateFrom = daemonStateExists
|
|
82
|
+
? daemonStatePath
|
|
83
|
+
: daemonLegacyExists
|
|
84
|
+
? daemonLegacyPath
|
|
85
|
+
: null;
|
|
86
|
+
if (migrateFrom) {
|
|
75
87
|
try {
|
|
76
|
-
mkdirSync(path.dirname(
|
|
88
|
+
mkdirSync(path.dirname(cliPath), { recursive: true, mode: 0o700 });
|
|
77
89
|
try {
|
|
78
|
-
renameSync(
|
|
90
|
+
renameSync(migrateFrom, cliPath);
|
|
79
91
|
}
|
|
80
92
|
catch (err) {
|
|
81
|
-
// EXDEV =
|
|
93
|
+
// EXDEV = old and canonical paths live on different filesystems
|
|
82
94
|
// (bind mounts, tmpfs overlays). `renameSync` cannot cross fs
|
|
83
|
-
// boundaries, so fall back to copy + unlink.
|
|
84
|
-
// next write would go to newPath while legacy still has the old
|
|
85
|
-
// payload — silent divergence the reviewer of §8 flagged.
|
|
95
|
+
// boundaries, so fall back to copy + unlink.
|
|
86
96
|
if (err.code === "EXDEV") {
|
|
87
|
-
copyFileSync(
|
|
88
|
-
unlinkSync(
|
|
97
|
+
copyFileSync(migrateFrom, cliPath);
|
|
98
|
+
unlinkSync(migrateFrom);
|
|
89
99
|
}
|
|
90
100
|
else {
|
|
91
101
|
throw err;
|
|
92
102
|
}
|
|
93
103
|
}
|
|
94
|
-
return
|
|
104
|
+
return cliPath;
|
|
95
105
|
}
|
|
96
106
|
catch (err) {
|
|
97
107
|
const e = err;
|
|
98
|
-
daemonLog.warn("working-memory: migration rename failed; reading
|
|
108
|
+
daemonLog.warn("working-memory: migration rename failed; reading daemon path", {
|
|
99
109
|
agentId,
|
|
100
|
-
oldPath,
|
|
101
|
-
newPath,
|
|
110
|
+
oldPath: migrateFrom,
|
|
111
|
+
newPath: cliPath,
|
|
102
112
|
code: e.code,
|
|
103
113
|
error: e.message ?? String(err),
|
|
104
114
|
});
|
|
105
|
-
return
|
|
115
|
+
return migrateFrom;
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
118
|
return null;
|
package/package.json
CHANGED
|
@@ -30,7 +30,11 @@ vi.mock("../log.js", () => ({
|
|
|
30
30
|
const wm = await import("../working-memory.js");
|
|
31
31
|
const { agentStateDir } = await import("../agent-workspace.js");
|
|
32
32
|
|
|
33
|
-
function
|
|
33
|
+
function cliPathFor(agentId: string): string {
|
|
34
|
+
return path.join(tmpHome, ".botcord", "memory", agentId, "working-memory.json");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function daemonStatePathFor(agentId: string): string {
|
|
34
38
|
return path.join(agentStateDir(agentId), "working-memory.json");
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -44,8 +48,14 @@ function writeLegacy(agentId: string, body: unknown): void {
|
|
|
44
48
|
writeFileSync(p, JSON.stringify(body));
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
function
|
|
48
|
-
const p =
|
|
51
|
+
function writeDaemonState(agentId: string, body: unknown): void {
|
|
52
|
+
const p = daemonStatePathFor(agentId);
|
|
53
|
+
mkdirSync(path.dirname(p), { recursive: true });
|
|
54
|
+
writeFileSync(p, JSON.stringify(body));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function writeCli(agentId: string, body: unknown): void {
|
|
58
|
+
const p = cliPathFor(agentId);
|
|
49
59
|
mkdirSync(path.dirname(p), { recursive: true });
|
|
50
60
|
writeFileSync(p, JSON.stringify(body));
|
|
51
61
|
}
|
|
@@ -79,9 +89,10 @@ describe("working-memory I/O", () => {
|
|
|
79
89
|
expect(got?.updatedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
80
90
|
});
|
|
81
91
|
|
|
82
|
-
it("writes land in the
|
|
92
|
+
it("writes land in the CLI-compatible memory dir", () => {
|
|
83
93
|
wm.updateWorkingMemory("ag_new", { goal: "g" });
|
|
84
|
-
expect(existsSync(
|
|
94
|
+
expect(existsSync(cliPathFor("ag_new"))).toBe(true);
|
|
95
|
+
expect(existsSync(daemonStatePathFor("ag_new"))).toBe(false);
|
|
85
96
|
expect(existsSync(legacyPathFor("ag_new"))).toBe(false);
|
|
86
97
|
});
|
|
87
98
|
|
|
@@ -136,34 +147,51 @@ describe("working-memory I/O", () => {
|
|
|
136
147
|
});
|
|
137
148
|
});
|
|
138
149
|
|
|
139
|
-
describe("working-memory migration
|
|
140
|
-
it("reads from
|
|
141
|
-
|
|
150
|
+
describe("working-memory migration", () => {
|
|
151
|
+
it("reads from CLI path when present and ignores daemon paths", () => {
|
|
152
|
+
writeCli("ag_mig", { version: 2, sections: { notes: "fresh" }, updatedAt: "2026-01-01" });
|
|
153
|
+
writeDaemonState("ag_mig", { version: 2, sections: { notes: "state" }, updatedAt: "2025-01-01" });
|
|
142
154
|
writeLegacy("ag_mig", { version: 2, sections: { notes: "stale" }, updatedAt: "2024-01-01" });
|
|
143
155
|
|
|
144
156
|
const got = wm.readWorkingMemory("ag_mig");
|
|
145
157
|
expect(got?.sections.notes).toBe("fresh");
|
|
146
|
-
//
|
|
158
|
+
// Old daemon paths are left in place when CLI wins; warning is emitted once.
|
|
159
|
+
expect(existsSync(daemonStatePathFor("ag_mig"))).toBe(true);
|
|
147
160
|
expect(existsSync(legacyPathFor("ag_mig"))).toBe(true);
|
|
148
161
|
expect(warnSpy).toHaveBeenCalled();
|
|
149
162
|
});
|
|
150
163
|
|
|
151
|
-
it("renames
|
|
164
|
+
it("renames daemon state → CLI path on first read when only daemon state exists", () => {
|
|
165
|
+
writeDaemonState("ag_onlystate", {
|
|
166
|
+
version: 2,
|
|
167
|
+
sections: { notes: "state notes" },
|
|
168
|
+
updatedAt: "2025-01-01",
|
|
169
|
+
});
|
|
170
|
+
expect(existsSync(cliPathFor("ag_onlystate"))).toBe(false);
|
|
171
|
+
|
|
172
|
+
const got = wm.readWorkingMemory("ag_onlystate");
|
|
173
|
+
expect(got?.sections.notes).toBe("state notes");
|
|
174
|
+
|
|
175
|
+
expect(existsSync(daemonStatePathFor("ag_onlystate"))).toBe(false);
|
|
176
|
+
expect(existsSync(cliPathFor("ag_onlystate"))).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("renames legacy daemon memory → CLI path on first read when only legacy exists", () => {
|
|
152
180
|
writeLegacy("ag_onlyold", {
|
|
153
181
|
version: 2,
|
|
154
182
|
sections: { notes: "old notes" },
|
|
155
183
|
updatedAt: "2024-01-01",
|
|
156
184
|
});
|
|
157
|
-
expect(existsSync(
|
|
185
|
+
expect(existsSync(cliPathFor("ag_onlyold"))).toBe(false);
|
|
158
186
|
|
|
159
187
|
const got = wm.readWorkingMemory("ag_onlyold");
|
|
160
188
|
expect(got?.sections.notes).toBe("old notes");
|
|
161
189
|
|
|
162
|
-
// Legacy moved away;
|
|
190
|
+
// Legacy moved away; CLI path now holds the data.
|
|
163
191
|
expect(existsSync(legacyPathFor("ag_onlyold"))).toBe(false);
|
|
164
|
-
expect(existsSync(
|
|
192
|
+
expect(existsSync(cliPathFor("ag_onlyold"))).toBe(true);
|
|
165
193
|
|
|
166
|
-
// Subsequent reads come from
|
|
194
|
+
// Subsequent reads come from CLI path — delete legacy dir tree to
|
|
167
195
|
// prove no re-read falls through to it.
|
|
168
196
|
const got2 = wm.readWorkingMemory("ag_onlyold");
|
|
169
197
|
expect(got2?.sections.notes).toBe("old notes");
|
|
@@ -180,13 +208,13 @@ describe("working-memory migration (§8)", () => {
|
|
|
180
208
|
updatedAt: "2024-01-01",
|
|
181
209
|
});
|
|
182
210
|
|
|
183
|
-
// Plant a regular file where the
|
|
211
|
+
// Plant a regular file where the CLI memory *directory* would live, so
|
|
184
212
|
// mkdirSync+renameSync inside the migration branch fails with ENOTDIR
|
|
185
|
-
// (
|
|
213
|
+
// (`~/.botcord/memory/<agentId>` already exists as a file). The
|
|
186
214
|
// migration path must log and fall back to reading the legacy file.
|
|
187
|
-
const home = path.join(tmpHome, ".botcord", "
|
|
215
|
+
const home = path.join(tmpHome, ".botcord", "memory");
|
|
188
216
|
mkdirSync(home, { recursive: true });
|
|
189
|
-
writeFileSync(path.join(home, "
|
|
217
|
+
writeFileSync(path.join(home, "ag_renamefail"), "not a dir");
|
|
190
218
|
|
|
191
219
|
const got = wm.readWorkingMemory("ag_renamefail");
|
|
192
220
|
expect(got?.sections.notes).toBe("still readable");
|
package/src/working-memory.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Working memory — persistent, account-scoped notes injected into every turn.
|
|
3
3
|
*
|
|
4
|
-
* Stored at `~/.botcord/
|
|
5
|
-
*
|
|
4
|
+
* Stored at `~/.botcord/memory/{agentId}/working-memory.json`, matching the
|
|
5
|
+
* @botcord/cli `botcord memory` command so writes made by an agent are visible
|
|
6
|
+
* to daemon context injection on the next turn.
|
|
6
7
|
*
|
|
7
8
|
* Ported from plugin/src/memory.ts (dropping workspace + OpenClaw runtime
|
|
8
9
|
* branches) and plugin/src/memory-protocol.ts (prompt builder).
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
unlinkSync,
|
|
17
18
|
writeFileSync,
|
|
18
19
|
} from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
19
21
|
import path from "node:path";
|
|
20
22
|
import { agentStateDir } from "./agent-workspace.js";
|
|
21
23
|
import { DAEMON_DIR_PATH } from "./config.js";
|
|
@@ -50,18 +52,21 @@ const RESERVED_TAGS_RE = /<\/?(?:current_memory|section_\w+)\b[^>]*>/gi;
|
|
|
50
52
|
// ── Path resolution ────────────────────────────────────────────────
|
|
51
53
|
|
|
52
54
|
/**
|
|
53
|
-
* Canonical per-agent
|
|
54
|
-
*
|
|
55
|
-
* `~/.botcord/daemon/memory/{agentId}` is migrated lazily on first read —
|
|
56
|
-
* see §8 of the daemon-agent-workspace plan.
|
|
55
|
+
* Canonical per-agent memory directory. This intentionally matches
|
|
56
|
+
* @botcord/cli's `botcord memory` path so the CLI and daemon share one store.
|
|
57
57
|
*/
|
|
58
58
|
export function resolveMemoryDir(agentId: string): string {
|
|
59
59
|
if (!agentId) throw new Error("resolveMemoryDir: agentId is required");
|
|
60
|
+
return path.join(homedir(), ".botcord", "memory", agentId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Previous daemon-owned location retained for one-shot migration on read. */
|
|
64
|
+
function daemonStateMemoryDir(agentId: string): string {
|
|
60
65
|
return agentStateDir(agentId);
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
/**
|
|
64
|
-
function
|
|
68
|
+
/** Older daemon location retained for one-shot migration on read. */
|
|
69
|
+
function daemonLegacyMemoryDir(agentId: string): string {
|
|
65
70
|
return path.join(DAEMON_DIR_PATH, "memory", agentId);
|
|
66
71
|
}
|
|
67
72
|
|
|
@@ -69,8 +74,12 @@ function workingMemoryPath(agentId: string): string {
|
|
|
69
74
|
return path.join(resolveMemoryDir(agentId), "working-memory.json");
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
function
|
|
73
|
-
return path.join(
|
|
77
|
+
function daemonStateWorkingMemoryPath(agentId: string): string {
|
|
78
|
+
return path.join(daemonStateMemoryDir(agentId), "working-memory.json");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function daemonLegacyWorkingMemoryPath(agentId: string): string {
|
|
82
|
+
return path.join(daemonLegacyMemoryDir(agentId), "working-memory.json");
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
// Migration conflict warnings are emitted at most once per agent per
|
|
@@ -79,59 +88,63 @@ function legacyWorkingMemoryPath(agentId: string): string {
|
|
|
79
88
|
const warnedMigrationConflict = new Set<string>();
|
|
80
89
|
|
|
81
90
|
/**
|
|
82
|
-
* Resolve the path to read from, migrating from
|
|
91
|
+
* Resolve the path to read from, migrating from daemon-only locations if
|
|
83
92
|
* necessary. Returns the path the caller should read, or `null` when no
|
|
84
93
|
* memory file exists anywhere.
|
|
85
|
-
*
|
|
86
|
-
* Migration branch (the `else if` on `legacyExists` below) is meant to be
|
|
87
|
-
* deleted one release after this change ships; see plan §8 step 6.
|
|
88
94
|
*/
|
|
89
95
|
function resolveReadPath(agentId: string): string | null {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
const cliPath = workingMemoryPath(agentId);
|
|
97
|
+
const daemonStatePath = daemonStateWorkingMemoryPath(agentId);
|
|
98
|
+
const daemonLegacyPath = daemonLegacyWorkingMemoryPath(agentId);
|
|
99
|
+
const cliExists = existsSync(cliPath);
|
|
100
|
+
const daemonStateExists = existsSync(daemonStatePath);
|
|
101
|
+
const daemonLegacyExists = existsSync(daemonLegacyPath);
|
|
102
|
+
|
|
103
|
+
if (cliExists) {
|
|
104
|
+
if ((daemonStateExists || daemonLegacyExists) && !warnedMigrationConflict.has(agentId)) {
|
|
97
105
|
warnedMigrationConflict.add(agentId);
|
|
98
|
-
daemonLog.warn("working-memory: both
|
|
106
|
+
daemonLog.warn("working-memory: both cli and daemon paths exist; using cli", {
|
|
99
107
|
agentId,
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
cliPath,
|
|
109
|
+
daemonStatePath,
|
|
110
|
+
daemonLegacyPath,
|
|
102
111
|
});
|
|
103
112
|
}
|
|
104
|
-
return
|
|
113
|
+
return cliPath;
|
|
105
114
|
}
|
|
106
|
-
|
|
115
|
+
|
|
116
|
+
const migrateFrom = daemonStateExists
|
|
117
|
+
? daemonStatePath
|
|
118
|
+
: daemonLegacyExists
|
|
119
|
+
? daemonLegacyPath
|
|
120
|
+
: null;
|
|
121
|
+
if (migrateFrom) {
|
|
107
122
|
try {
|
|
108
|
-
mkdirSync(path.dirname(
|
|
123
|
+
mkdirSync(path.dirname(cliPath), { recursive: true, mode: 0o700 });
|
|
109
124
|
try {
|
|
110
|
-
renameSync(
|
|
125
|
+
renameSync(migrateFrom, cliPath);
|
|
111
126
|
} catch (err) {
|
|
112
|
-
// EXDEV =
|
|
127
|
+
// EXDEV = old and canonical paths live on different filesystems
|
|
113
128
|
// (bind mounts, tmpfs overlays). `renameSync` cannot cross fs
|
|
114
|
-
// boundaries, so fall back to copy + unlink.
|
|
115
|
-
// next write would go to newPath while legacy still has the old
|
|
116
|
-
// payload — silent divergence the reviewer of §8 flagged.
|
|
129
|
+
// boundaries, so fall back to copy + unlink.
|
|
117
130
|
if ((err as NodeJS.ErrnoException).code === "EXDEV") {
|
|
118
|
-
copyFileSync(
|
|
119
|
-
unlinkSync(
|
|
131
|
+
copyFileSync(migrateFrom, cliPath);
|
|
132
|
+
unlinkSync(migrateFrom);
|
|
120
133
|
} else {
|
|
121
134
|
throw err;
|
|
122
135
|
}
|
|
123
136
|
}
|
|
124
|
-
return
|
|
137
|
+
return cliPath;
|
|
125
138
|
} catch (err) {
|
|
126
139
|
const e = err as NodeJS.ErrnoException;
|
|
127
|
-
daemonLog.warn("working-memory: migration rename failed; reading
|
|
140
|
+
daemonLog.warn("working-memory: migration rename failed; reading daemon path", {
|
|
128
141
|
agentId,
|
|
129
|
-
oldPath,
|
|
130
|
-
newPath,
|
|
142
|
+
oldPath: migrateFrom,
|
|
143
|
+
newPath: cliPath,
|
|
131
144
|
code: e.code,
|
|
132
145
|
error: e.message ?? String(err),
|
|
133
146
|
});
|
|
134
|
-
return
|
|
147
|
+
return migrateFrom;
|
|
135
148
|
}
|
|
136
149
|
}
|
|
137
150
|
return null;
|