@agentshouse/kit 0.1.0-alpha.11 → 0.1.0-alpha.13
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 +43 -5
- package/bin/connect-linux.sh +18 -2
- package/bin/kit.js +22 -2
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/binding.js +41 -0
- package/src/house.js +127 -51
- package/src/kit-work-stream.js +1 -0
- package/src/login.js +2 -1
- package/src/telegram.js +5 -3
- package/src/worker.js +209 -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.13/connect-linux.sh)
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The release asset pins one immutable multi-architecture image digest. It creates
|
|
@@ -20,8 +20,31 @@ 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, including one whose Run
|
|
31
|
+
was being acquired when the drain began, the bootstrap names it and leaves the
|
|
32
|
+
container as it is; rerun it once they end. Once nothing remains,
|
|
33
|
+
the resident stops renewing, releases its exact lease and stays quiescent, and
|
|
34
|
+
the bootstrap replaces the container with one on this release's image over the
|
|
35
|
+
same Kit home and workspace. The credential, Environment, Kit-home state and
|
|
36
|
+
everything under the workspace are kept; anything installed elsewhere in the old
|
|
37
|
+
container is not. A stopped container is replaced without a drain.
|
|
38
|
+
|
|
39
|
+
If House refuses the installed Kit with `kit_upgrade_required`, including in
|
|
40
|
+
its answer to a command the resident serves, the resident calls House no more,
|
|
41
|
+
neither to release nor to report. It keeps supervising
|
|
42
|
+
every running Session locally and never stops one to make progress. A Session
|
|
43
|
+
that ends keeps its exact binding, stop observation and outcome in Kit home,
|
|
44
|
+
and the drain answers ready once no supervised process runs. The updated Kit
|
|
45
|
+
waits for the old lease to expire, acquires the next one and reports each held
|
|
46
|
+
Session before it acquires any work, restoring no process and inventing no
|
|
47
|
+
outcome.
|
|
25
48
|
|
|
26
49
|
The versioned `bootstrap.json` is the machine-readable command source for this
|
|
27
50
|
release. It is exported from the npm package as `@agentshouse/kit/bootstrap`
|
|
@@ -981,7 +1004,12 @@ content, no prompt, and no resolved secret, and Session authority is always
|
|
|
981
1004
|
reissued by House rather than recovered from disk. It is evidence for
|
|
982
1005
|
reconciliation, never permission to resume or recreate work, and nothing keeps
|
|
983
1006
|
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.
|
|
1007
|
+
owner stop, and a retained Provider continuation each drop it. When the
|
|
1008
|
+
supervised process ends, the binding first records what the Kit observed: the
|
|
1009
|
+
sealed final text, whether the turn completed, the capacity or provider refusal,
|
|
1010
|
+
the execution failure and whether a Provider continuation is retained. The Kit
|
|
1011
|
+
then reports that stop, and a restarted Kit reports the same recorded stop
|
|
1012
|
+
instead of treating the session as lost.
|
|
985
1013
|
|
|
986
1014
|
A restarted `kit worker run` reconciles the records it kept before it acquires
|
|
987
1015
|
anything new. It reports every retained settlement note first, because those
|
|
@@ -1141,7 +1169,16 @@ cancels its request there, and the lease stays held when the command ends. The
|
|
|
1141
1169
|
resident serves one such command at a time and refuses another with
|
|
1142
1170
|
`environment_control_lease_active`, so two commands still never work at once.
|
|
1143
1171
|
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.
|
|
1172
|
+
command never acquires a lease of its own while a resident serves it. While an
|
|
1173
|
+
update drains, `state/update-drain` refuses every such command, `kit login` and
|
|
1174
|
+
`kit worker run` with `kit_update_draining` before it enrolls, acquires or
|
|
1175
|
+
delegates anything. A command already served keeps the resident's lease until
|
|
1176
|
+
it ends, while the resident refuses its acquisition of new work and its
|
|
1177
|
+
Telegram updates with `kit_update_draining`, which ends a served
|
|
1178
|
+
`kit telegram serve` once each of its activities reports the deliveries and
|
|
1179
|
+
files it already acquired; until it ends, the drain answers that it waits for
|
|
1180
|
+
that command and the bootstrap leaves the container as it is. The bootstrap
|
|
1181
|
+
removes the file when it replaces the container.
|
|
1145
1182
|
|
|
1146
1183
|
Interrupting the command cancels the running connector without advancing the
|
|
1147
1184
|
checkpoint.
|
|
@@ -1468,6 +1505,7 @@ $HOUSE_KIT_HOME/
|
|
|
1468
1505
|
state/attachments/<upload identity>.json
|
|
1469
1506
|
state/conversation-originals/<attachment identity>.json
|
|
1470
1507
|
state/sessions/<tmux session>.json
|
|
1508
|
+
state/update-drain
|
|
1471
1509
|
state/continuations/<run identity>.json
|
|
1472
1510
|
state/<instance>/credentials/google.json
|
|
1473
1511
|
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,23 @@ 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
|
+
: answer.serving === true
|
|
181
|
+
? "House Kit is draining for its update and waits for the Kit command it serves."
|
|
182
|
+
: `House Kit is draining for its update; House refused its lease release (${answer.refused}).`,
|
|
183
|
+
);
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
170
187
|
async function runSetup(args, directory) {
|
|
171
188
|
const { values } = declared(args, ["--house", "--config"], 0, { exact: true });
|
|
172
189
|
const house = await selectedHouse(values, directory);
|
|
@@ -965,7 +982,10 @@ async function main() {
|
|
|
965
982
|
const directory = process.env.HOUSE_KIT_HOME ?? join(homedir(), ".house-kit");
|
|
966
983
|
if (command === "login") return runLogin(args, directory);
|
|
967
984
|
if (command === "credential" && args[0] === "check") return runCredentialCheck(args.slice(1), directory);
|
|
968
|
-
if (command === "resident")
|
|
985
|
+
if (command === "resident") {
|
|
986
|
+
if (args[0] === "drain") return runResidentDrain(args.slice(1), directory);
|
|
987
|
+
return runResidentCommand(args, directory);
|
|
988
|
+
}
|
|
969
989
|
if (command === "setup") return runSetup(args, directory);
|
|
970
990
|
if (command === "apply") return runApply(args, directory);
|
|
971
991
|
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.13",
|
|
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.13/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.13/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
|
|
|
@@ -53,6 +56,10 @@ async function readCredential(directory) {
|
|
|
53
56
|
return stored;
|
|
54
57
|
}
|
|
55
58
|
|
|
59
|
+
function latchRefusal(answer, dependencies) {
|
|
60
|
+
if (answer?.error?.code === UPGRADE_REQUIRED) dependencies.contractRefused?.();
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
async function houseAnswer(house, path, init, dependencies, signal) {
|
|
57
64
|
const fetcher = dependencies.fetch ?? fetch;
|
|
58
65
|
let response;
|
|
@@ -70,6 +77,7 @@ async function houseAnswer(house, path, init, dependencies, signal) {
|
|
|
70
77
|
} catch {
|
|
71
78
|
answer = null;
|
|
72
79
|
}
|
|
80
|
+
latchRefusal(answer, dependencies);
|
|
73
81
|
throwHouseAnswer(response.status, answer, path);
|
|
74
82
|
if (answer === null) throw new KitRefusal("kit_house_unavailable", path);
|
|
75
83
|
return answer;
|
|
@@ -113,16 +121,31 @@ function residentSocket(directory) {
|
|
|
113
121
|
return join(directory, "run", "resident.sock");
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
export function updateDrain(directory) {
|
|
125
|
+
return join(directory, "state", "update-drain");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function refuseDrained(directory) {
|
|
129
|
+
if (existsSync(updateDrain(directory))) throw new KitRefusal(UPDATE_DRAINING);
|
|
130
|
+
}
|
|
131
|
+
|
|
116
132
|
function residentAnswer(status, code) {
|
|
117
133
|
const body = Buffer.from(JSON.stringify({ error: { code } })).toString("base64");
|
|
118
134
|
return { status, type: "application/json", body };
|
|
119
135
|
}
|
|
120
136
|
|
|
121
|
-
|
|
137
|
+
function newWork(path) {
|
|
138
|
+
return path === "/kit/telegram/updates" || (path.endsWith("/acquire") && path !== "/kit/commands/acquire");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function residentRelay(session, request, signal, draining) {
|
|
122
142
|
const path = typeof request.path === "string" ? request.path : "";
|
|
123
143
|
if (!path.startsWith("/kit/") || path.startsWith("/kit/lease")) {
|
|
124
144
|
return residentAnswer(403, "kit_resident_route_refused");
|
|
125
145
|
}
|
|
146
|
+
if (draining && newWork(path)) {
|
|
147
|
+
return residentAnswer(409, UPDATE_DRAINING);
|
|
148
|
+
}
|
|
126
149
|
let authorization;
|
|
127
150
|
try {
|
|
128
151
|
authorization = await heldControl(session, signal);
|
|
@@ -137,6 +160,7 @@ async function residentRelay(session, request, signal) {
|
|
|
137
160
|
signal,
|
|
138
161
|
});
|
|
139
162
|
const body = Buffer.from(await response.arrayBuffer());
|
|
163
|
+
if (response.status === 409) latchRefusal(JSON.parse(body.toString("utf8")), session.dependencies);
|
|
140
164
|
return {
|
|
141
165
|
status: response.status,
|
|
142
166
|
type: response.headers.get("content-type"),
|
|
@@ -147,63 +171,105 @@ async function residentRelay(session, request, signal) {
|
|
|
147
171
|
}
|
|
148
172
|
}
|
|
149
173
|
|
|
150
|
-
export async function serveResident(session, directory, signal) {
|
|
174
|
+
export async function serveResident(session, directory, signal, drain) {
|
|
151
175
|
const socket = residentSocket(directory);
|
|
152
176
|
await mkdir(dirname(socket), { recursive: true, mode: 0o700 });
|
|
153
177
|
await rm(socket, { force: true });
|
|
178
|
+
const connections = new Set();
|
|
154
179
|
let holder = null;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
let draining = false;
|
|
181
|
+
function answerDrain(connection) {
|
|
182
|
+
draining = true;
|
|
183
|
+
drain(holder !== null).then(
|
|
184
|
+
(answer) => {
|
|
185
|
+
if (!connection.destroyed) writeFrame(connection, answer);
|
|
186
|
+
connection.end();
|
|
187
|
+
},
|
|
188
|
+
() => connection.destroy(),
|
|
189
|
+
);
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
function hold(connection) {
|
|
193
|
+
if (holder !== null || draining || existsSync(updateDrain(directory))) {
|
|
194
|
+
writeFrame(connection, { refused: draining || holder === null ? UPDATE_DRAINING : LEASE_ACTIVE });
|
|
159
195
|
connection.end();
|
|
160
|
-
return;
|
|
196
|
+
return false;
|
|
161
197
|
}
|
|
162
198
|
holder = connection;
|
|
199
|
+
renewControl(session, signal).then(
|
|
200
|
+
() => writeFrame(connection, { held: true }),
|
|
201
|
+
(error) => {
|
|
202
|
+
writeFrame(connection, { refused: error.code ?? "kit_lease_rejected" });
|
|
203
|
+
connection.end();
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
const server = createServer((connection) => {
|
|
209
|
+
connections.add(connection);
|
|
210
|
+
connection.on("error", () => connection.destroy());
|
|
163
211
|
const relayed = new Map();
|
|
212
|
+
let held = null;
|
|
164
213
|
connection.on("close", () => {
|
|
214
|
+
connections.delete(connection);
|
|
165
215
|
if (holder === connection) holder = null;
|
|
166
216
|
for (const cancelled of relayed.values()) cancelled.abort();
|
|
167
217
|
});
|
|
168
|
-
|
|
169
|
-
() => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
);
|
|
218
|
+
readFrames(connection, {
|
|
219
|
+
onFrame: (request) => {
|
|
220
|
+
if (held === null) {
|
|
221
|
+
held = request.drain === true ? answerDrain(connection) : hold(connection);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!held) return;
|
|
225
|
+
if (request.cancel === true) {
|
|
226
|
+
relayed.get(request.id)?.abort();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const cancelled = new AbortController();
|
|
230
|
+
relayed.set(request.id, cancelled);
|
|
231
|
+
void residentRelay(session, request, AbortSignal.any([signal, cancelled.signal]), draining).then(
|
|
232
|
+
(answer) => {
|
|
233
|
+
relayed.delete(request.id);
|
|
234
|
+
if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
|
|
185
235
|
},
|
|
186
|
-
|
|
187
|
-
});
|
|
188
|
-
},
|
|
189
|
-
(error) => {
|
|
190
|
-
writeFrame(connection, { refused: error.code ?? "kit_lease_rejected" });
|
|
191
|
-
connection.end();
|
|
236
|
+
);
|
|
192
237
|
},
|
|
193
|
-
|
|
238
|
+
onRefusal: () => connection.destroy(),
|
|
239
|
+
});
|
|
194
240
|
});
|
|
195
241
|
await new Promise((resolve, reject) => {
|
|
196
242
|
server.once("error", reject);
|
|
197
243
|
server.listen(socket, resolve);
|
|
198
244
|
});
|
|
199
245
|
await chmod(socket, 0o600);
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
246
|
+
return {
|
|
247
|
+
serving: () => holder !== null,
|
|
248
|
+
async close() {
|
|
249
|
+
for (const connection of connections) connection.destroy();
|
|
250
|
+
await new Promise((resolve) => server.close(resolve));
|
|
251
|
+
await rm(socket, { force: true });
|
|
252
|
+
},
|
|
204
253
|
};
|
|
205
254
|
}
|
|
206
255
|
|
|
256
|
+
export function requestDrain(directory) {
|
|
257
|
+
const connection = connect(residentSocket(directory));
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
const unavailable = () => reject(new KitRefusal("kit_resident_unavailable"));
|
|
260
|
+
connection.on("error", unavailable);
|
|
261
|
+
connection.on("close", unavailable);
|
|
262
|
+
readFrames(connection, {
|
|
263
|
+
onFrame: (answer) => {
|
|
264
|
+
resolve(answer);
|
|
265
|
+
connection.end();
|
|
266
|
+
},
|
|
267
|
+
onRefusal: () => connection.destroy(),
|
|
268
|
+
});
|
|
269
|
+
writeFrame(connection, { drain: true });
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
207
273
|
async function residentDelegation(directory, fetcher) {
|
|
208
274
|
const connection = connect(residentSocket(directory));
|
|
209
275
|
const pending = new Map();
|
|
@@ -226,6 +292,7 @@ async function residentDelegation(directory, fetcher) {
|
|
|
226
292
|
onFrame: (frame) => (frame.id === undefined ? greet(frame) : settle(frame.id, frame)),
|
|
227
293
|
onRefusal: () => connection.destroy(),
|
|
228
294
|
});
|
|
295
|
+
writeFrame(connection, { hold: true });
|
|
229
296
|
const held = await greeting;
|
|
230
297
|
if (held === null) return null;
|
|
231
298
|
if (held.held !== true) {
|
|
@@ -276,6 +343,7 @@ async function residentDelegation(directory, fetcher) {
|
|
|
276
343
|
}
|
|
277
344
|
|
|
278
345
|
export async function openSession(options, dependencies = {}) {
|
|
346
|
+
if (options.resident !== true) refuseDrained(options.directory);
|
|
279
347
|
const session = {
|
|
280
348
|
house: options.house,
|
|
281
349
|
dependencies,
|
|
@@ -285,7 +353,7 @@ export async function openSession(options, dependencies = {}) {
|
|
|
285
353
|
signal: options.signal,
|
|
286
354
|
};
|
|
287
355
|
const resident =
|
|
288
|
-
options.acquire === true
|
|
356
|
+
options.acquire === true || options.resident === true
|
|
289
357
|
? null
|
|
290
358
|
: await residentDelegation(options.directory, dependencies.fetch ?? fetch);
|
|
291
359
|
if (resident !== null) {
|
|
@@ -293,24 +361,30 @@ export async function openSession(options, dependencies = {}) {
|
|
|
293
361
|
session.dependencies = { ...dependencies, fetch: resident.fetch };
|
|
294
362
|
return session;
|
|
295
363
|
}
|
|
364
|
+
session.dependencies = {
|
|
365
|
+
...dependencies,
|
|
366
|
+
contractRefused: () => {
|
|
367
|
+
session.refused = true;
|
|
368
|
+
},
|
|
369
|
+
};
|
|
296
370
|
const credential = await readCredential(options.directory);
|
|
297
|
-
|
|
298
|
-
session,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
);
|
|
371
|
+
const answer = await houseRequest(
|
|
372
|
+
session.house,
|
|
373
|
+
`Bearer ${credential}`,
|
|
374
|
+
"/kit/lease",
|
|
375
|
+
{},
|
|
376
|
+
session.dependencies,
|
|
377
|
+
session.signal,
|
|
378
|
+
).catch((error) => {
|
|
379
|
+
if (options.resident === true && session.refused) return null;
|
|
380
|
+
throw error;
|
|
381
|
+
});
|
|
382
|
+
if (answer !== null) Object.assign(session, heldLease(answer));
|
|
310
383
|
return session;
|
|
311
384
|
}
|
|
312
385
|
|
|
313
386
|
async function renewControl(session, signal) {
|
|
387
|
+
if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
|
|
314
388
|
Object.assign(
|
|
315
389
|
session,
|
|
316
390
|
heldLease(
|
|
@@ -328,13 +402,14 @@ async function renewControl(session, signal) {
|
|
|
328
402
|
|
|
329
403
|
export async function heldControl(session, signal = session.signal) {
|
|
330
404
|
if (session.resident !== undefined) return RESIDENT;
|
|
405
|
+
if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
|
|
331
406
|
if (session.expiresAt - session.now().getTime() <= LEASE_RENEWAL_MARGIN_MS) {
|
|
332
407
|
await renewControl(session, signal);
|
|
333
408
|
}
|
|
334
409
|
return `Bearer ${session.lease}`;
|
|
335
410
|
}
|
|
336
411
|
|
|
337
|
-
function controlSleep(milliseconds, signal) {
|
|
412
|
+
export function controlSleep(milliseconds, signal) {
|
|
338
413
|
return new Promise((resolve, reject) => {
|
|
339
414
|
if (signal.aborted) {
|
|
340
415
|
reject(new KitRefusal("kit_command_cancelled"));
|
|
@@ -382,6 +457,7 @@ export async function releaseControl(session, options = {}) {
|
|
|
382
457
|
if (session.lease === null) return;
|
|
383
458
|
const held = session.lease;
|
|
384
459
|
try {
|
|
460
|
+
if (session.refused) throw new KitRefusal(UPGRADE_REQUIRED);
|
|
385
461
|
await houseRequest(
|
|
386
462
|
session.house,
|
|
387
463
|
`Bearer ${held}`,
|
|
@@ -430,7 +506,7 @@ export async function maintainControl(session, operation, signal, { release = tr
|
|
|
430
506
|
throw new KitRefusal("kit_lease_rejected");
|
|
431
507
|
}
|
|
432
508
|
} catch (error) {
|
|
433
|
-
if (stopped) return;
|
|
509
|
+
if (stopped || session.refused) return;
|
|
434
510
|
rejectFailure(error);
|
|
435
511
|
maintenance.abort();
|
|
436
512
|
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/telegram.js
CHANGED
|
@@ -863,20 +863,21 @@ async function onePass(gateway, activity, work) {
|
|
|
863
863
|
try {
|
|
864
864
|
await work(gateway);
|
|
865
865
|
} catch (error) {
|
|
866
|
-
if (cancelled(error) || !(error instanceof KitRefusal)) throw error;
|
|
866
|
+
if (cancelled(error) || !(error instanceof KitRefusal) || error.code === "kit_update_draining") throw error;
|
|
867
867
|
gateway.summary.failures.push({ activity, item: null, code: error.code });
|
|
868
868
|
}
|
|
869
869
|
}
|
|
870
870
|
|
|
871
871
|
async function paceActivity(gateway, stopping, activity, work) {
|
|
872
872
|
try {
|
|
873
|
-
for (let index = 0; index < gateway.passes; index += 1) {
|
|
873
|
+
for (let index = 0; index < gateway.passes && !gateway.draining; index += 1) {
|
|
874
874
|
if (gateway.signal?.aborted === true) throw new KitRefusal("kit_command_cancelled");
|
|
875
875
|
await onePass(gateway, activity, work);
|
|
876
876
|
if (index + 1 < gateway.passes) await gateway.wait(TELEGRAM_GATEWAY_POLL_MS, gateway.signal);
|
|
877
877
|
}
|
|
878
878
|
} catch (error) {
|
|
879
|
-
|
|
879
|
+
if (error.code === "kit_update_draining") gateway.draining = true;
|
|
880
|
+
else stopping.abort();
|
|
880
881
|
throw error;
|
|
881
882
|
}
|
|
882
883
|
}
|
|
@@ -964,6 +965,7 @@ function gatewayContext(session, options, signal, summary = emptySummary()) {
|
|
|
964
965
|
gate: rateGate(() => session.now().getTime()),
|
|
965
966
|
wait: session.dependencies.wait ?? pause,
|
|
966
967
|
passes: options.passes ?? Number.POSITIVE_INFINITY,
|
|
968
|
+
draining: false,
|
|
967
969
|
summary,
|
|
968
970
|
};
|
|
969
971
|
}
|
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
|
}
|
|
@@ -186,6 +177,7 @@ async function supervise(context, bound, bridge) {
|
|
|
186
177
|
while (live) {
|
|
187
178
|
await Promise.race([terminal, delay(WORKER_SUPERVISION_POLL_MS)]);
|
|
188
179
|
if (!live) return null;
|
|
180
|
+
await context.update?.announce();
|
|
189
181
|
if (Date.now() >= commanded) {
|
|
190
182
|
commanded = Date.now() + WORKER_COMMAND_POLL_MS;
|
|
191
183
|
const polled = await pendingCommands(context, ["stop", "interrupt", "answer"]);
|
|
@@ -227,7 +219,7 @@ async function completeOwnerStop(context, bound, requestedAt) {
|
|
|
227
219
|
try {
|
|
228
220
|
await observeSessionStop(context.session, bound.session);
|
|
229
221
|
} catch (refusal) {
|
|
230
|
-
if (
|
|
222
|
+
if (undecided(context, refusal)) return { destroyed, confirmed: false, failure: null };
|
|
231
223
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
232
224
|
failure = { code: refusal.code, detail: refusal.message };
|
|
233
225
|
}
|
|
@@ -241,7 +233,7 @@ async function settleReported(context, bound) {
|
|
|
241
233
|
await reportRunOutcome(context.session, bound.run, bound.reported);
|
|
242
234
|
return { settled: true, failure: null };
|
|
243
235
|
} catch (refusal) {
|
|
244
|
-
if (undecided(refusal)) return { settled: false, failure: null };
|
|
236
|
+
if (undecided(context, refusal)) return { settled: false, failure: null };
|
|
245
237
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
246
238
|
if (refusal.code === "worker_run_settled") return { settled: true, failure: null };
|
|
247
239
|
return { settled: false, failure: { code: refusal.code, detail: refusal.message } };
|
|
@@ -375,50 +367,65 @@ async function superviseBound(context, bound, opened, start) {
|
|
|
375
367
|
held !== null &&
|
|
376
368
|
stopped?.providerContinuation === true &&
|
|
377
369
|
stopped.providerSessionId !== null;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
370
|
+
if (continuable) {
|
|
371
|
+
await writeContinuation(context.directory, { ...held, providerSessionId: stopped.providerSessionId });
|
|
372
|
+
}
|
|
373
|
+
const turnCompleted = stopped?.stopOutcome === "completed";
|
|
374
|
+
const blocks = turnCompleted ? assistantBlocks(outcome.assistant.join("\n\n")) : null;
|
|
375
|
+
bound.sealed = blocks === null ? null : { operation: operationId(), blocks };
|
|
376
|
+
bound.stopped = {
|
|
377
|
+
turnCompleted,
|
|
378
|
+
capacityExhausted,
|
|
379
|
+
providerRefused,
|
|
380
|
+
continuable,
|
|
381
|
+
failure: outcome.failure,
|
|
382
|
+
textAccepted: false,
|
|
383
|
+
};
|
|
384
|
+
await writeBinding(context.directory, bound);
|
|
385
|
+
return { ...outcome, ...(await settleStopped(context, bound)) };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function stoppedReport(bound) {
|
|
389
|
+
if (bound.stopped === null || bound.stopped === undefined) return null;
|
|
390
|
+
return terminalReport({ ...bound.stopped, conversation: bound.conversation === true });
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function settleStopped(context, bound) {
|
|
394
|
+
const { stopped } = bound;
|
|
395
|
+
const unresolved = (refused) => ({ failure: stopped.failure, refused, settled: false });
|
|
396
|
+
if (bound.sealed !== null) {
|
|
397
|
+
const refused = await dispatchSeal(context, bound);
|
|
398
|
+
if (refused !== null) return unresolved(refused);
|
|
399
|
+
}
|
|
400
|
+
let failure = stopped.failure;
|
|
385
401
|
try {
|
|
386
|
-
await observeSessionStop(context.session, bound.session,
|
|
402
|
+
await observeSessionStop(context.session, bound.session, stopped.textAccepted);
|
|
387
403
|
} catch (refusal) {
|
|
404
|
+
if (undecided(context, refusal)) return unresolved(null);
|
|
388
405
|
if (!(refusal instanceof KitRefusal) || refusal.code !== "conversation_final_text_pending") throw refusal;
|
|
389
|
-
|
|
406
|
+
failure ??= { code: refusal.code, detail: refusal.message };
|
|
390
407
|
await observeSessionStop(context.session, bound.session);
|
|
391
408
|
}
|
|
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,
|
|
409
|
+
const report = stoppedReport(bound);
|
|
410
|
+
if (report !== null) await removeContinuation(context.directory, bound.run);
|
|
411
|
+
try {
|
|
412
|
+
await reportContinuation(context.session, bound.session, {
|
|
413
|
+
providerContinuation: report === null,
|
|
414
|
+
providerCapacityExhausted: stopped.capacityExhausted,
|
|
405
415
|
});
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
416
|
+
} catch (refusal) {
|
|
417
|
+
if (undecided(context, refusal)) return unresolved(null);
|
|
418
|
+
if (!(refusal instanceof KitRefusal) || refusal.code !== "worker_run_settled") throw refusal;
|
|
419
|
+
await removeContinuation(context.directory, bound.run);
|
|
420
|
+
await removeBinding(context.directory, bound.tmux);
|
|
421
|
+
return { failure, refused: null, settled: true };
|
|
422
|
+
}
|
|
423
|
+
if (report === null) {
|
|
412
424
|
await removeBinding(context.directory, bound.tmux);
|
|
413
|
-
return
|
|
425
|
+
return { failure, refused: null, settled: true };
|
|
414
426
|
}
|
|
415
427
|
const settled = await reportTerminalState(context, bound, report);
|
|
416
|
-
return {
|
|
417
|
-
...outcome,
|
|
418
|
-
reported: settled.reported,
|
|
419
|
-
refused: settled.failure,
|
|
420
|
-
settled: settled.settled,
|
|
421
|
-
};
|
|
428
|
+
return { failure, reported: settled.reported, refused: settled.failure, settled: settled.settled };
|
|
422
429
|
}
|
|
423
430
|
|
|
424
431
|
function assignmentRoute(context, assignment) {
|
|
@@ -506,7 +513,7 @@ async function superviseAssignment(context, assignment, resumed = false) {
|
|
|
506
513
|
detail: refusal instanceof KitRefusal ? refusal.message : null,
|
|
507
514
|
};
|
|
508
515
|
if (resumed) return refuseContinuation(context, assignment, failure);
|
|
509
|
-
if (undecided(refusal)) throw refusal;
|
|
516
|
+
if (undecided(context, refusal)) throw refusal;
|
|
510
517
|
return refuseAssignment(context, assignment, failure);
|
|
511
518
|
}
|
|
512
519
|
const { bound, opened } = prepared;
|
|
@@ -555,7 +562,7 @@ async function settleAbsentSession(context, binding) {
|
|
|
555
562
|
providerCapacityExhausted: false,
|
|
556
563
|
});
|
|
557
564
|
} catch (refusal) {
|
|
558
|
-
if (undecided(refusal)) return { settled: false, failure: null };
|
|
565
|
+
if (undecided(context, refusal)) return { settled: false, failure: null };
|
|
559
566
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
560
567
|
}
|
|
561
568
|
await removeContinuation(context.directory, binding.run);
|
|
@@ -571,7 +578,7 @@ async function confirmRestorable(context, binding) {
|
|
|
571
578
|
try {
|
|
572
579
|
authority = await replaceSessionAuthority(context.session, binding.session);
|
|
573
580
|
} catch (refusal) {
|
|
574
|
-
if (undecided(refusal)) return { authority: null, unresolved: true };
|
|
581
|
+
if (undecided(context, refusal)) return { authority: null, unresolved: true };
|
|
575
582
|
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
576
583
|
await destroySession(context.runtime, binding.tmux);
|
|
577
584
|
await removeBinding(context.directory, binding.tmux);
|
|
@@ -593,17 +600,17 @@ async function reconcile(context) {
|
|
|
593
600
|
const commands = (await pendingCommands(context)).stops;
|
|
594
601
|
const carried = [];
|
|
595
602
|
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
603
|
if (binding.reported !== null) {
|
|
602
604
|
const settled = await settleReported(context, binding);
|
|
603
605
|
if (settled.settled) await removeBinding(context.directory, binding.tmux);
|
|
604
606
|
else unresolved.push(unresolvedRun(binding, settled.failure));
|
|
605
607
|
continue;
|
|
606
608
|
}
|
|
609
|
+
if (binding.stopped !== null) {
|
|
610
|
+
const settled = await settleStopped(context, binding);
|
|
611
|
+
if (!settled.settled) unresolved.push(unresolvedRun(binding, settled.refused));
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
607
614
|
const command = commands.find((pending) => pending.run === binding.run) ?? null;
|
|
608
615
|
const requestedAt = binding.ownerStop ?? command?.requestedAt ?? null;
|
|
609
616
|
if (requestedAt === null) {
|
|
@@ -643,7 +650,7 @@ async function reportHeldSessions(context) {
|
|
|
643
650
|
const continued = (await readContinuationRuns(context.directory)).map((run) => ({ run, reported: null }));
|
|
644
651
|
const held = new Map();
|
|
645
652
|
for (const entry of [...settlements, ...bindings, ...continued]) {
|
|
646
|
-
held.set(entry.run, held.get(entry.run) ?? entry.reported);
|
|
653
|
+
held.set(entry.run, held.get(entry.run) ?? entry.reported ?? stoppedReport(entry));
|
|
647
654
|
}
|
|
648
655
|
const reported = await reportSessions(
|
|
649
656
|
context.session,
|
|
@@ -670,17 +677,23 @@ async function afterEpochReport(context, pass) {
|
|
|
670
677
|
return pass();
|
|
671
678
|
}
|
|
672
679
|
|
|
680
|
+
function unlessDraining(context, act) {
|
|
681
|
+
return context.update?.requested ? null : act();
|
|
682
|
+
}
|
|
683
|
+
|
|
673
684
|
async function workerPass(context, dependencies, baseConfiguration) {
|
|
674
685
|
const { session, signal } = context;
|
|
675
686
|
const reconciled = await reconcile(context);
|
|
676
|
-
if (reconciled.run !== null || reconciled.unresolved.length > 0)
|
|
687
|
+
if (reconciled.run !== null || reconciled.unresolved.length > 0 || context.update?.requested) {
|
|
688
|
+
return reconciled;
|
|
689
|
+
}
|
|
677
690
|
context.busy = false;
|
|
678
691
|
const needsLogin = await queueAgentRefresh(context, dependencies, signal, baseConfiguration);
|
|
679
692
|
context.startLogins?.(needsLogin);
|
|
680
693
|
context.busy = true;
|
|
681
694
|
const { configuration } = context;
|
|
682
|
-
context.inbound = await reconcileInbound(context.directory, session);
|
|
683
|
-
const setup = await acquireSetup(session, signal);
|
|
695
|
+
context.inbound = await unlessDraining(context, () => reconcileInbound(context.directory, session));
|
|
696
|
+
const setup = await unlessDraining(context, () => acquireSetup(session, signal));
|
|
684
697
|
if (setup !== null) {
|
|
685
698
|
context.setup = await runSetup(
|
|
686
699
|
{
|
|
@@ -697,19 +710,21 @@ async function workerPass(context, dependencies, baseConfiguration) {
|
|
|
697
710
|
const needsLogin = await queueAgentRefresh(context, dependencies, signal, undefined, true);
|
|
698
711
|
context.startLogins?.(needsLogin);
|
|
699
712
|
}
|
|
700
|
-
const resumption = await acquireOneResumption(session);
|
|
713
|
+
const resumption = await unlessDraining(context, () => acquireOneResumption(session));
|
|
701
714
|
if (resumption !== null) return { ...reconciled, run: await superviseResumption(context, resumption) };
|
|
702
|
-
const assignment = await acquireOneRun(session);
|
|
715
|
+
const assignment = await unlessDraining(context, () => acquireOneRun(session));
|
|
703
716
|
if (assignment === null) return reconciled;
|
|
704
717
|
return { ...reconciled, run: await superviseAssignment(context, assignment) };
|
|
705
718
|
}
|
|
706
719
|
|
|
707
|
-
async function workerContext(options, dependencies) {
|
|
720
|
+
async function workerContext(options, dependencies, resident = false) {
|
|
708
721
|
const configuration = await readConfiguration(options.configuration);
|
|
709
722
|
const runtime = workerRuntime(options.directory, dependencies.tmux ?? "tmux");
|
|
710
723
|
const launcher = dependencies.launcher ?? [process.execPath, LAUNCHER_MAIN];
|
|
711
724
|
const tmux = await readyTmux(runtime);
|
|
712
|
-
const session =
|
|
725
|
+
const session = resident
|
|
726
|
+
? await residentSession(options, dependencies)
|
|
727
|
+
: await openSession({ ...options, acquire: true }, dependencies);
|
|
713
728
|
const context = {
|
|
714
729
|
configuration,
|
|
715
730
|
runtime,
|
|
@@ -827,14 +842,83 @@ function residentWake() {
|
|
|
827
842
|
};
|
|
828
843
|
}
|
|
829
844
|
|
|
845
|
+
function residentUpdate(context, wake) {
|
|
846
|
+
let requested = existsSync(updateDrain(context.directory));
|
|
847
|
+
let ready = null;
|
|
848
|
+
const waiting = [];
|
|
849
|
+
return {
|
|
850
|
+
get requested() {
|
|
851
|
+
return requested;
|
|
852
|
+
},
|
|
853
|
+
async request(serving) {
|
|
854
|
+
if (!requested) {
|
|
855
|
+
requested = true;
|
|
856
|
+
await writeText(updateDrain(context.directory), "");
|
|
857
|
+
}
|
|
858
|
+
wake.notify();
|
|
859
|
+
const sessions = await heldSessions(context);
|
|
860
|
+
if (ready !== null) return ready;
|
|
861
|
+
if (serving) return { ready: false, sessions, serving };
|
|
862
|
+
if (sessions.length > 0) return { ready: false, sessions };
|
|
863
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
864
|
+
},
|
|
865
|
+
answer(answer) {
|
|
866
|
+
for (const resolve of waiting.splice(0)) resolve(answer);
|
|
867
|
+
},
|
|
868
|
+
async announce() {
|
|
869
|
+
if (waiting.length === 0) return;
|
|
870
|
+
const sessions = await heldSessions(context);
|
|
871
|
+
if (sessions.length > 0) this.answer({ ready: false, sessions });
|
|
872
|
+
},
|
|
873
|
+
quiesce() {
|
|
874
|
+
ready = { ready: true, sessions: [] };
|
|
875
|
+
this.answer(ready);
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function heldSessions(context) {
|
|
881
|
+
const bindings = await readBindings(context.directory);
|
|
882
|
+
if (!context.session.refused) {
|
|
883
|
+
return [...bindings, ...(await readSettlements(context.directory))].map((held) => held.run);
|
|
884
|
+
}
|
|
885
|
+
const live = [];
|
|
886
|
+
for (const binding of bindings) {
|
|
887
|
+
if (binding.stopped === null && (await sessionPresent(context.runtime, binding.tmux))) {
|
|
888
|
+
live.push(binding.run);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return live;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
async function residentSession(options, dependencies) {
|
|
895
|
+
for (;;) {
|
|
896
|
+
try {
|
|
897
|
+
return await openSession({ ...options, resident: true }, dependencies);
|
|
898
|
+
} catch (refusal) {
|
|
899
|
+
if (refusal?.code !== "environment_control_lease_active") throw refusal;
|
|
900
|
+
await controlSleep(dependencies.residentIntervalMs ?? RESIDENT_INTERVAL_MS, options.signal);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
830
905
|
export async function runResident(options, dependencies = {}) {
|
|
831
|
-
const { context } = await workerContext(options, dependencies);
|
|
832
|
-
|
|
833
|
-
|
|
906
|
+
const { context } = await workerContext(options, dependencies, true);
|
|
907
|
+
const { session } = context;
|
|
908
|
+
const interval = dependencies.residentIntervalMs ?? RESIDENT_INTERVAL_MS;
|
|
909
|
+
const wake = residentWake();
|
|
910
|
+
const update = residentUpdate(context, wake);
|
|
911
|
+
context.update = update;
|
|
912
|
+
const served = await serveResident(session, context.directory, options.signal, (serving) => update.request(serving));
|
|
913
|
+
const serve = async (signal) => {
|
|
834
914
|
context.busy = true;
|
|
835
|
-
const wake = residentWake();
|
|
836
915
|
const watcher = new AbortController();
|
|
837
916
|
let failure = null;
|
|
917
|
+
const fail = (error) => {
|
|
918
|
+
if (signal.aborted || session.refused) return;
|
|
919
|
+
failure = error;
|
|
920
|
+
wake.notify();
|
|
921
|
+
};
|
|
838
922
|
const logins = new Map();
|
|
839
923
|
const attemptedLogins = new Map();
|
|
840
924
|
context.cancelLogins = (wanted) => {
|
|
@@ -853,6 +937,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
853
937
|
}
|
|
854
938
|
};
|
|
855
939
|
context.startLogins = (agents) => {
|
|
940
|
+
if (update.requested) return;
|
|
856
941
|
for (const agent of agents) {
|
|
857
942
|
const { kind, release } = agent;
|
|
858
943
|
if (logins.has(kind) || attemptedLogins.get(kind) === release) continue;
|
|
@@ -866,8 +951,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
866
951
|
})()
|
|
867
952
|
.catch((error) => {
|
|
868
953
|
if (loginSignal.aborted) return;
|
|
869
|
-
|
|
870
|
-
wake.notify();
|
|
954
|
+
fail(error);
|
|
871
955
|
})
|
|
872
956
|
.finally(() => logins.delete(kind));
|
|
873
957
|
logins.set(kind, { controller, task });
|
|
@@ -879,6 +963,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
879
963
|
let pointerPending = false;
|
|
880
964
|
let pointerTask = null;
|
|
881
965
|
const pointer = () => {
|
|
966
|
+
if (update.requested) return;
|
|
882
967
|
pointerPending = true;
|
|
883
968
|
if (pointerTask !== null) return;
|
|
884
969
|
pointerTask = (async () => {
|
|
@@ -890,8 +975,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
890
975
|
})()
|
|
891
976
|
.catch((error) => {
|
|
892
977
|
if (signal.aborted) return;
|
|
893
|
-
|
|
894
|
-
wake.notify();
|
|
978
|
+
fail(error);
|
|
895
979
|
})
|
|
896
980
|
.finally(() => {
|
|
897
981
|
pointerTask = null;
|
|
@@ -904,16 +988,22 @@ export async function runResident(options, dependencies = {}) {
|
|
|
904
988
|
AbortSignal.any([signal, watcher.signal]),
|
|
905
989
|
pointer,
|
|
906
990
|
dependencies,
|
|
907
|
-
).catch(
|
|
908
|
-
failure = error;
|
|
909
|
-
wake.notify();
|
|
910
|
-
});
|
|
991
|
+
).catch(fail);
|
|
911
992
|
try {
|
|
912
993
|
while (!signal.aborted) {
|
|
913
994
|
if (failure !== null) throw failure;
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
995
|
+
if (!session.refused) {
|
|
996
|
+
const configuration = await readConfiguration(options.configuration);
|
|
997
|
+
await controlledPass(context, dependencies, configuration, signal).catch((error) => {
|
|
998
|
+
if (!session.refused) throw error;
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
if (session.refused || update.requested) {
|
|
1002
|
+
const sessions = await heldSessions(context);
|
|
1003
|
+
if (sessions.length > 0) update.answer({ ready: false, sessions });
|
|
1004
|
+
else if (session.refused || (!served.serving() && logins.size === 0 && pointerTask === null)) return;
|
|
1005
|
+
}
|
|
1006
|
+
await wake.wait(interval, signal);
|
|
917
1007
|
}
|
|
918
1008
|
} finally {
|
|
919
1009
|
watcher.abort();
|
|
@@ -922,7 +1012,31 @@ export async function runResident(options, dependencies = {}) {
|
|
|
922
1012
|
for (const running of logins.values()) running.controller.abort();
|
|
923
1013
|
await Promise.all([...logins.values()].map((running) => running.task));
|
|
924
1014
|
await context.agentRefresh;
|
|
925
|
-
await closeResident();
|
|
926
1015
|
}
|
|
927
|
-
}
|
|
1016
|
+
};
|
|
1017
|
+
try {
|
|
1018
|
+
for (;;) {
|
|
1019
|
+
try {
|
|
1020
|
+
await maintainControl(session, serve, options.signal, { release: false });
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
await releaseControl(session);
|
|
1023
|
+
throw error;
|
|
1024
|
+
}
|
|
1025
|
+
if (session.refused) break;
|
|
1026
|
+
try {
|
|
1027
|
+
await releaseControl(session, { required: true });
|
|
1028
|
+
break;
|
|
1029
|
+
} catch (refusal) {
|
|
1030
|
+
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
1031
|
+
if (session.refused) break;
|
|
1032
|
+
update.answer({ ready: false, sessions: [], refused: refusal.code });
|
|
1033
|
+
}
|
|
1034
|
+
await wake.wait(interval, options.signal);
|
|
1035
|
+
}
|
|
1036
|
+
update.quiesce();
|
|
1037
|
+
while (!options.signal.aborted) await wake.wait(interval, options.signal);
|
|
1038
|
+
throw new KitRefusal("kit_command_cancelled");
|
|
1039
|
+
} finally {
|
|
1040
|
+
await served.close();
|
|
1041
|
+
}
|
|
928
1042
|
}
|