@lucid-agents/taskmarket 1.17.0 → 1.17.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.
@@ -20503,6 +20503,14 @@ var OutputLimitError = class extends Error {
20503
20503
  this.name = "OutputLimitError";
20504
20504
  }
20505
20505
  };
20506
+ var ManifestLimitError = class extends Error {
20507
+ limit;
20508
+ constructor(limit) {
20509
+ super(`manifest_limit:${limit}`);
20510
+ this.limit = limit;
20511
+ this.name = "ManifestLimitError";
20512
+ }
20513
+ };
20506
20514
 
20507
20515
  // ../../packages/remote-tools/dist/channel.js
20508
20516
  var MAX_MESSAGE_BYTES = 64 * 1024;
@@ -20731,6 +20739,7 @@ var MAX_CONTEXT_FILE_BYTES = 64 * 1024;
20731
20739
  var MAX_RESULT_LIMIT = 1e4;
20732
20740
  var MAX_GREP_CONTEXT = 50;
20733
20741
  var HEARTBEAT_INTERVAL_MS = 6e4;
20742
+ var SYNC_MAX_TOTAL_BYTES = 256 * 1024 * 1024;
20734
20743
 
20735
20744
  // ../../packages/remote-tools/dist/prompt.js
20736
20745
  var REMOTE_CONTEXT_FILE_NAMES = ["AGENTS.md", "CLAUDE.md"];
@@ -21337,6 +21346,7 @@ function shellQuote(value) {
21337
21346
  }
21338
21347
  var EXIT_NOT_FOUND = 64;
21339
21348
  var EXIT_NOT_DIR = 65;
21349
+ var EXIT_IS_DIR = 66;
21340
21350
  var KILL_GRACE_SECONDS = 2;
21341
21351
  function unitName(sessionTag, n) {
21342
21352
  if (!/^[a-z0-9]{1,16}$/.test(sessionTag))
@@ -21401,6 +21411,49 @@ function globCommand(dir, pattern, ignore, limit) {
21401
21411
  args.push("--glob", shellQuote(`!${glob}`));
21402
21412
  return `cd -- ${shellQuote(dir)} 2>/dev/null || { [ -e ${shellQuote(dir)} ] && exit ${EXIT_NOT_DIR}; exit ${EXIT_NOT_FOUND}; }; ${args.join(" ")} | head -z -n ${Math.max(1, Math.floor(limit))}; rc=\${PIPESTATUS[0]}; [ "$rc" -eq 141 ] && exit 0; exit "$rc"`;
21403
21413
  }
21414
+ function manifestCommand(dir, prune) {
21415
+ const pruned = prune.length ? `\\( ${prune.map((name) => `-name ${shellQuote(name)}`).join(" -o ")} \\) -prune -o ` : "";
21416
+ const find = `find . -mindepth 1 ${pruned}`;
21417
+ return [
21418
+ `cd -- ${shellQuote(dir)} 2>/dev/null || { [ -e ${shellQuote(dir)} ] && exit ${EXIT_NOT_DIR}; exit ${EXIT_NOT_FOUND}; }`,
21419
+ `${find}\\( -type f -o -type l \\) -printf '%y\\0%s\\0%l\\0%P\\0' || exit 1`,
21420
+ `printf '=\\0'`,
21421
+ `${find}-type f -printf '%P\\0' | xargs -0 -r sha256sum -z -- 2>/dev/null`,
21422
+ "exit 0"
21423
+ ].join("\n");
21424
+ }
21425
+ function parseManifestOutput(stdout) {
21426
+ const fields = stdout.toString("utf8").split("\0");
21427
+ let separator = 0;
21428
+ while (separator < fields.length && fields[separator] !== "=")
21429
+ separator += 4;
21430
+ if (separator >= fields.length)
21431
+ throw new Error("Malformed manifest output");
21432
+ const hashes = /* @__PURE__ */ new Map();
21433
+ for (const line of fields.slice(separator + 1)) {
21434
+ const match = /^([0-9a-f]{64}) [ *](.*)$/s.exec(line);
21435
+ if (match)
21436
+ hashes.set(match[2], match[1]);
21437
+ }
21438
+ const entries = [];
21439
+ for (let i = 0; i < separator; i += 4) {
21440
+ const [type, size, target, rel] = fields.slice(i, i + 4);
21441
+ if (type === "l")
21442
+ entries.push({ path: rel, type: "symlink", target });
21443
+ else if (type === "f") {
21444
+ entries.push({ path: rel, type: "file", size: Number(size), sha256: hashes.get(rel) ?? "" });
21445
+ }
21446
+ }
21447
+ return entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
21448
+ }
21449
+ function symlinkCommand(linkPath, target) {
21450
+ const p = shellQuote(linkPath);
21451
+ return `if [ -d ${p} ] && [ ! -L ${p} ]; then exit ${EXIT_IS_DIR}; fi; mkdir -p -- "$(dirname -- ${p})" && ln -sfn -- ${shellQuote(target)} ${p}`;
21452
+ }
21453
+ function removeCommand(filePath) {
21454
+ const p = shellQuote(filePath);
21455
+ return `if [ -d ${p} ] && [ ! -L ${p} ]; then exit ${EXIT_IS_DIR}; fi; rm -f -- ${p}`;
21456
+ }
21404
21457
  function parseGlobOutput(stdout) {
21405
21458
  return stdout.toString("utf8").split("\0").filter((entry) => entry.length > 0);
21406
21459
  }
@@ -22291,6 +22344,56 @@ var SshSandboxBackend = class {
22291
22344
  }
22292
22345
  return { isDirectory: kind === "D" };
22293
22346
  }
22347
+ async manifest(dir, options) {
22348
+ const stop = new AbortController();
22349
+ const onAbort = () => stop.abort();
22350
+ options.signal?.addEventListener("abort", onAbort, { once: true });
22351
+ const out = [];
22352
+ let entries = 0;
22353
+ let overLimit = false;
22354
+ try {
22355
+ const { exitCode, stderr } = await this.collect(manifestCommand(dir, options.prune), stop.signal, (chunk) => {
22356
+ out.push(chunk);
22357
+ for (const byte of chunk)
22358
+ if (byte === 0)
22359
+ entries += 1;
22360
+ if (entries > (options.limit + 1) * 5 && !overLimit) {
22361
+ overLimit = true;
22362
+ stop.abort();
22363
+ }
22364
+ });
22365
+ if (exitCode === EXIT_NOT_FOUND)
22366
+ throw new SandboxFileError("ENOENT", "scandir", dir);
22367
+ if (exitCode === EXIT_NOT_DIR)
22368
+ throw new SandboxFileError("ENOTDIR", "scandir", dir);
22369
+ if (exitCode !== 0)
22370
+ throw new Error(`find failed in the sandbox (exit ${exitCode}): ${stderr}`);
22371
+ } catch (error) {
22372
+ if (overLimit)
22373
+ throw new ManifestLimitError(options.limit);
22374
+ throw error;
22375
+ } finally {
22376
+ options.signal?.removeEventListener("abort", onAbort);
22377
+ }
22378
+ const parsed = parseManifestOutput(Buffer.concat(out));
22379
+ if (parsed.length > options.limit)
22380
+ throw new ManifestLimitError(options.limit);
22381
+ return parsed;
22382
+ }
22383
+ async symlink(path6, target, options = {}) {
22384
+ await this.fileCommand(symlinkCommand(path6, target), "symlink", path6, options.signal);
22385
+ }
22386
+ async remove(path6, options = {}) {
22387
+ await this.fileCommand(removeCommand(path6), "unlink", path6, options.signal);
22388
+ }
22389
+ async fileCommand(command, syscall, path6, signal) {
22390
+ const { exitCode, stderr } = await this.collect(command, signal, () => void 0);
22391
+ if (exitCode === EXIT_IS_DIR)
22392
+ throw new SandboxFileError("EISDIR", syscall, path6);
22393
+ if (exitCode !== 0) {
22394
+ throw new SandboxFileError("EIO", syscall, path6, stderr || `exit ${exitCode}`);
22395
+ }
22396
+ }
22294
22397
  async exec(command, cwd, options) {
22295
22398
  const { conn } = await this.ensure(options.signal);
22296
22399
  const unit = this.nextUnit();
@@ -2006,11 +2006,293 @@ init_cjs_shims();
2006
2006
  var import_node_path4 = __toESM(require("path"), 1);
2007
2007
  var import_pi_coding_agent = require("@earendil-works/pi-coding-agent");
2008
2008
 
2009
+ // src/lib/agent/budget-ui.ts
2010
+ init_cjs_shims();
2011
+
2012
+ // src/lib/agent/budget-view.ts
2013
+ init_cjs_shims();
2014
+ var USDC_DECIMALS = 6;
2015
+ function formatUsdc(atomic2) {
2016
+ let value;
2017
+ try {
2018
+ value = typeof atomic2 === "bigint" ? atomic2 : BigInt(atomic2);
2019
+ } catch {
2020
+ return "$?";
2021
+ }
2022
+ const negative = value < 0n;
2023
+ const abs = negative ? -value : value;
2024
+ const scale = 10n ** BigInt(USDC_DECIMALS);
2025
+ const fraction = (abs % scale).toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
2026
+ return `${negative ? "-" : ""}$${abs / scale}.${fraction.padEnd(2, "0")}`;
2027
+ }
2028
+ function formatCredits(value) {
2029
+ const number = Number(value);
2030
+ if (!Number.isFinite(number)) return value;
2031
+ return number >= 100 ? number.toFixed(0) : number.toFixed(1);
2032
+ }
2033
+ function remainingBudget(view) {
2034
+ const channels = view.channels.filter((channel) => channel.state === "open" && !channel.retired).reduce(
2035
+ (sum, channel) => sum + BigInt(channel.amounts.refundable) + BigInt(channel.amounts.lifetimeRoom),
2036
+ 0n
2037
+ );
2038
+ if (!view.budget) return channels;
2039
+ const left = BigInt(view.budget.cap) - BigInt(view.budget.used);
2040
+ const budget = left > 0n ? left : 0n;
2041
+ return channels < budget ? channels : budget;
2042
+ }
2043
+ function blockingPurchases(view) {
2044
+ const ids = new Set(view.unresolved);
2045
+ for (const channel of view.channels) {
2046
+ if (channel.pendingOperation?.kind === "payment") ids.add(channel.pendingOperation.id);
2047
+ }
2048
+ return [...ids];
2049
+ }
2050
+ function statusBarText(view) {
2051
+ const parts = [];
2052
+ parts.push(view.lastTurn ? `turn ${formatUsdc(view.lastTurn.chargedAtomic)}` : "turn -");
2053
+ const active = view.channels.filter((channel) => !channel.retired);
2054
+ if (active.length > 0) {
2055
+ const open = active.some((channel) => channel.state === "open");
2056
+ parts.push(
2057
+ open ? `budget ${formatUsdc(remainingBudget(view))} left` : `channel ${active[0].state.replace("_", " ")}`
2058
+ );
2059
+ }
2060
+ const compute = view.compute;
2061
+ if (compute?.configured && compute.availableCredits !== null) {
2062
+ const burn = compute.burnCreditsPerHour !== null ? ` at ${formatCredits(compute.burnCreditsPerHour)}/h` : "";
2063
+ parts.push(
2064
+ `compute ${formatCredits(compute.availableCredits)} cr${burn}${compute.low ? " LOW: /credits" : ""}`
2065
+ );
2066
+ } else if (compute && !compute.configured) {
2067
+ parts.push("compute: not set up");
2068
+ }
2069
+ const blocking = blockingPurchases(view);
2070
+ if (blocking.length > 0) {
2071
+ const id = blocking[0].length > 12 ? `${blocking[0].slice(0, 12)}...` : blocking[0];
2072
+ parts.push(`purchase ${id} unresolved: new turns blocked, see /budget`);
2073
+ }
2074
+ return parts.join(" | ");
2075
+ }
2076
+ function channelLines(channel) {
2077
+ const a = channel.amounts;
2078
+ return [
2079
+ `Channel ${channel.rail}${channel.retired ? " (retired)" : ""} ${channel.channelId} (${channel.network}, ${channel.route}): ${channel.state.replace("_", " ")}`,
2080
+ ` Locked ${formatUsdc(a.locked)} (deposited, not spent); charged ${formatUsdc(a.charged)}; pending vouchers ${formatUsdc(a.pending)}; refunded ${formatUsdc(a.refunded)}`,
2081
+ ` Refundable ${formatUsdc(a.refundable)}: what further turns can use and a close returns`,
2082
+ ` Budget ${formatUsdc(a.budget)}; ${formatUsdc(a.lifetimeRoom)} may still be deposited; next deposit ${formatUsdc(a.depositTarget)}${Number(a.depositing) > 0 ? `; ${formatUsdc(a.depositing)} being deposited` : ""}`,
2083
+ ...channel.pendingOperation ? [
2084
+ channel.pendingOperation.kind === "payment" ? ` Purchase ${channel.pendingOperation.id} is unresolved and blocks new turns: taskmarket agent reconcile ${channel.pendingOperation.id}` : ` A refund or withdrawal (${channel.pendingOperation.id}) is unresolved: taskmarket agent close checks it again`
2085
+ ] : [],
2086
+ ...channel.blockingDivergences.length > 0 ? [
2087
+ ` Blocked: the journal, the router and the chain disagree (${channel.blockingDivergences.join(", ")})`
2088
+ ] : []
2089
+ ];
2090
+ }
2091
+ function budgetLines(view) {
2092
+ const lines2 = [
2093
+ `This session: ${view.session.turns} paid turn${view.session.turns === 1 ? "" : "s"}, ${formatUsdc(view.session.chargedAtomic)}${view.lastTurn ? `; last turn ${formatUsdc(view.lastTurn.chargedAtomic)}` : ""}`
2094
+ ];
2095
+ for (const channel of view.channels) lines2.push(...channelLines(channel));
2096
+ for (const allocation of view.allocations) {
2097
+ lines2.push(
2098
+ `Allocation ${allocation.allocationId} (${allocation.exposure})${allocation.closed ? ", closed" : ""}: used ${formatUsdc(allocation.used.total)} of ${formatUsdc(allocation.caps.amount)}${allocation.caps.perDay ? `; today ${formatUsdc(allocation.used.today)} of ${formatUsdc(allocation.caps.perDay)}` : ""}`
2099
+ );
2100
+ }
2101
+ for (const id of blockingPurchases(view)) {
2102
+ lines2.push(uncertainPurchaseMessage(id));
2103
+ }
2104
+ const compute = view.compute;
2105
+ if (compute) lines2.push(...computeLines(compute));
2106
+ return lines2;
2107
+ }
2108
+ function computeLines(compute) {
2109
+ if (!compute.configured) {
2110
+ return ["Compute credit: not set up; run taskmarket agent credits setup"];
2111
+ }
2112
+ if (compute.error !== void 0 || compute.availableCredits === null) {
2113
+ return [`Compute credit: unavailable (${compute.error ?? "not read"})`];
2114
+ }
2115
+ return [
2116
+ `Compute credit: ${formatCredits(compute.availableCredits)} available${compute.burnCreditsPerHour !== null ? `, burning ${formatCredits(compute.burnCreditsPerHour)} an hour while the sandbox runs` : ""}${compute.low ? ". Low: buy more with /credits" : ""}`,
2117
+ ...compute.pendingPurchases > 0 ? [
2118
+ ` ${compute.pendingPurchases} purchase(s) not settled yet: taskmarket agent credits reconcile`
2119
+ ] : [],
2120
+ ...compute.autoTopUp ? [
2121
+ ` Auto-top-up: $${compute.autoTopUp.amountUsd} below ${compute.autoTopUp.thresholdCredits} credits, at most ${compute.autoTopUp.maxChunksPerDay} a day`
2122
+ ] : []
2123
+ ];
2124
+ }
2125
+ var LOST_RESPONSE_TEXT = "It was charged, and the response cannot be recovered or regenerated: xgate delivers it live only and keeps no copy. Asking again is a new paid turn.";
2126
+ function uncertainPurchaseMessage(invocationId2) {
2127
+ return `The outcome of purchase ${invocationId2} is unknown. It is never sent again, and new turns on its channel are blocked until it is reconciled: taskmarket agent reconcile ${invocationId2}. If it was charged, its response cannot be recovered or regenerated.`;
2128
+ }
2129
+ var CHARGED_UNDELIVERED = /* @__PURE__ */ new Set([
2130
+ "result_too_large",
2131
+ "response_rejected",
2132
+ "invalid_response",
2133
+ "response_hook_failed"
2134
+ ]);
2135
+ function purchaseFailureNotice(failure) {
2136
+ if (failure.pending) {
2137
+ return {
2138
+ level: "warning",
2139
+ text: uncertainPurchaseMessage(failure.invocationId || "(unknown)")
2140
+ };
2141
+ }
2142
+ const charged = failure.payment?.chargedAtomic;
2143
+ if (charged !== void 0 || CHARGED_UNDELIVERED.has(failure.code)) {
2144
+ return {
2145
+ level: "error",
2146
+ text: `This turn was paid${charged !== void 0 ? ` (${formatUsdc(charged)})` : ""} but its response was lost (${failure.code}). ${LOST_RESPONSE_TEXT}`
2147
+ };
2148
+ }
2149
+ return void 0;
2150
+ }
2151
+
2152
+ // src/lib/agent/budget-ui.ts
2153
+ var BUDGET_STATUS_KEY = "xgate-taskmarket";
2154
+ function isBudgetView(value) {
2155
+ const view = value;
2156
+ return !!view && view.version === 1 && Array.isArray(view.channels) && Array.isArray(view.unresolved) && Array.isArray(view.allocations) && typeof view.session === "object";
2157
+ }
2158
+ function errorCode(error) {
2159
+ const code = error?.code;
2160
+ return typeof code === "string" ? code : "controller_unavailable";
2161
+ }
2162
+ function withPurchaseNotices(transport, notify) {
2163
+ const wrapped = {
2164
+ models: (signal) => transport.models(signal),
2165
+ async purchase(invocation, signal, onResponse) {
2166
+ try {
2167
+ return await transport.purchase(invocation, signal, onResponse);
2168
+ } catch (error) {
2169
+ const failure = error;
2170
+ if (typeof failure?.pending === "boolean" && typeof failure.code === "string") {
2171
+ const notice = purchaseFailureNotice({
2172
+ code: failure.code,
2173
+ invocationId: typeof failure.invocationId === "string" && failure.invocationId ? failure.invocationId : invocation.id,
2174
+ pending: failure.pending,
2175
+ ...failure.payment ? { payment: failure.payment } : {}
2176
+ });
2177
+ if (notice) notify(notice.text, notice.level);
2178
+ }
2179
+ throw error;
2180
+ }
2181
+ }
2182
+ };
2183
+ if (transport.status) wrapped.status = (signal) => transport.status(signal);
2184
+ if (transport.receipts) wrapped.receipts = (signal) => transport.receipts(signal);
2185
+ if (transport.reconcile) {
2186
+ wrapped.reconcile = (id, signal) => transport.reconcile(id, signal);
2187
+ }
2188
+ return wrapped;
2189
+ }
2190
+ function isOffer(value) {
2191
+ const offer = value;
2192
+ return !!offer && typeof offer.usdPerCredit === "string" && Array.isArray(offer.denominations) && offer.denominations.every(
2193
+ (entry) => Number.isSafeInteger(entry?.amountUsd) && typeof entry?.credits === "string"
2194
+ );
2195
+ }
2196
+ function registerBudgetUi(pi2, channel) {
2197
+ let context;
2198
+ let inFlight;
2199
+ const read = async () => {
2200
+ if (!channel) throw Object.assign(new Error("unavailable"), { code: "controller_unavailable" });
2201
+ const value = await channel.budget();
2202
+ if (!isBudgetView(value)) throw Object.assign(new Error("invalid"), { code: "invalid_budget" });
2203
+ return value;
2204
+ };
2205
+ const refresh = (ctx) => {
2206
+ if (ctx) context = ctx;
2207
+ inFlight ??= read().then((view) => {
2208
+ context?.ui.setStatus(BUDGET_STATUS_KEY, statusBarText(view));
2209
+ return view;
2210
+ }).catch(() => {
2211
+ context?.ui.setStatus(BUDGET_STATUS_KEY, "budget unavailable");
2212
+ return void 0;
2213
+ }).finally(() => {
2214
+ inFlight = void 0;
2215
+ });
2216
+ return inFlight;
2217
+ };
2218
+ pi2.on("session_start", async (_event, ctx) => {
2219
+ await refresh(ctx);
2220
+ });
2221
+ pi2.on("turn_end", async (_event, ctx) => {
2222
+ await refresh(ctx);
2223
+ });
2224
+ pi2.registerCommand("budget", {
2225
+ description: "Show what this session cost, the channel (locked, charged, pending vouchers, refundable), allocations, unresolved purchases and compute credit",
2226
+ handler: async (_args, ctx) => {
2227
+ context = ctx;
2228
+ try {
2229
+ const view = await read();
2230
+ ctx.ui.setStatus(BUDGET_STATUS_KEY, statusBarText(view));
2231
+ ctx.ui.notify(budgetLines(view).join("\n"), "info");
2232
+ } catch (error) {
2233
+ ctx.ui.notify(`The budget is unavailable (${errorCode(error)})`, "error");
2234
+ }
2235
+ }
2236
+ });
2237
+ pi2.registerCommand("credits", {
2238
+ description: "Show compute credit and buy a chunk from your compute allocation (asks first; /credits <usd> picks the size)",
2239
+ handler: async (args, ctx) => {
2240
+ context = ctx;
2241
+ if (!channel) {
2242
+ ctx.ui.notify("Compute credit is unavailable (controller_unavailable)", "error");
2243
+ return;
2244
+ }
2245
+ try {
2246
+ const view = await read();
2247
+ if (view.compute) ctx.ui.notify(computeLines(view.compute).join("\n"), "info");
2248
+ if (view.compute && !view.compute.configured) return;
2249
+ const offer = await channel.creditOffer();
2250
+ if (!isOffer(offer) || offer.denominations.length === 0) {
2251
+ ctx.ui.notify("Compute credit is not on sale right now", "error");
2252
+ return;
2253
+ }
2254
+ const requested = args.trim().replace(/^\$/, "");
2255
+ const size = requested ? offer.denominations.find((entry) => String(entry.amountUsd) === requested) : offer.denominations.find((entry) => entry.default) ?? offer.denominations[0];
2256
+ if (!size) {
2257
+ ctx.ui.notify(
2258
+ `Compute credit is sold in $${offer.denominations.map((entry) => entry.amountUsd).join(", $")} chunks`,
2259
+ "error"
2260
+ );
2261
+ return;
2262
+ }
2263
+ if (!ctx.hasUI) {
2264
+ ctx.ui.notify("Buying compute credit needs the interactive session", "error");
2265
+ return;
2266
+ }
2267
+ const approved = await ctx.ui.confirm(
2268
+ "Buy compute credit",
2269
+ `Buy $${size.amountUsd} of compute credit (${size.credits} credits at $${offer.usdPerCredit} each) in USDC from your compute allocation? Its daily and monthly caps apply.`
2270
+ );
2271
+ if (!approved) return;
2272
+ const bought = await channel.buyCredits(size.amountUsd);
2273
+ ctx.ui.notify(
2274
+ `Bought ${bought?.purchase?.credits ?? size.credits} credits; ${bought?.balance?.availableCredits ?? "?"} available`,
2275
+ "info"
2276
+ );
2277
+ await refresh(ctx);
2278
+ } catch (error) {
2279
+ const code = errorCode(error);
2280
+ ctx.ui.notify(
2281
+ code === "purchase_pending" ? "Nothing was bought: an earlier compute purchase may have been paid and is not settled yet. Run taskmarket agent credits reconcile; it never pays again." : code === "controller_unavailable" || code === "aborted" ? "The controller did not answer, so the purchase may still complete. It is never repeated automatically; check with /budget or taskmarket agent credits status." : `Nothing was bought (${code})`,
2282
+ "error"
2283
+ );
2284
+ }
2285
+ }
2286
+ });
2287
+ return { refresh };
2288
+ }
2289
+
2009
2290
  // src/lib/agent/inference-channel.ts
2010
2291
  init_cjs_shims();
2011
2292
  var import_net = __toESM(require("net"), 1);
2012
2293
  var MAX_MESSAGE_BYTES = 48 * 1024 * 1024;
2013
2294
  var VIEW_TIMEOUT_MS = 6e4;
2295
+ var BUY_TIMEOUT_MS = 5 * 6e4;
2014
2296
  var CODE = /^[a-z0-9_]{1,64}$/;
2015
2297
  function lines(stream, onLine, onOverflow, maxBytes) {
2016
2298
  let chunks = [];
@@ -2147,8 +2429,8 @@ var ChannelXgateTransport = class _ChannelXgateTransport {
2147
2429
  this.requests.write(line);
2148
2430
  return { id, reply };
2149
2431
  }
2150
- async view(body, signal) {
2151
- const sent = this.send(body, { timeoutMs: VIEW_TIMEOUT_MS, signal });
2432
+ async view(body, signal, timeoutMs = VIEW_TIMEOUT_MS) {
2433
+ const sent = this.send(body, { timeoutMs, signal });
2152
2434
  if (sent === "too_large") throw new XgateError("message_too_large");
2153
2435
  if (!sent) throw new XgateError("controller_unavailable");
2154
2436
  let reply;
@@ -2211,6 +2493,25 @@ var ChannelXgateTransport = class _ChannelXgateTransport {
2211
2493
  async reconcile(invocationId2, signal) {
2212
2494
  return (await this.view({ op: "reconcile", invocationId: invocationId2 }, signal)).value;
2213
2495
  }
2496
+ /** The controller's budget view (`BudgetView`) for the status bar and `/budget`. */
2497
+ async budget(signal) {
2498
+ return (await this.view({ op: "budget" }, signal)).value;
2499
+ }
2500
+ /** The compute credit sizes and price the controller can buy, for `/credits`. */
2501
+ async creditOffer(signal) {
2502
+ return (await this.view({ op: "credit_offer" }, signal)).value;
2503
+ }
2504
+ /**
2505
+ * `/credits`: ask the controller to buy one chunk of compute credit. The controller's purchase
2506
+ * is journaled and capped; a reply lost here never makes it buy again.
2507
+ */
2508
+ async buyCredits(amountUsd, signal) {
2509
+ return (await this.view(
2510
+ { op: "buy_credits", ...amountUsd !== void 0 ? { amountUsd } : {} },
2511
+ signal,
2512
+ BUY_TIMEOUT_MS
2513
+ )).value;
2514
+ }
2214
2515
  };
2215
2516
  function unavailableTransport() {
2216
2517
  return {
@@ -2236,20 +2537,24 @@ function openTransport() {
2236
2537
  }
2237
2538
  async function taskmarketXgate(pi2) {
2238
2539
  const transport = openTransport();
2239
- await createXgateExtension({ transport })(pi2);
2540
+ const channel = transport instanceof ChannelXgateTransport ? transport : void 0;
2240
2541
  let context;
2241
2542
  let controllerGone = false;
2242
2543
  pi2.on("session_start", async (_event, ctx) => {
2243
2544
  context = ctx;
2244
2545
  if (controllerGone) ctx.shutdown();
2245
2546
  });
2246
- if (transport instanceof ChannelXgateTransport) {
2247
- transport.onClose((reason) => {
2547
+ if (channel) {
2548
+ channel.onClose((reason) => {
2248
2549
  log(`The taskmarket controller is gone (${reason}); ending the session`);
2249
2550
  controllerGone = true;
2250
2551
  context?.shutdown();
2251
2552
  });
2252
2553
  }
2554
+ await createXgateExtension({
2555
+ transport: withPurchaseNotices(transport, (text2, level) => context?.ui.notify(text2, level))
2556
+ })(pi2);
2557
+ registerBudgetUi(pi2, channel);
2253
2558
  pi2.on("model_select", async (event, ctx) => {
2254
2559
  if (event.model.provider === "xgate") return;
2255
2560
  const fallback = event.previousModel?.provider === "xgate" ? event.previousModel : ctx.modelRegistry.getAvailable().find((model) => model.provider === "xgate");
@@ -0,0 +1,83 @@
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
+ openAgentChannels
4
+ } from "./chunk-GJQUALXN.js";
5
+ import {
6
+ X402_INVOCATION_ID_PATTERN
7
+ } from "./chunk-FRV3ZHUR.js";
8
+ import {
9
+ AgentLaunchError
10
+ } from "./chunk-YM4S6IE4.js";
11
+ import {
12
+ AGENT_VIEW_VERSION,
13
+ LOST_RESPONSE_TEXT,
14
+ formatUsdc
15
+ } from "./chunk-73RXOYME.js";
16
+ import "./chunk-S7SJMJ2I.js";
17
+
18
+ // src/lib/agent/reconcile.ts
19
+ function outcomeOf(status) {
20
+ if (status.state === "in_progress") return "in_progress";
21
+ if (status.charge === "manually_resolved") return "resolved_manually";
22
+ if (status.uncertain) return "uncertain";
23
+ if (status.charge === "settled") return "charged";
24
+ if (status.charge === "none" || status.charge === "released") return "not_charged";
25
+ return "uncertain";
26
+ }
27
+ function messageFor(outcome, status) {
28
+ const charged = status.payment?.settledAmount;
29
+ switch (outcome) {
30
+ case "charged":
31
+ return `Charged ${charged !== void 0 ? formatUsdc(charged) : "an amount the router committed"}. If its response did not reach you: ${LOST_RESPONSE_TEXT}`;
32
+ case "not_charged":
33
+ return "Nothing was charged. The request is not sent again; ask again in the agent if you still want an answer.";
34
+ case "in_progress":
35
+ return "It is still being sent by another process; reconcile it again once that finishes.";
36
+ case "resolved_manually":
37
+ return "It was resolved by hand; its recorded amount stands.";
38
+ default:
39
+ 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.";
40
+ }
41
+ }
42
+ async function railFor(rails, invocationId) {
43
+ for (const rail of rails) {
44
+ try {
45
+ await rail.payer.status(invocationId);
46
+ return rail;
47
+ } catch (error) {
48
+ if (error.code !== "invocation_not_found") throw error;
49
+ }
50
+ const lease = (await rail.payer.channelStatus()).pendingOperation;
51
+ if (lease?.kind === "payment" && lease.id === invocationId) return rail;
52
+ }
53
+ throw new AgentLaunchError(
54
+ "invocation_not_found",
55
+ `No agent purchase ${invocationId} is recorded on this device`
56
+ );
57
+ }
58
+ async function reconcileInvocation(invocationId, deps = {}) {
59
+ if (!X402_INVOCATION_ID_PATTERN.test(invocationId)) {
60
+ throw new AgentLaunchError("invalid_invocation_id", `Not an invocation ID: ${invocationId}`);
61
+ }
62
+ const opened = await (deps.openChannels ?? openAgentChannels)();
63
+ const rail = await railFor([...opened.rails, ...opened.retired ?? []], invocationId);
64
+ const status = await rail.payer.reconcile(invocationId);
65
+ const lease = (await rail.payer.channelStatus()).pendingOperation;
66
+ const outcome = outcomeOf(status);
67
+ return {
68
+ version: AGENT_VIEW_VERSION,
69
+ kind: "taskmarket.agent.reconcile",
70
+ invocationId,
71
+ rail: rail.rail.id,
72
+ outcome,
73
+ state: status.state,
74
+ charge: status.charge,
75
+ authorizedAtomic: status.payment?.authorizedAmount ?? null,
76
+ chargedAtomic: status.payment?.settledAmount ?? null,
77
+ blocksChannel: lease?.kind === "payment" && lease.id === invocationId,
78
+ message: messageFor(outcome, status)
79
+ };
80
+ }
81
+ export {
82
+ reconcileInvocation
83
+ };
@@ -0,0 +1,18 @@
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
+ statusLines
6
+ } from "./chunk-XXD7TWLP.js";
7
+ import "./chunk-LDCFKFF6.js";
8
+ import "./chunk-52Q2DCZN.js";
9
+ import "./chunk-GJQUALXN.js";
10
+ import "./chunk-FRV3ZHUR.js";
11
+ import "./chunk-YM4S6IE4.js";
12
+ import "./chunk-73RXOYME.js";
13
+ import "./chunk-S7SJMJ2I.js";
14
+ export {
15
+ collectAgentStatus,
16
+ listSandboxes,
17
+ statusLines
18
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lucid-agents/taskmarket",
3
- "version": "1.17.0",
3
+ "version": "1.17.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "taskmarket": "./dist/index.js"
@@ -28,11 +28,11 @@
28
28
  "tsx": "^4.7.0",
29
29
  "typescript": "^5.3.3",
30
30
  "vitest": "^2.0.0",
31
+ "@taskmarket/agent-host": "0.0.1",
31
32
  "@taskmarket/contracts": "0.0.0",
32
- "@taskmarket/prettier-config": "0.0.0",
33
33
  "@taskmarket/eslint-config": "0.0.0",
34
+ "@taskmarket/prettier-config": "0.0.0",
34
35
  "@taskmarket/remote-tools": "0.0.1",
35
- "@taskmarket/agent-host": "0.0.1",
36
36
  "@taskmarket/shared": "2.0.0",
37
37
  "@taskmarket/x402-payer": "0.1.0"
38
38
  },