humanish 0.109.1 → 0.110.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.
Files changed (164) hide show
  1. package/ARCHITECTURE.md +14 -11
  2. package/CHANGELOG.md +24 -0
  3. package/CONTEXT.md +8 -6
  4. package/README.md +97 -48
  5. package/dist/actors/computer-use/loop/ending.js +4 -4
  6. package/dist/actors/local-agent/claude-participant.d.ts +74 -0
  7. package/dist/actors/local-agent/claude-participant.js +243 -0
  8. package/dist/actors/local-agent/claude-session.d.ts +16 -3
  9. package/dist/actors/local-agent/claude-session.js +64 -23
  10. package/dist/actors/local-agent/cli.d.ts +11 -0
  11. package/dist/actors/local-agent/cli.js +103 -24
  12. package/dist/actors/local-agent/readiness.d.ts +2 -2
  13. package/dist/actors/local-agent/readiness.js +18 -11
  14. package/dist/cli/commands/analyze.js +21 -5
  15. package/dist/cli/commands/comms.js +25 -7
  16. package/dist/cli/commands/observe.js +4 -2
  17. package/dist/cli/commands/run-command.js +4 -3
  18. package/dist/cli/commands/run-signals.d.ts +2 -0
  19. package/dist/cli/commands/run-signals.js +96 -44
  20. package/dist/cli/commands/runs.d.ts +5 -0
  21. package/dist/cli/commands/runs.js +89 -67
  22. package/dist/cli/commands/setup.js +19 -5
  23. package/dist/cli/commands/study-format.js +29 -2
  24. package/dist/cli/commands/study-route-computer-use.js +2 -0
  25. package/dist/cli/commands/study-route-scripted.js +2 -0
  26. package/dist/cli/commands/study-route-shared-world.js +2 -0
  27. package/dist/cli/commands/study-route-terminal.js +2 -0
  28. package/dist/cli/commands/study.js +4 -3
  29. package/dist/cli/commands/tui.js +4 -3
  30. package/dist/cli/commands/watch.js +2 -2
  31. package/dist/cli/doctor.d.ts +7 -2
  32. package/dist/cli/doctor.js +47 -8
  33. package/dist/cli/findings.d.ts +103 -0
  34. package/dist/cli/findings.js +316 -0
  35. package/dist/cli/first-run-path.d.ts +5 -4
  36. package/dist/cli/first-run-path.js +14 -7
  37. package/dist/cli/io.d.ts +17 -3
  38. package/dist/cli/io.js +22 -4
  39. package/dist/cli/product-sentence.d.ts +3 -3
  40. package/dist/cli/product-sentence.js +3 -3
  41. package/dist/comms/receiving-runtime.js +1 -1
  42. package/dist/comms/setup.js +1 -1
  43. package/dist/evidence/redaction.d.ts +7 -3
  44. package/dist/evidence/redaction.js +68 -24
  45. package/dist/feedback/export-bundle.js +6 -1
  46. package/dist/feedback/export.js +3 -0
  47. package/dist/keys/env-file.d.ts +4 -0
  48. package/dist/keys/env-file.js +17 -0
  49. package/dist/keys/key-resolution.d.ts +7 -0
  50. package/dist/keys/key-resolution.js +10 -8
  51. package/dist/observer/data.d.ts +14 -1
  52. package/dist/observer/data.js +19 -9
  53. package/dist/observer/library.d.ts +4 -0
  54. package/dist/observer/library.js +18 -15
  55. package/dist/observer/render.js +8 -2
  56. package/dist/observer/run-routes.d.ts +2 -0
  57. package/dist/observer/run-routes.js +43 -34
  58. package/dist/observer-app.html +3 -3
  59. package/dist/routes/computer-use/bundle.js +3 -1
  60. package/dist/routes/computer-use/fanout-records.js +1 -1
  61. package/dist/routes/computer-use/live-phase.d.ts +1 -0
  62. package/dist/routes/computer-use/participant-execution.js +3 -1
  63. package/dist/routes/computer-use/participant-model.js +5 -2
  64. package/dist/routes/computer-use/plan.js +4 -4
  65. package/dist/routes/computer-use/preflight.js +2 -2
  66. package/dist/routes/computer-use/result.js +17 -28
  67. package/dist/routes/computer-use/single-bundle.js +7 -1
  68. package/dist/routes/computer-use/types.d.ts +6 -0
  69. package/dist/routes/scripted/bundle.js +4 -2
  70. package/dist/routes/scripted/plan.js +2 -2
  71. package/dist/routes/scripted/result.d.ts +3 -5
  72. package/dist/routes/scripted/result.js +16 -22
  73. package/dist/routes/scripted/scenario.js +4 -4
  74. package/dist/routes/scripted/types.d.ts +1 -0
  75. package/dist/routes/shared-world/bundle-records.js +6 -1
  76. package/dist/routes/shared-world/bundle.js +2 -0
  77. package/dist/routes/shared-world/plan.js +1 -1
  78. package/dist/routes/shared-world/result.js +22 -22
  79. package/dist/routes/shared-world/route.js +1 -1
  80. package/dist/routes/shared-world/types.d.ts +2 -0
  81. package/dist/routes/terminal/bundle.js +4 -2
  82. package/dist/routes/terminal/credentials.js +1 -1
  83. package/dist/routes/terminal/dry-run.js +8 -12
  84. package/dist/routes/terminal/ledger.d.ts +3 -3
  85. package/dist/routes/terminal/ledger.js +6 -6
  86. package/dist/routes/terminal/live-finish.js +23 -17
  87. package/dist/routes/terminal/plan.js +4 -4
  88. package/dist/routes/terminal/result.d.ts +6 -7
  89. package/dist/routes/terminal/result.js +6 -22
  90. package/dist/routes/terminal/session.js +1 -1
  91. package/dist/routes/terminal/types.d.ts +3 -0
  92. package/dist/run/bundle-shape.d.ts +3 -1
  93. package/dist/run/bundle-shape.js +26 -1
  94. package/dist/run/bundle.d.ts +30 -0
  95. package/dist/run/display.d.ts +59 -0
  96. package/dist/run/display.js +106 -0
  97. package/dist/run/dry-run.js +11 -7
  98. package/dist/run/judge.d.ts +3 -2
  99. package/dist/run/judge.js +10 -0
  100. package/dist/run/pricing.js +1 -1
  101. package/dist/run/projection.d.ts +0 -2
  102. package/dist/run/projection.js +0 -11
  103. package/dist/run/reclaim-outcomes.d.ts +89 -0
  104. package/dist/run/reclaim-outcomes.js +213 -0
  105. package/dist/run/reclaim.d.ts +18 -14
  106. package/dist/run/reclaim.js +179 -105
  107. package/dist/run/results.d.ts +11 -18
  108. package/dist/run/results.js +2 -2
  109. package/dist/run/run-index.d.ts +11 -1
  110. package/dist/run/run-index.js +11 -0
  111. package/dist/run/run.d.ts +34 -18
  112. package/dist/run/run.js +177 -72
  113. package/dist/run/sandbox-creates.d.ts +59 -0
  114. package/dist/run/sandbox-creates.js +138 -0
  115. package/dist/run/sandbox-ids.d.ts +53 -0
  116. package/dist/run/sandbox-ids.js +198 -0
  117. package/dist/run/sandbox-receipts.d.ts +16 -0
  118. package/dist/run/sandbox-receipts.js +70 -0
  119. package/dist/run/stats.d.ts +5 -0
  120. package/dist/run/stats.js +8 -4
  121. package/dist/run/status.d.ts +6 -9
  122. package/dist/run/status.js +7 -20
  123. package/dist/run/stored-runs.d.ts +5 -14
  124. package/dist/run/stored-runs.js +31 -137
  125. package/dist/run/streams.d.ts +3 -2
  126. package/dist/run-study.js +6 -1
  127. package/dist/study/doctor.d.ts +1 -1
  128. package/dist/study/doctor.js +7 -4
  129. package/dist/study/init-templates.js +2 -2
  130. package/dist/study/init.js +14 -3
  131. package/dist/study/keys.d.ts +12 -0
  132. package/dist/study/keys.js +32 -0
  133. package/dist/study/persona.d.ts +4 -0
  134. package/dist/study/persona.js +4 -2
  135. package/dist/study/types.d.ts +1 -1
  136. package/dist/substrates/e2b/peer-install.d.ts +51 -0
  137. package/dist/substrates/e2b/peer-install.js +161 -0
  138. package/dist/substrates/e2b/sandbox.d.ts +34 -6
  139. package/dist/substrates/e2b/sandbox.js +128 -35
  140. package/dist/substrates/e2b/sdk.d.ts +33 -0
  141. package/dist/substrates/e2b/sdk.js +21 -21
  142. package/dist/substrates/shell.d.ts +2 -0
  143. package/dist/substrates/shell.js +4 -0
  144. package/dist/tui-app.js +133 -133
  145. package/dist/verify/artifacts.d.ts +4 -0
  146. package/dist/verify/artifacts.js +8 -0
  147. package/dist/verify/liveness.d.ts +19 -3
  148. package/dist/verify/liveness.js +53 -28
  149. package/dist/verify/verify.d.ts +4 -1
  150. package/dist/verify/verify.js +27 -3
  151. package/docs/architecture/actor-contract.md +2 -1
  152. package/docs/architecture/observer.md +22 -7
  153. package/docs/architecture/restricted-codex-analysis.md +1 -1
  154. package/docs/architecture/task-protocol-support.md +13 -13
  155. package/docs/architecture/terminal-product-route.md +1 -1
  156. package/docs/contracts/run-bundle.md +90 -7
  157. package/docs/contracts/schemas.md +39 -22
  158. package/docs/contracts/study-analysis.md +45 -0
  159. package/docs/contracts/study-costs.md +8 -0
  160. package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
  161. package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
  162. package/docs/principles/invariants-and-defaults.md +11 -8
  163. package/package.json +5 -3
  164. package/skills/humanish/SKILL.md +18 -4
@@ -1,7 +1,7 @@
1
1
  import type { PreparedOutputRoot } from "../../run/contained-output.js";
2
2
  import { type DesktopReleaseResult, type OwnedDesktopAllocation } from "../desktop-session.js";
3
3
  import type { StudyConfig } from "../../study/types.js";
4
- import { type E2BDesktopCreateOptions, type E2BDesktopModule, type E2BDesktopSandbox } from "./sdk.js";
4
+ import { type E2BDesktopCreateOptions, type E2BDesktopModule, type E2BDesktopSandbox, type E2BListedSandbox } from "./sdk.js";
5
5
  /** Where an allocation's receipt goes: the run's prepared root, under a public-safe participant
6
6
  * label. The receipt line saves it as `laneId`. */
7
7
  interface E2BSandboxReceiptTarget {
@@ -16,7 +16,7 @@ export interface E2BSandboxRequest {
16
16
  retry?: TransientRetryHooks;
17
17
  /**
18
18
  * Required so every caller decides. `null` is only for a caller that could not open a journal;
19
- * that sandbox is reclaimable by its create-time `timeoutMs` alone.
19
+ * that sandbox carries no owner tags and is reclaimable by its create-time `timeoutMs` alone.
20
20
  */
21
21
  receipt: E2BSandboxReceiptTarget | null;
22
22
  }
@@ -57,13 +57,39 @@ export type E2BSandboxDestroyOutcome = {
57
57
  state: "kill-failed";
58
58
  detail: string;
59
59
  };
60
- /**
61
- * Kill one sandbox by its exact recorded id, for reclaim, and map the shared release result onto
62
- * reclaim's persisted outcome. It never lists the account.
63
- */
60
+ /** Kill one sandbox by its exact id, for reclaim, and map the shared release result onto
61
+ * reclaim's persisted outcome. */
64
62
  export declare function destroyE2BSandbox(module: E2BDesktopModule, sandboxId: string, options: {
65
63
  requestTimeoutMs: number;
66
64
  }): Promise<E2BSandboxDestroyOutcome>;
65
+ /** What `reclaim --check` learned about one sandbox by its exact id. */
66
+ export type E2BSandboxPresence = {
67
+ state: "running" | "already-gone";
68
+ } | {
69
+ state: "check-failed";
70
+ detail: string;
71
+ };
72
+ /**
73
+ * Ask E2B whether one sandbox still exists, by exact id. A SandboxNotFoundError, recognized by
74
+ * type, means gone; any other error leaves the answer open.
75
+ */
76
+ export declare function inspectE2BSandbox(module: E2BDesktopModule, sandboxId: string, options: {
77
+ requestTimeoutMs: number;
78
+ }): Promise<E2BSandboxPresence>;
79
+ /** What E2B listed for a run's owner tags. `sandboxes` holds exact matches only. */
80
+ export interface E2BTagSearch {
81
+ status: "done" | "unavailable" | "failed";
82
+ sandboxes: E2BListedSandbox[];
83
+ detail?: string;
84
+ }
85
+ /**
86
+ * List the sandboxes E2B matches to every one of `tags`, filtered server-side. Each result is
87
+ * checked against every tag again, so a server that ignored the filter still cannot hand back
88
+ * another run's sandbox, and the search stops after a few pages instead of walking the account.
89
+ */
90
+ export declare function findE2BSandboxesByTags(module: E2BDesktopModule, tags: Record<string, string>, options: {
91
+ requestTimeoutMs: number;
92
+ }): Promise<E2BTagSearch>;
67
93
  /** Versioned public template built by runtime/browser-media/e2b-template.mjs. */
68
94
  export declare const E2B_SPEECH_TEMPLATE = "7409n13kr83f7g7abx5g";
69
95
  /** The desktop template a study asks for; undefined selects the SDK default. */
@@ -74,6 +100,8 @@ export declare function e2bDesktopTemplate(config: {
74
100
  export interface TransientRetryHooks {
75
101
  onRetry?: (reason: string) => void;
76
102
  sleep?: (ms: number) => Promise<void>;
103
+ /** False skips the retry, as when the run started stopping during the first attempt. */
104
+ canRetry?: () => boolean;
77
105
  }
78
106
  /** Wall-clock pause before the single retry; envd routing settles within a few seconds. */
79
107
  export declare const TRANSIENT_RETRY_DELAY_MS = 3000;
@@ -1,13 +1,16 @@
1
1
  // E2B sandbox acquisition and release. Every route that allocates an E2B sandbox calls this module,
2
- // so each allocation runs the same steps in the same order: create, one retry on a transient
2
+ // so each allocation runs the same steps in the same order: register the create with the run's
3
+ // in-process registry, tag the sandbox with the run's owner tags, create, one retry on a transient
3
4
  // provider error, capture of the exact id and kill authority, and a provider-qualified receipt in
4
5
  // the run directory before the caller gets the handle. The desktop startup guard
5
6
  // (guardDesktopSandboxCreate) is installed by loadE2BDesktopModule, so a create whose desktop
6
- // startup fails has already reclaimed its handle when the error reaches the retry here.
7
+ // startup fails has already reclaimed its handle when the error reaches the retry here, and it
8
+ // reports the id to the registry before desktop startup begins.
7
9
  import { redactText, toErrorMessage } from "../../evidence/redaction.js";
8
- import { appendSandboxReceipt } from "../../run/sandbox-receipts.js";
10
+ import { beginSandboxCreate, recordSandboxOwnerOnce, SandboxCreateRefusedError, sandboxOwnerTags, } from "../../run/sandbox-creates.js";
11
+ import { appendSandboxOwner, appendSandboxReceipt } from "../../run/sandbox-receipts.js";
9
12
  import { ownDesktopAllocation, } from "../desktop-session.js";
10
- import { E2B_DEBUG_KILL_DETAIL, E2BDesktopStartupError, e2bDebugMode, isSandboxNotFoundError, } from "./sdk.js";
13
+ import { E2B_DEBUG_KILL_DETAIL, E2BDesktopStartupError, e2bDebugMode, isSandboxNotFoundError, observeDesktopAllocation, } from "./sdk.js";
11
14
  /** A desktop sandbox, on the stock `desktop` template or a named one. */
12
15
  export function acquireE2BDesktopSandbox(request) {
13
16
  return acquire(request, request.template);
@@ -17,33 +20,57 @@ export function acquireE2BShellSandbox(request) {
17
20
  return acquire(request, undefined);
18
21
  }
19
22
  async function acquire(request, template) {
20
- const { module, options } = request;
21
- // Without a template the SDK's one-argument create chooses its stock desktop template.
22
- const sandbox = await withOneRetryOnTransientE2BError(() => template === undefined
23
- ? module.Sandbox.create(options)
24
- : module.Sandbox.create(template, options), request.retry);
25
- const allocation = ownE2BSandbox(module, sandbox.sandboxId);
23
+ const { module } = request;
24
+ const target = request.receipt;
25
+ // Refused before any provider call once the run is stopping.
26
+ const ticket = target === null ? undefined : beginSandboxCreate(target.root, target.participantId);
26
27
  try {
27
- if (request.receipt !== null) {
28
- const { root, participantId, now = Date.now } = request.receipt;
29
- // Best effort by contract: a failed write leaves the TTL as the only backstop and never
30
- // fails the participant.
31
- await appendSandboxReceipt(root, {
32
- at: new Date(now()).toISOString(),
33
- laneId: participantId,
34
- provider: "e2b",
35
- sandboxId: allocation.resourceId,
36
- ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
37
- });
28
+ // The owner tags let reclaim find this sandbox on E2B even if its id never reaches this
29
+ // process: a create that throws after E2B allocated, or a process that dies mid-create. The
30
+ // journal records them first, so reclaim knows which tags to search for.
31
+ const owner = target === null ? undefined : sandboxOwnerTags(target.root);
32
+ if (target !== null && owner !== undefined)
33
+ await recordSandboxOwnerOnce(target.root, () => appendSandboxOwner(target.root, owner));
34
+ const options = owner === undefined
35
+ ? request.options
36
+ : { ...request.options, metadata: { ...request.options.metadata, ...owner } };
37
+ if (ticket !== undefined)
38
+ observeDesktopAllocation(options, (sandboxId) => ticket.created(sandboxId));
39
+ const sandbox = await withOneRetryOnTransientE2BError(() => {
40
+ if (ticket?.stopping())
41
+ throw new SandboxCreateRefusedError();
42
+ // Without a template the SDK's one-argument create chooses its stock desktop template.
43
+ return template === undefined
44
+ ? module.Sandbox.create(options)
45
+ : module.Sandbox.create(template, options);
46
+ }, { ...request.retry, canRetry: () => ticket?.stopping() !== true });
47
+ ticket?.created(sandbox.sandboxId);
48
+ const allocation = ownE2BSandbox(module, sandbox.sandboxId, ticket);
49
+ try {
50
+ if (target !== null) {
51
+ const { root, participantId, now = Date.now } = target;
52
+ // Best effort by contract: a failed write leaves the owner tags and the TTL as the
53
+ // backstops and never fails the participant.
54
+ await appendSandboxReceipt(root, {
55
+ at: new Date(now()).toISOString(),
56
+ laneId: participantId,
57
+ provider: "e2b",
58
+ sandboxId: allocation.resourceId,
59
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
60
+ });
61
+ }
38
62
  }
63
+ catch (error) {
64
+ // The caller never receives this sandbox, so release it here. A throwing or invalid injected
65
+ // clock is the known case.
66
+ await allocation.close();
67
+ throw error;
68
+ }
69
+ return { sandbox, allocation };
39
70
  }
40
- catch (error) {
41
- // The caller never receives this sandbox, so release it here. A throwing or invalid injected
42
- // clock is the known case.
43
- await allocation.close();
44
- throw error;
71
+ finally {
72
+ ticket?.settled();
45
73
  }
46
- return { sandbox, allocation };
47
74
  }
48
75
  /** The provider's kill method bound to its class, or undefined when the SDK has none. */
49
76
  function boundKill(module) {
@@ -57,7 +84,7 @@ function boundKill(module) {
57
84
  * recognized by type, means the same. Every other throw is unconfirmed, whatever its message
58
85
  * says: the SDK answers a 404 with false before it throws, so "not found" or "404" in a thrown
59
86
  * message comes from some other failure, such as a trace id. A true in debug mode sent no
60
- * request, so it confirms nothing. Never list the account.
87
+ * request, so it confirms nothing.
61
88
  */
62
89
  async function killById(kill, sandboxId, options) {
63
90
  if (kill === undefined)
@@ -84,12 +111,18 @@ async function killById(kill, sandboxId, options) {
84
111
  return { status: "unconfirmed", reason: "release_failed", error };
85
112
  }
86
113
  }
87
- function ownE2BSandbox(module, resourceId) {
114
+ function ownE2BSandbox(module, resourceId, ticket) {
88
115
  // Bind the provider method now, before any hook can replace it on the shared module.
89
116
  const kill = boundKill(module);
90
117
  return ownDesktopAllocation({
91
118
  resourceId,
92
- release: () => killById(kill, resourceId, { requestTimeoutMs: 60_000 }),
119
+ release: async () => {
120
+ const released = await killById(kill, resourceId, { requestTimeoutMs: 60_000 });
121
+ // The signal handler skips a sandbox its route already released.
122
+ if (released.status === "released")
123
+ ticket?.released(resourceId);
124
+ return released;
125
+ },
93
126
  });
94
127
  }
95
128
  /**
@@ -136,10 +169,8 @@ export function releaseUnavailableDetail(detail) {
136
169
  ? "installed @e2b/desktop SDK does not expose Sandbox.kill; server-side kill-on-timeout will reclaim the sandbox"
137
170
  : `${detail}; if the sandbox exists, server-side kill-on-timeout will reclaim it`;
138
171
  }
139
- /**
140
- * Kill one sandbox by its exact recorded id, for reclaim, and map the shared release result onto
141
- * reclaim's persisted outcome. It never lists the account.
142
- */
172
+ /** Kill one sandbox by its exact id, for reclaim, and map the shared release result onto
173
+ * reclaim's persisted outcome. */
143
174
  export async function destroyE2BSandbox(module, sandboxId, options) {
144
175
  const released = await killById(boundKill(module), sandboxId, options);
145
176
  if (released.status === "released")
@@ -156,6 +187,68 @@ export async function destroyE2BSandbox(module, sandboxId, options) {
156
187
  return { state: "kill-failed", detail: redactText(toErrorMessage(released.error)) };
157
188
  }
158
189
  }
190
+ /**
191
+ * Ask E2B whether one sandbox still exists, by exact id. A SandboxNotFoundError, recognized by
192
+ * type, means gone; any other error leaves the answer open.
193
+ */
194
+ export async function inspectE2BSandbox(module, sandboxId, options) {
195
+ if (typeof module.Sandbox.getInfo !== "function")
196
+ return {
197
+ state: "check-failed",
198
+ detail: "the installed @e2b/desktop SDK has no Sandbox.getInfo",
199
+ };
200
+ try {
201
+ await module.Sandbox.getInfo(sandboxId, options);
202
+ return { state: "running" };
203
+ }
204
+ catch (error) {
205
+ if (isSandboxNotFoundError(error))
206
+ return { state: "already-gone" };
207
+ return { state: "check-failed", detail: redactText(toErrorMessage(error)) };
208
+ }
209
+ }
210
+ const TAG_SEARCH_PAGE_SIZE = 100;
211
+ // One run tags at most a few dozen sandboxes. More pages than this means the filter was not
212
+ // applied, and reading on would walk the whole account.
213
+ const TAG_SEARCH_MAX_PAGES = 3;
214
+ /**
215
+ * List the sandboxes E2B matches to every one of `tags`, filtered server-side. Each result is
216
+ * checked against every tag again, so a server that ignored the filter still cannot hand back
217
+ * another run's sandbox, and the search stops after a few pages instead of walking the account.
218
+ */
219
+ export async function findE2BSandboxesByTags(module, tags, options) {
220
+ if (typeof module.Sandbox.list !== "function")
221
+ return {
222
+ status: "unavailable",
223
+ sandboxes: [],
224
+ detail: "the installed @e2b/desktop SDK has no Sandbox.list",
225
+ };
226
+ const sandboxes = [];
227
+ try {
228
+ const pages = module.Sandbox.list({
229
+ query: { metadata: tags },
230
+ limit: TAG_SEARCH_PAGE_SIZE,
231
+ requestTimeoutMs: options.requestTimeoutMs,
232
+ });
233
+ for (let page = 0; pages.hasNext; page += 1) {
234
+ if (page === TAG_SEARCH_MAX_PAGES)
235
+ return {
236
+ status: "failed",
237
+ sandboxes,
238
+ detail: `E2B returned more than ${TAG_SEARCH_MAX_PAGES * TAG_SEARCH_PAGE_SIZE} sandboxes for one run's tags, so the search stopped before reading the rest`,
239
+ };
240
+ for (const listed of await pages.nextItems()) {
241
+ const metadata = listed.metadata ?? {};
242
+ if (Object.entries(tags).every(([key, value]) => metadata[key] === value))
243
+ sandboxes.push(listed);
244
+ }
245
+ }
246
+ return { status: "done", sandboxes };
247
+ }
248
+ catch (error) {
249
+ return { status: "failed", sandboxes, detail: redactText(toErrorMessage(error)) };
250
+ }
251
+ }
159
252
  /** Versioned public template built by runtime/browser-media/e2b-template.mjs. */
160
253
  export const E2B_SPEECH_TEMPLATE = "7409n13kr83f7g7abx5g";
161
254
  /** The desktop template a study asks for; undefined selects the SDK default. */
@@ -206,7 +299,7 @@ export async function withOneRetryOnTransientE2BError(attempt, hooks) {
206
299
  return await attempt();
207
300
  }
208
301
  catch (error) {
209
- if (!isTransientE2BError(error))
302
+ if (!isTransientE2BError(error) || hooks?.canRetry?.() === false)
210
303
  throw error;
211
304
  const reason = error instanceof Error ? error.message : String(error);
212
305
  hooks?.onRetry?.(reason);
@@ -27,7 +27,32 @@ export interface E2BDesktopModule {
27
27
  getInfo?(sandboxId: string, options?: {
28
28
  requestTimeoutMs?: number;
29
29
  }): Promise<E2BSandboxInfo>;
30
+ /**
31
+ * List sandboxes whose metadata matches every `query.metadata` pair, filtered server-side.
32
+ * Reclaim calls it only with a run's owner tags (sandboxOwnerTags in src/run/sandbox-creates.ts)
33
+ * and re-checks each tag on what comes back. The e2b SDK on the supported @e2b/desktop range
34
+ * (2.38 and later) returns a paginator.
35
+ */
36
+ list?(options?: E2BSandboxListOptions): E2BSandboxPaginator;
37
+ };
38
+ }
39
+ interface E2BSandboxListOptions {
40
+ query?: {
41
+ metadata?: Record<string, string>;
30
42
  };
43
+ limit?: number;
44
+ requestTimeoutMs?: number;
45
+ }
46
+ interface E2BSandboxPaginator {
47
+ readonly hasNext: boolean;
48
+ nextItems(): Promise<E2BListedSandbox[]>;
49
+ }
50
+ /** One sandbox as the list endpoint describes it. */
51
+ export interface E2BListedSandbox {
52
+ sandboxId: string;
53
+ metadata?: Record<string, string>;
54
+ /** `running` or `paused`. */
55
+ state?: string;
31
56
  }
32
57
  interface E2BSandboxInfo {
33
58
  id?: string;
@@ -161,6 +186,14 @@ export interface E2BDesktopSandbox {
161
186
  }
162
187
  export declare function loadE2BDesktopModule(): Promise<E2BDesktopModule>;
163
188
  export declare const DESKTOP_CREATE_CLEANUP_TIMEOUT_MS = 10000;
189
+ /**
190
+ * Call `observer` with the sandbox id as soon as the guarded SDK constructs the handle for a
191
+ * create made with this exact options object, before desktop startup runs. Startup can take
192
+ * seconds after E2B created the sandbox; this is the earliest point the id exists in this process.
193
+ * A module without the guard (a test fake) never calls it, and create's own result still reports
194
+ * the id.
195
+ */
196
+ export declare function observeDesktopAllocation(options: E2BDesktopCreateOptions, observer: (sandboxId: string) => void): void;
164
197
  type DesktopCreateCleanup = "killed" | "already_gone" | "unconfirmed";
165
198
  /** Startup failed after this call acquired a handle. No credentials/options are included here. */
166
199
  export declare class E2BDesktopStartupError extends Error {
@@ -5,27 +5,32 @@
5
5
  // E2BDesktopSandbox is the command, file, stream, launch and input subset the participants use; `open` is
6
6
  // optional because older SDKs lack it (the computer-use route then falls back to launch). It
7
7
  // satisfies the executor's E2BDesktopLike port (src/substrates/e2b/desktop-executor.ts) as is.
8
- import { sep } from "node:path";
9
- import { fileURLToPath } from "node:url";
10
8
  import { protectDesktopScreenshotCleanup } from "./desktop-screenshot-cleanup.js";
9
+ import { desktopPeerAdvice, humanishInstall } from "./peer-install.js";
11
10
  export async function loadE2BDesktopModule() {
12
11
  try {
13
12
  return guardDesktopSandboxCreate((await import("@e2b/desktop")));
14
13
  }
15
14
  catch (error) {
16
15
  if (isMissingE2BDesktopDependency(error)) {
17
- throw new Error(runningFromProject()
18
- ? "Live E2B desktop launch requires the optional peer @e2b/desktop. Install it beside humanish " +
19
- "in this project: `npm i -D @e2b/desktop`."
20
- : "Live E2B desktop launch requires the optional peer @e2b/desktop, and humanish is running from an " +
21
- "npx cache. Node resolves the peer relative to humanish itself, so a copy installed in this project " +
22
- "is not found. Install both into the project and run it from there: " +
23
- "`npm i -D humanish @e2b/desktop` then `npx humanish run <study>`.");
16
+ const { where, advice } = desktopPeerAdvice(humanishInstall());
17
+ throw new Error(`Live E2B desktop launch requires the optional peer @e2b/desktop${where}. ${advice}`);
24
18
  }
25
19
  throw error;
26
20
  }
27
21
  }
28
22
  export const DESKTOP_CREATE_CLEANUP_TIMEOUT_MS = 10_000;
23
+ const allocationObservers = new WeakMap();
24
+ /**
25
+ * Call `observer` with the sandbox id as soon as the guarded SDK constructs the handle for a
26
+ * create made with this exact options object, before desktop startup runs. Startup can take
27
+ * seconds after E2B created the sandbox; this is the earliest point the id exists in this process.
28
+ * A module without the guard (a test fake) never calls it, and create's own result still reports
29
+ * the id.
30
+ */
31
+ export function observeDesktopAllocation(options, observer) {
32
+ allocationObservers.set(options, observer);
33
+ }
29
34
  /** Startup failed after this call acquired a handle. No credentials/options are included here. */
30
35
  export class E2BDesktopStartupError extends Error {
31
36
  cleanup;
@@ -67,6 +72,7 @@ export function guardDesktopSandboxCreate(module) {
67
72
  : DESKTOP_CREATE_CLEANUP_TIMEOUT_MS;
68
73
  let cleanupOwned;
69
74
  let restoreKill;
75
+ const observeAllocation = createOptions === undefined ? undefined : allocationObservers.get(createOptions);
70
76
  // oxlint-disable-next-line typescript/no-this-alias -- the attempt subclasses whichever SDK class was called
71
77
  const CallingSandbox = this;
72
78
  class AttemptSandbox extends CallingSandbox {
@@ -91,6 +97,12 @@ export function guardDesktopSandboxCreate(module) {
91
97
  else
92
98
  Reflect.deleteProperty(this, "kill");
93
99
  };
100
+ try {
101
+ observeAllocation?.(this.sandboxId);
102
+ }
103
+ catch {
104
+ // An observer that throws must not fail the create; the resolved handle reports the id.
105
+ }
94
106
  }
95
107
  }
96
108
  try {
@@ -144,18 +156,6 @@ async function reclaimFailedDesktopCreate(kill, timeoutMs) {
144
156
  clearTimeout(timer);
145
157
  }
146
158
  }
147
- /**
148
- * Is humanish running from this project's node_modules, or from an npx cache?
149
- *
150
- * It decides which advice is true. `npx humanish@latest` resolves its optional peer relative to
151
- * itself, so "install @e2b/desktop in this project" is advice that cannot work there, and that
152
- * message cost two cold verification runs before the difference was noticed. A one-shot npx
153
- * invocation cannot do a live desktop run at all; humanish has to be installed alongside the peer.
154
- */
155
- function runningFromProject() {
156
- const here = fileURLToPath(import.meta.url);
157
- return here.startsWith(`${process.cwd()}${sep}node_modules${sep}`);
158
- }
159
159
  function isMissingE2BDesktopDependency(error) {
160
160
  const value = error;
161
161
  return value.code === "ERR_MODULE_NOT_FOUND" && value.message?.includes("@e2b/desktop") === true;
@@ -39,6 +39,8 @@ export declare function runOrThrow(shell: Shell, command: string, options?: Shel
39
39
  export declare function throwOnExit(result: ShellResult): ShellResult;
40
40
  /** Single-quote a value for a Shell command line. */
41
41
  export declare function shellQuote(value: string): string;
42
+ /** A command-line argument, single-quoted only when the shell would split or expand it. */
43
+ export declare function shellArg(value: string): string;
42
44
  /** Sanitized, whitespace-collapsed, length-capped tail of command output. */
43
45
  export declare function tailOf(value: string | undefined): string;
44
46
  export {};
@@ -16,6 +16,10 @@ export function throwOnExit(result) {
16
16
  export function shellQuote(value) {
17
17
  return `'${value.replace(/'/g, "'\\''")}'`;
18
18
  }
19
+ /** A command-line argument, single-quoted only when the shell would split or expand it. */
20
+ export function shellArg(value) {
21
+ return /^[\w@%+=:,./-]+$/.test(value) ? value : shellQuote(value);
22
+ }
19
23
  /** Sanitized, whitespace-collapsed, length-capped tail of command output. */
20
24
  export function tailOf(value) {
21
25
  return (value ?? "").trim().replace(/\s+/g, " ").slice(-240);