@opengeni/runtime 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-BHA2SU3H.js → chunk-RYDCVTMW.js} +1959 -208
- package/dist/chunk-RYDCVTMW.js.map +1 -0
- package/dist/{index-otZkoLYq.d.ts → index-DIiiwQY2.d.ts} +693 -55
- package/dist/index.d.ts +293 -121
- package/dist/index.js +1819 -786
- package/dist/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +1 -1
- package/dist/sandbox/index.js +79 -3
- package/package.json +21 -20
- package/src/codex-tool-search.ts +185 -31
- package/src/context-compaction.ts +78 -243
- package/src/history-sanitizer.ts +172 -20
- package/src/index.ts +2573 -897
- package/src/metrics.ts +28 -2
- package/src/sandbox/capabilities.ts +12 -3
- package/src/sandbox/channel-a.ts +420 -72
- package/src/sandbox/display-stack.ts +18 -4
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +398 -112
- package/src/sandbox/op-correlation.ts +70 -0
- package/src/sandbox/providers/index.ts +9 -3
- package/src/sandbox/providers/modal.ts +99 -24
- package/src/sandbox/providers/selfhosted.ts +1 -4
- package/src/sandbox/recording.ts +146 -90
- package/src/sandbox/routing/backend-resolver.ts +144 -10
- package/src/sandbox/routing/routing-session.ts +129 -25
- package/src/sandbox/select.ts +60 -8
- package/src/sandbox/selfhosted/capabilities.ts +59 -25
- package/src/sandbox/selfhosted/control-rpc.ts +160 -28
- package/src/sandbox/selfhosted/fault-rendering.ts +226 -0
- package/src/sandbox/selfhosted/op-observer.ts +54 -0
- package/src/sandbox/selfhosted/op-stream.ts +801 -0
- package/src/sandbox/selfhosted/op-testing.ts +468 -0
- package/src/sandbox/selfhosted/op-transport.ts +151 -0
- package/src/sandbox/selfhosted/retry-policy.ts +177 -0
- package/src/sandbox/selfhosted/session.ts +498 -55
- package/src/sandbox/selfhosted/testing.ts +60 -19
- package/src/sandbox/stream-port.ts +17 -5
- package/src/sandbox/stream-token.ts +8 -2
- package/src/sandbox/terminal-server.ts +18 -4
- package/src/sandbox-computer.ts +332 -70
- package/src/usage-telemetry.ts +73 -0
- package/dist/chunk-BHA2SU3H.js.map +0 -1
- package/src/image-history.ts +0 -149
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// The selfhosted control-op RETRY POLICY — a small, PURE decision function the
|
|
2
|
+
// `SelfhostedSession.call()` loop consults after each control-plane reply.
|
|
3
|
+
//
|
|
4
|
+
// Before this, a typed AgentError surfaced straight to the model as a failed tool
|
|
5
|
+
// call: a `DRAINING` backpressure rejection (the machine was momentarily at its
|
|
6
|
+
// concurrent-work capacity) and a transient `TIMEOUT` / `agent_reconnecting` blip
|
|
7
|
+
// both looked like hard failures even though the op never ran (DRAINING) or was a
|
|
8
|
+
// pure read that is safe to re-issue (a read that timed out). This policy retries
|
|
9
|
+
// exactly those two, with a hard safety line around anything that MUTATES state.
|
|
10
|
+
//
|
|
11
|
+
// It is intentionally a pure function (op kind + error + prior-retry counts +
|
|
12
|
+
// injected jitter → a decision) so the whole retry matrix is unit-testable with no
|
|
13
|
+
// clock, no transport, and no session. The sleep + jitter side effects live behind
|
|
14
|
+
// the injected `SelfhostedRetryClock` so tests are deterministic.
|
|
15
|
+
|
|
16
|
+
import type { SelfhostedControlError } from "./control-rpc";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The read-only, idempotent-safe control ops that MAY be retried after a TIMEOUT
|
|
20
|
+
* / `agent_reconnecting` blip.
|
|
21
|
+
*
|
|
22
|
+
* A timed-out request is AT-LEAST-ONCE on the wire: the agent may have RECEIVED
|
|
23
|
+
* and fully EXECUTED the op before its reply was lost, so re-issuing it is only
|
|
24
|
+
* safe when a second execution is observably identical to one — i.e. a pure read.
|
|
25
|
+
* Every MUTATING op (`exec`, `git`, `fsWrite`/`fsMove`/`fsMkdir`/`fsRemove`, and
|
|
26
|
+
* the pty/desktop input ops) is DELIBERATELY EXCLUDED: re-running a mutation that
|
|
27
|
+
* already ran on the machine is a real correctness hazard (a duplicated write, a
|
|
28
|
+
* second `git push`, a re-applied command). Only DRAINING (a pre-admission
|
|
29
|
+
* rejection — see `decideSelfhostedRetry`) is safe to retry for those.
|
|
30
|
+
*/
|
|
31
|
+
export const SELFHOSTED_IDEMPOTENT_READONLY_OPS: ReadonlySet<string> = new Set([
|
|
32
|
+
"ping",
|
|
33
|
+
"metrics",
|
|
34
|
+
"fsStat",
|
|
35
|
+
"fsList",
|
|
36
|
+
"fsRead",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* DRAINING is a pre-admission backpressure rejection (the op never started), so it
|
|
41
|
+
* is safe to retry for ANY op kind. The budget is split by op class:
|
|
42
|
+
*
|
|
43
|
+
* - NON-EXEC control ops (fs/git/desktop/pty) keep a SHORT budget — 3 retries,
|
|
44
|
+
* worst-case backoff 500+1000+2000 = 3.5s (≤5s). Their start latency matters
|
|
45
|
+
* (fs/git ops gate the model's next step), so a saturated machine should surface
|
|
46
|
+
* quickly rather than stall.
|
|
47
|
+
* - EXEC gets a LONG, patient budget — 10 retries, worst-case ≈55.5s (<60s). exec
|
|
48
|
+
* start is not latency-critical, and the agent-side admission pool is (until a
|
|
49
|
+
* later agent release) a FLAT 8 permits with no per-class split: a burst of slow
|
|
50
|
+
* execs can hold every permit and blanket-DRAIN all fs/git ops. A patient exec
|
|
51
|
+
* retry converts those DRAINING storms into mild queueing instead of a wall of
|
|
52
|
+
* failed tool calls, and pairs with the deliberately modest exec deadline (which,
|
|
53
|
+
* for the same 8-permit reason, defaults to 2min, not 5).
|
|
54
|
+
*/
|
|
55
|
+
export const SELFHOSTED_DRAINING_MAX_RETRIES = 3;
|
|
56
|
+
export const SELFHOSTED_EXEC_DRAINING_MAX_RETRIES = 10;
|
|
57
|
+
/** A read-only op that timed out is retried at most once (see the at-least-once
|
|
58
|
+
* note above — even a read gets a single re-issue, not an unbounded loop). */
|
|
59
|
+
export const SELFHOSTED_TIMEOUT_MAX_RETRIES = 1;
|
|
60
|
+
/** A fault the transport synthesized PRE-SEND (no connection / no responder) — the
|
|
61
|
+
* op provably never reached the machine, so it is safe to re-issue for ANY op kind.
|
|
62
|
+
* Short bounded budget (≈3.5s summed): enough to ride out a NATS reconnect blip
|
|
63
|
+
* quietly, not so long it stalls the model on a machine that is genuinely down. */
|
|
64
|
+
export const SELFHOSTED_NEVER_SENT_MAX_RETRIES = 3;
|
|
65
|
+
|
|
66
|
+
/** Full-jitter backoff base (ms). Delay N is sampled in `[0, base * factor^N)`. */
|
|
67
|
+
export const SELFHOSTED_RETRY_BACKOFF_BASE_MS = 500;
|
|
68
|
+
export const SELFHOSTED_RETRY_BACKOFF_FACTOR = 2;
|
|
69
|
+
/** Per-delay ceiling: 500 → 1s → 2s → 4s → 8s, then flat at 8s. Bounds the summed
|
|
70
|
+
* backoff — non-exec (3 retries) ≈3.5s ≤5s; exec (10 retries) ≈55.5s <60s. */
|
|
71
|
+
export const SELFHOSTED_RETRY_BACKOFF_CAP_MS = 8_000;
|
|
72
|
+
|
|
73
|
+
export type SelfhostedRetryDecision =
|
|
74
|
+
| { readonly action: "fail" }
|
|
75
|
+
| { readonly action: "retry"; readonly delayMs: number };
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Full-jitter exponential backoff. Returns a delay in
|
|
79
|
+
* `[0, min(cap, base * factor^attempt))`. `jitter` is an injected sample in
|
|
80
|
+
* `[0, 1)` so the delay is fully deterministic under test.
|
|
81
|
+
*/
|
|
82
|
+
export function selfhostedRetryBackoffMs(attempt: number, jitter: number): number {
|
|
83
|
+
const uncapped = SELFHOSTED_RETRY_BACKOFF_BASE_MS * SELFHOSTED_RETRY_BACKOFF_FACTOR ** attempt;
|
|
84
|
+
const ceiling = Math.min(SELFHOSTED_RETRY_BACKOFF_CAP_MS, uncapped);
|
|
85
|
+
const clampedJitter = Math.min(0.999_999, Math.max(0, jitter));
|
|
86
|
+
return Math.floor(clampedJitter * ceiling);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* PURE retry policy for one selfhosted control-op reply.
|
|
91
|
+
*
|
|
92
|
+
* - `error.draining` (DRAINING — pre-admission host-work backpressure; the op was
|
|
93
|
+
* rejected at the pool gate and NEVER started) → retry for ANY op kind, since
|
|
94
|
+
* nothing executed. exec gets the long `SELFHOSTED_EXEC_DRAINING_MAX_RETRIES`
|
|
95
|
+
* budget (≈60s of patient queueing under the flat 8-permit pool); every other op
|
|
96
|
+
* keeps the short `SELFHOSTED_DRAINING_MAX_RETRIES` budget (≤5s).
|
|
97
|
+
* - `error.neverSent` (a pre-send AGENT_OFFLINE synthesis — no connection / no
|
|
98
|
+
* responder; the op provably never reached the machine) → retry for ANY op kind
|
|
99
|
+
* up to `SELFHOSTED_NEVER_SENT_MAX_RETRIES`. At-least-once does NOT apply (nothing
|
|
100
|
+
* executed), so even a mutation is safe to re-issue; this heals a NATS reconnect
|
|
101
|
+
* blip quietly. A truly-offline machine surfaces honestly after the short budget.
|
|
102
|
+
* - `error.reason === "agent_reconnecting"` (a TIMEOUT / transient blip AFTER send)
|
|
103
|
+
* → retry at most `SELFHOSTED_TIMEOUT_MAX_RETRIES`, and ONLY for a read-only
|
|
104
|
+
* idempotent-safe op (`SELFHOSTED_IDEMPOTENT_READONLY_OPS`). A timed-out
|
|
105
|
+
* MUTATION is never retried here — it may already have run (at-least-once).
|
|
106
|
+
* - Everything else → no retry at this layer:
|
|
107
|
+
* · FENCED is retried by the routing proxy against a RE-RESOLVED backend
|
|
108
|
+
* (retrying here under the same stale epoch would just re-fence);
|
|
109
|
+
* · AGENT_OFFLINE / OS / PROTOCOL / PAYLOAD_TOO_LARGE / CONSENT_REQUIRED are
|
|
110
|
+
* not transient and must surface to the caller.
|
|
111
|
+
*
|
|
112
|
+
* The draining and timeout budgets are SEPARATE counters so a read that first hit
|
|
113
|
+
* backpressure still keeps its single timeout re-issue (and vice versa); both are
|
|
114
|
+
* bounded, so the total number of attempts — and the summed backoff — is bounded.
|
|
115
|
+
*/
|
|
116
|
+
export function decideSelfhostedRetry(input: {
|
|
117
|
+
opKind: string;
|
|
118
|
+
error: SelfhostedControlError;
|
|
119
|
+
drainingRetries: number;
|
|
120
|
+
timeoutRetries: number;
|
|
121
|
+
/** Prior never-sent retries. Optional (default 0) — only the pre-send AGENT_OFFLINE
|
|
122
|
+
* branch reads it, so callers that cannot hit that class may omit it. */
|
|
123
|
+
neverSentRetries?: number;
|
|
124
|
+
jitter: number;
|
|
125
|
+
}): SelfhostedRetryDecision {
|
|
126
|
+
const { opKind, error, drainingRetries, timeoutRetries, neverSentRetries = 0, jitter } = input;
|
|
127
|
+
|
|
128
|
+
if (error.neverSent) {
|
|
129
|
+
// Provably not executed (the request never reached the machine) → safe to
|
|
130
|
+
// re-issue for ANY op kind. Bounded so a genuinely-offline machine surfaces
|
|
131
|
+
// honestly rather than hanging the turn.
|
|
132
|
+
if (neverSentRetries >= SELFHOSTED_NEVER_SENT_MAX_RETRIES) {
|
|
133
|
+
return { action: "fail" };
|
|
134
|
+
}
|
|
135
|
+
return { action: "retry", delayMs: selfhostedRetryBackoffMs(neverSentRetries, jitter) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (error.draining) {
|
|
139
|
+
// exec is patient (the flat 8-permit pool needs longer than 5s to free a slot);
|
|
140
|
+
// every other op keeps the short budget so fs/git latency stays snappy.
|
|
141
|
+
const maxDrainingRetries =
|
|
142
|
+
opKind === "exec" ? SELFHOSTED_EXEC_DRAINING_MAX_RETRIES : SELFHOSTED_DRAINING_MAX_RETRIES;
|
|
143
|
+
if (drainingRetries >= maxDrainingRetries) {
|
|
144
|
+
return { action: "fail" };
|
|
145
|
+
}
|
|
146
|
+
return { action: "retry", delayMs: selfhostedRetryBackoffMs(drainingRetries, jitter) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (error.reason === "agent_reconnecting") {
|
|
150
|
+
if (!SELFHOSTED_IDEMPOTENT_READONLY_OPS.has(opKind)) {
|
|
151
|
+
// A timed-out mutation may already have executed on the machine — never
|
|
152
|
+
// re-issue it (at-least-once). Surface the reconnecting error instead.
|
|
153
|
+
return { action: "fail" };
|
|
154
|
+
}
|
|
155
|
+
if (timeoutRetries >= SELFHOSTED_TIMEOUT_MAX_RETRIES) {
|
|
156
|
+
return { action: "fail" };
|
|
157
|
+
}
|
|
158
|
+
return { action: "retry", delayMs: selfhostedRetryBackoffMs(timeoutRetries, jitter) };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { action: "fail" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The injected clock the retry loop drives — real in production, a deterministic
|
|
166
|
+
* fake in tests. `jitter()` returns a full-jitter sample in `[0, 1)`.
|
|
167
|
+
*/
|
|
168
|
+
export interface SelfhostedRetryClock {
|
|
169
|
+
sleep(ms: number): Promise<void>;
|
|
170
|
+
jitter(): number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The production clock: a real timer + `Math.random()` jitter. */
|
|
174
|
+
export const defaultSelfhostedRetryClock: SelfhostedRetryClock = {
|
|
175
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
176
|
+
jitter: () => Math.random(),
|
|
177
|
+
};
|