@agentshouse/kit 0.1.0-alpha.11 → 0.1.0-alpha.12
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/README.md +36 -5
- package/bin/connect-linux.sh +18 -2
- package/bin/kit.js +20 -2
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/binding.js +41 -0
- package/src/house.js +116 -50
- package/src/kit-work-stream.js +1 -0
- package/src/login.js +2 -1
- package/src/worker.js +202 -95
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ On Linux `amd64` or `arm64` with a working Docker Engine, run the published,
|
|
|
10
10
|
versioned bootstrap once:
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
13
|
+
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.12/connect-linux.sh)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The release asset pins one immutable multi-architecture image digest. It creates
|
|
@@ -20,8 +20,29 @@ and starts the container with Docker's `unless-stopped` lifecycle. Pass
|
|
|
20
20
|
`--workspace /absolute/path` for one other workspace root or `--manual` for
|
|
21
21
|
the existing URL and one-time-code login route. `--house <origin>` selects
|
|
22
22
|
another House and is retained in Kit home. A repeated invocation reuses the
|
|
23
|
-
same container, credential and Environment
|
|
24
|
-
|
|
23
|
+
same container, credential and Environment.
|
|
24
|
+
|
|
25
|
+
Running a newer release's bootstrap the same way is the only update. When the
|
|
26
|
+
installed container runs another image, the bootstrap pulls this release's
|
|
27
|
+
image and asks the running resident Kit to drain with `kit resident drain`. The
|
|
28
|
+
resident stops acquiring Runs, resumptions, setup and agent CLI work, while the
|
|
29
|
+
Sessions it supervises keep their renewal, owner stops, answers and reports.
|
|
30
|
+
While any Agent Session is still running or unreported, the bootstrap names it
|
|
31
|
+
and leaves the container as it is; rerun it once they end. Once nothing remains,
|
|
32
|
+
the resident stops renewing, releases its exact lease and stays quiescent, and
|
|
33
|
+
the bootstrap replaces the container with one on this release's image over the
|
|
34
|
+
same Kit home and workspace. The credential, Environment, Kit-home state and
|
|
35
|
+
everything under the workspace are kept; anything installed elsewhere in the old
|
|
36
|
+
container is not. A stopped container is replaced without a drain.
|
|
37
|
+
|
|
38
|
+
If House refuses the installed Kit with `kit_upgrade_required`, the resident
|
|
39
|
+
calls House no more, neither to release nor to report. It keeps supervising
|
|
40
|
+
every running Session locally and never stops one to make progress. A Session
|
|
41
|
+
that ends keeps its exact binding, stop observation and outcome in Kit home,
|
|
42
|
+
and the drain answers ready once no supervised process runs. The updated Kit
|
|
43
|
+
waits for the old lease to expire, acquires the next one and reports each held
|
|
44
|
+
Session before it acquires any work, restoring no process and inventing no
|
|
45
|
+
outcome.
|
|
25
46
|
|
|
26
47
|
The versioned `bootstrap.json` is the machine-readable command source for this
|
|
27
48
|
release. It is exported from the npm package as `@agentshouse/kit/bootstrap`
|
|
@@ -981,7 +1002,12 @@ content, no prompt, and no resolved secret, and Session authority is always
|
|
|
981
1002
|
reissued by House rather than recovered from disk. It is evidence for
|
|
982
1003
|
reconciliation, never permission to resume or recreate work, and nothing keeps
|
|
983
1004
|
it once House has answered for the Run: a settled terminal state, a confirmed
|
|
984
|
-
owner stop, and a retained Provider continuation each drop it.
|
|
1005
|
+
owner stop, and a retained Provider continuation each drop it. When the
|
|
1006
|
+
supervised process ends, the binding first records what the Kit observed: the
|
|
1007
|
+
sealed final text, whether the turn completed, the capacity or provider refusal,
|
|
1008
|
+
the execution failure and whether a Provider continuation is retained. The Kit
|
|
1009
|
+
then reports that stop, and a restarted Kit reports the same recorded stop
|
|
1010
|
+
instead of treating the session as lost.
|
|
985
1011
|
|
|
986
1012
|
A restarted `kit worker run` reconciles the records it kept before it acquires
|
|
987
1013
|
anything new. It reports every retained settlement note first, because those
|
|
@@ -1141,7 +1167,11 @@ cancels its request there, and the lease stays held when the command ends. The
|
|
|
1141
1167
|
resident serves one such command at a time and refuses another with
|
|
1142
1168
|
`environment_control_lease_active`, so two commands still never work at once.
|
|
1143
1169
|
A resident whose lease House refuses answers the command with that refusal; the
|
|
1144
|
-
command never acquires a lease of its own while a resident serves it.
|
|
1170
|
+
command never acquires a lease of its own while a resident serves it. While an
|
|
1171
|
+
update drains, `state/update-drain` refuses every such command, `kit login` and
|
|
1172
|
+
`kit worker run` with `kit_update_draining` before it enrolls, acquires or
|
|
1173
|
+
delegates anything, and the drain request ends the delegation of a command
|
|
1174
|
+
already served; the bootstrap removes the file when it replaces the container.
|
|
1145
1175
|
|
|
1146
1176
|
Interrupting the command cancels the running connector without advancing the
|
|
1147
1177
|
checkpoint.
|
|
@@ -1468,6 +1498,7 @@ $HOUSE_KIT_HOME/
|
|
|
1468
1498
|
state/attachments/<upload identity>.json
|
|
1469
1499
|
state/conversation-originals/<attachment identity>.json
|
|
1470
1500
|
state/sessions/<tmux session>.json
|
|
1501
|
+
state/update-drain
|
|
1471
1502
|
state/continuations/<run identity>.json
|
|
1472
1503
|
state/<instance>/credentials/google.json
|
|
1473
1504
|
state/<instance>/checkpoints/<service>.json
|
package/bin/connect-linux.sh
CHANGED
|
@@ -37,6 +37,10 @@ login_kit() {
|
|
|
37
37
|
done
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
start_resident() {
|
|
41
|
+
docker run -d --name "$NAME" --restart unless-stopped --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --label "agentshouse.house=$HOUSE" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 "$IMAGE" resident --house "$HOUSE" >/dev/null
|
|
42
|
+
}
|
|
43
|
+
|
|
40
44
|
if [[ "${1:-}" == kit ]]; then
|
|
41
45
|
FORWARDING=1
|
|
42
46
|
FORWARD=("${@:2}")
|
|
@@ -114,7 +118,8 @@ fi
|
|
|
114
118
|
DOCKER_RUN=(docker run --rm --network host --user "$(id -u):$(id -g)" --mount "$KIT_HOME_MOUNT" --mount "$WORKSPACE_MOUNT" --env HOME=/kit-home --env HOUSE_KIT_HOME=/kit-home --env HOUSE_KIT_CONTAINER=1 --env "HOUSE_KIT_HOSTNAME=$(hostname)" "$IMAGE")
|
|
115
119
|
|
|
116
120
|
if docker container inspect "$NAME" >/dev/null 2>&1; then
|
|
117
|
-
|
|
121
|
+
INSTALLED_IMAGE=$(docker inspect --format '{{.Config.Image}}' "$NAME")
|
|
122
|
+
((FORWARDING == 0)) || [[ "$INSTALLED_IMAGE" == "$IMAGE" ]] || refuse 'an installed Kit uses a different image; rerun this bootstrap without arguments to update it'
|
|
118
123
|
[[ "$(docker inspect --format '{{.HostConfig.RestartPolicy.Name}}' "$NAME")" == unless-stopped ]] || refuse 'the installed Kit has a different lifecycle'
|
|
119
124
|
[[ "$(docker inspect --format '{{len .Mounts}}' "$NAME")" == 2 ]] || refuse 'the installed Kit has unexpected mounts'
|
|
120
125
|
[[ "$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/kit-home"}}{{.Source}}{{end}}{{end}}' "$NAME")" == "$KIT_HOME" ]] || refuse 'the installed Kit uses another Kit home'
|
|
@@ -142,6 +147,17 @@ if docker container inspect "$NAME" >/dev/null 2>&1; then
|
|
|
142
147
|
fi
|
|
143
148
|
exit
|
|
144
149
|
fi
|
|
150
|
+
if [[ "$INSTALLED_IMAGE" != "$IMAGE" ]]; then
|
|
151
|
+
docker pull --quiet --platform "linux/$ARCH" "$IMAGE" >/dev/null
|
|
152
|
+
if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
|
|
153
|
+
docker exec "$NAME" kit resident drain
|
|
154
|
+
fi
|
|
155
|
+
docker rm -f "$NAME" >/dev/null
|
|
156
|
+
rm -f "$KIT_HOME/state/update-drain"
|
|
157
|
+
start_resident
|
|
158
|
+
printf 'House Kit updated for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
|
159
|
+
exit 0
|
|
160
|
+
fi
|
|
145
161
|
"${DOCKER_RUN[@]}" credential check --house "$HOUSE"
|
|
146
162
|
if [[ "$(docker inspect --format '{{.State.Running}}' "$NAME")" == true ]]; then
|
|
147
163
|
printf 'House Kit is already running for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
|
@@ -169,5 +185,5 @@ else
|
|
|
169
185
|
fi
|
|
170
186
|
|
|
171
187
|
"${DOCKER_RUN[@]}" apply --house "$HOUSE"
|
|
172
|
-
|
|
188
|
+
start_resident
|
|
173
189
|
printf 'House Kit connected for Environment %s.\n' "$(cat "$KIT_HOME/environment")"
|
package/bin/kit.js
CHANGED
|
@@ -9,7 +9,7 @@ import { setupEnvironment } from "../src/setup.js";
|
|
|
9
9
|
import { signInConnector } from "../src/connector-login.js";
|
|
10
10
|
import { attachSession } from "../src/attach.js";
|
|
11
11
|
import { runResident, runWorkers } from "../src/worker.js";
|
|
12
|
-
import { openSession, releaseControl } from "../src/house.js";
|
|
12
|
+
import { openSession, releaseControl, requestDrain } from "../src/house.js";
|
|
13
13
|
import { GOOGLE_OAUTH_CLIENT } from "../src/instance/google.js";
|
|
14
14
|
import { reportHost } from "../src/host.js";
|
|
15
15
|
import { signInAgent, updateAgents } from "../src/agents.js";
|
|
@@ -167,6 +167,21 @@ async function runResidentCommand(args, directory) {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
async function runResidentDrain(args, directory) {
|
|
171
|
+
declared(args, [], 0, { exact: true });
|
|
172
|
+
const answer = await requestDrain(directory);
|
|
173
|
+
if (answer.ready === true) {
|
|
174
|
+
write("House Kit is ready for its update.");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
write(
|
|
178
|
+
answer.sessions.length > 0
|
|
179
|
+
? `House Kit is draining for its update and waits for Agent Sessions ${answer.sessions.join(", ")}.`
|
|
180
|
+
: `House Kit is draining for its update; House refused its lease release (${answer.refused}).`,
|
|
181
|
+
);
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
170
185
|
async function runSetup(args, directory) {
|
|
171
186
|
const { values } = declared(args, ["--house", "--config"], 0, { exact: true });
|
|
172
187
|
const house = await selectedHouse(values, directory);
|
|
@@ -965,7 +980,10 @@ async function main() {
|
|
|
965
980
|
const directory = process.env.HOUSE_KIT_HOME ?? join(homedir(), ".house-kit");
|
|
966
981
|
if (command === "login") return runLogin(args, directory);
|
|
967
982
|
if (command === "credential" && args[0] === "check") return runCredentialCheck(args.slice(1), directory);
|
|
968
|
-
if (command === "resident")
|
|
983
|
+
if (command === "resident") {
|
|
984
|
+
if (args[0] === "drain") return runResidentDrain(args.slice(1), directory);
|
|
985
|
+
return runResidentCommand(args, directory);
|
|
986
|
+
}
|
|
969
987
|
if (command === "setup") return runSetup(args, directory);
|
|
970
988
|
if (command === "apply") return runApply(args, directory);
|
|
971
989
|
if (command === "worker") {
|
package/bootstrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.12",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"linux": {
|
|
6
|
-
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.12/connect-linux.sh)",
|
|
7
7
|
"asset": {
|
|
8
8
|
"name": "connect-linux.sh",
|
|
9
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.12/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
package/package.json
CHANGED
package/src/binding.js
CHANGED
|
@@ -15,6 +15,18 @@ function bindingPath(directory, tmux) {
|
|
|
15
15
|
return join(bindingDirectory(directory), `${tmux}.json`);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function stoppedNote(stopped) {
|
|
19
|
+
if (stopped === null || stopped === undefined) return null;
|
|
20
|
+
return {
|
|
21
|
+
turn_completed: stopped.turnCompleted,
|
|
22
|
+
capacity_exhausted: stopped.capacityExhausted,
|
|
23
|
+
provider_refused: stopped.providerRefused,
|
|
24
|
+
continuable: stopped.continuable,
|
|
25
|
+
failure: stopped.failure,
|
|
26
|
+
text_accepted: stopped.textAccepted,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
export async function writeBinding(directory, binding) {
|
|
19
31
|
const reported = binding.reported ?? null;
|
|
20
32
|
await writeNote(bindingPath(directory, binding.tmux), {
|
|
@@ -29,6 +41,7 @@ export async function writeBinding(directory, binding) {
|
|
|
29
41
|
sealed: binding.sealed === null || binding.sealed === undefined
|
|
30
42
|
? null
|
|
31
43
|
: { operation: binding.sealed.operation, blocks: binding.sealed.blocks },
|
|
44
|
+
stopped: stoppedNote(binding.stopped),
|
|
32
45
|
reported: reported === null ? null : reportedNote(reported),
|
|
33
46
|
});
|
|
34
47
|
}
|
|
@@ -45,6 +58,31 @@ function retainedSeal(stored) {
|
|
|
45
58
|
return { operation: stored.operation, blocks: stored.blocks };
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
function retainedFailure(stored) {
|
|
62
|
+
if (stored === null) return null;
|
|
63
|
+
if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
|
|
64
|
+
if (typeof stored.code !== "string" || stored.code.length === 0) return undefined;
|
|
65
|
+
if (stored.detail !== null && typeof stored.detail !== "string") return undefined;
|
|
66
|
+
return { code: stored.code, detail: stored.detail };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function retainedStop(stored) {
|
|
70
|
+
if (stored === null || stored === undefined) return null;
|
|
71
|
+
if (typeof stored !== "object" || Array.isArray(stored)) return undefined;
|
|
72
|
+
const flags = ["turn_completed", "capacity_exhausted", "provider_refused", "continuable", "text_accepted"];
|
|
73
|
+
if (flags.some((flag) => typeof stored[flag] !== "boolean")) return undefined;
|
|
74
|
+
const failure = retainedFailure(stored.failure);
|
|
75
|
+
if (failure === undefined) return undefined;
|
|
76
|
+
return {
|
|
77
|
+
turnCompleted: stored.turn_completed,
|
|
78
|
+
capacityExhausted: stored.capacity_exhausted,
|
|
79
|
+
providerRefused: stored.provider_refused,
|
|
80
|
+
continuable: stored.continuable,
|
|
81
|
+
failure,
|
|
82
|
+
textAccepted: stored.text_accepted,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
48
86
|
function completeBinding(directory, stored) {
|
|
49
87
|
if (typeof stored !== "object" || stored === null || Array.isArray(stored)) return null;
|
|
50
88
|
const named = [stored.run, stored.session, stored.tmux, stored.socket, stored.nonce];
|
|
@@ -54,6 +92,8 @@ function completeBinding(directory, stored) {
|
|
|
54
92
|
if (stored.owner_stop !== null && typeof stored.owner_stop !== "string") return null;
|
|
55
93
|
const sealed = retainedSeal(stored.sealed);
|
|
56
94
|
if (sealed === undefined) return null;
|
|
95
|
+
const stopped = retainedStop(stored.stopped);
|
|
96
|
+
if (stopped === undefined) return null;
|
|
57
97
|
const reported = retainedReport(stored.reported);
|
|
58
98
|
if (reported === undefined) return null;
|
|
59
99
|
return {
|
|
@@ -65,6 +105,7 @@ function completeBinding(directory, stored) {
|
|
|
65
105
|
conversation: stored.conversation === true,
|
|
66
106
|
ownerStop: stored.owner_stop,
|
|
67
107
|
sealed,
|
|
108
|
+
stopped,
|
|
68
109
|
reported,
|
|
69
110
|
};
|
|
70
111
|
}
|
package/src/house.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { chmod, mkdir, readFile, rm } from "node:fs/promises";
|
|
2
3
|
import { connect, createServer } from "node:net";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
@@ -39,6 +40,8 @@ import { houseRefusal, KitRefusal, throwHouseAnswer } from "./refusal.js";
|
|
|
39
40
|
const LEASE = /^ahl_[A-Za-z0-9_-]+$/;
|
|
40
41
|
const RESIDENT = "Resident";
|
|
41
42
|
const LEASE_ACTIVE = "environment_control_lease_active";
|
|
43
|
+
const UPGRADE_REQUIRED = "kit_upgrade_required";
|
|
44
|
+
const UPDATE_DRAINING = "kit_update_draining";
|
|
42
45
|
const ASSISTANT_DRAFT_STATES = ["streamed", "unsupported"];
|
|
43
46
|
const ITEM_OUTCOMES = ["accepted", "duplicate", "rejected"];
|
|
44
47
|
|
|
@@ -70,6 +73,7 @@ async function houseAnswer(house, path, init, dependencies, signal) {
|
|
|
70
73
|
} catch {
|
|
71
74
|
answer = null;
|
|
72
75
|
}
|
|
76
|
+
if (answer?.error?.code === UPGRADE_REQUIRED) dependencies.contractRefused?.();
|
|
73
77
|
throwHouseAnswer(response.status, answer, path);
|
|
74
78
|
if (answer === null) throw new KitRefusal("kit_house_unavailable", path);
|
|
75
79
|
return answer;
|
|
@@ -113,6 +117,14 @@ function residentSocket(directory) {
|
|
|
113
117
|
return join(directory, "run", "resident.sock");
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
export function updateDrain(directory) {
|
|
121
|
+
return join(directory, "state", "update-drain");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function refuseDrained(directory) {
|
|
125
|
+
if (existsSync(updateDrain(directory))) throw new KitRefusal(UPDATE_DRAINING);
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
function residentAnswer(status, code) {
|
|
117
129
|
const body = Buffer.from(JSON.stringify({ error: { code } })).toString("base64");
|
|
118
130
|
return { status, type: "application/json", body };
|
|
@@ -147,63 +159,107 @@ async function residentRelay(session, request, signal) {
|
|
|
147
159
|
}
|
|
148
160
|
}
|
|
149
161
|
|
|
150
|
-
export async function serveResident(session, directory, signal) {
|
|
162
|
+
export async function serveResident(session, directory, signal, drain) {
|
|
151
163
|
const socket = residentSocket(directory);
|
|
152
164
|
await mkdir(dirname(socket), { recursive: true, mode: 0o700 });
|
|
153
165
|
await rm(socket, { force: true });
|
|
166
|
+
const connections = new Set();
|
|
154
167
|
let holder = null;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
let draining = false;
|
|
169
|
+
function answerDrain(connection) {
|
|
170
|
+
draining = true;
|
|
171
|
+
const answered = drain();
|
|
172
|
+
holder?.destroy();
|
|
173
|
+
answered.then(
|
|
174
|
+
(answer) => {
|
|
175
|
+
if (!connection.destroyed) writeFrame(connection, answer);
|
|
176
|
+
connection.end();
|
|
177
|
+
},
|
|
178
|
+
() => connection.destroy(),
|
|
179
|
+
);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
function hold(connection) {
|
|
183
|
+
if (holder !== null || draining || existsSync(updateDrain(directory))) {
|
|
184
|
+
writeFrame(connection, { refused: holder === null ? UPDATE_DRAINING : LEASE_ACTIVE });
|
|
159
185
|
connection.end();
|
|
160
|
-
return;
|
|
186
|
+
return false;
|
|
161
187
|
}
|
|
162
188
|
holder = connection;
|
|
189
|
+
renewControl(session, signal).then(
|
|
190
|
+
() => writeFrame(connection, { held: true }),
|
|
191
|
+
(error) => {
|
|
192
|
+
writeFrame(connection, { refused: error.code ?? "kit_lease_rejected" });
|
|
193
|
+
connection.end();
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
const server = createServer((connection) => {
|
|
199
|
+
connections.add(connection);
|
|
200
|
+
connection.on("error", () => connection.destroy());
|
|
163
201
|
const relayed = new Map();
|
|
202
|
+
let held = null;
|
|
164
203
|
connection.on("close", () => {
|
|
204
|
+
connections.delete(connection);
|
|
165
205
|
if (holder === connection) holder = null;
|
|
166
206
|
for (const cancelled of relayed.values()) cancelled.abort();
|
|
167
207
|
});
|
|
168
|
-
|
|
169
|
-
() => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
);
|
|
208
|
+
readFrames(connection, {
|
|
209
|
+
onFrame: (request) => {
|
|
210
|
+
if (held === null) {
|
|
211
|
+
held = request.drain === true ? answerDrain(connection) : hold(connection);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!held) return;
|
|
215
|
+
if (request.cancel === true) {
|
|
216
|
+
relayed.get(request.id)?.abort();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const cancelled = new AbortController();
|
|
220
|
+
relayed.set(request.id, cancelled);
|
|
221
|
+
void residentRelay(session, request, AbortSignal.any([signal, cancelled.signal])).then(
|
|
222
|
+
(answer) => {
|
|
223
|
+
relayed.delete(request.id);
|
|
224
|
+
if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
|
|
185
225
|
},
|
|
186
|
-
|
|
187
|
-
});
|
|
226
|
+
);
|
|
188
227
|
},
|
|
189
|
-
(
|
|
190
|
-
|
|
191
|
-
connection.end();
|
|
192
|
-
},
|
|
193
|
-
);
|
|
228
|
+
onRefusal: () => connection.destroy(),
|
|
229
|
+
});
|
|
194
230
|
});
|
|
195
231
|
await new Promise((resolve, reject) => {
|
|
196
232
|
server.once("error", reject);
|
|
197
233
|
server.listen(socket, resolve);
|
|
198
234
|
});
|
|
199
235
|
await chmod(socket, 0o600);
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
236
|
+
return {
|
|
237
|
+
serving: () => holder !== null,
|
|
238
|
+
async close() {
|
|
239
|
+
for (const connection of connections) connection.destroy();
|
|
240
|
+
await new Promise((resolve) => server.close(resolve));
|
|
241
|
+
await rm(socket, { force: true });
|
|
242
|
+
},
|
|
204
243
|
};
|
|
205
244
|
}
|
|
206
245
|
|
|
246
|
+
export function requestDrain(directory) {
|
|
247
|
+
const connection = connect(residentSocket(directory));
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
const unavailable = () => reject(new KitRefusal("kit_resident_unavailable"));
|
|
250
|
+
connection.on("error", unavailable);
|
|
251
|
+
connection.on("close", unavailable);
|
|
252
|
+
readFrames(connection, {
|
|
253
|
+
onFrame: (answer) => {
|
|
254
|
+
resolve(answer);
|
|
255
|
+
connection.end();
|
|
256
|
+
},
|
|
257
|
+
onRefusal: () => connection.destroy(),
|
|
258
|
+
});
|
|
259
|
+
writeFrame(connection, { drain: true });
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
207
263
|
async function residentDelegation(directory, fetcher) {
|
|
208
264
|
const connection = connect(residentSocket(directory));
|
|
209
265
|
const pending = new Map();
|
|
@@ -226,6 +282,7 @@ async function residentDelegation(directory, fetcher) {
|
|
|
226
282
|
onFrame: (frame) => (frame.id === undefined ? greet(frame) : settle(frame.id, frame)),
|
|
227
283
|
onRefusal: () => connection.destroy(),
|
|
228
284
|
});
|
|
285
|
+
writeFrame(connection, { hold: true });
|
|
229
286
|
const held = await greeting;
|
|
230
287
|
if (held === null) return null;
|
|
231
288
|
if (held.held !== true) {
|
|
@@ -276,6 +333,7 @@ async function residentDelegation(directory, fetcher) {
|
|
|
276
333
|
}
|
|
277
334
|
|
|
278
335
|
export async function openSession(options, dependencies = {}) {
|
|
336
|
+
if (options.resident !== true) refuseDrained(options.directory);
|
|
279
337
|
const session = {
|
|
280
338
|
house: options.house,
|
|
281
339
|
dependencies,
|
|
@@ -285,7 +343,7 @@ export async function openSession(options, dependencies = {}) {
|
|
|
285
343
|
signal: options.signal,
|
|
286
344
|
};
|
|
287
345
|
const resident =
|
|
288
|
-
options.acquire === true
|
|
346
|
+
options.acquire === true || options.resident === true
|
|
289
347
|
? null
|
|
290
348
|
: await residentDelegation(options.directory, dependencies.fetch ?? fetch);
|
|
291
349
|
if (resident !== null) {
|
|
@@ -293,24 +351,30 @@ export async function openSession(options, dependencies = {}) {
|
|
|
293
351
|
session.dependencies = { ...dependencies, fetch: resident.fetch };
|
|
294
352
|
return session;
|
|
295
353
|
}
|
|
354
|
+
session.dependencies = {
|
|
355
|
+
...dependencies,
|
|
356
|
+
contractRefused: () => {
|
|
357
|
+
session.refused = true;
|
|
358
|
+
},
|
|
359
|
+
};
|
|
296
360
|
const credential = await readCredential(options.directory);
|
|
297
|
-
|
|
298
|
-
session,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
);
|
|
361
|
+
const answer = await houseRequest(
|
|
362
|
+
session.house,
|
|
363
|
+
`Bearer ${credential}`,
|
|
364
|
+
"/kit/lease",
|
|
365
|
+
{},
|
|
366
|
+
session.dependencies,
|
|
367
|
+
session.signal,
|
|
368
|
+
).catch((error) => {
|
|
369
|
+
if (options.resident === true && session.refused) return null;
|
|
370
|
+
throw error;
|
|
371
|
+
});
|
|
372
|
+
if (answer !== null) Object.assign(session, heldLease(answer));
|
|
310
373
|
return session;
|
|
311
374
|
}
|
|
312
375
|
|
|
313
376
|
async function renewControl(session, signal) {
|
|
377
|
+
if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
|
|
314
378
|
Object.assign(
|
|
315
379
|
session,
|
|
316
380
|
heldLease(
|
|
@@ -328,13 +392,14 @@ async function renewControl(session, signal) {
|
|
|
328
392
|
|
|
329
393
|
export async function heldControl(session, signal = session.signal) {
|
|
330
394
|
if (session.resident !== undefined) return RESIDENT;
|
|
395
|
+
if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
|
|
331
396
|
if (session.expiresAt - session.now().getTime() <= LEASE_RENEWAL_MARGIN_MS) {
|
|
332
397
|
await renewControl(session, signal);
|
|
333
398
|
}
|
|
334
399
|
return `Bearer ${session.lease}`;
|
|
335
400
|
}
|
|
336
401
|
|
|
337
|
-
function controlSleep(milliseconds, signal) {
|
|
402
|
+
export function controlSleep(milliseconds, signal) {
|
|
338
403
|
return new Promise((resolve, reject) => {
|
|
339
404
|
if (signal.aborted) {
|
|
340
405
|
reject(new KitRefusal("kit_command_cancelled"));
|
|
@@ -382,6 +447,7 @@ export async function releaseControl(session, options = {}) {
|
|
|
382
447
|
if (session.lease === null) return;
|
|
383
448
|
const held = session.lease;
|
|
384
449
|
try {
|
|
450
|
+
if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
|
|
385
451
|
await houseRequest(
|
|
386
452
|
session.house,
|
|
387
453
|
`Bearer ${held}`,
|
|
@@ -430,7 +496,7 @@ export async function maintainControl(session, operation, signal, { release = tr
|
|
|
430
496
|
throw new KitRefusal("kit_lease_rejected");
|
|
431
497
|
}
|
|
432
498
|
} catch (error) {
|
|
433
|
-
if (stopped) return;
|
|
499
|
+
if (stopped || session.refused) return;
|
|
434
500
|
rejectFailure(error);
|
|
435
501
|
maintenance.abort();
|
|
436
502
|
return;
|
package/src/kit-work-stream.js
CHANGED
|
@@ -49,6 +49,7 @@ async function connected(session, signal, available, dependencies) {
|
|
|
49
49
|
});
|
|
50
50
|
socket.on("unexpected-response", (_request, response) => {
|
|
51
51
|
response.resume();
|
|
52
|
+
if (response.statusCode === 409) session.dependencies.contractRefused?.();
|
|
52
53
|
const refusal =
|
|
53
54
|
response.statusCode === 409 ? new KitRefusal("kit_upgrade_required") : null;
|
|
54
55
|
socket.terminate();
|
package/src/login.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
8
8
|
import { stdin, stdout } from "node:process";
|
|
9
9
|
import { KIT_CREDENTIAL } from "./contract.js";
|
|
10
10
|
import { launchBase, prepareLaunchBase } from "./directory.js";
|
|
11
|
-
import { houseRequest } from "./house.js";
|
|
11
|
+
import { houseRequest, refuseDrained } from "./house.js";
|
|
12
12
|
import { KitRefusal } from "./refusal.js";
|
|
13
13
|
import { writeText } from "./state.js";
|
|
14
14
|
|
|
@@ -260,6 +260,7 @@ export async function openBrowser(url) {
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
export async function connectHouse(options, dependencies = {}) {
|
|
263
|
+
refuseDrained(options.directory);
|
|
263
264
|
await prepareLaunchBase(launchBase(dependencies));
|
|
264
265
|
if (options.presentation === "manual") return login(options, dependencies);
|
|
265
266
|
const callback = await loopback();
|
package/src/worker.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
rejectResumption,
|
|
45
46
|
callHouseMcp,
|
|
46
47
|
commitRun,
|
|
48
|
+
controlSleep,
|
|
47
49
|
maintainControl,
|
|
48
50
|
mutatingCall,
|
|
49
51
|
observeSessionStart,
|
|
@@ -56,14 +58,17 @@ import {
|
|
|
56
58
|
toolMutations,
|
|
57
59
|
reportRunOutcome,
|
|
58
60
|
reportSessionHeartbeat,
|
|
61
|
+
releaseControl,
|
|
59
62
|
reportSessions,
|
|
60
63
|
serveResident,
|
|
61
64
|
sessionAuthority,
|
|
62
65
|
streamAssistantDraft,
|
|
66
|
+
updateDrain,
|
|
63
67
|
} from "./house.js";
|
|
64
68
|
import { operationId } from "./operation.js";
|
|
65
69
|
import { runSetup, signInWantedAgent } from "./setup.js";
|
|
66
70
|
import { readSettlements, removeSettlement, writeSettlement } from "./settlement.js";
|
|
71
|
+
import { writeText } from "./state.js";
|
|
67
72
|
import { failedReport, terminalReport } from "./terminal.js";
|
|
68
73
|
import {
|
|
69
74
|
createSession,
|
|
@@ -75,47 +80,31 @@ import {
|
|
|
75
80
|
} from "./tmux.js";
|
|
76
81
|
|
|
77
82
|
const LAUNCHER_MAIN = fileURLToPath(new URL("launcher-main.js", import.meta.url));
|
|
83
|
+
const RESIDENT_INTERVAL_MS = 30_000;
|
|
78
84
|
|
|
79
85
|
async function dispatchSeal(context, bound) {
|
|
80
86
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return { accepted, failure: null, unresolved: false, refused: null };
|
|
87
|
+
await acceptAssistantText(context.session, bound.session, bound.sealed);
|
|
88
|
+
bound.stopped.textAccepted = true;
|
|
84
89
|
} catch (refusal) {
|
|
85
90
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
86
91
|
const named = { code: refusal.code, detail: refusal.message };
|
|
87
|
-
if (undecided(refusal)) return
|
|
88
|
-
|
|
89
|
-
return { accepted: null, failure: named, unresolved: false, refused: null };
|
|
92
|
+
if (undecided(context, refusal)) return named;
|
|
93
|
+
bound.stopped.failure ??= named;
|
|
90
94
|
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function resolveSeal(context, bound) {
|
|
94
95
|
bound.sealed = null;
|
|
95
96
|
await writeBinding(context.directory, bound);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
async function sealAssistantText(context, bound, said) {
|
|
99
|
-
const blocks = assistantBlocks(said.join("\n\n"));
|
|
100
|
-
if (blocks === null) return { accepted: null, failure: null, unresolved: false, refused: null };
|
|
101
|
-
bound.sealed = { operation: operationId(), blocks };
|
|
102
|
-
await writeBinding(context.directory, bound);
|
|
103
|
-
return dispatchSeal(context, bound);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function recoverAssistantSeal(context, binding) {
|
|
107
|
-
if (binding.sealed === null) return { unresolved: false, refused: null };
|
|
108
|
-
const sealed = await dispatchSeal(context, binding);
|
|
109
|
-
return { unresolved: sealed.unresolved, refused: sealed.refused };
|
|
97
|
+
return null;
|
|
110
98
|
}
|
|
111
99
|
|
|
112
100
|
function unresolvedRun(bound, refused = null) {
|
|
113
101
|
return { run: bound.run, session: bound.session, refused };
|
|
114
102
|
}
|
|
115
103
|
|
|
116
|
-
function undecided(refusal) {
|
|
104
|
+
function undecided(context, refusal) {
|
|
117
105
|
return (
|
|
118
106
|
houseFailed(refusal) ||
|
|
107
|
+
context.session.refused === true ||
|
|
119
108
|
(refusal instanceof KitRefusal &&
|
|
120
109
|
["kit_command_cancelled", "kit_session_token_rejected"].includes(refusal.code))
|
|
121
110
|
);
|
|
@@ -125,7 +114,9 @@ async function pendingCommands(context, kinds = ["stop"]) {
|
|
|
125
114
|
try {
|
|
126
115
|
return await acquireCommands(context.session, WORKER_COMMAND_BATCH_MAX, kinds);
|
|
127
116
|
} catch (refusal) {
|
|
128
|
-
if (houseFailed(refusal)
|
|
117
|
+
if (houseFailed(refusal) || context.session.refused === true) {
|
|
118
|
+
return { stops: [], answers: [], interrupts: [] };
|
|
119
|
+
}
|
|
129
120
|
throw refusal;
|
|
130
121
|
}
|
|
131
122
|
}
|
|
@@ -227,7 +218,7 @@ async function completeOwnerStop(context, bound, requestedAt) {
|
|
|
227
218
|
try {
|
|
228
219
|
await observeSessionStop(context.session, bound.session);
|
|
229
220
|
} catch (refusal) {
|
|
230
|
-
if (
|
|
221
|
+
if (undecided(context, refusal)) return { destroyed, confirmed: false, failure: null };
|
|
231
222
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
232
223
|
failure = { code: refusal.code, detail: refusal.message };
|
|
233
224
|
}
|
|
@@ -241,7 +232,7 @@ async function settleReported(context, bound) {
|
|
|
241
232
|
await reportRunOutcome(context.session, bound.run, bound.reported);
|
|
242
233
|
return { settled: true, failure: null };
|
|
243
234
|
} catch (refusal) {
|
|
244
|
-
if (undecided(refusal)) return { settled: false, failure: null };
|
|
235
|
+
if (undecided(context, refusal)) return { settled: false, failure: null };
|
|
245
236
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
246
237
|
if (refusal.code === "worker_run_settled") return { settled: true, failure: null };
|
|
247
238
|
return { settled: false, failure: { code: refusal.code, detail: refusal.message } };
|
|
@@ -375,50 +366,65 @@ async function superviseBound(context, bound, opened, start) {
|
|
|
375
366
|
held !== null &&
|
|
376
367
|
stopped?.providerContinuation === true &&
|
|
377
368
|
stopped.providerSessionId !== null;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
369
|
+
if (continuable) {
|
|
370
|
+
await writeContinuation(context.directory, { ...held, providerSessionId: stopped.providerSessionId });
|
|
371
|
+
}
|
|
372
|
+
const turnCompleted = stopped?.stopOutcome === "completed";
|
|
373
|
+
const blocks = turnCompleted ? assistantBlocks(outcome.assistant.join("\n\n")) : null;
|
|
374
|
+
bound.sealed = blocks === null ? null : { operation: operationId(), blocks };
|
|
375
|
+
bound.stopped = {
|
|
376
|
+
turnCompleted,
|
|
377
|
+
capacityExhausted,
|
|
378
|
+
providerRefused,
|
|
379
|
+
continuable,
|
|
380
|
+
failure: outcome.failure,
|
|
381
|
+
textAccepted: false,
|
|
382
|
+
};
|
|
383
|
+
await writeBinding(context.directory, bound);
|
|
384
|
+
return { ...outcome, ...(await settleStopped(context, bound)) };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function stoppedReport(bound) {
|
|
388
|
+
if (bound.stopped === null || bound.stopped === undefined) return null;
|
|
389
|
+
return terminalReport({ ...bound.stopped, conversation: bound.conversation === true });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function settleStopped(context, bound) {
|
|
393
|
+
const { stopped } = bound;
|
|
394
|
+
const unresolved = (refused) => ({ failure: stopped.failure, refused, settled: false });
|
|
395
|
+
if (bound.sealed !== null) {
|
|
396
|
+
const refused = await dispatchSeal(context, bound);
|
|
397
|
+
if (refused !== null) return unresolved(refused);
|
|
398
|
+
}
|
|
399
|
+
let failure = stopped.failure;
|
|
385
400
|
try {
|
|
386
|
-
await observeSessionStop(context.session, bound.session,
|
|
401
|
+
await observeSessionStop(context.session, bound.session, stopped.textAccepted);
|
|
387
402
|
} catch (refusal) {
|
|
403
|
+
if (undecided(context, refusal)) return unresolved(null);
|
|
388
404
|
if (!(refusal instanceof KitRefusal) || refusal.code !== "conversation_final_text_pending") throw refusal;
|
|
389
|
-
|
|
405
|
+
failure ??= { code: refusal.code, detail: refusal.message };
|
|
390
406
|
await observeSessionStop(context.session, bound.session);
|
|
391
407
|
}
|
|
392
|
-
const report =
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
continuable,
|
|
399
|
-
});
|
|
400
|
-
const retained = report === null;
|
|
401
|
-
if (retained) {
|
|
402
|
-
await writeContinuation(context.directory, {
|
|
403
|
-
...held,
|
|
404
|
-
providerSessionId: stopped.providerSessionId,
|
|
408
|
+
const report = stoppedReport(bound);
|
|
409
|
+
if (report !== null) await removeContinuation(context.directory, bound.run);
|
|
410
|
+
try {
|
|
411
|
+
await reportContinuation(context.session, bound.session, {
|
|
412
|
+
providerContinuation: report === null,
|
|
413
|
+
providerCapacityExhausted: stopped.capacityExhausted,
|
|
405
414
|
});
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
});
|
|
411
|
-
if (retained) {
|
|
415
|
+
} catch (refusal) {
|
|
416
|
+
if (undecided(context, refusal)) return unresolved(null);
|
|
417
|
+
if (!(refusal instanceof KitRefusal) || refusal.code !== "worker_run_settled") throw refusal;
|
|
418
|
+
await removeContinuation(context.directory, bound.run);
|
|
412
419
|
await removeBinding(context.directory, bound.tmux);
|
|
413
|
-
return
|
|
420
|
+
return { failure, refused: null, settled: true };
|
|
421
|
+
}
|
|
422
|
+
if (report === null) {
|
|
423
|
+
await removeBinding(context.directory, bound.tmux);
|
|
424
|
+
return { failure, refused: null, settled: true };
|
|
414
425
|
}
|
|
415
426
|
const settled = await reportTerminalState(context, bound, report);
|
|
416
|
-
return {
|
|
417
|
-
...outcome,
|
|
418
|
-
reported: settled.reported,
|
|
419
|
-
refused: settled.failure,
|
|
420
|
-
settled: settled.settled,
|
|
421
|
-
};
|
|
427
|
+
return { failure, reported: settled.reported, refused: settled.failure, settled: settled.settled };
|
|
422
428
|
}
|
|
423
429
|
|
|
424
430
|
function assignmentRoute(context, assignment) {
|
|
@@ -506,7 +512,7 @@ async function superviseAssignment(context, assignment, resumed = false) {
|
|
|
506
512
|
detail: refusal instanceof KitRefusal ? refusal.message : null,
|
|
507
513
|
};
|
|
508
514
|
if (resumed) return refuseContinuation(context, assignment, failure);
|
|
509
|
-
if (undecided(refusal)) throw refusal;
|
|
515
|
+
if (undecided(context, refusal)) throw refusal;
|
|
510
516
|
return refuseAssignment(context, assignment, failure);
|
|
511
517
|
}
|
|
512
518
|
const { bound, opened } = prepared;
|
|
@@ -555,7 +561,7 @@ async function settleAbsentSession(context, binding) {
|
|
|
555
561
|
providerCapacityExhausted: false,
|
|
556
562
|
});
|
|
557
563
|
} catch (refusal) {
|
|
558
|
-
if (undecided(refusal)) return { settled: false, failure: null };
|
|
564
|
+
if (undecided(context, refusal)) return { settled: false, failure: null };
|
|
559
565
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
560
566
|
}
|
|
561
567
|
await removeContinuation(context.directory, binding.run);
|
|
@@ -571,7 +577,7 @@ async function confirmRestorable(context, binding) {
|
|
|
571
577
|
try {
|
|
572
578
|
authority = await replaceSessionAuthority(context.session, binding.session);
|
|
573
579
|
} catch (refusal) {
|
|
574
|
-
if (undecided(refusal)) return { authority: null, unresolved: true };
|
|
580
|
+
if (undecided(context, refusal)) return { authority: null, unresolved: true };
|
|
575
581
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
576
582
|
await destroySession(context.runtime, binding.tmux);
|
|
577
583
|
await removeBinding(context.directory, binding.tmux);
|
|
@@ -593,17 +599,17 @@ async function reconcile(context) {
|
|
|
593
599
|
const commands = (await pendingCommands(context)).stops;
|
|
594
600
|
const carried = [];
|
|
595
601
|
for (const binding of bindings) {
|
|
596
|
-
const resealed = await recoverAssistantSeal(context, binding);
|
|
597
|
-
if (resealed.unresolved) {
|
|
598
|
-
unresolved.push(unresolvedRun(binding, resealed.refused));
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
602
|
if (binding.reported !== null) {
|
|
602
603
|
const settled = await settleReported(context, binding);
|
|
603
604
|
if (settled.settled) await removeBinding(context.directory, binding.tmux);
|
|
604
605
|
else unresolved.push(unresolvedRun(binding, settled.failure));
|
|
605
606
|
continue;
|
|
606
607
|
}
|
|
608
|
+
if (binding.stopped !== null) {
|
|
609
|
+
const settled = await settleStopped(context, binding);
|
|
610
|
+
if (!settled.settled) unresolved.push(unresolvedRun(binding, settled.refused));
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
607
613
|
const command = commands.find((pending) => pending.run === binding.run) ?? null;
|
|
608
614
|
const requestedAt = binding.ownerStop ?? command?.requestedAt ?? null;
|
|
609
615
|
if (requestedAt === null) {
|
|
@@ -643,7 +649,7 @@ async function reportHeldSessions(context) {
|
|
|
643
649
|
const continued = (await readContinuationRuns(context.directory)).map((run) => ({ run, reported: null }));
|
|
644
650
|
const held = new Map();
|
|
645
651
|
for (const entry of [...settlements, ...bindings, ...continued]) {
|
|
646
|
-
held.set(entry.run, held.get(entry.run) ?? entry.reported);
|
|
652
|
+
held.set(entry.run, held.get(entry.run) ?? entry.reported ?? stoppedReport(entry));
|
|
647
653
|
}
|
|
648
654
|
const reported = await reportSessions(
|
|
649
655
|
context.session,
|
|
@@ -670,17 +676,23 @@ async function afterEpochReport(context, pass) {
|
|
|
670
676
|
return pass();
|
|
671
677
|
}
|
|
672
678
|
|
|
679
|
+
function unlessDraining(context, act) {
|
|
680
|
+
return context.update?.requested ? null : act();
|
|
681
|
+
}
|
|
682
|
+
|
|
673
683
|
async function workerPass(context, dependencies, baseConfiguration) {
|
|
674
684
|
const { session, signal } = context;
|
|
675
685
|
const reconciled = await reconcile(context);
|
|
676
|
-
if (reconciled.run !== null || reconciled.unresolved.length > 0)
|
|
686
|
+
if (reconciled.run !== null || reconciled.unresolved.length > 0 || context.update?.requested) {
|
|
687
|
+
return reconciled;
|
|
688
|
+
}
|
|
677
689
|
context.busy = false;
|
|
678
690
|
const needsLogin = await queueAgentRefresh(context, dependencies, signal, baseConfiguration);
|
|
679
691
|
context.startLogins?.(needsLogin);
|
|
680
692
|
context.busy = true;
|
|
681
693
|
const { configuration } = context;
|
|
682
|
-
context.inbound = await reconcileInbound(context.directory, session);
|
|
683
|
-
const setup = await acquireSetup(session, signal);
|
|
694
|
+
context.inbound = await unlessDraining(context, () => reconcileInbound(context.directory, session));
|
|
695
|
+
const setup = await unlessDraining(context, () => acquireSetup(session, signal));
|
|
684
696
|
if (setup !== null) {
|
|
685
697
|
context.setup = await runSetup(
|
|
686
698
|
{
|
|
@@ -697,19 +709,21 @@ async function workerPass(context, dependencies, baseConfiguration) {
|
|
|
697
709
|
const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
698
710
|
context.startLogins?.(needsLogin);
|
|
699
711
|
}
|
|
700
|
-
const resumption = await acquireOneResumption(session);
|
|
712
|
+
const resumption = await unlessDraining(context, () => acquireOneResumption(session));
|
|
701
713
|
if (resumption !== null) return { ...reconciled, run: await superviseResumption(context, resumption) };
|
|
702
|
-
const assignment = await acquireOneRun(session);
|
|
714
|
+
const assignment = await unlessDraining(context, () => acquireOneRun(session));
|
|
703
715
|
if (assignment === null) return reconciled;
|
|
704
716
|
return { ...reconciled, run: await superviseAssignment(context, assignment) };
|
|
705
717
|
}
|
|
706
718
|
|
|
707
|
-
async function workerContext(options, dependencies) {
|
|
719
|
+
async function workerContext(options, dependencies, resident = false) {
|
|
708
720
|
const configuration = await readConfiguration(options.configuration);
|
|
709
721
|
const runtime = workerRuntime(options.directory, dependencies.tmux ?? "tmux");
|
|
710
722
|
const launcher = dependencies.launcher ?? [process.execPath, LAUNCHER_MAIN];
|
|
711
723
|
const tmux = await readyTmux(runtime);
|
|
712
|
-
const session =
|
|
724
|
+
const session = resident
|
|
725
|
+
? await residentSession(options, dependencies)
|
|
726
|
+
: await openSession({ ...options, acquire: true }, dependencies);
|
|
713
727
|
const context = {
|
|
714
728
|
configuration,
|
|
715
729
|
runtime,
|
|
@@ -827,14 +841,77 @@ function residentWake() {
|
|
|
827
841
|
};
|
|
828
842
|
}
|
|
829
843
|
|
|
844
|
+
function residentUpdate(context, wake) {
|
|
845
|
+
let requested = existsSync(updateDrain(context.directory));
|
|
846
|
+
let ready = null;
|
|
847
|
+
const waiting = [];
|
|
848
|
+
return {
|
|
849
|
+
get requested() {
|
|
850
|
+
return requested;
|
|
851
|
+
},
|
|
852
|
+
async request() {
|
|
853
|
+
if (!requested) {
|
|
854
|
+
requested = true;
|
|
855
|
+
await writeText(updateDrain(context.directory), "");
|
|
856
|
+
}
|
|
857
|
+
wake.notify();
|
|
858
|
+
if (ready !== null) return ready;
|
|
859
|
+
const sessions = await heldSessions(context);
|
|
860
|
+
if (sessions.length > 0) return { ready: false, sessions };
|
|
861
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
862
|
+
},
|
|
863
|
+
answer(answer) {
|
|
864
|
+
for (const resolve of waiting.splice(0)) resolve(answer);
|
|
865
|
+
},
|
|
866
|
+
quiesce() {
|
|
867
|
+
ready = { ready: true, sessions: [] };
|
|
868
|
+
this.answer(ready);
|
|
869
|
+
},
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
async function heldSessions(context) {
|
|
874
|
+
const bindings = await readBindings(context.directory);
|
|
875
|
+
if (!context.session.refused) {
|
|
876
|
+
return [...bindings, ...(await readSettlements(context.directory))].map((held) => held.run);
|
|
877
|
+
}
|
|
878
|
+
const live = [];
|
|
879
|
+
for (const binding of bindings) {
|
|
880
|
+
if (binding.stopped === null && (await sessionPresent(context.runtime, binding.tmux))) {
|
|
881
|
+
live.push(binding.run);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return live;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async function residentSession(options, dependencies) {
|
|
888
|
+
for (;;) {
|
|
889
|
+
try {
|
|
890
|
+
return await openSession({ ...options, resident: true }, dependencies);
|
|
891
|
+
} catch (refusal) {
|
|
892
|
+
if (refusal?.code !== "environment_control_lease_active") throw refusal;
|
|
893
|
+
await controlSleep(dependencies.residentIntervalMs ?? RESIDENT_INTERVAL_MS, options.signal);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
830
898
|
export async function runResident(options, dependencies = {}) {
|
|
831
|
-
const { context } = await workerContext(options, dependencies);
|
|
832
|
-
|
|
833
|
-
|
|
899
|
+
const { context } = await workerContext(options, dependencies, true);
|
|
900
|
+
const { session } = context;
|
|
901
|
+
const interval = dependencies.residentIntervalMs ?? RESIDENT_INTERVAL_MS;
|
|
902
|
+
const wake = residentWake();
|
|
903
|
+
const update = residentUpdate(context, wake);
|
|
904
|
+
context.update = update;
|
|
905
|
+
const served = await serveResident(session, context.directory, options.signal, () => update.request());
|
|
906
|
+
const serve = async (signal) => {
|
|
834
907
|
context.busy = true;
|
|
835
|
-
const wake = residentWake();
|
|
836
908
|
const watcher = new AbortController();
|
|
837
909
|
let failure = null;
|
|
910
|
+
const fail = (error) => {
|
|
911
|
+
if (signal.aborted || session.refused) return;
|
|
912
|
+
failure = error;
|
|
913
|
+
wake.notify();
|
|
914
|
+
};
|
|
838
915
|
const logins = new Map();
|
|
839
916
|
const attemptedLogins = new Map();
|
|
840
917
|
context.cancelLogins = (wanted) => {
|
|
@@ -853,6 +930,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
853
930
|
}
|
|
854
931
|
};
|
|
855
932
|
context.startLogins = (agents) => {
|
|
933
|
+
if (update.requested) return;
|
|
856
934
|
for (const agent of agents) {
|
|
857
935
|
const { kind, release } = agent;
|
|
858
936
|
if (logins.has(kind) || attemptedLogins.get(kind) === release) continue;
|
|
@@ -866,8 +944,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
866
944
|
})()
|
|
867
945
|
.catch((error) => {
|
|
868
946
|
if (loginSignal.aborted) return;
|
|
869
|
-
|
|
870
|
-
wake.notify();
|
|
947
|
+
fail(error);
|
|
871
948
|
})
|
|
872
949
|
.finally(() => logins.delete(kind));
|
|
873
950
|
logins.set(kind, { controller, task });
|
|
@@ -879,6 +956,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
879
956
|
let pointerPending = false;
|
|
880
957
|
let pointerTask = null;
|
|
881
958
|
const pointer = () => {
|
|
959
|
+
if (update.requested) return;
|
|
882
960
|
pointerPending = true;
|
|
883
961
|
if (pointerTask !== null) return;
|
|
884
962
|
pointerTask = (async () => {
|
|
@@ -890,8 +968,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
890
968
|
})()
|
|
891
969
|
.catch((error) => {
|
|
892
970
|
if (signal.aborted) return;
|
|
893
|
-
|
|
894
|
-
wake.notify();
|
|
971
|
+
fail(error);
|
|
895
972
|
})
|
|
896
973
|
.finally(() => {
|
|
897
974
|
pointerTask = null;
|
|
@@ -904,16 +981,22 @@ export async function runResident(options, dependencies = {}) {
|
|
|
904
981
|
AbortSignal.any([signal, watcher.signal]),
|
|
905
982
|
pointer,
|
|
906
983
|
dependencies,
|
|
907
|
-
).catch(
|
|
908
|
-
failure = error;
|
|
909
|
-
wake.notify();
|
|
910
|
-
});
|
|
984
|
+
).catch(fail);
|
|
911
985
|
try {
|
|
912
986
|
while (!signal.aborted) {
|
|
913
987
|
if (failure !== null) throw failure;
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
988
|
+
if (!session.refused) {
|
|
989
|
+
const configuration = await readConfiguration(options.configuration);
|
|
990
|
+
await controlledPass(context, dependencies, configuration, signal).catch((error) => {
|
|
991
|
+
if (!session.refused) throw error;
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
if (session.refused || update.requested) {
|
|
995
|
+
const sessions = await heldSessions(context);
|
|
996
|
+
if (sessions.length > 0) update.answer({ ready: false, sessions });
|
|
997
|
+
else if (session.refused || (!served.serving() && logins.size === 0 && pointerTask === null)) return;
|
|
998
|
+
}
|
|
999
|
+
await wake.wait(interval, signal);
|
|
917
1000
|
}
|
|
918
1001
|
} finally {
|
|
919
1002
|
watcher.abort();
|
|
@@ -922,7 +1005,31 @@ export async function runResident(options, dependencies = {}) {
|
|
|
922
1005
|
for (const running of logins.values()) running.controller.abort();
|
|
923
1006
|
await Promise.all([...logins.values()].map((running) => running.task));
|
|
924
1007
|
await context.agentRefresh;
|
|
925
|
-
await closeResident();
|
|
926
1008
|
}
|
|
927
|
-
}
|
|
1009
|
+
};
|
|
1010
|
+
try {
|
|
1011
|
+
for (;;) {
|
|
1012
|
+
try {
|
|
1013
|
+
await maintainControl(session, serve, options.signal, { release: false });
|
|
1014
|
+
} catch (error) {
|
|
1015
|
+
await releaseControl(session);
|
|
1016
|
+
throw error;
|
|
1017
|
+
}
|
|
1018
|
+
if (session.refused) break;
|
|
1019
|
+
try {
|
|
1020
|
+
await releaseControl(session, { required: true });
|
|
1021
|
+
break;
|
|
1022
|
+
} catch (refusal) {
|
|
1023
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
1024
|
+
if (session.refused) break;
|
|
1025
|
+
update.answer({ ready: false, sessions: [], refused: refusal.code });
|
|
1026
|
+
}
|
|
1027
|
+
await wake.wait(interval, options.signal);
|
|
1028
|
+
}
|
|
1029
|
+
update.quiesce();
|
|
1030
|
+
while (!options.signal.aborted) await wake.wait(interval, options.signal);
|
|
1031
|
+
throw new KitRefusal("kit_command_cancelled");
|
|
1032
|
+
} finally {
|
|
1033
|
+
await served.close();
|
|
1034
|
+
}
|
|
928
1035
|
}
|