@lucid-agents/taskmarket 1.17.2 → 1.18.1

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.
@@ -73,6 +73,11 @@ function statusBarText(view) {
73
73
  parts.push(
74
74
  open ? `budget ${formatUsdc(remainingBudget(view))} left` : `channel ${active[0].state.replace("_", " ")}`
75
75
  );
76
+ const depositing = active.reduce(
77
+ (sum, channel) => sum + BigInt(channel.amounts.depositing),
78
+ 0n
79
+ );
80
+ if (depositing > 0n) parts.push(`deposit ${formatUsdc(depositing)} confirming`);
76
81
  }
77
82
  const compute = view.compute;
78
83
  if (compute?.configured && compute.availableCredits !== null) {
@@ -0,0 +1,447 @@
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
+ x402FeePost
4
+ } from "./chunk-3LNZ766H.js";
5
+ import {
6
+ createWalletAccountFromKeystore,
7
+ signMessage
8
+ } from "./chunk-5VMWXDNO.js";
9
+ import {
10
+ READ_AUTH_ADDRESS_HEADER,
11
+ READ_AUTH_SIGNATURE_HEADER,
12
+ TASK_ACCESS_GRANT_HEADER,
13
+ apiGet,
14
+ apiPost,
15
+ buildClaimMessage,
16
+ buildReadAuthMessage,
17
+ buildSubmitMessage,
18
+ getTaskmarketHome,
19
+ isRecordingPendingResponse,
20
+ loadKeystore
21
+ } from "./chunk-YB2B5DPR.js";
22
+
23
+ // src/lib/read-auth.ts
24
+ async function signReadAuth() {
25
+ let keystore;
26
+ try {
27
+ keystore = await loadKeystore();
28
+ } catch {
29
+ return null;
30
+ }
31
+ try {
32
+ const account = await createWalletAccountFromKeystore(keystore);
33
+ const signature = await account.signMessage({
34
+ message: buildReadAuthMessage(keystore.walletAddress)
35
+ });
36
+ return {
37
+ walletAddress: keystore.walletAddress,
38
+ headers: {
39
+ [READ_AUTH_ADDRESS_HEADER]: keystore.walletAddress,
40
+ [READ_AUTH_SIGNATURE_HEADER]: signature
41
+ }
42
+ };
43
+ } catch {
44
+ return { walletAddress: keystore.walletAddress, headers: {} };
45
+ }
46
+ }
47
+
48
+ // src/lib/task-access-grants.ts
49
+ import { promises as fs } from "fs";
50
+ import path from "path";
51
+ function getGrantsPath() {
52
+ return path.join(getTaskmarketHome(), "task-access-grants.json");
53
+ }
54
+ async function readGrantsFile() {
55
+ try {
56
+ const raw = await fs.readFile(getGrantsPath(), "utf-8");
57
+ return JSON.parse(raw);
58
+ } catch {
59
+ return {};
60
+ }
61
+ }
62
+ async function loadTaskAccessGrant(taskId) {
63
+ const grants = await readGrantsFile();
64
+ const record = grants[taskId];
65
+ if (!record) return null;
66
+ if (new Date(record.expiresAt).getTime() <= Date.now()) return null;
67
+ return record.grant;
68
+ }
69
+ async function taskAccessGrantHeaders(taskId) {
70
+ const grant = await loadTaskAccessGrant(taskId);
71
+ return grant ? { [TASK_ACCESS_GRANT_HEADER]: grant } : {};
72
+ }
73
+ async function saveTaskAccessGrant(taskId, grant, expiresAt) {
74
+ const grants = await readGrantsFile();
75
+ grants[taskId] = { grant, expiresAt };
76
+ const dir = path.dirname(getGrantsPath());
77
+ await fs.mkdir(dir, { recursive: true });
78
+ await fs.writeFile(getGrantsPath(), JSON.stringify(grants, null, 2), {
79
+ mode: 384
80
+ });
81
+ }
82
+
83
+ // src/sdk/artifact-upload.ts
84
+ import { extname } from "path";
85
+ import { Readable } from "stream";
86
+ import https from "https";
87
+ import http from "http";
88
+ function mimeTypeForPath(filePath) {
89
+ switch (extname(filePath).toLowerCase()) {
90
+ case ".png":
91
+ return "image/png";
92
+ case ".jpg":
93
+ case ".jpeg":
94
+ return "image/jpeg";
95
+ case ".webp":
96
+ return "image/webp";
97
+ case ".gif":
98
+ return "image/gif";
99
+ case ".svg":
100
+ return "image/svg+xml";
101
+ case ".pdf":
102
+ return "application/pdf";
103
+ case ".mp4":
104
+ return "video/mp4";
105
+ case ".webm":
106
+ return "video/webm";
107
+ case ".mp3":
108
+ return "audio/mpeg";
109
+ case ".wav":
110
+ return "audio/wav";
111
+ case ".md":
112
+ return "text/markdown";
113
+ case ".txt":
114
+ return "text/plain";
115
+ case ".json":
116
+ return "application/json";
117
+ case ".zip":
118
+ return "application/zip";
119
+ default:
120
+ return "application/octet-stream";
121
+ }
122
+ }
123
+ function putArtifactBytes(uploadUrl, data, mimeType, onProgress) {
124
+ return new Promise((resolve, reject) => {
125
+ const url = new URL(uploadUrl);
126
+ const transport = url.protocol === "https:" ? https : http;
127
+ const req = transport.request(
128
+ url,
129
+ {
130
+ method: "PUT",
131
+ headers: {
132
+ "Content-Type": mimeType,
133
+ "Content-Length": String(data.length)
134
+ }
135
+ },
136
+ (res) => {
137
+ const chunks = [];
138
+ res.on("data", (chunk) => chunks.push(chunk));
139
+ res.on("end", () => {
140
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
141
+ resolve();
142
+ } else {
143
+ const body = Buffer.concat(chunks).toString("utf8").slice(0, 500);
144
+ reject(
145
+ new Error(
146
+ `Upload failed (${res.statusCode ?? "unknown"}): ${body || "no response body"}`
147
+ )
148
+ );
149
+ }
150
+ });
151
+ }
152
+ );
153
+ req.on("error", reject);
154
+ let uploaded = 0;
155
+ const stream = Readable.from([data]);
156
+ stream.on("data", (chunk) => {
157
+ uploaded += chunk.length;
158
+ onProgress?.(Math.round(uploaded / data.length * 100));
159
+ });
160
+ stream.on("error", reject);
161
+ stream.pipe(req);
162
+ });
163
+ }
164
+
165
+ // src/lib/task-sdk.ts
166
+ async function putWithProgress(upload) {
167
+ process.stderr.write(` Uploading ${upload.fileName}...
168
+ `);
169
+ await putArtifactBytes(upload.uploadUrl, upload.data, upload.mimeType, (percent) => {
170
+ process.stderr.write(`\r ${upload.fileName}: ${percent}%`);
171
+ });
172
+ process.stderr.write("\n");
173
+ }
174
+ var cliTaskTransport = {
175
+ get: (path2, headers) => apiGet(path2, { headers }),
176
+ // A write the caller gave no key is sent exactly as before the SDK existed, with no options,
177
+ // so `apiPost` resolves its key itself (explicit, then TASKMARKET_IDEMPOTENCY_KEY, then fresh).
178
+ post: (path2, body, options) => options === void 0 ? apiPost(path2, body) : apiPost(path2, body, options),
179
+ feePost: (path2, body, options) => x402FeePost(path2, body, options),
180
+ put: putWithProgress
181
+ };
182
+ function keystoreTaskSigner(keystore) {
183
+ return {
184
+ address: keystore.walletAddress,
185
+ signMessage: (message) => signMessage(message, keystore)
186
+ };
187
+ }
188
+ function cliTaskReadContext() {
189
+ return {
190
+ transport: cliTaskTransport,
191
+ readAuth: signReadAuth,
192
+ accessGrantHeaders: taskAccessGrantHeaders
193
+ };
194
+ }
195
+ function cliTaskWriteContext(keystore) {
196
+ return { ...cliTaskReadContext(), signer: keystoreTaskSigner(keystore) };
197
+ }
198
+
199
+ // src/sdk/tasks.ts
200
+ import { promises as fsPromises } from "fs";
201
+ import { basename } from "path";
202
+ import { createHash } from "crypto";
203
+ import { keccak256 } from "viem";
204
+ var TaskOperationError = class extends Error {
205
+ code;
206
+ constructor(code, message) {
207
+ super(message);
208
+ this.name = "TaskOperationError";
209
+ this.code = code;
210
+ }
211
+ };
212
+ function withChainQuery(path2, chainId) {
213
+ if (chainId === void 0) return path2;
214
+ return `${path2}${path2.includes("?") ? "&" : "?"}chainId=${chainId}`;
215
+ }
216
+ function withChainBody(body, chainId) {
217
+ return chainId === void 0 ? body : { ...body, chainId };
218
+ }
219
+ function writeOptions(idempotencyKey) {
220
+ return idempotencyKey === void 0 ? void 0 : { idempotencyKey };
221
+ }
222
+ async function readAuthHeaders(ctx) {
223
+ return (await ctx.readAuth?.())?.headers ?? {};
224
+ }
225
+ async function getTask(ctx, ref) {
226
+ const headers = {
227
+ ...await readAuthHeaders(ctx),
228
+ ...await ctx.accessGrantHeaders?.(ref.taskId) ?? {}
229
+ };
230
+ const task = await ctx.transport.get(
231
+ withChainQuery(`/api/tasks/${ref.taskId}`, ref.chainId),
232
+ headers
233
+ );
234
+ return task ?? null;
235
+ }
236
+ async function getTaskTerms(ctx, ref) {
237
+ const headers = {
238
+ ...await readAuthHeaders(ctx),
239
+ ...await ctx.accessGrantHeaders?.(ref.taskId) ?? {}
240
+ };
241
+ const detail = await ctx.transport.get(
242
+ withChainQuery(`/api/tasks/${ref.taskId}/terms`, ref.chainId),
243
+ headers
244
+ );
245
+ return detail ?? null;
246
+ }
247
+ async function listTasks(ctx, filter = {}) {
248
+ const params = new URLSearchParams();
249
+ if (filter.chainId !== void 0) params.set("chainId", String(filter.chainId));
250
+ if (filter.q) params.set("q", filter.q);
251
+ if (filter.sort) params.set("sort", filter.sort);
252
+ if (filter.status) params.set("status", filter.status);
253
+ if (filter.phase) params.set("phase", filter.phase);
254
+ if (filter.mode) params.set("mode", filter.mode);
255
+ if (filter.auctionType) params.set("auctionType", filter.auctionType);
256
+ if (filter.tags) params.set("tags", filter.tags);
257
+ if (filter.minReward) params.set("minReward", filter.minReward);
258
+ if (filter.maxReward) params.set("maxReward", filter.maxReward);
259
+ if (filter.deadlineHours) params.set("deadlineHours", filter.deadlineHours);
260
+ if (filter.limit) params.set("limit", filter.limit);
261
+ if (filter.cursor) params.set("cursor", filter.cursor);
262
+ const result = await ctx.transport.get(
263
+ `/api/tasks?${params.toString()}`,
264
+ await readAuthHeaders(ctx)
265
+ );
266
+ return { tasks: result.tasks, hasMore: result.hasMore, nextCursor: result.nextCursor };
267
+ }
268
+ async function listTaskSubmissions(ctx, ref) {
269
+ const headers = await readAuthHeaders(ctx);
270
+ return await ctx.transport.get(
271
+ withChainQuery(`/api/tasks/${ref.taskId}/submissions`, ref.chainId),
272
+ headers
273
+ );
274
+ }
275
+ async function listWorkerSubmissions(ctx, options = {}) {
276
+ const auth = await ctx.readAuth?.();
277
+ const address = options.address ?? auth?.walletAddress;
278
+ if (!address) {
279
+ throw new TaskOperationError(
280
+ "no_address",
281
+ "No worker address was given and there is no reading wallet to default to"
282
+ );
283
+ }
284
+ return ctx.transport.get(`/api/submissions/mine?workerAddress=${encodeURIComponent(address)}`, {
285
+ ...auth?.headers ?? {}
286
+ });
287
+ }
288
+ async function getWorkerInbox(ctx) {
289
+ const auth = await ctx.readAuth?.();
290
+ if (!auth?.walletAddress) {
291
+ throw new TaskOperationError("no_address", "There is no reading wallet to ask about");
292
+ }
293
+ const raw = await ctx.transport.get(
294
+ `/api/agents/inbox?address=${encodeURIComponent(auth.walletAddress)}`,
295
+ auth.headers
296
+ );
297
+ return {
298
+ address: auth.walletAddress,
299
+ proven: Object.keys(auth.headers).length > 0,
300
+ inbox: {
301
+ asRequester: raw?.asRequester ?? [],
302
+ asWorker: raw?.asWorker ?? [],
303
+ invitedPrivateTasks: raw?.invitedPrivateTasks ?? []
304
+ }
305
+ };
306
+ }
307
+ async function getAgentStats(ctx, options) {
308
+ return await ctx.transport.get(
309
+ withChainQuery(
310
+ `/api/agents/stats?address=${encodeURIComponent(options.address)}`,
311
+ options.chainId
312
+ ),
313
+ {}
314
+ );
315
+ }
316
+ async function claimTask(ctx, ref, options = {}) {
317
+ const signature = await ctx.signer.signMessage(buildClaimMessage(ref.taskId));
318
+ const { data, idempotencyKey } = await ctx.transport.post(
319
+ `/api/tasks/${ref.taskId}/claim`,
320
+ withChainBody({ workerAddress: ctx.signer.address, signature }, ref.chainId),
321
+ writeOptions(options.idempotencyKey)
322
+ );
323
+ return { claimId: data.claimId, idempotencyKey };
324
+ }
325
+ async function authorizeUploads(ctx, ref) {
326
+ return { signature: await ctx.signer.signMessage(buildSubmitMessage(ref.taskId)) };
327
+ }
328
+ async function readArtifactFile(file) {
329
+ const fileName = file.fileName ?? basename(file.path);
330
+ const data = await fsPromises.readFile(file.path);
331
+ if (data.length === 0) {
332
+ throw new TaskOperationError("empty_artifact", `File is empty: ${fileName}`);
333
+ }
334
+ return {
335
+ data,
336
+ artifact: {
337
+ fileName,
338
+ mimeType: file.mimeType ?? mimeTypeForPath(file.path),
339
+ role: file.role,
340
+ sizeBytes: data.length,
341
+ sha256Hash: createHash("sha256").update(data).digest("hex"),
342
+ keccak256Hash: keccak256(new Uint8Array(data))
343
+ }
344
+ };
345
+ }
346
+ async function requestArtifactUpload(ctx, ref, artifact, authorization) {
347
+ const {
348
+ data: { uploadUrl, artifactKey }
349
+ } = await ctx.transport.post(
350
+ `/api/tasks/${ref.taskId}/submissions/request-upload-url`,
351
+ withChainBody(
352
+ {
353
+ taskId: ref.taskId,
354
+ workerAddress: ctx.signer.address,
355
+ signature: authorization.signature,
356
+ fileName: artifact.fileName,
357
+ mimeType: artifact.mimeType,
358
+ role: artifact.role,
359
+ sizeBytes: artifact.sizeBytes
360
+ },
361
+ ref.chainId
362
+ )
363
+ );
364
+ return { uploadUrl, artifactKey };
365
+ }
366
+ async function uploadArtifact(ctx, ref, file, authorization) {
367
+ const { data, artifact } = await readArtifactFile(file);
368
+ const { uploadUrl, artifactKey } = await requestArtifactUpload(ctx, ref, artifact, authorization);
369
+ await ctx.transport.put({
370
+ uploadUrl,
371
+ data,
372
+ mimeType: artifact.mimeType,
373
+ fileName: artifact.fileName
374
+ });
375
+ return { artifactKey, ...artifact };
376
+ }
377
+ async function submitUploadedArtifacts(ctx, ref, artifacts, options = {}) {
378
+ if (artifacts.length === 0) {
379
+ throw new TaskOperationError("no_artifacts", "At least one artifact is required");
380
+ }
381
+ const signature = await ctx.signer.signMessage(
382
+ buildSubmitMessage(
383
+ ref.taskId,
384
+ artifacts.map((artifact) => artifact.artifactKey)
385
+ )
386
+ );
387
+ const { data, idempotencyKey } = await ctx.transport.feePost(
388
+ `/api/tasks/${ref.taskId}/submissions/from-keys`,
389
+ withChainBody(
390
+ {
391
+ taskId: ref.taskId,
392
+ workerAddress: ctx.signer.address,
393
+ artifacts,
394
+ signature
395
+ },
396
+ ref.chainId
397
+ ),
398
+ options
399
+ );
400
+ if (isRecordingPendingResponse(data)) {
401
+ return {
402
+ submissionId: data.submissionId,
403
+ idempotencyKey,
404
+ recordingPending: {
405
+ ...typeof data.intentId === "string" ? { intentId: data.intentId } : {},
406
+ ...typeof data.txHash === "string" ? { txHash: data.txHash } : {}
407
+ }
408
+ };
409
+ }
410
+ return { submissionId: data.submissionId, idempotencyKey };
411
+ }
412
+ async function submitTaskFiles(ctx, ref, files, options = {}) {
413
+ if (files.length === 0) {
414
+ throw new TaskOperationError("no_artifacts", "At least one file is required");
415
+ }
416
+ const authorization = await authorizeUploads(ctx, ref);
417
+ const artifacts = await Promise.all(
418
+ files.map((file) => uploadArtifact(ctx, ref, file, authorization))
419
+ );
420
+ return submitUploadedArtifacts(ctx, ref, artifacts, options);
421
+ }
422
+
423
+ export {
424
+ signReadAuth,
425
+ taskAccessGrantHeaders,
426
+ saveTaskAccessGrant,
427
+ mimeTypeForPath,
428
+ putArtifactBytes,
429
+ cliTaskTransport,
430
+ keystoreTaskSigner,
431
+ cliTaskReadContext,
432
+ cliTaskWriteContext,
433
+ TaskOperationError,
434
+ getTask,
435
+ getTaskTerms,
436
+ listTasks,
437
+ listTaskSubmissions,
438
+ listWorkerSubmissions,
439
+ getWorkerInbox,
440
+ getAgentStats,
441
+ claimTask,
442
+ authorizeUploads,
443
+ readArtifactFile,
444
+ requestArtifactUpload,
445
+ submitUploadedArtifacts,
446
+ submitTaskFiles
447
+ };
@@ -6,21 +6,23 @@ import {
6
6
  createBaseChannelRpcReader,
7
7
  createWalletAccountFromKeystore,
8
8
  createX402Allocations,
9
- getTaskmarketHome,
10
9
  getX402JournalPath,
11
10
  getX402PolicyPath,
12
- keystoreExists,
13
11
  listX402Payments,
14
- loadKeystore,
15
12
  loadX402Policy,
16
13
  rpcUrlForNetwork
17
- } from "./chunk-FRV3ZHUR.js";
14
+ } from "./chunk-5VMWXDNO.js";
18
15
  import {
19
- AgentLaunchError
20
- } from "./chunk-YM4S6IE4.js";
16
+ getTaskmarketHome,
17
+ keystoreExists,
18
+ loadKeystore
19
+ } from "./chunk-YB2B5DPR.js";
21
20
  import {
22
21
  formatUsdc
23
- } from "./chunk-73RXOYME.js";
22
+ } from "./chunk-EB6OJBF5.js";
23
+ import {
24
+ AgentLaunchError
25
+ } from "./chunk-YM4S6IE4.js";
24
26
 
25
27
  // src/lib/agent/channels.ts
26
28
  import { randomUUID as randomUUID2 } from "crypto";
@@ -334,7 +336,11 @@ async function createAgentRails(options) {
334
336
  budgetAmount: rail.limits.total,
335
337
  depositAmount: rail.limits.deposit,
336
338
  policyRuleId: rail.policyRuleId,
337
- ...options.fetch ? { fetch: options.fetch } : {}
339
+ ...options.fetch ? { fetch: options.fetch } : {},
340
+ confirmationWait: {
341
+ ...options.confirmationWait,
342
+ ...options.onConfirmationWait ? { onWait: (progress) => options.onConfirmationWait(rail, progress) } : {}
343
+ }
338
344
  });
339
345
  const transport = createTaskMarketTransport({
340
346
  payer,
@@ -438,10 +444,12 @@ function createAgentTransport(rails) {
438
444
  };
439
445
  }
440
446
  async function reconcileLeftoverOperations(rails, log = () => void 0) {
441
- for (const { rail, payer } of rails) {
447
+ const usable = [];
448
+ const blocked = [];
449
+ for (const entry of rails) {
450
+ const { rail, payer } = entry;
442
451
  let pending = (await payer.channelStatus()).pendingOperation;
443
- if (!pending) continue;
444
- if (pending.kind === "payment") {
452
+ if (pending?.kind === "payment") {
445
453
  log(`Reconciling payment ${pending.id} left on rail ${rail.id}`);
446
454
  try {
447
455
  await payer.reconcile(pending.id);
@@ -451,13 +459,22 @@ async function reconcileLeftoverOperations(rails, log = () => void 0) {
451
459
  );
452
460
  }
453
461
  pending = (await payer.channelStatus()).pendingOperation;
454
- if (!pending) continue;
455
462
  }
463
+ if (!pending) {
464
+ usable.push(entry);
465
+ continue;
466
+ }
467
+ const reason = `The ${rail.id} channel has an unresolved ${pending.kind} operation ${pending.id} from an earlier session; it must be reconciled (taskmarket agent reconcile ${pending.id}) before anything else is bought on that channel.`;
468
+ log(`${reason} Its models are left out of this session.`);
469
+ blocked.push(reason);
470
+ }
471
+ if (usable.length === 0 && blocked.length > 0) {
456
472
  throw new AgentLaunchError(
457
473
  "channel_operation_pending",
458
- `The ${rail.id} channel has an unresolved ${pending.kind} operation ${pending.id} from an earlier session; it must be reconciled before anything else is bought on that channel. Try again later.`
474
+ `${blocked.join(" ")} Try again later.`
459
475
  );
460
476
  }
477
+ return usable;
461
478
  }
462
479
 
463
480
  // src/lib/agent/policy.ts
@@ -469,7 +486,7 @@ var XGATE_ROUTES = [
469
486
  "/providers/anthropic/v1/messages",
470
487
  "/providers/openai/v1/responses"
471
488
  ];
472
- var WORKSPACE_CONSENT_VERSION = "boat-eu-2026-10";
489
+ var WORKSPACE_CONSENT_VERSION = "boat-eu-2026-10-sync";
473
490
  var address = z.string().regex(/^0x[0-9a-fA-F]{40}$/, "expected a 0x-prefixed 20-byte address");
474
491
  var atomic2 = z.string().regex(/^(0|[1-9][0-9]{0,30})$/, "expected an integer amount string");
475
492
  var positiveAtomic = atomic2.refine((value) => BigInt(value) > 0n, "expected an amount above 0");
@@ -669,15 +686,31 @@ async function assessAgentPolicy(policy, options) {
669
686
  }
670
687
  return { reasons, usage };
671
688
  }
672
- function selectModels(policy, requested) {
673
- if (requested === void 0) return [...policy.models];
674
- if (!policy.models.includes(requested)) {
689
+ function requireApprovedModel(policy, requested) {
690
+ if (requested !== void 0 && !policy.models.includes(requested)) {
675
691
  throw new AgentLaunchError(
676
692
  "model_not_allowed",
677
693
  `Model ${requested} is not in the approved spending policy (${policy.models.join(", ")})`
678
694
  );
679
695
  }
680
- return [requested];
696
+ }
697
+ function planSessionModels(options) {
698
+ const { approved, offered, requested, previous } = options;
699
+ const unavailable = approved.filter((model) => !offered.includes(model));
700
+ if (offered.length === 0) {
701
+ throw new AgentLaunchError(
702
+ "no_qualified_models",
703
+ `None of the approved models (${approved.join(", ")}) is qualified by xgate on a route the policy pays for`
704
+ );
705
+ }
706
+ if (requested !== void 0 && !offered.includes(requested)) {
707
+ throw new AgentLaunchError(
708
+ "model_not_offered",
709
+ `Model ${requested} is approved, but xgate does not offer it on a route a usable rail of the policy pays for, so this session cannot buy it. This session offers ${offered.join(", ")}`
710
+ );
711
+ }
712
+ const start = requested ?? (previous !== void 0 && offered.includes(previous) ? previous : offered[0]);
713
+ return { start, unavailable };
681
714
  }
682
715
 
683
716
  // src/lib/agent/retired.ts
@@ -738,7 +771,9 @@ async function openAgentChannels(options = {}) {
738
771
  models: options.models ? options.models(policy) : [...policy.models],
739
772
  walletAddress: keystore.walletAddress,
740
773
  getAccount,
741
- ...options.fetch ? { fetch: options.fetch } : {}
774
+ ...options.fetch ? { fetch: options.fetch } : {},
775
+ ...options.onConfirmationWait ? { onConfirmationWait: options.onConfirmationWait } : {},
776
+ ...options.confirmationWait ? { confirmationWait: options.confirmationWait } : {}
742
777
  };
743
778
  const rails = await createAgentRails(common);
744
779
  const retired = await createAgentRails({
@@ -863,9 +898,7 @@ async function uncertainPurchases(rails, statuses = /* @__PURE__ */ new Map()) {
863
898
  for (const record2 of records) {
864
899
  if (!record2.invocationId || record2.rail?.id !== BASE_CHANNEL_RAIL_ID) continue;
865
900
  if (!UNDECIDED.has(record2.state)) continue;
866
- const rail = rails.find(
867
- (r) => record2.origin === r.rail.origin && record2.pathname === r.rail.route
868
- );
901
+ const rail = rails.find((r) => record2.funding?.sessionKey === r.payer.sessionKey);
869
902
  if (!rail) continue;
870
903
  const lease = statuses.get(rail.rail.id)?.pendingOperation;
871
904
  found.push({
@@ -955,6 +988,34 @@ function outcomeOf(operation) {
955
988
  if (operation.state === "failed") return "failed";
956
989
  return "pending";
957
990
  }
991
+ var REFUND_WAIT_TIMEOUT_MS = 9e4;
992
+ var REFUND_WAIT_INTERVAL_MS = 3e3;
993
+ async function awaitRefund(rail, operation, wait = {}) {
994
+ const timeoutMs = wait.timeoutMs ?? REFUND_WAIT_TIMEOUT_MS;
995
+ const intervalMs = Math.max(1, wait.intervalMs ?? REFUND_WAIT_INTERVAL_MS);
996
+ const sleep = wait.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
997
+ let current = operation;
998
+ let waitedMs = 0;
999
+ while ((current.state === "dispatched" || current.state === "unknown") && current.transaction && waitedMs < timeoutMs) {
1000
+ try {
1001
+ wait.onWait?.({
1002
+ transaction: current.transaction,
1003
+ amount: current.amount,
1004
+ waitedMs,
1005
+ timeoutMs
1006
+ });
1007
+ } catch {
1008
+ }
1009
+ const step = Math.min(intervalMs, timeoutMs - waitedMs);
1010
+ await sleep(step);
1011
+ waitedMs += step;
1012
+ try {
1013
+ current = await rail.payer.reconcileOperation(current.operationId);
1014
+ } catch {
1015
+ }
1016
+ }
1017
+ return current;
1018
+ }
958
1019
  async function closeSession(allocationId2) {
959
1020
  try {
960
1021
  await allocationsReader().close(allocationId2);
@@ -975,7 +1036,11 @@ function closeLines(channel) {
975
1036
  }
976
1037
  async function closeChannel(rail, confirm, options = {}) {
977
1038
  const retired = options.retired ?? false;
978
- const status = await rail.payer.channelStatus();
1039
+ let status = await rail.payer.channelStatus();
1040
+ if (status.pendingOperation?.kind === "payment" && options.reconcilePurchase) {
1041
+ await options.reconcilePurchase(status.pendingOperation.id);
1042
+ status = await rail.payer.channelStatus();
1043
+ }
979
1044
  const channel = await channelView(rail, status, { retired });
980
1045
  const result = async (outcome, operation2, reconciled) => {
981
1046
  const after = operation2 ? await channelView(rail, void 0, { retired }) : channel;
@@ -1002,7 +1067,11 @@ async function closeChannel(rail, confirm, options = {}) {
1002
1067
  if (status.closed) return result("closed", null, false);
1003
1068
  const lease = status.pendingOperation;
1004
1069
  if (lease?.kind === "lifecycle") {
1005
- const operation2 = await rail.payer.reconcileOperation(lease.id);
1070
+ const operation2 = await awaitRefund(
1071
+ rail,
1072
+ await rail.payer.reconcileOperation(lease.id),
1073
+ options.wait
1074
+ );
1006
1075
  return result(outcomeOf(operation2), operation2, true);
1007
1076
  }
1008
1077
  if (lease?.kind === "payment") {
@@ -1026,10 +1095,17 @@ async function closeChannel(rail, confirm, options = {}) {
1026
1095
  if (!await confirm(closeLines(channel))) {
1027
1096
  throw new AgentLaunchError("not_approved", "The channel was not closed");
1028
1097
  }
1029
- const operation = await rail.payer.refund({
1030
- operationId: `agent-close-${randomUUID2()}`,
1031
- close: true
1032
- });
1098
+ const operationId = `agent-close-${randomUUID2()}`;
1099
+ let operation;
1100
+ try {
1101
+ operation = await rail.payer.refund({ operationId, close: true });
1102
+ } catch (error) {
1103
+ if (error.code !== "outcome_uncertain") throw error;
1104
+ const recorded = await rail.payer.reconcileOperation(operationId).catch(() => void 0);
1105
+ if (!recorded || recorded.state !== "dispatched" && recorded.state !== "unknown") throw error;
1106
+ operation = recorded;
1107
+ }
1108
+ operation = await awaitRefund(rail, operation, options.wait);
1033
1109
  return result(outcomeOf(operation), operation, false);
1034
1110
  }
1035
1111
 
@@ -1047,7 +1123,8 @@ export {
1047
1123
  loadAgentPolicy,
1048
1124
  saveAgentPolicy,
1049
1125
  assessAgentPolicy,
1050
- selectModels,
1126
+ requireApprovedModel,
1127
+ planSessionModels,
1051
1128
  settledRetiredRails,
1052
1129
  openAgentChannels,
1053
1130
  selectChannel,