@lucid-agents/taskmarket 1.17.1 → 1.18.0
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/THIRD_PARTY_NOTICES.md +54 -27
- package/dist/agent-compatibility.json +1 -1
- package/dist/{budget-view-XKZC3GY5.js → budget-view-JPZ7HWR2.js} +1 -1
- package/dist/{channels-Q7XMSUSO.js → channels-544VRSKF.js} +4 -3
- package/dist/chunk-2QJMUHDT.js +351 -0
- package/dist/{launch-DE65XO5C.js → chunk-64HQ7ZF2.js} +4977 -2385
- package/dist/chunk-75LS6XS6.js +10302 -0
- package/dist/{chunk-73RXOYME.js → chunk-EB6OJBF5.js} +5 -0
- package/dist/chunk-ESPAXW2P.js +831 -0
- package/dist/chunk-JJUF3KZY.js +447 -0
- package/dist/chunk-L6ZVRENW.js +868 -0
- package/dist/{chunk-F5SS4NUF.js → chunk-MO5KMUTL.js} +106 -29
- package/dist/chunk-S4A6V3J3.js +7518 -0
- package/dist/{chunk-EIHZYRIU.js → chunk-W7CXEYST.js} +23 -23
- package/dist/chunk-ZIC2YCLV.js +65 -0
- package/dist/{doctor-BY2PBHS7.js → doctor-5FUGI42V.js} +13 -10
- package/dist/index.js +2110 -739
- package/dist/launch-C5HUFUU2.js +6364 -0
- package/dist/pi-extensions/remote-tools.cjs +174 -0
- package/dist/pi-extensions/xgate.cjs +1039 -25
- package/dist/{status-3Q55PBFC.js → prompter-LSICEYSQ.js} +3 -12
- package/dist/reconcile-Q7XFHDQQ.js +194 -0
- package/dist/status-SDLSTJIA.js +28 -0
- package/dist/sync-pull-TKM4EDGH.js +409 -0
- package/package.json +1 -1
- package/dist/chunk-3S63AB3C.js +0 -16068
- package/dist/chunk-JAZ5BWPB.js +0 -181
- package/dist/chunk-MI2GPMCZ.js +0 -154
- package/dist/reconcile-VEMPNIAO.js +0 -83
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { createRequire as __tmCreateRequire } from 'node:module'; import { fileURLToPath as __tmFileURLToPath } from 'node:url'; import { dirname as __tmDirname } from 'node:path'; const require = __tmCreateRequire(import.meta.url); const __tmFilename = __tmFileURLToPath(import.meta.url); const __tmDirnameValue = __tmDirname(__tmFilename);
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
statusLines
|
|
6
|
-
} from "./chunk-JAZ5BWPB.js";
|
|
7
|
-
import "./chunk-MI2GPMCZ.js";
|
|
8
|
-
import "./chunk-EIHZYRIU.js";
|
|
9
|
-
import "./chunk-F5SS4NUF.js";
|
|
10
|
-
import "./chunk-3S63AB3C.js";
|
|
3
|
+
terminalPrompter
|
|
4
|
+
} from "./chunk-ZIC2YCLV.js";
|
|
11
5
|
import "./chunk-YM4S6IE4.js";
|
|
12
|
-
import "./chunk-73RXOYME.js";
|
|
13
6
|
import "./chunk-S7SJMJ2I.js";
|
|
14
7
|
export {
|
|
15
|
-
|
|
16
|
-
listSandboxes,
|
|
17
|
-
statusLines
|
|
8
|
+
terminalPrompter
|
|
18
9
|
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { createRequire as __tmCreateRequire } from 'node:module'; import { fileURLToPath as __tmFileURLToPath } from 'node:url'; import { dirname as __tmDirname } from 'node:path'; const require = __tmCreateRequire(import.meta.url); const __tmFilename = __tmFileURLToPath(import.meta.url); const __tmDirnameValue = __tmDirname(__tmFilename);
|
|
2
|
+
import {
|
|
3
|
+
channelView,
|
|
4
|
+
openAgentChannels,
|
|
5
|
+
selectChannel
|
|
6
|
+
} from "./chunk-MO5KMUTL.js";
|
|
7
|
+
import {
|
|
8
|
+
BASE_CHANNEL_RAIL_ID,
|
|
9
|
+
X402_INVOCATION_ID_PATTERN,
|
|
10
|
+
getX402JournalPath,
|
|
11
|
+
listX402Payments
|
|
12
|
+
} from "./chunk-S4A6V3J3.js";
|
|
13
|
+
import "./chunk-75LS6XS6.js";
|
|
14
|
+
import {
|
|
15
|
+
AGENT_VIEW_VERSION,
|
|
16
|
+
LOST_RESPONSE_TEXT,
|
|
17
|
+
formatUsdc
|
|
18
|
+
} from "./chunk-EB6OJBF5.js";
|
|
19
|
+
import {
|
|
20
|
+
AgentLaunchError
|
|
21
|
+
} from "./chunk-YM4S6IE4.js";
|
|
22
|
+
import "./chunk-S7SJMJ2I.js";
|
|
23
|
+
|
|
24
|
+
// src/lib/agent/reconcile.ts
|
|
25
|
+
function outcomeOf(status) {
|
|
26
|
+
if (status.state === "in_progress") return "in_progress";
|
|
27
|
+
if (status.charge === "manually_resolved") return "resolved_manually";
|
|
28
|
+
if (status.uncertain) return "uncertain";
|
|
29
|
+
if (status.charge === "settled") return "charged";
|
|
30
|
+
if (status.charge === "none" || status.charge === "released") return "not_charged";
|
|
31
|
+
return "uncertain";
|
|
32
|
+
}
|
|
33
|
+
function messageFor(outcome, status) {
|
|
34
|
+
const charged = status.payment?.settledAmount;
|
|
35
|
+
switch (outcome) {
|
|
36
|
+
case "charged":
|
|
37
|
+
return `Charged ${charged !== void 0 ? formatUsdc(charged) : "an amount the router committed"}. If its response did not reach you: ${LOST_RESPONSE_TEXT}`;
|
|
38
|
+
case "not_charged":
|
|
39
|
+
return "Nothing was charged. The request is not sent again; ask again in the agent if you still want an answer.";
|
|
40
|
+
case "in_progress":
|
|
41
|
+
return "It is still being sent by another process; reconcile it again once that finishes.";
|
|
42
|
+
case "resolved_manually":
|
|
43
|
+
return "It was resolved by hand; its recorded amount stands.";
|
|
44
|
+
default:
|
|
45
|
+
return "Its outcome is still unknown. It keeps its maximum counted and blocks new turns on its channel; reconcile it again later. It is never sent again.";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var code = (error) => error?.code;
|
|
49
|
+
async function locate(rails, invocationId) {
|
|
50
|
+
let owner;
|
|
51
|
+
for (const rail of rails) {
|
|
52
|
+
try {
|
|
53
|
+
await rail.payer.status(invocationId);
|
|
54
|
+
owner = rail;
|
|
55
|
+
break;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (code(error) !== "invocation_not_found") throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const leased = [];
|
|
61
|
+
for (const rail of rails) {
|
|
62
|
+
const lease = (await rail.payer.channelStatus()).pendingOperation;
|
|
63
|
+
if (lease?.kind === "payment" && lease.id === invocationId) leased.push(rail);
|
|
64
|
+
}
|
|
65
|
+
return { owner, leased };
|
|
66
|
+
}
|
|
67
|
+
async function settle(rails, invocationId, only) {
|
|
68
|
+
const { owner, leased } = await locate(rails, invocationId);
|
|
69
|
+
if (only && owner && owner !== only) {
|
|
70
|
+
throw new AgentLaunchError(
|
|
71
|
+
"invocation_not_found",
|
|
72
|
+
`Purchase ${invocationId} was paid on the ${owner.rail.id} channel ${owner.payer.channelId}, not on ${only.payer.channelId}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const releasedFrom = [];
|
|
76
|
+
if (owner) {
|
|
77
|
+
for (const rail2 of leased.filter((candidate) => candidate !== owner)) {
|
|
78
|
+
try {
|
|
79
|
+
await rail2.payer.reconcile(invocationId);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (code(error) !== "invocation_not_found") throw error;
|
|
82
|
+
}
|
|
83
|
+
const lease2 = (await rail2.payer.channelStatus()).pendingOperation;
|
|
84
|
+
if (!(lease2?.kind === "payment" && lease2.id === invocationId))
|
|
85
|
+
releasedFrom.push(rail2.rail.id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const rail = owner ?? (only && leased.includes(only) ? only : leased[0]);
|
|
89
|
+
if (!rail) {
|
|
90
|
+
throw new AgentLaunchError(
|
|
91
|
+
"invocation_not_found",
|
|
92
|
+
only ? `No agent purchase ${invocationId} is recorded on channel ${only.payer.channelId}` : `No agent purchase ${invocationId} is recorded on this device`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const status = await rail.payer.reconcile(invocationId);
|
|
96
|
+
const lease = (await rail.payer.channelStatus()).pendingOperation;
|
|
97
|
+
const outcome = outcomeOf(status);
|
|
98
|
+
return {
|
|
99
|
+
version: AGENT_VIEW_VERSION,
|
|
100
|
+
kind: "taskmarket.agent.reconcile",
|
|
101
|
+
invocationId,
|
|
102
|
+
rail: rail.rail.id,
|
|
103
|
+
outcome,
|
|
104
|
+
state: status.state,
|
|
105
|
+
charge: status.charge,
|
|
106
|
+
authorizedAtomic: status.payment?.authorizedAmount ?? null,
|
|
107
|
+
chargedAtomic: status.payment?.settledAmount ?? null,
|
|
108
|
+
blocksChannel: lease?.kind === "payment" && lease.id === invocationId,
|
|
109
|
+
releasedFrom,
|
|
110
|
+
message: messageFor(outcome, status)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
var chosen = (choice) => choice.rail !== void 0 || choice.channel !== void 0;
|
|
114
|
+
async function reconcileInvocation(invocationId, deps = {}) {
|
|
115
|
+
if (!X402_INVOCATION_ID_PATTERN.test(invocationId)) {
|
|
116
|
+
throw new AgentLaunchError("invalid_invocation_id", `Not an invocation ID: ${invocationId}`);
|
|
117
|
+
}
|
|
118
|
+
const opened = await (deps.openChannels ?? openAgentChannels)();
|
|
119
|
+
const rails = [...opened.rails, ...opened.retired ?? []];
|
|
120
|
+
const only = deps.choice && chosen(deps.choice) ? selectChannel(opened, deps.choice).rail : void 0;
|
|
121
|
+
return settle(rails, invocationId, only);
|
|
122
|
+
}
|
|
123
|
+
var UNDECIDED = /* @__PURE__ */ new Set([
|
|
124
|
+
"reserved",
|
|
125
|
+
"approval_pending",
|
|
126
|
+
"ready",
|
|
127
|
+
"dispatched",
|
|
128
|
+
"unknown",
|
|
129
|
+
"settled_amount_unknown"
|
|
130
|
+
]);
|
|
131
|
+
async function openPurchases(rail) {
|
|
132
|
+
const records = await listX402Payments({}, getX402JournalPath());
|
|
133
|
+
const byId = new Map(records.map((record) => [record.id, record]));
|
|
134
|
+
const found = [];
|
|
135
|
+
for (const record of records) {
|
|
136
|
+
if (!record.invocationId || record.rail?.id !== BASE_CHANNEL_RAIL_ID || record.funding?.sessionKey !== rail.payer.sessionKey)
|
|
137
|
+
continue;
|
|
138
|
+
const deposit = record.funding.depositId ? byId.get(record.funding.depositId) : void 0;
|
|
139
|
+
const depositOpen = deposit !== void 0 && ["reserved", "dispatched", "unknown"].includes(deposit.state);
|
|
140
|
+
if (UNDECIDED.has(record.state) || depositOpen)
|
|
141
|
+
found.push({ invocationId: record.invocationId, createdAt: record.createdAt });
|
|
142
|
+
}
|
|
143
|
+
return found.sort((a, b) => a.createdAt.localeCompare(b.createdAt)).map((p) => p.invocationId);
|
|
144
|
+
}
|
|
145
|
+
async function reconcileChannelPurchases(choice, deps = {}) {
|
|
146
|
+
const opened = await (deps.openChannels ?? openAgentChannels)();
|
|
147
|
+
const { rail, retired } = selectChannel(opened, choice);
|
|
148
|
+
const rails = [...opened.rails, ...opened.retired ?? []];
|
|
149
|
+
const lease = (await rail.payer.channelStatus()).pendingOperation;
|
|
150
|
+
const ids = [
|
|
151
|
+
.../* @__PURE__ */ new Set([...lease?.kind === "payment" ? [lease.id] : [], ...await openPurchases(rail)])
|
|
152
|
+
];
|
|
153
|
+
const purchases = [];
|
|
154
|
+
let releasedLease = null;
|
|
155
|
+
for (const id of ids) {
|
|
156
|
+
try {
|
|
157
|
+
const view = await settle(rails, id);
|
|
158
|
+
if (view.releasedFrom.includes(rail.rail.id)) releasedLease = id;
|
|
159
|
+
purchases.push(view);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
purchases.push({
|
|
162
|
+
invocationId: id,
|
|
163
|
+
outcome: "failed",
|
|
164
|
+
code: typeof code(error) === "string" ? code(error) : "request_failed",
|
|
165
|
+
message: error.message
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const status = await rail.payer.channelStatus();
|
|
170
|
+
const channel = await channelView(rail, status, { retired });
|
|
171
|
+
const next = status.pendingOperation ? `It is still held by ${status.pendingOperation.kind} ${status.pendingOperation.id}; reconcile again later.` : status.closed ? "The channel is closed." : BigInt(status.funding.refundable) > 0n ? `Nothing holds it now. To refund its ${formatUsdc(status.funding.refundable)}: taskmarket agent close --channel ${status.channelId}` : "Nothing holds it now.";
|
|
172
|
+
return {
|
|
173
|
+
version: AGENT_VIEW_VERSION,
|
|
174
|
+
kind: "taskmarket.agent.reconcile-channel",
|
|
175
|
+
rail: rail.rail.id,
|
|
176
|
+
channelId: rail.payer.channelId,
|
|
177
|
+
retired,
|
|
178
|
+
purchases,
|
|
179
|
+
releasedLease,
|
|
180
|
+
channel,
|
|
181
|
+
message: ids.length === 0 ? `No purchase on this channel is undecided. ${next}` : `${ids.length} purchase(s) reconciled. ${next}`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
async function reconcileLeasedPurchase(opened, invocationId) {
|
|
185
|
+
try {
|
|
186
|
+
await settle([...opened.rails, ...opened.retired ?? []], invocationId);
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export {
|
|
191
|
+
reconcileChannelPurchases,
|
|
192
|
+
reconcileInvocation,
|
|
193
|
+
reconcileLeasedPurchase
|
|
194
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createRequire as __tmCreateRequire } from 'node:module'; import { fileURLToPath as __tmFileURLToPath } from 'node:url'; import { dirname as __tmDirname } from 'node:path'; const require = __tmCreateRequire(import.meta.url); const __tmFilename = __tmFileURLToPath(import.meta.url); const __tmDirnameValue = __tmDirname(__tmFilename);
|
|
2
|
+
import {
|
|
3
|
+
collectAgentStatus,
|
|
4
|
+
listSandboxes,
|
|
5
|
+
readSyncStatus,
|
|
6
|
+
readTaskStatus,
|
|
7
|
+
statusLines,
|
|
8
|
+
syncLines,
|
|
9
|
+
taskLines
|
|
10
|
+
} from "./chunk-2QJMUHDT.js";
|
|
11
|
+
import "./chunk-L6ZVRENW.js";
|
|
12
|
+
import "./chunk-64HQ7ZF2.js";
|
|
13
|
+
import "./chunk-W7CXEYST.js";
|
|
14
|
+
import "./chunk-MO5KMUTL.js";
|
|
15
|
+
import "./chunk-S4A6V3J3.js";
|
|
16
|
+
import "./chunk-75LS6XS6.js";
|
|
17
|
+
import "./chunk-EB6OJBF5.js";
|
|
18
|
+
import "./chunk-YM4S6IE4.js";
|
|
19
|
+
import "./chunk-S7SJMJ2I.js";
|
|
20
|
+
export {
|
|
21
|
+
collectAgentStatus,
|
|
22
|
+
listSandboxes,
|
|
23
|
+
readSyncStatus,
|
|
24
|
+
readTaskStatus,
|
|
25
|
+
statusLines,
|
|
26
|
+
syncLines,
|
|
27
|
+
taskLines
|
|
28
|
+
};
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { createRequire as __tmCreateRequire } from 'node:module'; import { fileURLToPath as __tmFileURLToPath } from 'node:url'; import { dirname as __tmDirname } from 'node:path'; const require = __tmCreateRequire(import.meta.url); const __tmFilename = __tmFileURLToPath(import.meta.url); const __tmDirnameValue = __tmDirname(__tmFilename);
|
|
2
|
+
import {
|
|
3
|
+
SYNC_VIEW_VERSION,
|
|
4
|
+
SessionSync,
|
|
5
|
+
captureProblemMessage,
|
|
6
|
+
changeLine,
|
|
7
|
+
countsLine,
|
|
8
|
+
fullDiff,
|
|
9
|
+
leftOutNote,
|
|
10
|
+
savedState,
|
|
11
|
+
savedWorkspace,
|
|
12
|
+
summarize,
|
|
13
|
+
unsavedPaths
|
|
14
|
+
} from "./chunk-ESPAXW2P.js";
|
|
15
|
+
import {
|
|
16
|
+
BrokerConnector,
|
|
17
|
+
SshSandboxBackend,
|
|
18
|
+
applyChanges,
|
|
19
|
+
captureProblem,
|
|
20
|
+
claimProject,
|
|
21
|
+
diffWorkspace,
|
|
22
|
+
discardCapture,
|
|
23
|
+
forceUnclaim,
|
|
24
|
+
loadSyncState,
|
|
25
|
+
orphanOf,
|
|
26
|
+
releaseClaim,
|
|
27
|
+
unresolvedCount,
|
|
28
|
+
updateSyncState
|
|
29
|
+
} from "./chunk-64HQ7ZF2.js";
|
|
30
|
+
import {
|
|
31
|
+
apiUrl,
|
|
32
|
+
loadKeystore
|
|
33
|
+
} from "./chunk-75LS6XS6.js";
|
|
34
|
+
import {
|
|
35
|
+
AgentLaunchError
|
|
36
|
+
} from "./chunk-YM4S6IE4.js";
|
|
37
|
+
import "./chunk-S7SJMJ2I.js";
|
|
38
|
+
|
|
39
|
+
// src/lib/agent/sync-pull.ts
|
|
40
|
+
import { randomUUID } from "crypto";
|
|
41
|
+
import { promises as fs } from "fs";
|
|
42
|
+
var NONE = { clean: 0, conflict: 0, blocked: 0 };
|
|
43
|
+
var RECOVER_TIMEOUT_MS = 3e5;
|
|
44
|
+
async function brokerSandbox(sandboxId) {
|
|
45
|
+
const keystore = await loadKeystore();
|
|
46
|
+
const broker = new BrokerConnector({
|
|
47
|
+
apiUrl: apiUrl(),
|
|
48
|
+
deviceId: keystore.deviceId,
|
|
49
|
+
apiToken: keystore.apiToken,
|
|
50
|
+
purpose: "interactive",
|
|
51
|
+
idempotencyKey: randomUUID(),
|
|
52
|
+
sandboxId
|
|
53
|
+
});
|
|
54
|
+
const backend = new SshSandboxBackend({ connector: broker });
|
|
55
|
+
return { backend, stop: () => broker.stop(), close: () => backend.close() };
|
|
56
|
+
}
|
|
57
|
+
function leftOf(counts) {
|
|
58
|
+
return counts.clean + counts.conflict + counts.blocked + (counts.notSaved ?? 0) + (counts.tooDeep ?? 0);
|
|
59
|
+
}
|
|
60
|
+
async function confirmed(options, io, question, lines) {
|
|
61
|
+
if (options.yes === true) return true;
|
|
62
|
+
if (!io.interactive) return false;
|
|
63
|
+
const prompter = io.prompter();
|
|
64
|
+
try {
|
|
65
|
+
io.say(lines);
|
|
66
|
+
return await prompter.confirm(question);
|
|
67
|
+
} finally {
|
|
68
|
+
prompter.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function runPull(options, io, cwd = process.cwd()) {
|
|
72
|
+
const project = await fs.realpath(cwd);
|
|
73
|
+
if (options.forceUnclaim) return unclaim(options, io, project);
|
|
74
|
+
const state = await claimProject(project, "pull");
|
|
75
|
+
try {
|
|
76
|
+
return await pullClaimed(options, io, project, state);
|
|
77
|
+
} finally {
|
|
78
|
+
await releaseClaim(project).catch(() => void 0);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function unclaim(options, io, project) {
|
|
82
|
+
const view = {
|
|
83
|
+
version: SYNC_VIEW_VERSION,
|
|
84
|
+
kind: "taskmarket.agent.pull",
|
|
85
|
+
project,
|
|
86
|
+
capturedAt: null,
|
|
87
|
+
changes: [],
|
|
88
|
+
counts: NONE,
|
|
89
|
+
applied: [],
|
|
90
|
+
skipped: [],
|
|
91
|
+
keptLocal: [],
|
|
92
|
+
remaining: NONE,
|
|
93
|
+
discarded: false,
|
|
94
|
+
recovered: false,
|
|
95
|
+
unclaimed: false,
|
|
96
|
+
note: "Nothing holds this project."
|
|
97
|
+
};
|
|
98
|
+
const claim = (await loadSyncState(project)).claim;
|
|
99
|
+
if (!claim || claim.pid === process.pid) return view;
|
|
100
|
+
const question = `Release this project from process ${claim.pid} (${claim.purpose}, claimed ${claim.claimedAt})? Only do this if no taskmarket agent session or pull is running for it.`;
|
|
101
|
+
if (!await confirmed(options, io, question, [])) {
|
|
102
|
+
throw new AgentLaunchError(
|
|
103
|
+
"not_approved",
|
|
104
|
+
"Nothing was released; pass --yes to release without a prompt"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const dropped = await forceUnclaim(project);
|
|
108
|
+
return dropped ? {
|
|
109
|
+
...view,
|
|
110
|
+
unclaimed: true,
|
|
111
|
+
note: `Released the project from process ${dropped.pid}. Run taskmarket agent status to see what the last session left.`
|
|
112
|
+
} : view;
|
|
113
|
+
}
|
|
114
|
+
async function pullClaimed(options, io, project, initial) {
|
|
115
|
+
let state = initial;
|
|
116
|
+
const view = {
|
|
117
|
+
version: SYNC_VIEW_VERSION,
|
|
118
|
+
kind: "taskmarket.agent.pull",
|
|
119
|
+
project,
|
|
120
|
+
capturedAt: null,
|
|
121
|
+
changes: [],
|
|
122
|
+
counts: NONE,
|
|
123
|
+
applied: [],
|
|
124
|
+
skipped: [],
|
|
125
|
+
keptLocal: [],
|
|
126
|
+
remaining: NONE,
|
|
127
|
+
discarded: false,
|
|
128
|
+
recovered: false,
|
|
129
|
+
unclaimed: false,
|
|
130
|
+
note: "No session left changes to pull in this project."
|
|
131
|
+
};
|
|
132
|
+
const problem = captureProblem(state);
|
|
133
|
+
if (problem) {
|
|
134
|
+
view.capturedAt = problem.at;
|
|
135
|
+
if (options.discard) {
|
|
136
|
+
const question = `Drop the record of the changes left in ${problem.sandboxId ? `sandbox ${problem.sandboxId}` : "the last sandbox"}? They were never saved here and cannot be pulled afterwards.`;
|
|
137
|
+
if (!await confirmed(options, io, question, [captureProblemMessage(problem)])) {
|
|
138
|
+
throw new AgentLaunchError(
|
|
139
|
+
"not_approved",
|
|
140
|
+
"Nothing was dropped; pass --yes to drop the record without a prompt"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
await discardCapture(project);
|
|
144
|
+
return {
|
|
145
|
+
...view,
|
|
146
|
+
discarded: true,
|
|
147
|
+
note: "The record was dropped. A sandbox the session left running stops on its own once idle."
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (!options.recover) {
|
|
151
|
+
throw new AgentLaunchError(
|
|
152
|
+
problem.kind === "interrupted" ? "sync_capture_interrupted" : "sync_capture_failed",
|
|
153
|
+
captureProblemMessage(problem)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const orphan = problem ? void 0 : orphanOf(state);
|
|
158
|
+
if (options.recover && (problem || orphan)) {
|
|
159
|
+
const target = problem ?? {
|
|
160
|
+
sandboxId: orphan.sandboxId,
|
|
161
|
+
base: orphan.base
|
|
162
|
+
};
|
|
163
|
+
if (!problem && state.capture?.state === "saved" && unresolvedCount(state.capture.pending) > 0) {
|
|
164
|
+
throw new AgentLaunchError(
|
|
165
|
+
"sync_pull_pending",
|
|
166
|
+
"Pull or drop the changes already saved here (taskmarket agent pull, or --discard) before recovering another sandbox."
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (!target.sandboxId || !target.base) {
|
|
170
|
+
throw new AgentLaunchError(
|
|
171
|
+
"sync_recover_unavailable",
|
|
172
|
+
"No sandbox was recorded for the last session, so there is nothing to recover from. Drop the record with taskmarket agent pull --discard."
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
let opened;
|
|
176
|
+
try {
|
|
177
|
+
opened = await (io.openSandbox ?? brokerSandbox)(target.sandboxId);
|
|
178
|
+
await opened.backend.connect();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw new AgentLaunchError(
|
|
181
|
+
"sync_recover_unavailable",
|
|
182
|
+
`Sandbox ${target.sandboxId} cannot be reached (${error.message}); if it is gone, its changes are too. Drop the record with taskmarket agent pull --discard.`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
let outcome;
|
|
186
|
+
try {
|
|
187
|
+
const recovery = new SessionSync({
|
|
188
|
+
projectRoot: project,
|
|
189
|
+
backend: opened.backend,
|
|
190
|
+
base: target.base,
|
|
191
|
+
uploaded: true,
|
|
192
|
+
includeSecrets: [],
|
|
193
|
+
sandboxId: target.sandboxId,
|
|
194
|
+
...state.claim?.nonce ? { claimNonce: state.claim.nonce } : {}
|
|
195
|
+
});
|
|
196
|
+
outcome = await recovery.capture(AbortSignal.timeout(RECOVER_TIMEOUT_MS), {
|
|
197
|
+
endSession: false
|
|
198
|
+
});
|
|
199
|
+
} finally {
|
|
200
|
+
await opened.close().catch(() => void 0);
|
|
201
|
+
}
|
|
202
|
+
if (!outcome.saved) {
|
|
203
|
+
throw new AgentLaunchError(
|
|
204
|
+
"sync_capture_failed",
|
|
205
|
+
`The changes in sandbox ${target.sandboxId} could not be saved again (${outcome.reason}). The sandbox was left as it is; drop the record with taskmarket agent pull --discard if they cannot be recovered.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
await opened.stop().catch(
|
|
209
|
+
(error) => io.say([
|
|
210
|
+
`Sandbox ${target.sandboxId} could not be stopped (${error.message}); it stops on its own once idle.`
|
|
211
|
+
])
|
|
212
|
+
);
|
|
213
|
+
view.recovered = true;
|
|
214
|
+
state = await loadSyncState(project);
|
|
215
|
+
} else if (options.recover) {
|
|
216
|
+
throw new AgentLaunchError(
|
|
217
|
+
"sync_recover_unavailable",
|
|
218
|
+
"There is nothing to recover: the last session saved its changes (or left none). Review them with taskmarket agent pull."
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const capture = state.capture;
|
|
222
|
+
if (capture?.state !== "saved" && options.discard && orphan) {
|
|
223
|
+
await discardCapture(project);
|
|
224
|
+
return {
|
|
225
|
+
...view,
|
|
226
|
+
discarded: true,
|
|
227
|
+
note: "The record of the killed --no-sync session was dropped; its sandbox stops on its own once idle."
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (capture?.state !== "saved") {
|
|
231
|
+
return view.recovered ? { ...view, note: "Recovered: the sandbox had no changes to bring back." } : view;
|
|
232
|
+
}
|
|
233
|
+
view.capturedAt = capture.capturedAt;
|
|
234
|
+
if (capture.leftOut?.length) view.leftOut = capture.leftOut;
|
|
235
|
+
const unsaved = unsavedPaths(capture.manifest);
|
|
236
|
+
let deep = (capture.leftOut ?? []).filter((dir) => dir.reason === "too_deep");
|
|
237
|
+
const withDeep = (counts2) => deep.length > 0 ? { ...counts2, tooDeep: deep.length } : counts2;
|
|
238
|
+
let base = capture.base;
|
|
239
|
+
const compare = () => diffWorkspace(savedWorkspace(project, capture.manifest), project, base);
|
|
240
|
+
let changeSet = await compare();
|
|
241
|
+
let { changes, counts } = summarize(changeSet, unsaved);
|
|
242
|
+
counts = withDeep(counts);
|
|
243
|
+
const keepLocal = options.keepLocal ?? [];
|
|
244
|
+
const overwrite = options.overwrite ?? [];
|
|
245
|
+
const deepKept = keepLocal.filter((rel) => deep.some((dir) => dir.path === rel));
|
|
246
|
+
for (const rel of keepLocal) {
|
|
247
|
+
if (deepKept.includes(rel)) continue;
|
|
248
|
+
if (!changes.some((change) => change.path === rel)) {
|
|
249
|
+
throw new AgentLaunchError(
|
|
250
|
+
"sync_invalid_path",
|
|
251
|
+
`--keep-local names a path that is not among the saved changes: ${rel}`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (overwrite.includes(rel)) {
|
|
255
|
+
throw new AgentLaunchError(
|
|
256
|
+
"sync_invalid_path",
|
|
257
|
+
`--keep-local and --overwrite both name ${rel}; choose one`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (keepLocal.length > 0 && !options.discard) {
|
|
262
|
+
const byPath = new Map(capture.manifest.map((entry) => [entry.path, entry]));
|
|
263
|
+
const entries = new Map(base.entries.map((entry) => [entry.path, entry]));
|
|
264
|
+
for (const rel of keepLocal) {
|
|
265
|
+
const kept = savedState(byPath.get(rel));
|
|
266
|
+
if (kept) entries.set(rel, kept);
|
|
267
|
+
else entries.delete(rel);
|
|
268
|
+
}
|
|
269
|
+
base = {
|
|
270
|
+
version: 1,
|
|
271
|
+
entries: [...entries.values()].sort(
|
|
272
|
+
(a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
|
|
273
|
+
)
|
|
274
|
+
};
|
|
275
|
+
changeSet = await compare();
|
|
276
|
+
deep = deep.filter((dir) => !deepKept.includes(dir.path));
|
|
277
|
+
({ changes, counts } = summarize(changeSet, unsaved));
|
|
278
|
+
counts = withDeep(counts);
|
|
279
|
+
const resolved = base;
|
|
280
|
+
const leftOut = (capture.leftOut ?? []).filter((dir) => !deepKept.includes(dir.path));
|
|
281
|
+
view.leftOut = leftOut.length > 0 ? leftOut : void 0;
|
|
282
|
+
await updateSyncState(project, (next) => {
|
|
283
|
+
if (next.capture?.state === "saved") {
|
|
284
|
+
next.capture.base = resolved;
|
|
285
|
+
next.capture.pending = counts;
|
|
286
|
+
next.capture.leftOut = leftOut;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
view.keptLocal = [...keepLocal];
|
|
290
|
+
if (leftOf(counts) === 0) {
|
|
291
|
+
await discardCapture(project);
|
|
292
|
+
return {
|
|
293
|
+
...view,
|
|
294
|
+
changes,
|
|
295
|
+
counts,
|
|
296
|
+
note: `Kept your version of ${keepLocal.join(", ")}; nothing is left to pull.`
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
Object.assign(view, { changes, counts, remaining: counts });
|
|
301
|
+
if (JSON.stringify(counts) !== JSON.stringify(capture.pending)) {
|
|
302
|
+
const current = counts;
|
|
303
|
+
await updateSyncState(project, (next) => {
|
|
304
|
+
if (next.capture?.state === "saved") next.capture.pending = current;
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
if (options.discard) {
|
|
308
|
+
const approved2 = await confirmed(
|
|
309
|
+
options,
|
|
310
|
+
io,
|
|
311
|
+
`Drop these ${changes.length} saved change(s)? They cannot be pulled afterwards.`,
|
|
312
|
+
pullReviewLines(view)
|
|
313
|
+
);
|
|
314
|
+
if (!approved2) {
|
|
315
|
+
throw new AgentLaunchError(
|
|
316
|
+
"not_approved",
|
|
317
|
+
"Nothing was dropped; pass --yes to drop without a prompt"
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
await discardCapture(project);
|
|
321
|
+
return { ...view, remaining: NONE, discarded: true, note: "The saved changes were dropped." };
|
|
322
|
+
}
|
|
323
|
+
if (changes.length === 0 && deep.length === 0) {
|
|
324
|
+
await discardCapture(project);
|
|
325
|
+
return { ...view, note: "This project already matches what the session left." };
|
|
326
|
+
}
|
|
327
|
+
if (changes.length === 0) {
|
|
328
|
+
io.say(pullReviewLines(view));
|
|
329
|
+
return {
|
|
330
|
+
...view,
|
|
331
|
+
note: `Nothing here can be applied: ${deep.length} director(ies) nested too deep were not saved. Acknowledge each with --keep-local <dir>, or drop everything with --discard.`
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const conflicts = new Set(changes.filter((c) => c.status === "conflict").map((c) => c.path));
|
|
335
|
+
for (const rel of overwrite) {
|
|
336
|
+
if (!conflicts.has(rel)) {
|
|
337
|
+
throw new AgentLaunchError(
|
|
338
|
+
"sync_invalid_path",
|
|
339
|
+
`--overwrite names a path that is not in conflict: ${rel}`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
io.say(pullReviewLines(view));
|
|
344
|
+
if (options.diff) {
|
|
345
|
+
view.diff = fullDiff(changeSet, Number.MAX_SAFE_INTEGER);
|
|
346
|
+
io.say([view.diff]);
|
|
347
|
+
}
|
|
348
|
+
const applicable = counts.clean + overwrite.length;
|
|
349
|
+
let approved = options.apply === true;
|
|
350
|
+
if (!approved && applicable > 0 && io.interactive) {
|
|
351
|
+
const prompter = io.prompter();
|
|
352
|
+
try {
|
|
353
|
+
if (!view.diff) {
|
|
354
|
+
const answer = await prompter.ask("Show the full diff first? (yes/no)", "no");
|
|
355
|
+
if (answer.trim().toLowerCase() === "yes")
|
|
356
|
+
io.say([fullDiff(changeSet, Number.MAX_SAFE_INTEGER)]);
|
|
357
|
+
}
|
|
358
|
+
approved = await prompter.confirm(
|
|
359
|
+
`Apply ${applicable} change(s) to ${project}${overwrite.length ? `, overwriting ${overwrite.join(", ")}` : ""}?`
|
|
360
|
+
);
|
|
361
|
+
} finally {
|
|
362
|
+
prompter.close();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (!approved || applicable === 0) {
|
|
366
|
+
return {
|
|
367
|
+
...view,
|
|
368
|
+
note: applicable === 0 ? unresolvedCount(counts) > 0 ? "Nothing here can be applied as it is. For each conflict, take the sandbox version with --overwrite <path> or keep yours with --keep-local <path>; a change that was not saved can only be dropped (--keep-local <path>, or --discard for all)." : "Nothing here can be applied; what is left is listed only. Drop it with --discard." : "Nothing was applied. Pass --apply to apply the clean changes."
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const result = await applyChanges(changeSet, base, { overwrite });
|
|
372
|
+
base = result.base;
|
|
373
|
+
const remaining = withDeep(summarize(await compare(), unsaved).counts);
|
|
374
|
+
const left = leftOf(remaining);
|
|
375
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
376
|
+
if (left === 0) {
|
|
377
|
+
await discardCapture(project);
|
|
378
|
+
}
|
|
379
|
+
await updateSyncState(project, (next) => {
|
|
380
|
+
if (next.capture?.state === "saved") {
|
|
381
|
+
next.capture.base = result.base;
|
|
382
|
+
next.capture.pending = remaining;
|
|
383
|
+
}
|
|
384
|
+
if (next.session) {
|
|
385
|
+
next.session.lastPullAt = now;
|
|
386
|
+
next.session.pending = { ...remaining, checkedAt: now };
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
return {
|
|
390
|
+
...view,
|
|
391
|
+
applied: result.applied,
|
|
392
|
+
skipped: result.skipped,
|
|
393
|
+
remaining,
|
|
394
|
+
note: left === 0 ? `Applied ${result.applied.length} change(s); nothing is left to pull.` : `Applied ${result.applied.length} change(s); ${left} remain saved for a later pull.`
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function pullReviewLines(view) {
|
|
398
|
+
const leftOut = view.leftOut?.length ? [leftOutNote(view.leftOut)] : [];
|
|
399
|
+
if (view.capturedAt === null || view.changes.length === 0) return leftOut;
|
|
400
|
+
return [
|
|
401
|
+
`Changes the session left in its sandbox (saved ${view.capturedAt}) for ${view.project}: ${countsLine(view.counts)}.`,
|
|
402
|
+
...view.changes.map(changeLine),
|
|
403
|
+
...leftOut
|
|
404
|
+
];
|
|
405
|
+
}
|
|
406
|
+
export {
|
|
407
|
+
pullReviewLines,
|
|
408
|
+
runPull
|
|
409
|
+
};
|