@runuai/host 0.9.146 → 0.9.148
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/lib/agent-cli.ts +17 -2
- package/lib/agents/attach-compatibility.ts +51 -0
- package/lib/agents/codex.ts +128 -12
- package/lib/agents/durable-proc.ts +2 -0
- package/lib/agents/machine-durable.ts +15 -0
- package/lib/agents/session-state.ts +107 -0
- package/lib/agents/transport.ts +116 -53
- package/lib/agents/types.ts +3 -3
- package/lib/orchestrator.ts +1294 -73
- package/lib/stopped-task-status-queue.ts +24 -6
- package/lib/ulid.ts +10 -1
- package/package.json +1 -1
- package/src/main.ts +18 -6
- package/src/protocol.ts +35 -3
package/lib/agents/transport.ts
CHANGED
|
@@ -33,9 +33,9 @@ import {
|
|
|
33
33
|
} from "node:fs";
|
|
34
34
|
import { basename, dirname, join } from "node:path";
|
|
35
35
|
|
|
36
|
-
import { and, eq } from "drizzle-orm";
|
|
36
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
37
37
|
|
|
38
|
-
import { getDb, schema } from "../db";
|
|
38
|
+
import { getDb, schema, type Db } from "../db";
|
|
39
39
|
import { taskWorkspaceDir } from "../env";
|
|
40
40
|
import { machineSessionAuthorityEnv } from "../runtime-authority";
|
|
41
41
|
import { requireContainerRuntimeOperational } from "../runtime-guard";
|
|
@@ -56,9 +56,12 @@ import {
|
|
|
56
56
|
type LineHandler,
|
|
57
57
|
} from "./proc";
|
|
58
58
|
import { isEnvironmentTransportProvider } from "../task-environment/types";
|
|
59
|
+
import { attachCompatibilityKeysMatch } from "./attach-compatibility";
|
|
59
60
|
|
|
60
61
|
/** The shared surface adapters program against (LineProcess's shape). */
|
|
61
62
|
export interface LineTransport {
|
|
63
|
+
/** Exact durable row generation owned by this transport, when durable. */
|
|
64
|
+
readonly durableSessionDir?: string;
|
|
62
65
|
onLine(handler: LineHandler): void;
|
|
63
66
|
onExit(handler: ExitHandler): void;
|
|
64
67
|
writeLine(value: unknown): void;
|
|
@@ -84,7 +87,7 @@ export interface AgentTransportOptions {
|
|
|
84
87
|
* protocol state can't outlive the host process (codex handshake).
|
|
85
88
|
*/
|
|
86
89
|
allowAttach: boolean;
|
|
87
|
-
/** Host-owned launch-
|
|
90
|
+
/** Host-owned launch-settings generation. Both absent remains compatible. */
|
|
88
91
|
attachCompatibilityKey?: string;
|
|
89
92
|
kind: string;
|
|
90
93
|
debugLabel?: string;
|
|
@@ -120,6 +123,73 @@ function durableEnabled(): boolean {
|
|
|
120
123
|
return process.env.UAI_DURABLE_SESSIONS !== "0";
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
/** Durable rows are keyed by task + agent for lookup, but callbacks belong to
|
|
127
|
+
* one exact generation. A delayed offset/exit from the predecessor must never
|
|
128
|
+
* mutate the replacement row after its upsert. */
|
|
129
|
+
function durableRowCallbacks(
|
|
130
|
+
db: Db,
|
|
131
|
+
taskId: string,
|
|
132
|
+
agentId: string,
|
|
133
|
+
sessionDir: string,
|
|
134
|
+
): {
|
|
135
|
+
persistOffset(offset: number): void;
|
|
136
|
+
markClosed(): void;
|
|
137
|
+
} {
|
|
138
|
+
const ownedRow = () =>
|
|
139
|
+
and(
|
|
140
|
+
eq(schema.hostAgentSessions.taskId, taskId),
|
|
141
|
+
eq(schema.hostAgentSessions.agentId, agentId),
|
|
142
|
+
eq(schema.hostAgentSessions.sessionDir, sessionDir),
|
|
143
|
+
);
|
|
144
|
+
return {
|
|
145
|
+
persistOffset(offset: number): void {
|
|
146
|
+
db.update(schema.hostAgentSessions)
|
|
147
|
+
.set({ outboxOffset: offset, updatedAt: Date.now() })
|
|
148
|
+
.where(ownedRow())
|
|
149
|
+
.run();
|
|
150
|
+
},
|
|
151
|
+
markClosed(): void {
|
|
152
|
+
db.update(schema.hostAgentSessions)
|
|
153
|
+
.set({ status: "closed", updatedAt: Date.now() })
|
|
154
|
+
.where(ownedRow())
|
|
155
|
+
.run();
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Claim a compatible durable row and promote a one-release legacy key to the
|
|
161
|
+
* rich launch fingerprint. The exact generation/key fence prevents a stale
|
|
162
|
+
* host process from rewriting a replacement row after reading its snapshot. */
|
|
163
|
+
function claimCompatibleAttachRow(
|
|
164
|
+
db: Db,
|
|
165
|
+
taskId: string,
|
|
166
|
+
agentId: string,
|
|
167
|
+
sessionDir: string,
|
|
168
|
+
stored: string | null | undefined,
|
|
169
|
+
desired: string | null | undefined,
|
|
170
|
+
): boolean {
|
|
171
|
+
if (!attachCompatibilityKeysMatch(stored, desired)) return false;
|
|
172
|
+
const storedKey = stored ?? null;
|
|
173
|
+
const desiredKey = desired ?? null;
|
|
174
|
+
if (storedKey === desiredKey) return true;
|
|
175
|
+
if (desiredKey === null) return false;
|
|
176
|
+
const result = db
|
|
177
|
+
.update(schema.hostAgentSessions)
|
|
178
|
+
.set({ attachCompatibilityKey: desiredKey, updatedAt: Date.now() })
|
|
179
|
+
.where(
|
|
180
|
+
and(
|
|
181
|
+
eq(schema.hostAgentSessions.taskId, taskId),
|
|
182
|
+
eq(schema.hostAgentSessions.agentId, agentId),
|
|
183
|
+
eq(schema.hostAgentSessions.sessionDir, sessionDir),
|
|
184
|
+
storedKey === null
|
|
185
|
+
? isNull(schema.hostAgentSessions.attachCompatibilityKey)
|
|
186
|
+
: eq(schema.hostAgentSessions.attachCompatibilityKey, storedKey),
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
.run();
|
|
190
|
+
return result.changes === 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
123
193
|
export function createAgentTransport(opts: AgentTransportOptions): LineTransport {
|
|
124
194
|
// ADR-121: machine-backed sessions ride the environment's own transport —
|
|
125
195
|
// the container-runtime preflight is a docker/apple concern a machine task
|
|
@@ -161,29 +231,6 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
161
231
|
)
|
|
162
232
|
.get();
|
|
163
233
|
|
|
164
|
-
const persistOffset = (offset: number): void => {
|
|
165
|
-
db.update(schema.hostAgentSessions)
|
|
166
|
-
.set({ outboxOffset: offset, updatedAt: Date.now() })
|
|
167
|
-
.where(
|
|
168
|
-
and(
|
|
169
|
-
eq(schema.hostAgentSessions.taskId, opts.taskId),
|
|
170
|
-
eq(schema.hostAgentSessions.agentId, opts.agentId),
|
|
171
|
-
),
|
|
172
|
-
)
|
|
173
|
-
.run();
|
|
174
|
-
};
|
|
175
|
-
const markClosed = (): void => {
|
|
176
|
-
db.update(schema.hostAgentSessions)
|
|
177
|
-
.set({ status: "closed", updatedAt: Date.now() })
|
|
178
|
-
.where(
|
|
179
|
-
and(
|
|
180
|
-
eq(schema.hostAgentSessions.taskId, opts.taskId),
|
|
181
|
-
eq(schema.hostAgentSessions.agentId, opts.agentId),
|
|
182
|
-
),
|
|
183
|
-
)
|
|
184
|
-
.run();
|
|
185
|
-
};
|
|
186
|
-
|
|
187
234
|
const tailKey = `${opts.taskId}:${opts.agentId}`;
|
|
188
235
|
|
|
189
236
|
// ---- Attach: a previous host process left this agent's runner alive. ----
|
|
@@ -192,10 +239,23 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
192
239
|
row &&
|
|
193
240
|
row.status === "running" &&
|
|
194
241
|
row.containerName === opts.environment.durableIdentity &&
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
242
|
+
row.kind === opts.kind &&
|
|
243
|
+
heartbeatFresh(row.sessionDir) &&
|
|
244
|
+
claimCompatibleAttachRow(
|
|
245
|
+
db,
|
|
246
|
+
opts.taskId,
|
|
247
|
+
opts.agentId,
|
|
248
|
+
row.sessionDir,
|
|
249
|
+
row.attachCompatibilityKey,
|
|
250
|
+
opts.attachCompatibilityKey,
|
|
251
|
+
)
|
|
198
252
|
) {
|
|
253
|
+
const { persistOffset, markClosed } = durableRowCallbacks(
|
|
254
|
+
db,
|
|
255
|
+
opts.taskId,
|
|
256
|
+
opts.agentId,
|
|
257
|
+
row.sessionDir,
|
|
258
|
+
);
|
|
199
259
|
const proc = new DurableProcess({
|
|
200
260
|
hostSessionDir: row.sessionDir,
|
|
201
261
|
initialOutboxOffset: row.outboxOffset,
|
|
@@ -240,6 +300,12 @@ export function createAgentTransport(opts: AgentTransportOptions): LineTransport
|
|
|
240
300
|
opts.agentId,
|
|
241
301
|
basename(sessionDir),
|
|
242
302
|
);
|
|
303
|
+
const { persistOffset, markClosed } = durableRowCallbacks(
|
|
304
|
+
db,
|
|
305
|
+
opts.taskId,
|
|
306
|
+
opts.agentId,
|
|
307
|
+
sessionDir,
|
|
308
|
+
);
|
|
243
309
|
const runnerInEnvironment = join(environmentSessionDir, "runner.mjs");
|
|
244
310
|
try {
|
|
245
311
|
copyFileSync(runnerScriptPath(), runnerInSession);
|
|
@@ -350,28 +416,6 @@ function machineDurableTransport(opts: AgentTransportOptions): LineTransport {
|
|
|
350
416
|
),
|
|
351
417
|
)
|
|
352
418
|
.get();
|
|
353
|
-
const persistOffset = (offset: number): void => {
|
|
354
|
-
db.update(schema.hostAgentSessions)
|
|
355
|
-
.set({ outboxOffset: offset, updatedAt: Date.now() })
|
|
356
|
-
.where(
|
|
357
|
-
and(
|
|
358
|
-
eq(schema.hostAgentSessions.taskId, opts.taskId),
|
|
359
|
-
eq(schema.hostAgentSessions.agentId, opts.agentId),
|
|
360
|
-
),
|
|
361
|
-
)
|
|
362
|
-
.run();
|
|
363
|
-
};
|
|
364
|
-
const markClosed = (): void => {
|
|
365
|
-
db.update(schema.hostAgentSessions)
|
|
366
|
-
.set({ status: "closed", updatedAt: Date.now() })
|
|
367
|
-
.where(
|
|
368
|
-
and(
|
|
369
|
-
eq(schema.hostAgentSessions.taskId, opts.taskId),
|
|
370
|
-
eq(schema.hostAgentSessions.agentId, opts.agentId),
|
|
371
|
-
),
|
|
372
|
-
)
|
|
373
|
-
.run();
|
|
374
|
-
};
|
|
375
419
|
const tailKey = `${opts.taskId}:${opts.agentId}`;
|
|
376
420
|
|
|
377
421
|
// ---- Attach: a previous host process left this machine's runner alive. --
|
|
@@ -382,9 +426,22 @@ function machineDurableTransport(opts: AgentTransportOptions): LineTransport {
|
|
|
382
426
|
row &&
|
|
383
427
|
row.status === "running" &&
|
|
384
428
|
row.containerName === environment.durableIdentity &&
|
|
385
|
-
|
|
386
|
-
|
|
429
|
+
row.kind === opts.kind &&
|
|
430
|
+
claimCompatibleAttachRow(
|
|
431
|
+
db,
|
|
432
|
+
opts.taskId,
|
|
433
|
+
opts.agentId,
|
|
434
|
+
row.sessionDir,
|
|
435
|
+
row.attachCompatibilityKey,
|
|
436
|
+
opts.attachCompatibilityKey,
|
|
437
|
+
)
|
|
387
438
|
) {
|
|
439
|
+
const { persistOffset, markClosed } = durableRowCallbacks(
|
|
440
|
+
db,
|
|
441
|
+
opts.taskId,
|
|
442
|
+
opts.agentId,
|
|
443
|
+
row.sessionDir,
|
|
444
|
+
);
|
|
388
445
|
const proc = new MachineDurableProcess({
|
|
389
446
|
environment,
|
|
390
447
|
sessionDir: row.sessionDir,
|
|
@@ -406,6 +463,12 @@ function machineDurableTransport(opts: AgentTransportOptions): LineTransport {
|
|
|
406
463
|
environment.descriptor.workspacePath,
|
|
407
464
|
opts.agentId,
|
|
408
465
|
);
|
|
466
|
+
const { persistOffset, markClosed } = durableRowCallbacks(
|
|
467
|
+
db,
|
|
468
|
+
opts.taskId,
|
|
469
|
+
opts.agentId,
|
|
470
|
+
sessionDir,
|
|
471
|
+
);
|
|
409
472
|
const proc = new MachineDurableProcess({
|
|
410
473
|
environment,
|
|
411
474
|
sessionDir,
|
package/lib/agents/types.ts
CHANGED
|
@@ -180,9 +180,9 @@ export interface AgentSessionFactory {
|
|
|
180
180
|
/** Host-derived restricted execution policy (ADR-083). Never supplied by
|
|
181
181
|
* the browser or stored on the roster entry. */
|
|
182
182
|
executionProfile?: "communicator";
|
|
183
|
-
/** Durable runners are keyed by task+agent.
|
|
184
|
-
*
|
|
185
|
-
* a process launched with incompatible
|
|
183
|
+
/** Durable runners are keyed by task+agent. Host-owned launch settings
|
|
184
|
+
* supply a compatibility key so restart recovery replaces, rather than
|
|
185
|
+
* attaches, a process launched with incompatible model/effort/policy. */
|
|
186
186
|
attachCompatibilityKey?: string;
|
|
187
187
|
/** ADR-048: extra per-agent process environment (e.g. this agent's own
|
|
188
188
|
* UAI_TASK_TOKEN so its `uai` CLI carries only its own permissions). */
|