negotium 0.2.1 → 0.2.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/dist/agent-helpers.js +2 -2
- package/dist/agent-helpers.js.map +2 -2
- package/dist/hosted-agent.js +2 -2
- package/dist/hosted-agent.js.map +2 -2
- package/dist/main.js +168 -24
- package/dist/main.js.map +4 -4
- package/dist/mcp-factories.js +2 -2
- package/dist/mcp-factories.js.map +2 -2
- package/dist/runtime/src/migration/single-user.ts +210 -26
- package/dist/runtime/src/version.ts +1 -1
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -40,7 +40,9 @@ interface MoveOperation {
|
|
|
40
40
|
source: string;
|
|
41
41
|
staged: string;
|
|
42
42
|
destination?: string;
|
|
43
|
-
|
|
43
|
+
destinationStaged?: string;
|
|
44
|
+
collision?: "keep-destination" | "replace-destination" | "merge-jsonl" | "merge-tasks";
|
|
45
|
+
state: "pending" | "staged" | "source-staged" | "destination-staged" | "placed";
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
interface MigrationJournal {
|
|
@@ -70,7 +72,9 @@ function assertNoActiveNode(stateDir: string): void {
|
|
|
70
72
|
const infoPath = join(stateDir, runtimeName, "node-daemon.json");
|
|
71
73
|
if (!existsSync(infoPath)) continue;
|
|
72
74
|
try {
|
|
73
|
-
const value = JSON.parse(readFileSync(infoPath, "utf8")) as {
|
|
75
|
+
const value = JSON.parse(readFileSync(infoPath, "utf8")) as {
|
|
76
|
+
pid?: number;
|
|
77
|
+
};
|
|
74
78
|
if (typeof value.pid === "number" && processIsAlive(value.pid)) {
|
|
75
79
|
throw new Error(`Refusing migration while Negotium pid ${value.pid} is active.`);
|
|
76
80
|
}
|
|
@@ -121,17 +125,60 @@ function addDirectoryContents(
|
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
function
|
|
128
|
+
function pathWithin(path: string, root: string): boolean {
|
|
129
|
+
return path === root || path.startsWith(`${root}/`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function collisionPolicy(
|
|
133
|
+
operation: MoveOperation,
|
|
134
|
+
stateDir: string,
|
|
135
|
+
): NonNullable<MoveOperation["collision"]> | undefined {
|
|
136
|
+
const destination = operation.destination;
|
|
137
|
+
if (!destination || !existsSync(destination)) return undefined;
|
|
138
|
+
if (pathWithin(destination, join(stateDir, "runtime"))) return "keep-destination";
|
|
139
|
+
if (pathWithin(destination, join(stateDir, "binaries"))) return "keep-destination";
|
|
140
|
+
if (pathWithin(destination, join(stateDir, "secrets"))) return "replace-destination";
|
|
141
|
+
if (destination === join(stateDir, "data", "vault", "vault.db")) {
|
|
142
|
+
return "replace-destination";
|
|
143
|
+
}
|
|
144
|
+
if (pathWithin(destination, join(stateDir, "browser", "profiles"))) {
|
|
145
|
+
return "replace-destination";
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
pathWithin(destination, join(stateDir, "data", "conversations")) &&
|
|
149
|
+
destination.endsWith(".jsonl")
|
|
150
|
+
) {
|
|
151
|
+
return "merge-jsonl";
|
|
152
|
+
}
|
|
153
|
+
if (pathWithin(destination, join(stateDir, "data", "tasks")) && destination.endsWith(".json")) {
|
|
154
|
+
return "merge-tasks";
|
|
155
|
+
}
|
|
156
|
+
if (
|
|
157
|
+
statSync(operation.source).isFile() &&
|
|
158
|
+
statSync(destination).isFile() &&
|
|
159
|
+
readFileSync(operation.source).equals(readFileSync(destination))
|
|
160
|
+
) {
|
|
161
|
+
return "keep-destination";
|
|
162
|
+
}
|
|
163
|
+
throw new Error(`Migration collision at ${destination}.`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function configureCollisions(
|
|
167
|
+
operations: MoveOperation[],
|
|
168
|
+
stateDir: string,
|
|
169
|
+
stagingRoot: string,
|
|
170
|
+
): void {
|
|
125
171
|
const destinations = new Set<string>();
|
|
126
172
|
for (const operation of operations) {
|
|
127
173
|
if (!operation.destination) continue;
|
|
128
|
-
if (existsSync(operation.destination)) {
|
|
129
|
-
throw new Error(`Migration collision at ${operation.destination}.`);
|
|
130
|
-
}
|
|
131
174
|
if (destinations.has(operation.destination)) {
|
|
132
175
|
throw new Error(`Multiple legacy paths target ${operation.destination}.`);
|
|
133
176
|
}
|
|
134
177
|
destinations.add(operation.destination);
|
|
178
|
+
operation.collision = collisionPolicy(operation, stateDir);
|
|
179
|
+
if (operation.collision && operation.collision !== "keep-destination") {
|
|
180
|
+
operation.destinationStaged = join(stagingRoot, randomUUID());
|
|
181
|
+
}
|
|
135
182
|
}
|
|
136
183
|
}
|
|
137
184
|
|
|
@@ -141,16 +188,115 @@ function writeJournal(path: string, journal: MigrationJournal): void {
|
|
|
141
188
|
|
|
142
189
|
function rollbackFilesystem(journal: MigrationJournal, journalPath: string): void {
|
|
143
190
|
for (const operation of [...journal.operations].reverse()) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
191
|
+
if (operation.state === "placed") {
|
|
192
|
+
if (operation.collision === "keep-destination" || !operation.destination) {
|
|
193
|
+
if (existsSync(operation.staged) && !existsSync(operation.source)) {
|
|
194
|
+
mkdirSync(dirname(operation.source), { recursive: true });
|
|
195
|
+
renameSync(operation.staged, operation.source);
|
|
196
|
+
}
|
|
197
|
+
} else if (operation.destination && existsSync(operation.destination)) {
|
|
198
|
+
if (operation.collision === "merge-jsonl" || operation.collision === "merge-tasks") {
|
|
199
|
+
rmSync(operation.destination, { recursive: true, force: true });
|
|
200
|
+
if (existsSync(operation.staged) && !existsSync(operation.source)) {
|
|
201
|
+
mkdirSync(dirname(operation.source), { recursive: true });
|
|
202
|
+
renameSync(operation.staged, operation.source);
|
|
203
|
+
}
|
|
204
|
+
} else if (!existsSync(operation.source)) {
|
|
205
|
+
mkdirSync(dirname(operation.source), { recursive: true });
|
|
206
|
+
renameSync(operation.destination, operation.source);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} else if (existsSync(operation.staged) && !existsSync(operation.source)) {
|
|
210
|
+
mkdirSync(dirname(operation.source), { recursive: true });
|
|
211
|
+
renameSync(operation.staged, operation.source);
|
|
212
|
+
}
|
|
213
|
+
if (
|
|
214
|
+
operation.destination &&
|
|
215
|
+
operation.destinationStaged &&
|
|
216
|
+
existsSync(operation.destinationStaged) &&
|
|
217
|
+
!existsSync(operation.destination)
|
|
218
|
+
) {
|
|
219
|
+
mkdirSync(dirname(operation.destination), { recursive: true });
|
|
220
|
+
renameSync(operation.destinationStaged, operation.destination);
|
|
221
|
+
}
|
|
148
222
|
operation.state = "pending";
|
|
149
223
|
}
|
|
150
224
|
journal.phase = "filesystem";
|
|
151
225
|
writeJournal(journalPath, journal);
|
|
152
226
|
}
|
|
153
227
|
|
|
228
|
+
function mergedJsonl(source: string, destination: string): string {
|
|
229
|
+
const lines = new Set<string>();
|
|
230
|
+
for (const path of [source, destination]) {
|
|
231
|
+
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
232
|
+
if (line) lines.add(line);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return lines.size > 0 ? `${[...lines].join("\n")}\n` : "";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface TaskFileShape {
|
|
239
|
+
version: 1;
|
|
240
|
+
tasks: Array<{ id: string; blockedBy?: string[]; [key: string]: unknown }>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function readTaskFile(path: string): TaskFileShape {
|
|
244
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<TaskFileShape>;
|
|
245
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.tasks)) {
|
|
246
|
+
throw new Error(`Invalid task file during migration: ${path}`);
|
|
247
|
+
}
|
|
248
|
+
return parsed as TaskFileShape;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function mergedTasks(source: string, destination: string): string {
|
|
252
|
+
const legacy = readTaskFile(source).tasks;
|
|
253
|
+
const current = readTaskFile(destination).tasks;
|
|
254
|
+
const merged = legacy.map((task) => ({ ...task }));
|
|
255
|
+
const usedIds = new Set(merged.map((task) => String(task.id)));
|
|
256
|
+
let nextId = Math.max(0, ...[...usedIds].map(Number).filter(Number.isInteger)) + 1;
|
|
257
|
+
const remapped = new Map<string, string>();
|
|
258
|
+
|
|
259
|
+
for (const task of current) {
|
|
260
|
+
const originalId = String(task.id);
|
|
261
|
+
const exact = merged.find(
|
|
262
|
+
(candidate) =>
|
|
263
|
+
candidate.id === originalId && JSON.stringify(candidate) === JSON.stringify(task),
|
|
264
|
+
);
|
|
265
|
+
if (exact) {
|
|
266
|
+
remapped.set(originalId, originalId);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const id = usedIds.has(originalId) ? String(nextId++) : originalId;
|
|
270
|
+
usedIds.add(id);
|
|
271
|
+
remapped.set(originalId, id);
|
|
272
|
+
merged.push({ ...task, id });
|
|
273
|
+
}
|
|
274
|
+
for (const task of merged.slice(legacy.length)) {
|
|
275
|
+
if (task.blockedBy) task.blockedBy = task.blockedBy.map((id) => remapped.get(id) ?? id);
|
|
276
|
+
}
|
|
277
|
+
return `${JSON.stringify({ version: 1, tasks: merged }, null, 2)}\n`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function placeOperation(operation: MoveOperation): void {
|
|
281
|
+
if (!operation.destination || operation.collision === "keep-destination") return;
|
|
282
|
+
mkdirSync(dirname(operation.destination), { recursive: true });
|
|
283
|
+
if (operation.collision === "merge-jsonl") {
|
|
284
|
+
writeFileSync(
|
|
285
|
+
operation.destination,
|
|
286
|
+
mergedJsonl(operation.staged, operation.destinationStaged!),
|
|
287
|
+
);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (operation.collision === "merge-tasks") {
|
|
291
|
+
writeFileSync(
|
|
292
|
+
operation.destination,
|
|
293
|
+
mergedTasks(operation.staged, operation.destinationStaged!),
|
|
294
|
+
);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
renameSync(operation.staged, operation.destination);
|
|
298
|
+
}
|
|
299
|
+
|
|
154
300
|
function tablesWithColumn(database: InstanceType<typeof Database>, columnName: string): string[] {
|
|
155
301
|
const tables = database
|
|
156
302
|
.query<{ name: string }, []>(
|
|
@@ -512,7 +658,7 @@ export function migrateSingleUserState(
|
|
|
512
658
|
state: "pending",
|
|
513
659
|
});
|
|
514
660
|
}
|
|
515
|
-
|
|
661
|
+
configureCollisions(operations, stateDir, stagingRoot);
|
|
516
662
|
journal = {
|
|
517
663
|
id: SINGLE_USER_MIGRATION_ID,
|
|
518
664
|
sourcePrincipal: source,
|
|
@@ -529,30 +675,57 @@ export function migrateSingleUserState(
|
|
|
529
675
|
try {
|
|
530
676
|
for (const operation of journal.operations) {
|
|
531
677
|
// Reconcile a crash between rename(2) and the following journal write.
|
|
678
|
+
if (operation.state === "staged") operation.state = "source-staged";
|
|
679
|
+
if (operation.state === "pending" && !existsSync(operation.source)) {
|
|
680
|
+
if (existsSync(operation.staged)) operation.state = "source-staged";
|
|
681
|
+
else if (operation.destination && existsSync(operation.destination)) {
|
|
682
|
+
operation.state = "placed";
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (operation.state === "pending") {
|
|
686
|
+
mkdirSync(dirname(operation.staged), { recursive: true });
|
|
687
|
+
renameSync(operation.source, operation.staged);
|
|
688
|
+
operation.state = "source-staged";
|
|
689
|
+
writeJournal(journalPath, journal);
|
|
690
|
+
}
|
|
532
691
|
if (
|
|
533
|
-
operation.state === "
|
|
534
|
-
|
|
535
|
-
|
|
692
|
+
operation.state === "source-staged" &&
|
|
693
|
+
operation.collision === "replace-destination" &&
|
|
694
|
+
operation.destination &&
|
|
695
|
+
operation.destinationStaged &&
|
|
696
|
+
!existsSync(operation.staged) &&
|
|
697
|
+
existsSync(operation.destination) &&
|
|
698
|
+
existsSync(operation.destinationStaged)
|
|
536
699
|
) {
|
|
537
|
-
operation.state = "
|
|
700
|
+
operation.state = "placed";
|
|
701
|
+
writeJournal(journalPath, journal);
|
|
538
702
|
}
|
|
539
703
|
if (
|
|
540
|
-
operation.state === "staged" &&
|
|
704
|
+
operation.state === "destination-staged" &&
|
|
705
|
+
operation.collision !== "merge-jsonl" &&
|
|
706
|
+
operation.collision !== "merge-tasks" &&
|
|
707
|
+
operation.collision !== "keep-destination" &&
|
|
541
708
|
operation.destination &&
|
|
542
709
|
!existsSync(operation.staged) &&
|
|
543
710
|
existsSync(operation.destination)
|
|
544
711
|
) {
|
|
545
712
|
operation.state = "placed";
|
|
713
|
+
writeJournal(journalPath, journal);
|
|
546
714
|
}
|
|
547
|
-
if (operation.state === "
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
715
|
+
if (operation.state === "source-staged") {
|
|
716
|
+
if (operation.destination && operation.destinationStaged) {
|
|
717
|
+
if (existsSync(operation.destination) && !existsSync(operation.destinationStaged)) {
|
|
718
|
+
renameSync(operation.destination, operation.destinationStaged);
|
|
719
|
+
}
|
|
720
|
+
if (!existsSync(operation.destinationStaged)) {
|
|
721
|
+
throw new Error(`Missing collision backup for ${operation.destination}.`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
operation.state = "destination-staged";
|
|
551
725
|
writeJournal(journalPath, journal);
|
|
552
726
|
}
|
|
553
|
-
if (operation.state === "staged"
|
|
554
|
-
|
|
555
|
-
renameSync(operation.staged, operation.destination);
|
|
727
|
+
if (operation.state === "destination-staged") {
|
|
728
|
+
placeOperation(operation);
|
|
556
729
|
operation.state = "placed";
|
|
557
730
|
writeJournal(journalPath, journal);
|
|
558
731
|
}
|
|
@@ -598,10 +771,21 @@ export function migrateSingleUserState(
|
|
|
598
771
|
migratedTables,
|
|
599
772
|
};
|
|
600
773
|
for (const store of ["conversations", "tasks", "users"]) {
|
|
601
|
-
rmSync(join(stateDir, "data", store, source), {
|
|
774
|
+
rmSync(join(stateDir, "data", store, source), {
|
|
775
|
+
recursive: true,
|
|
776
|
+
force: true,
|
|
777
|
+
});
|
|
602
778
|
}
|
|
603
|
-
rmSync(join(stateDir, "
|
|
604
|
-
|
|
779
|
+
rmSync(join(stateDir, "run"), { recursive: true, force: true });
|
|
780
|
+
rmSync(join(stateDir, "bin"), { recursive: true, force: true });
|
|
781
|
+
rmSync(join(stateDir, "workspace", "browser-profiles"), {
|
|
782
|
+
recursive: true,
|
|
783
|
+
force: true,
|
|
784
|
+
});
|
|
785
|
+
writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, {
|
|
786
|
+
mode: 0o600,
|
|
787
|
+
flag: "wx",
|
|
788
|
+
});
|
|
605
789
|
journal.phase = "complete";
|
|
606
790
|
rmSync(stagingRoot, { recursive: true, force: true });
|
|
607
791
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const NEGOTIUM_VERSION = "0.2.
|
|
1
|
+
export const NEGOTIUM_VERSION = "0.2.2";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const NEGOTIUM_VERSION = "0.2.
|
|
1
|
+
export declare const NEGOTIUM_VERSION = "0.2.2";
|