@prohost/cli 0.16.3 → 0.16.4
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 +10 -0
- package/README.md +4 -1
- package/dist/host/api.d.ts +7 -1
- package/dist/host/api.js +15 -1
- package/dist/host/scheduler.d.ts +5 -1
- package/dist/host/scheduler.js +7 -1
- package/dist/host/service.d.ts +5 -0
- package/dist/host/service.js +38 -3
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.16.4
|
|
10
|
+
|
|
11
|
+
- **`409 conversation_busy` on claim** (ProhostAI serializes a conversation's turns): the run
|
|
12
|
+
stays queued instead of being skipped. Its slot goes to other work, and it is claimed again
|
|
13
|
+
when ProhostAI resends its frame (the earlier turn ended) or after a backoff of 30 s doubling to
|
|
14
|
+
at most 2 min — never in a tight loop. While it waits it does not hold its conversation's place
|
|
15
|
+
in the queue, so the earlier turn it waits for starts even if its frame came second; a resend
|
|
16
|
+
that arrives while its claim is still in flight is kept. Any other `409` is still another
|
|
17
|
+
computer's run. Works with servers that never send it.
|
|
18
|
+
|
|
9
19
|
## 0.16.3
|
|
10
20
|
|
|
11
21
|
- **A dedicated service records its terminal switch in its home's code-host settings** as it
|
package/README.md
CHANGED
|
@@ -97,7 +97,10 @@ program (no terminal) relies on `--expect-account`.
|
|
|
97
97
|
2. The run waits its turn (below). When a slot is free and its AI login has
|
|
98
98
|
room, the computer **claims** it (`POST /v1/computers/{id}/runs/{run_id}/claim`).
|
|
99
99
|
Exactly one computer ever gets a claim; a `409` means another computer took
|
|
100
|
-
it, and it is skipped.
|
|
100
|
+
it, and it is skipped. A `409 conversation_busy` means an earlier turn of the
|
|
101
|
+
same employee's conversation is still open: the run stays queued (its slot
|
|
102
|
+
goes to other work) and is claimed when ProhostAI sends its frame again, or
|
|
103
|
+
after a backoff of 30 s doubling to at most 2 min.
|
|
101
104
|
3. The claim returns the run, a short-lived **run token** and the employee's
|
|
102
105
|
**skills**. The skills are written into the employee's workspace —
|
|
103
106
|
`.claude/skills/<key>/…` for Claude Code; `.codex/skills/<key>/…` plus a
|
package/dist/host/api.d.ts
CHANGED
|
@@ -61,6 +61,13 @@ export type ClaimResult = {
|
|
|
61
61
|
| {
|
|
62
62
|
kind: 'elsewhere';
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* `409 conversation_busy`: an earlier turn of the same employee's conversation is still open.
|
|
66
|
+
* The run stays queued here; the server sends its frame again when that turn ends.
|
|
67
|
+
*/
|
|
68
|
+
| {
|
|
69
|
+
kind: 'busy';
|
|
70
|
+
}
|
|
64
71
|
/** Gone server-side (404 / 410): nothing to run. */
|
|
65
72
|
| {
|
|
66
73
|
kind: 'gone';
|
|
@@ -78,5 +85,4 @@ export type ClaimResult = {
|
|
|
78
85
|
status: number;
|
|
79
86
|
error?: string;
|
|
80
87
|
};
|
|
81
|
-
/** Claim a run for this computer. Exactly one computer ever gets a 200. */
|
|
82
88
|
export declare function claimRun(options: ComputerApiOptions, computerId: string, runId: string): Promise<ClaimResult>;
|
package/dist/host/api.js
CHANGED
|
@@ -94,6 +94,20 @@ function parseSkills(value) {
|
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
96
|
/** Claim a run for this computer. Exactly one computer ever gets a 200. */
|
|
97
|
+
/** A refusal's code: `{detail: {error}}`, `{detail: "<code>"}` or `{error}`, matched exactly by the caller. */
|
|
98
|
+
function errorCode(body) {
|
|
99
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
100
|
+
return undefined;
|
|
101
|
+
const top = body;
|
|
102
|
+
const detail = top.detail;
|
|
103
|
+
if (typeof detail === 'string')
|
|
104
|
+
return detail;
|
|
105
|
+
if (detail && typeof detail === 'object' && !Array.isArray(detail)) {
|
|
106
|
+
const code = detail.error;
|
|
107
|
+
return typeof code === 'string' ? code : undefined;
|
|
108
|
+
}
|
|
109
|
+
return typeof top.error === 'string' ? top.error : undefined;
|
|
110
|
+
}
|
|
97
111
|
export async function claimRun(options, computerId, runId) {
|
|
98
112
|
const response = await callJson(options, claimPath(computerId, runId), {});
|
|
99
113
|
if (response.status === 200 || response.status === 201) {
|
|
@@ -112,7 +126,7 @@ export async function claimRun(options, computerId, runId) {
|
|
|
112
126
|
};
|
|
113
127
|
}
|
|
114
128
|
if (response.status === 409)
|
|
115
|
-
return { kind: 'elsewhere' };
|
|
129
|
+
return errorCode(response.body) === 'conversation_busy' ? { kind: 'busy' } : { kind: 'elsewhere' };
|
|
116
130
|
if (response.status === 404 || response.status === 410)
|
|
117
131
|
return { kind: 'gone', status: response.status };
|
|
118
132
|
if (response.status === 0 || response.status === 429 || response.status >= 500) {
|
package/dist/host/scheduler.d.ts
CHANGED
|
@@ -67,8 +67,12 @@ export declare class HostScheduler<T extends QueuedRun = QueuedRun> {
|
|
|
67
67
|
* run keeps its place and does not block the others.
|
|
68
68
|
*
|
|
69
69
|
* Taking it marks it running; call {@link finish} when it is done.
|
|
70
|
+
*
|
|
71
|
+
* A `parked` run (one the server said must wait for another turn of its conversation) neither
|
|
72
|
+
* starts nor holds its lane: the next run of the lane — possibly the very turn it waits for —
|
|
73
|
+
* is the lane's candidate instead. Runs of one lane still never run side by side.
|
|
70
74
|
*/
|
|
71
|
-
take(config: SchedulerConfig, eligible?: (run: T) => boolean): T | undefined;
|
|
75
|
+
take(config: SchedulerConfig, eligible?: (run: T) => boolean, parked?: (run: T) => boolean): T | undefined;
|
|
72
76
|
/** A started run is done. */
|
|
73
77
|
finish(runId: string): void;
|
|
74
78
|
/** A taken run could not start after all: back in the queue, in its old place. */
|
package/dist/host/scheduler.js
CHANGED
|
@@ -66,14 +66,20 @@ export class HostScheduler {
|
|
|
66
66
|
* run keeps its place and does not block the others.
|
|
67
67
|
*
|
|
68
68
|
* Taking it marks it running; call {@link finish} when it is done.
|
|
69
|
+
*
|
|
70
|
+
* A `parked` run (one the server said must wait for another turn of its conversation) neither
|
|
71
|
+
* starts nor holds its lane: the next run of the lane — possibly the very turn it waits for —
|
|
72
|
+
* is the lane's candidate instead. Runs of one lane still never run side by side.
|
|
69
73
|
*/
|
|
70
|
-
take(config, eligible = () => true) {
|
|
74
|
+
take(config, eligible = () => true, parked = () => false) {
|
|
71
75
|
if (config.held)
|
|
72
76
|
return undefined;
|
|
73
77
|
const busyLanes = new Set([...this.running.values()].map((r) => r.lane));
|
|
74
78
|
const seenLanes = new Set();
|
|
75
79
|
const candidates = [];
|
|
76
80
|
for (const run of this.queue) {
|
|
81
|
+
if (parked(run))
|
|
82
|
+
continue;
|
|
77
83
|
// Only the oldest run of each lane is a candidate: later ones wait their turn.
|
|
78
84
|
if (seenLanes.has(run.lane))
|
|
79
85
|
continue;
|
package/dist/host/service.d.ts
CHANGED
|
@@ -112,6 +112,11 @@ export interface HostServiceResult {
|
|
|
112
112
|
runsStarted: number;
|
|
113
113
|
runsSkipped: number;
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* How long a run whose conversation is busy waits before it is claimed again, unless the server
|
|
117
|
+
* sends its frame first (it does when the earlier turn ends): 30 s, doubling, at most 2 min.
|
|
118
|
+
*/
|
|
119
|
+
export declare function busyBackoffMs(waits: number): number;
|
|
115
120
|
/** The CLI's own entry file: when it changes on disk, an update was installed. */
|
|
116
121
|
export declare function entrypointStamp(entry?: string): () => boolean;
|
|
117
122
|
export declare function runHostService(options: HostServiceOptions): Promise<HostServiceResult>;
|
package/dist/host/service.js
CHANGED
|
@@ -82,6 +82,13 @@ const ACK_RETRY = 503;
|
|
|
82
82
|
const RECONNECT_MAX_DELAY_MS = 30_000;
|
|
83
83
|
/** An employee id names a folder: nothing else gets in. */
|
|
84
84
|
const SAFE_ID = /^[A-Za-z0-9-]{1,64}$/;
|
|
85
|
+
/**
|
|
86
|
+
* How long a run whose conversation is busy waits before it is claimed again, unless the server
|
|
87
|
+
* sends its frame first (it does when the earlier turn ends): 30 s, doubling, at most 2 min.
|
|
88
|
+
*/
|
|
89
|
+
export function busyBackoffMs(waits) {
|
|
90
|
+
return Math.min(30_000 * 2 ** waits, 120_000);
|
|
91
|
+
}
|
|
85
92
|
function timestamp() {
|
|
86
93
|
return new Date().toISOString().slice(11, 19);
|
|
87
94
|
}
|
|
@@ -260,6 +267,8 @@ async function serveHost(options) {
|
|
|
260
267
|
}
|
|
261
268
|
/** Runs being claimed right now, and the login each will run on. */
|
|
262
269
|
const claiming = new Map();
|
|
270
|
+
/** Runs the server resent while their claim was in flight: a `conversation_busy` answer to that claim is already stale. */
|
|
271
|
+
const resentWhileClaiming = new Set();
|
|
263
272
|
function runsOnLogin(accountKey) {
|
|
264
273
|
let n = 0;
|
|
265
274
|
for (const run of active.values())
|
|
@@ -489,7 +498,9 @@ async function serveHost(options) {
|
|
|
489
498
|
return false;
|
|
490
499
|
const pick = pool.pick(runtimeOf(run.agentId, run.data), conversationOf(run.lane), config.share_logins[runtimeOf(run.agentId, run.data)], runsOnLogin);
|
|
491
500
|
return pick.kind === 'use' || pick.kind === 'move';
|
|
492
|
-
}
|
|
501
|
+
},
|
|
502
|
+
// Waiting on an earlier turn of its conversation: that turn may be queued behind it.
|
|
503
|
+
(run) => Boolean(run.busyWaits) && run.notBefore > now());
|
|
493
504
|
if (!next)
|
|
494
505
|
break;
|
|
495
506
|
background.push(startRun(next));
|
|
@@ -516,6 +527,19 @@ async function serveHost(options) {
|
|
|
516
527
|
const claim = await claimRun(computerApi, computer.computer_id, queued.runId).finally(() => {
|
|
517
528
|
claiming.delete(queued.runId);
|
|
518
529
|
});
|
|
530
|
+
const resent = resentWhileClaiming.delete(queued.runId);
|
|
531
|
+
if (claim.kind === 'busy' && !draining) {
|
|
532
|
+
// Serialized server-side: an earlier turn of this conversation is still open. Not dropped,
|
|
533
|
+
// not failed: queued again (its slot is free for other work) until the server resends its
|
|
534
|
+
// frame, or its backoff ends. A resend that came while this claim was in flight already
|
|
535
|
+
// said the earlier turn ended: claimed again at once.
|
|
536
|
+
const waits = queued.busyWaits ?? 0;
|
|
537
|
+
scheduler.requeue({ ...queued, busyWaits: waits + 1, notBefore: resent ? 0 : now() + busyBackoffMs(waits) });
|
|
538
|
+
if (waits === 0)
|
|
539
|
+
log(`${timestamp()} ◦ run ${queued.runId.slice(0, 12)} waits — an earlier turn of its conversation is still open`);
|
|
540
|
+
pump();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
519
543
|
if (claim.kind !== 'claimed') {
|
|
520
544
|
if (claim.kind === 'retry' && queued.claimAttempts + 1 < MAX_CLAIM_ATTEMPTS && !draining) {
|
|
521
545
|
scheduler.requeue({ ...queued, claimAttempts: queued.claimAttempts + 1, notBefore: now() + 2_000 * 2 ** queued.claimAttempts });
|
|
@@ -525,7 +549,7 @@ async function serveHost(options) {
|
|
|
525
549
|
result.runsSkipped += 1;
|
|
526
550
|
const why = claim.kind === 'elsewhere'
|
|
527
551
|
? 'another computer has it'
|
|
528
|
-
: claim.kind === 'gone'
|
|
552
|
+
: claim.kind === 'gone' || claim.kind === 'busy'
|
|
529
553
|
? 'it is no longer waiting'
|
|
530
554
|
: `the claim was refused (${claim.status}${'error' in claim && claim.error ? `: ${claim.error}` : ''})`;
|
|
531
555
|
log(`${timestamp()} ◦ run ${queued.runId.slice(0, 12)} skipped — ${why}`);
|
|
@@ -1016,8 +1040,19 @@ async function serveHost(options) {
|
|
|
1016
1040
|
return [ACK_IGNORED];
|
|
1017
1041
|
if (typeof data.computer_id === 'string' && data.computer_id !== computer.computer_id)
|
|
1018
1042
|
return [ACK_IGNORED];
|
|
1019
|
-
if (scheduler.has(runId) || active.has(runId) || ledger.get(runId))
|
|
1043
|
+
if (scheduler.has(runId) || active.has(runId) || ledger.get(runId)) {
|
|
1044
|
+
// The server's resend of a run waiting on its busy conversation: the earlier turn ended, so
|
|
1045
|
+
// it is claimed now rather than at the end of its backoff.
|
|
1046
|
+
const waiting = scheduler.queued().find((run) => run.runId === runId);
|
|
1047
|
+
if (waiting?.busyWaits && waiting.notBefore > now()) {
|
|
1048
|
+
waiting.notBefore = 0;
|
|
1049
|
+
pump();
|
|
1050
|
+
}
|
|
1051
|
+
else if (claiming.has(runId)) {
|
|
1052
|
+
resentWhileClaiming.add(runId);
|
|
1053
|
+
}
|
|
1020
1054
|
return [ACK_ACCEPTED];
|
|
1055
|
+
}
|
|
1021
1056
|
if (draining)
|
|
1022
1057
|
return [ACK_RETRY, 'draining'];
|
|
1023
1058
|
if (!config.ai_employees_enabled)
|
package/dist/version.d.ts
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export declare const CLI_VERSION = "0.16.
|
|
10
|
+
export declare const CLI_VERSION = "0.16.4";
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
|
-
export declare const USER_AGENT = "prohost-cli/0.16.
|
|
12
|
+
export declare const USER_AGENT = "prohost-cli/0.16.4";
|
package/dist/version.js
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export const CLI_VERSION = '0.16.
|
|
10
|
+
export const CLI_VERSION = '0.16.4';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|