@nylorun/runtime 0.46.0-beta → 0.46.2-beta
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/CHANGELOG.md +20 -0
- package/dist/harness/clock-jump.d.ts +18 -0
- package/dist/harness/clock-jump.js +15 -0
- package/dist/harness/main.js +3 -1
- package/dist/harness/pod.d.ts +5 -2
- package/dist/harness/pod.js +9 -8
- package/dist/harness/service.d.ts +1 -0
- package/dist/harness/service.js +1 -0
- package/dist/harness/ws-client.d.ts +15 -2
- package/dist/harness/ws-client.js +55 -21
- package/dist/management-openapi.json +1 -1
- package/dist/openapi.json +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.46.2-beta
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4b91451: Pods on Agent Substrate (preview):
|
|
8
|
+
|
|
9
|
+
- **A crashed actor recovers.** A wake that finds the actor crashed, or that crashes it, reverts the actor to its last snapshot once and resumes it, as Substrate requires. Before, the Agent stayed failed until it was reset.
|
|
10
|
+
- **A wake during an idle suspend is no longer lost.** The resume waits for the suspend to finish instead of dropping the wake.
|
|
11
|
+
- **One resume call per wake.** It is no longer cancelled after 2 minutes, which also cancelled the restore in progress. It is tried again only while Substrate is busy, for at most 5 minutes (the Runtime's own ready deadline). It stops as soon as a later request replaces it, and holds no lock during the call.
|
|
12
|
+
- **Each resume is logged** as `substrate resumed` with its time in `ms`.
|
|
13
|
+
- **Removed:**
|
|
14
|
+
- `RES_OPTIONS`, which is no longer set in the actor's environment. The edge's `/etc/hosts` pin already spares the engine any DNS lookup, and the Agent's own resolver is its own.
|
|
15
|
+
- The shutdown-time sweep, since the Runtime never sends a shutdown time.
|
|
16
|
+
|
|
17
|
+
## 0.46.1-beta
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- 3a9933c: A pod's engine rejoins at once after an Agent Substrate restore. When the process resumes from a memory snapshot, the engine notices the jump in the wall clock (a 100 ms timer finding more than 1.5 s gone), drops the connection attempt the freeze caught in flight, its host-token renew or join included, and dials again without a backoff pause. Before, it waited for TCP to resend the packet lost with the old network: 1 s, or 3 s on a second loss, which the Phase 1 exit test measured in 4 of 10 wakes. Only pod engines watch for this (`harness_clock_jumped` in their log); an open connection is never dropped by it.
|
|
22
|
+
|
|
3
23
|
## 0.46.0-beta
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notices that the process was frozen and resumed, as an Agent Substrate actor is by a memory
|
|
3
|
+
* snapshot's restore: a timer that should fire every `intervalMs` finds the wall clock moved on
|
|
4
|
+
* by more than `jumpMs` beyond that. A pod's engine uses it to drop a connection attempt the
|
|
5
|
+
* freeze caught in flight (its packets went with the old network, so it would wait for TCP to
|
|
6
|
+
* send them again, 1 s and then 3 s; P1.8's third run) and to dial again at once.
|
|
7
|
+
*
|
|
8
|
+
* An event loop held up as long (a long GC pause) reads as a jump too; the caller only redials,
|
|
9
|
+
* so that costs one connection attempt, never a working connection.
|
|
10
|
+
*/
|
|
11
|
+
export interface ClockJumpWatch {
|
|
12
|
+
stop(): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function watchClockJumps(onJump: (jumpedMs: number) => void, options?: {
|
|
15
|
+
intervalMs?: number;
|
|
16
|
+
jumpMs?: number;
|
|
17
|
+
now?: () => number;
|
|
18
|
+
}): ClockJumpWatch;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function watchClockJumps(onJump, options = {}) {
|
|
2
|
+
const intervalMs = options.intervalMs ?? 100;
|
|
3
|
+
const jumpMs = options.jumpMs ?? 1_500;
|
|
4
|
+
const now = options.now ?? Date.now;
|
|
5
|
+
let last = now();
|
|
6
|
+
const timer = setInterval(() => {
|
|
7
|
+
const at = now();
|
|
8
|
+
const late = at - last - intervalMs;
|
|
9
|
+
last = at;
|
|
10
|
+
if (late > jumpMs)
|
|
11
|
+
onJump(late);
|
|
12
|
+
}, intervalMs);
|
|
13
|
+
timer.unref();
|
|
14
|
+
return { stop: () => clearInterval(timer) };
|
|
15
|
+
}
|
package/dist/harness/main.js
CHANGED
|
@@ -70,13 +70,15 @@ export async function runHarness(stack, baseline) {
|
|
|
70
70
|
if (!pod.afterSeed)
|
|
71
71
|
await prepare();
|
|
72
72
|
}
|
|
73
|
-
const token = pod ? () => pod.token() : config.token;
|
|
73
|
+
const token = pod ? (signal) => pod.token(signal) : config.token;
|
|
74
74
|
if (token === undefined)
|
|
75
75
|
throw new Error("--service harness needs NYLORUN_HARNESS_TOKEN");
|
|
76
76
|
const runTokens = harnessRunTokens();
|
|
77
77
|
const service = startHarnessService({
|
|
78
78
|
url: config.url,
|
|
79
79
|
token,
|
|
80
|
+
// A pod's engine may be frozen and resumed (an Agent Substrate restore): redial at once.
|
|
81
|
+
...(pod ? { redialOnClockJump: true } : {}),
|
|
80
82
|
...(pod
|
|
81
83
|
? { sandboxBackends: [localBackend({ env: baseline, proxyEnv: () => pod.proxyEnv(), harnessRoot: config.root })] }
|
|
82
84
|
: {}),
|
package/dist/harness/pod.d.ts
CHANGED
|
@@ -23,8 +23,11 @@ export interface DiskSetupReport {
|
|
|
23
23
|
readonly output?: string;
|
|
24
24
|
}
|
|
25
25
|
export interface PodHost {
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* A current host token, for the next connection. Throws when neither renewal nor join works,
|
|
28
|
+
* or when `signal` drops the attempt.
|
|
29
|
+
*/
|
|
30
|
+
token(signal?: AbortSignal): Promise<string>;
|
|
28
31
|
/** `HTTPS_PROXY` and friends with the current egress token; none before the first join. */
|
|
29
32
|
proxyEnv(): Record<string, string>;
|
|
30
33
|
/** The host epoch of the tokens held. */
|
package/dist/harness/pod.js
CHANGED
|
@@ -38,7 +38,8 @@ export function podHost(config, logger, options = {}) {
|
|
|
38
38
|
let afterSeed = false;
|
|
39
39
|
/** The CA in the bundle on disk, once one is written. */
|
|
40
40
|
let bundled;
|
|
41
|
-
const post = async (path, init) => {
|
|
41
|
+
const post = async (path, init, signal) => {
|
|
42
|
+
const timeout = AbortSignal.timeout(hostTuning().harness.podJoinTimeoutMs);
|
|
42
43
|
const response = await call(`${config.httpUrl}/nylorun/harness/v1/host/${path}`, {
|
|
43
44
|
method: "POST",
|
|
44
45
|
headers: {
|
|
@@ -48,7 +49,7 @@ export function podHost(config, logger, options = {}) {
|
|
|
48
49
|
...(path === "join" && imageEngine ? { [IMAGE_ENGINE_HEADER]: RUNTIME_VERSION } : {}),
|
|
49
50
|
},
|
|
50
51
|
...(init.body === undefined ? {} : { body: JSON.stringify(init.body) }),
|
|
51
|
-
signal: AbortSignal.timeout
|
|
52
|
+
signal: signal ? AbortSignal.any([timeout, signal]) : timeout,
|
|
52
53
|
});
|
|
53
54
|
const text = await response.text();
|
|
54
55
|
if (response.status === 401 || response.status === 400)
|
|
@@ -97,7 +98,7 @@ export function podHost(config, logger, options = {}) {
|
|
|
97
98
|
return id;
|
|
98
99
|
}
|
|
99
100
|
};
|
|
100
|
-
const join = async () => {
|
|
101
|
+
const join = async (signal) => {
|
|
101
102
|
const joinToken = (await readFile(config.joinFile, "utf8")).trim();
|
|
102
103
|
const volume = await volumeId();
|
|
103
104
|
held = await post("join", {
|
|
@@ -107,7 +108,7 @@ export function podHost(config, logger, options = {}) {
|
|
|
107
108
|
joinToken,
|
|
108
109
|
...(volume === undefined ? {} : { volumeId: volume }),
|
|
109
110
|
},
|
|
110
|
-
});
|
|
111
|
+
}, signal);
|
|
111
112
|
logger.info("sandbox_host_joined", { sandboxId: config.sandboxId, epoch: held.epoch });
|
|
112
113
|
return held;
|
|
113
114
|
};
|
|
@@ -122,11 +123,11 @@ export function podHost(config, logger, options = {}) {
|
|
|
122
123
|
signal: AbortSignal.timeout(hostTuning().harness.podRequestTimeoutMs),
|
|
123
124
|
});
|
|
124
125
|
};
|
|
125
|
-
const renew = async () => {
|
|
126
|
+
const renew = async (signal) => {
|
|
126
127
|
if (!held)
|
|
127
128
|
return undefined;
|
|
128
129
|
try {
|
|
129
|
-
held = await post("renew", { bearer: held.hostToken });
|
|
130
|
+
held = await post("renew", { bearer: held.hostToken }, signal);
|
|
130
131
|
return held;
|
|
131
132
|
}
|
|
132
133
|
catch (error) {
|
|
@@ -143,8 +144,8 @@ export function podHost(config, logger, options = {}) {
|
|
|
143
144
|
}, options.renewMs ?? hostTuning().harness.podRenewMs);
|
|
144
145
|
timer.unref();
|
|
145
146
|
return {
|
|
146
|
-
async token() {
|
|
147
|
-
const current = (await renew()) ?? (await join());
|
|
147
|
+
async token(signal) {
|
|
148
|
+
const current = (await renew(signal)) ?? (await join(signal));
|
|
148
149
|
return current.hostToken;
|
|
149
150
|
},
|
|
150
151
|
proxyEnv() {
|
|
@@ -7,6 +7,7 @@ import { type EngineTuning } from "../tuning.js";
|
|
|
7
7
|
export interface HarnessServiceOptions {
|
|
8
8
|
readonly url: string;
|
|
9
9
|
readonly token: HarnessClientOptions["token"];
|
|
10
|
+
readonly redialOnClockJump?: boolean;
|
|
10
11
|
/** Where workspaces (and their records) live. */
|
|
11
12
|
readonly paths: {
|
|
12
13
|
readonly sandboxes: string;
|
package/dist/harness/service.js
CHANGED
|
@@ -5,18 +5,24 @@
|
|
|
5
5
|
* core's `workspace.*` requests (`serve`). When the connection is lost its runs stop (core gives
|
|
6
6
|
* their segments to the next advance, which resumes them by replay) and the client connects
|
|
7
7
|
* again after a jittered backoff (`harness.reconnect` in the Host tuning, 0.5 to 10 s).
|
|
8
|
+
*
|
|
9
|
+
* A pod's engine (`redialOnClockJump`) also watches for a freeze and resume (an Agent Substrate
|
|
10
|
+
* restore): it drops the connection attempt in flight, token request included, and dials again
|
|
11
|
+
* at once, instead of waiting for TCP to resend what the freeze lost.
|
|
8
12
|
*/
|
|
9
13
|
import { type HarnessChannel, type RequestHandler } from "@nylorun/core/harness-api";
|
|
10
14
|
import { type HarnessOptions } from "@nylorun/harness/api";
|
|
11
15
|
import { type WsPortOptions } from "./ws-port.js";
|
|
16
|
+
import { watchClockJumps } from "./clock-jump.js";
|
|
12
17
|
export interface HarnessClientOptions extends Omit<HarnessOptions, "channel" | "logger"> {
|
|
13
18
|
/** The Harness API, e.g. `ws://runtime:4200/nylorun/harness/v1` (`NYLORUN_HARNESS_URL`). */
|
|
14
19
|
readonly url: string;
|
|
15
20
|
/**
|
|
16
21
|
* `NYLORUN_HARNESS_TOKEN`, or, for a pod sandbox's engine (F7.2), a function that answers
|
|
17
|
-
* a current host token before each connection (it renews, or joins again).
|
|
22
|
+
* a current host token before each connection (it renews, or joins again). The signal aborts
|
|
23
|
+
* it when the attempt is dropped.
|
|
18
24
|
*/
|
|
19
|
-
readonly token: string | (() => Promise<string>);
|
|
25
|
+
readonly token: string | ((signal?: AbortSignal) => Promise<string>);
|
|
20
26
|
readonly logger: {
|
|
21
27
|
info(message: string, fields?: Record<string, unknown>): void;
|
|
22
28
|
warn(message: string, fields?: Record<string, unknown>): void;
|
|
@@ -29,6 +35,13 @@ export interface HarnessClientOptions extends Omit<HarnessOptions, "channel" | "
|
|
|
29
35
|
readonly maxMs: number;
|
|
30
36
|
};
|
|
31
37
|
readonly liveness?: Omit<WsPortOptions, "ping">;
|
|
38
|
+
/**
|
|
39
|
+
* Drop an attempt in flight and dial again at once when the process was frozen and resumed
|
|
40
|
+
* (`clock-jump.ts`): a pod's engine, which Agent Substrate suspends to a memory snapshot.
|
|
41
|
+
*/
|
|
42
|
+
readonly redialOnClockJump?: boolean;
|
|
43
|
+
/** The clock and timings `redialOnClockJump` watches with (tests set them). */
|
|
44
|
+
readonly clockJumps?: Parameters<typeof watchClockJumps>[1];
|
|
32
45
|
}
|
|
33
46
|
export interface HarnessClient {
|
|
34
47
|
/** Resolves once the first connection's `hello` is answered. */
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* core's `workspace.*` requests (`serve`). When the connection is lost its runs stop (core gives
|
|
6
6
|
* their segments to the next advance, which resumes them by replay) and the client connects
|
|
7
7
|
* again after a jittered backoff (`harness.reconnect` in the Host tuning, 0.5 to 10 s).
|
|
8
|
+
*
|
|
9
|
+
* A pod's engine (`redialOnClockJump`) also watches for a freeze and resume (an Agent Substrate
|
|
10
|
+
* restore): it drops the connection attempt in flight, token request included, and dials again
|
|
11
|
+
* at once, instead of waiting for TCP to resend what the freeze lost.
|
|
8
12
|
*/
|
|
9
13
|
import { createChannel } from "@nylorun/core/harness-api";
|
|
10
14
|
import { createHarness } from "@nylorun/harness/api";
|
|
@@ -12,6 +16,7 @@ import { WebSocket } from "ws";
|
|
|
12
16
|
import { HARNESS_API_HEADER, HARNESS_API_TRANSPORT, HARNESS_MAX_PAYLOAD } from "@nylorun/core/harness-api";
|
|
13
17
|
import { wsPort } from "./ws-port.js";
|
|
14
18
|
import { hostTuning } from "../tuning.js";
|
|
19
|
+
import { watchClockJumps } from "./clock-jump.js";
|
|
15
20
|
export function connectHarness(options) {
|
|
16
21
|
const { logger } = options;
|
|
17
22
|
const tuning = hostTuning().harness;
|
|
@@ -20,30 +25,49 @@ export function connectHarness(options) {
|
|
|
20
25
|
let connected = false;
|
|
21
26
|
let current;
|
|
22
27
|
let wake;
|
|
28
|
+
/** The connection attempt in flight (token, then handshake), until it opens or fails. */
|
|
29
|
+
let attempting;
|
|
30
|
+
let attempt = 0;
|
|
23
31
|
let markReady;
|
|
24
32
|
const ready = new Promise((resolve) => (markReady = resolve));
|
|
25
33
|
const open = async () => {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
socket.once("unexpected-response", (_request, response) => {
|
|
38
|
-
socket.removeAllListeners("error");
|
|
39
|
-
socket.on("error", () => undefined);
|
|
40
|
-
reject(new Error(`The Harness API refused the connection: ${response.statusCode ?? 0}`));
|
|
41
|
-
response.resume();
|
|
42
|
-
socket.terminate();
|
|
43
|
-
});
|
|
44
|
-
socket.once("error", reject);
|
|
45
|
-
});
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
attempting = controller;
|
|
36
|
+
try {
|
|
37
|
+
const token = typeof options.token === "string" ? options.token : await options.token(controller.signal);
|
|
38
|
+
controller.signal.throwIfAborted();
|
|
39
|
+
return await dial(token, controller.signal);
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
if (attempting === controller)
|
|
43
|
+
attempting = undefined;
|
|
44
|
+
}
|
|
46
45
|
};
|
|
46
|
+
const dial = (token, signal) => new Promise((resolve, reject) => {
|
|
47
|
+
const socket = new WebSocket(options.url, {
|
|
48
|
+
headers: { authorization: `Bearer ${token}`, [HARNESS_API_HEADER]: HARNESS_API_TRANSPORT },
|
|
49
|
+
maxPayload: HARNESS_MAX_PAYLOAD,
|
|
50
|
+
handshakeTimeout: tuning.handshakeTimeoutMs,
|
|
51
|
+
});
|
|
52
|
+
socket.once("open", () => {
|
|
53
|
+
socket.removeAllListeners("error");
|
|
54
|
+
resolve(socket);
|
|
55
|
+
});
|
|
56
|
+
socket.once("unexpected-response", (_request, response) => {
|
|
57
|
+
socket.removeAllListeners("error");
|
|
58
|
+
socket.on("error", () => undefined);
|
|
59
|
+
reject(new Error(`The Harness API refused the connection: ${response.statusCode ?? 0}`));
|
|
60
|
+
response.resume();
|
|
61
|
+
socket.terminate();
|
|
62
|
+
});
|
|
63
|
+
socket.once("error", reject);
|
|
64
|
+
signal.addEventListener("abort", () => {
|
|
65
|
+
socket.removeAllListeners("error");
|
|
66
|
+
socket.on("error", () => undefined);
|
|
67
|
+
socket.terminate();
|
|
68
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("connection attempt dropped"));
|
|
69
|
+
}, { once: true });
|
|
70
|
+
});
|
|
47
71
|
const pause = (ms) => new Promise((resolve) => {
|
|
48
72
|
const timer = setTimeout(done, ms);
|
|
49
73
|
timer.unref();
|
|
@@ -81,8 +105,16 @@ export function connectHarness(options) {
|
|
|
81
105
|
current = undefined;
|
|
82
106
|
}
|
|
83
107
|
};
|
|
108
|
+
const watch = options.redialOnClockJump
|
|
109
|
+
? watchClockJumps((jumpedMs) => {
|
|
110
|
+
const inFlight = attempting !== undefined;
|
|
111
|
+
logger.info("harness_clock_jumped", { jumpedMs, redial: inFlight || wake !== undefined });
|
|
112
|
+
attempt = 0;
|
|
113
|
+
attempting?.abort(new Error(`the process resumed after ${jumpedMs} ms`));
|
|
114
|
+
wake?.();
|
|
115
|
+
}, options.clockJumps)
|
|
116
|
+
: undefined;
|
|
84
117
|
const loop = (async () => {
|
|
85
|
-
let attempt = 0;
|
|
86
118
|
while (!stopped) {
|
|
87
119
|
const started = Date.now();
|
|
88
120
|
try {
|
|
@@ -113,6 +145,8 @@ export function connectHarness(options) {
|
|
|
113
145
|
channel: () => current?.channel,
|
|
114
146
|
async stop(waitMs = tuning.stopMs) {
|
|
115
147
|
stopped = true;
|
|
148
|
+
watch?.stop();
|
|
149
|
+
attempting?.abort(new Error("harness stopped"));
|
|
116
150
|
wake?.();
|
|
117
151
|
const live = current;
|
|
118
152
|
if (live) {
|
package/dist/openapi.json
CHANGED
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const RUNTIME_VERSION = "0.46.
|
|
1
|
+
export declare const RUNTIME_VERSION = "0.46.2-beta";
|
package/dist/version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Kept in step with package.json by runtime/server/scripts/check-package.mjs, which runs in npm run check.
|
|
2
2
|
// release:prepare also rewrites this when it bumps @nylorun/runtime.
|
|
3
|
-
export const RUNTIME_VERSION = "0.46.
|
|
3
|
+
export const RUNTIME_VERSION = "0.46.2-beta";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nylorun/runtime",
|
|
3
|
-
"version": "0.46.
|
|
3
|
+
"version": "0.46.2-beta",
|
|
4
4
|
"description": "Nylorun Runtime: the agent execution Host on Postgres, Restate and S2, and model providers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"db:studio": "drizzle-kit studio"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@ag-ui/core": "~1.0.
|
|
62
|
+
"@ag-ui/core": "~1.0.4",
|
|
63
63
|
"@asteasolutions/zod-to-openapi": "^9.1.0",
|
|
64
64
|
"@earendil-works/pi-ai": "1.3.0",
|
|
65
65
|
"@hono/node-server": "^2.1.5",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@a2a-js/sdk": "~1.3.0",
|
|
88
|
-
"@ag-ui/client": "1.0.
|
|
88
|
+
"@ag-ui/client": "1.0.4",
|
|
89
89
|
"@scalar/openapi-parser": "^0.29.13",
|
|
90
90
|
"@types/node": "^26.6.5",
|
|
91
91
|
"@types/pg": "^8.23.1",
|