@tangle-network/agent-provider-tangle 1.1.0 → 1.1.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.
|
@@ -71,6 +71,8 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
|
|
|
71
71
|
// Usage is measured per execution, so the log collects what runs through
|
|
72
72
|
// this handle and the observation reports the newest record it holds.
|
|
73
73
|
const usageLog = createExecutionUsageLog();
|
|
74
|
+
// The single destroy this handle performs, once it has been asked for.
|
|
75
|
+
let destruction;
|
|
74
76
|
const terminals = capabilities.interactiveTerminal?.attach === true
|
|
75
77
|
? createTangleTerminalRegistry(box)
|
|
76
78
|
: undefined;
|
|
@@ -310,7 +312,31 @@ export async function sandboxInstanceAsEnvironment(box, providerName, client, de
|
|
|
310
312
|
throw new Error("Tangle sandbox client cannot delete this environment");
|
|
311
313
|
assertOptionKeys(options, ["signal"], "Tangle destroy");
|
|
312
314
|
options?.signal?.throwIfAborted();
|
|
313
|
-
|
|
315
|
+
// One environment has more than one owner of its end. A runtime
|
|
316
|
+
// that destroys on settle AND tears the executor down calls this
|
|
317
|
+
// twice for the same box, and the platform refuses the second
|
|
318
|
+
// DELETE with "A sandbox lifecycle operation is already in
|
|
319
|
+
// progress" while the first one's cleanup still holds the
|
|
320
|
+
// sandbox's lifecycle lease. Measured 2026-09-01 on
|
|
321
|
+
// `sandbox-b525e04a551b`: of two concurrent deletes one answered
|
|
322
|
+
// 200 `destroyed` and the other 409, and a third delete 0.4 s
|
|
323
|
+
// after the 200 was still refused. That refusal cost two
|
|
324
|
+
// completed agent turns their result
|
|
325
|
+
// (tangle-network/agent-sdk#280), because the second destroy ran
|
|
326
|
+
// in a caller's `finally` and its throw replaced the run's own
|
|
327
|
+
// outcome.
|
|
328
|
+
//
|
|
329
|
+
// So destroy is answered once per environment and every later
|
|
330
|
+
// caller joins that answer. This is not a retry and hides no
|
|
331
|
+
// platform error: a delete that fails is not remembered, and the
|
|
332
|
+
// next caller issues a real one.
|
|
333
|
+
destruction ??= awaitWithSignal(box.delete(options), options?.signal)
|
|
334
|
+
.then(() => undefined)
|
|
335
|
+
.catch((error) => {
|
|
336
|
+
destruction = undefined;
|
|
337
|
+
throw error;
|
|
338
|
+
});
|
|
339
|
+
await awaitWithSignal(destruction, options?.signal);
|
|
314
340
|
options?.signal?.throwIfAborted();
|
|
315
341
|
},
|
|
316
342
|
}
|
|
@@ -11,6 +11,9 @@ export function sandboxInstanceAsExactProcessEnvironment(box, providerName) {
|
|
|
11
11
|
const process = box.process;
|
|
12
12
|
const fs = box.fs;
|
|
13
13
|
const destroy = box.delete.bind(box);
|
|
14
|
+
// The same single delete this environment performs, once it is asked for.
|
|
15
|
+
// See the destroy() below for why one is all the platform accepts.
|
|
16
|
+
let destruction;
|
|
14
17
|
return {
|
|
15
18
|
id: box.id,
|
|
16
19
|
provider: providerName,
|
|
@@ -177,7 +180,22 @@ export function sandboxInstanceAsExactProcessEnvironment(box, providerName) {
|
|
|
177
180
|
async destroy(options = {}) {
|
|
178
181
|
assertSignalOptions(options, "Tangle exact process destroy");
|
|
179
182
|
options.signal?.throwIfAborted();
|
|
180
|
-
|
|
183
|
+
// Deleting one sandbox twice is refused, not repeated: the platform
|
|
184
|
+
// holds a per-sandbox lifecycle lease past the first DELETE's own
|
|
185
|
+
// response while its deferred cleanup runs, so a second DELETE inside
|
|
186
|
+
// that window answers "A sandbox lifecycle operation is already in
|
|
187
|
+
// progress". Measured 2026-09-01 (issue #280) on the streaming
|
|
188
|
+
// environment, which has the same shape and the same platform route.
|
|
189
|
+
// This environment is destroyed once and later callers join that
|
|
190
|
+
// answer; a delete that fails is not remembered, so nothing hides a
|
|
191
|
+
// real error.
|
|
192
|
+
destruction ??= awaitWithSignal(destroy(), options.signal)
|
|
193
|
+
.then(() => undefined)
|
|
194
|
+
.catch((error) => {
|
|
195
|
+
destruction = undefined;
|
|
196
|
+
throw error;
|
|
197
|
+
});
|
|
198
|
+
await awaitWithSignal(destruction, options.signal);
|
|
181
199
|
options.signal?.throwIfAborted();
|
|
182
200
|
},
|
|
183
201
|
};
|
|
@@ -14,18 +14,18 @@ export declare const DEFAULT_TANGLE_READY_TIMEOUT_MS = 120000;
|
|
|
14
14
|
* A response that already reports `running` skips that wait, and `running`
|
|
15
15
|
* alone is not usable: the SDK's own `waitFor` treats the target as reached
|
|
16
16
|
* only when `filesystemIncarnationReadiness` is `ready`, because the box's
|
|
17
|
-
* filesystem is still being built until then.
|
|
18
|
-
* that
|
|
19
|
-
* on it, and the first turn lands on that lock.
|
|
17
|
+
* filesystem is still being built until then. An environment composed before
|
|
18
|
+
* that is an environment that cannot accept a turn.
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
20
|
+
* WHAT THIS WAIT IS NOT FOR. An earlier version of this comment claimed the
|
|
21
|
+
* wait also closed the platform's "A sandbox lifecycle operation is already in
|
|
22
|
+
* progress", and that claim is withdrawn: measured 2026-09-01 (issue #280),
|
|
23
|
+
* that refusal comes from `DELETE /v1/sandboxes/:id`, not from a turn. The
|
|
24
|
+
* platform guards exactly three routes with the per-sandbox lifecycle lease —
|
|
25
|
+
* resume, stop and delete — and the runtime path a prompt takes is not one of
|
|
26
|
+
* them, so no readiness wait here can prevent it and none is trying to. The
|
|
27
|
+
* collision is answered where it happens, in `destroy()`
|
|
28
|
+
* (`tangle-environment.ts`).
|
|
29
29
|
*
|
|
30
30
|
* Readiness also decides what the environment can claim. Composing an
|
|
31
31
|
* environment reads the sandbox's deployment capability document once, and a
|
package/dist/tangle-readiness.js
CHANGED
|
@@ -15,18 +15,18 @@ export const DEFAULT_TANGLE_READY_TIMEOUT_MS = 120_000;
|
|
|
15
15
|
* A response that already reports `running` skips that wait, and `running`
|
|
16
16
|
* alone is not usable: the SDK's own `waitFor` treats the target as reached
|
|
17
17
|
* only when `filesystemIncarnationReadiness` is `ready`, because the box's
|
|
18
|
-
* filesystem is still being built until then.
|
|
19
|
-
* that
|
|
20
|
-
* on it, and the first turn lands on that lock.
|
|
18
|
+
* filesystem is still being built until then. An environment composed before
|
|
19
|
+
* that is an environment that cannot accept a turn.
|
|
21
20
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
21
|
+
* WHAT THIS WAIT IS NOT FOR. An earlier version of this comment claimed the
|
|
22
|
+
* wait also closed the platform's "A sandbox lifecycle operation is already in
|
|
23
|
+
* progress", and that claim is withdrawn: measured 2026-09-01 (issue #280),
|
|
24
|
+
* that refusal comes from `DELETE /v1/sandboxes/:id`, not from a turn. The
|
|
25
|
+
* platform guards exactly three routes with the per-sandbox lifecycle lease —
|
|
26
|
+
* resume, stop and delete — and the runtime path a prompt takes is not one of
|
|
27
|
+
* them, so no readiness wait here can prevent it and none is trying to. The
|
|
28
|
+
* collision is answered where it happens, in `destroy()`
|
|
29
|
+
* (`tangle-environment.ts`).
|
|
30
30
|
*
|
|
31
31
|
* Readiness also decides what the environment can claim. Composing an
|
|
32
32
|
* environment reads the sandbox's deployment capability document once, and a
|