@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.
Files changed (44) hide show
  1. package/dist/{chunk-BHA2SU3H.js → chunk-RYDCVTMW.js} +1959 -208
  2. package/dist/chunk-RYDCVTMW.js.map +1 -0
  3. package/dist/{index-otZkoLYq.d.ts → index-DIiiwQY2.d.ts} +693 -55
  4. package/dist/index.d.ts +293 -121
  5. package/dist/index.js +1819 -786
  6. package/dist/index.js.map +1 -1
  7. package/dist/sandbox/index.d.ts +1 -1
  8. package/dist/sandbox/index.js +79 -3
  9. package/package.json +21 -20
  10. package/src/codex-tool-search.ts +185 -31
  11. package/src/context-compaction.ts +78 -243
  12. package/src/history-sanitizer.ts +172 -20
  13. package/src/index.ts +2573 -897
  14. package/src/metrics.ts +28 -2
  15. package/src/sandbox/capabilities.ts +12 -3
  16. package/src/sandbox/channel-a.ts +420 -72
  17. package/src/sandbox/display-stack.ts +18 -4
  18. package/src/sandbox/errors.ts +13 -0
  19. package/src/sandbox/index.ts +398 -112
  20. package/src/sandbox/op-correlation.ts +70 -0
  21. package/src/sandbox/providers/index.ts +9 -3
  22. package/src/sandbox/providers/modal.ts +99 -24
  23. package/src/sandbox/providers/selfhosted.ts +1 -4
  24. package/src/sandbox/recording.ts +146 -90
  25. package/src/sandbox/routing/backend-resolver.ts +144 -10
  26. package/src/sandbox/routing/routing-session.ts +129 -25
  27. package/src/sandbox/select.ts +60 -8
  28. package/src/sandbox/selfhosted/capabilities.ts +59 -25
  29. package/src/sandbox/selfhosted/control-rpc.ts +160 -28
  30. package/src/sandbox/selfhosted/fault-rendering.ts +226 -0
  31. package/src/sandbox/selfhosted/op-observer.ts +54 -0
  32. package/src/sandbox/selfhosted/op-stream.ts +801 -0
  33. package/src/sandbox/selfhosted/op-testing.ts +468 -0
  34. package/src/sandbox/selfhosted/op-transport.ts +151 -0
  35. package/src/sandbox/selfhosted/retry-policy.ts +177 -0
  36. package/src/sandbox/selfhosted/session.ts +498 -55
  37. package/src/sandbox/selfhosted/testing.ts +60 -19
  38. package/src/sandbox/stream-port.ts +17 -5
  39. package/src/sandbox/stream-token.ts +8 -2
  40. package/src/sandbox/terminal-server.ts +18 -4
  41. package/src/sandbox-computer.ts +332 -70
  42. package/src/usage-telemetry.ts +73 -0
  43. package/dist/chunk-BHA2SU3H.js.map +0 -1
  44. 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
+ };