@agentshouse/kit 0.1.0-alpha.12 → 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 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.12/connect-linux.sh)
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
@@ -27,16 +27,18 @@ installed container runs another image, the bootstrap pulls this release's
27
27
  image and asks the running resident Kit to drain with `kit resident drain`. The
28
28
  resident stops acquiring Runs, resumptions, setup and agent CLI work, while the
29
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,
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,
32
33
  the resident stops renewing, releases its exact lease and stays quiescent, and
33
34
  the bootstrap replaces the container with one on this release's image over the
34
35
  same Kit home and workspace. The credential, Environment, Kit-home state and
35
36
  everything under the workspace are kept; anything installed elsewhere in the old
36
37
  container is not. A stopped container is replaced without a drain.
37
38
 
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
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
40
42
  every running Session locally and never stops one to make progress. A Session
41
43
  that ends keeps its exact binding, stop observation and outcome in Kit home,
42
44
  and the drain answers ready once no supervised process runs. The updated Kit
@@ -1170,8 +1172,13 @@ A resident whose lease House refuses answers the command with that refusal; the
1170
1172
  command never acquires a lease of its own while a resident serves it. While an
1171
1173
  update drains, `state/update-drain` refuses every such command, `kit login` and
1172
1174
  `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.
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.
1175
1182
 
1176
1183
  Interrupting the command cancels the running connector without advancing the
1177
1184
  checkpoint.
package/bin/kit.js CHANGED
@@ -177,7 +177,9 @@ async function runResidentDrain(args, directory) {
177
177
  write(
178
178
  answer.sessions.length > 0
179
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}).`,
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}).`,
181
183
  );
182
184
  process.exitCode = 1;
183
185
  }
package/bootstrap.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.1.0-alpha.12",
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.12/connect-linux.sh)",
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.12/connect-linux.sh"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.13",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/house.js CHANGED
@@ -56,6 +56,10 @@ async function readCredential(directory) {
56
56
  return stored;
57
57
  }
58
58
 
59
+ function latchRefusal(answer, dependencies) {
60
+ if (answer?.error?.code === UPGRADE_REQUIRED) dependencies.contractRefused?.();
61
+ }
62
+
59
63
  async function houseAnswer(house, path, init, dependencies, signal) {
60
64
  const fetcher = dependencies.fetch ?? fetch;
61
65
  let response;
@@ -73,7 +77,7 @@ async function houseAnswer(house, path, init, dependencies, signal) {
73
77
  } catch {
74
78
  answer = null;
75
79
  }
76
- if (answer?.error?.code === UPGRADE_REQUIRED) dependencies.contractRefused?.();
80
+ latchRefusal(answer, dependencies);
77
81
  throwHouseAnswer(response.status, answer, path);
78
82
  if (answer === null) throw new KitRefusal("kit_house_unavailable", path);
79
83
  return answer;
@@ -130,11 +134,18 @@ function residentAnswer(status, code) {
130
134
  return { status, type: "application/json", body };
131
135
  }
132
136
 
133
- async function residentRelay(session, request, signal) {
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) {
134
142
  const path = typeof request.path === "string" ? request.path : "";
135
143
  if (!path.startsWith("/kit/") || path.startsWith("/kit/lease")) {
136
144
  return residentAnswer(403, "kit_resident_route_refused");
137
145
  }
146
+ if (draining && newWork(path)) {
147
+ return residentAnswer(409, UPDATE_DRAINING);
148
+ }
138
149
  let authorization;
139
150
  try {
140
151
  authorization = await heldControl(session, signal);
@@ -149,6 +160,7 @@ async function residentRelay(session, request, signal) {
149
160
  signal,
150
161
  });
151
162
  const body = Buffer.from(await response.arrayBuffer());
163
+ if (response.status === 409) latchRefusal(JSON.parse(body.toString("utf8")), session.dependencies);
152
164
  return {
153
165
  status: response.status,
154
166
  type: response.headers.get("content-type"),
@@ -168,9 +180,7 @@ export async function serveResident(session, directory, signal, drain) {
168
180
  let draining = false;
169
181
  function answerDrain(connection) {
170
182
  draining = true;
171
- const answered = drain();
172
- holder?.destroy();
173
- answered.then(
183
+ drain(holder !== null).then(
174
184
  (answer) => {
175
185
  if (!connection.destroyed) writeFrame(connection, answer);
176
186
  connection.end();
@@ -181,7 +191,7 @@ export async function serveResident(session, directory, signal, drain) {
181
191
  }
182
192
  function hold(connection) {
183
193
  if (holder !== null || draining || existsSync(updateDrain(directory))) {
184
- writeFrame(connection, { refused: holder === null ? UPDATE_DRAINING : LEASE_ACTIVE });
194
+ writeFrame(connection, { refused: draining || holder === null ? UPDATE_DRAINING : LEASE_ACTIVE });
185
195
  connection.end();
186
196
  return false;
187
197
  }
@@ -218,7 +228,7 @@ export async function serveResident(session, directory, signal, drain) {
218
228
  }
219
229
  const cancelled = new AbortController();
220
230
  relayed.set(request.id, cancelled);
221
- void residentRelay(session, request, AbortSignal.any([signal, cancelled.signal])).then(
231
+ void residentRelay(session, request, AbortSignal.any([signal, cancelled.signal]), draining).then(
222
232
  (answer) => {
223
233
  relayed.delete(request.id);
224
234
  if (!connection.destroyed) writeFrame(connection, { id: request.id, ...answer });
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
- stopping.abort();
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
@@ -177,6 +177,7 @@ async function supervise(context, bound, bridge) {
177
177
  while (live) {
178
178
  await Promise.race([terminal, delay(WORKER_SUPERVISION_POLL_MS)]);
179
179
  if (!live) return null;
180
+ await context.update?.announce();
180
181
  if (Date.now() >= commanded) {
181
182
  commanded = Date.now() + WORKER_COMMAND_POLL_MS;
182
183
  const polled = await pendingCommands(context, ["stop", "interrupt", "answer"]);
@@ -849,20 +850,26 @@ function residentUpdate(context, wake) {
849
850
  get requested() {
850
851
  return requested;
851
852
  },
852
- async request() {
853
+ async request(serving) {
853
854
  if (!requested) {
854
855
  requested = true;
855
856
  await writeText(updateDrain(context.directory), "");
856
857
  }
857
858
  wake.notify();
858
- if (ready !== null) return ready;
859
859
  const sessions = await heldSessions(context);
860
+ if (ready !== null) return ready;
861
+ if (serving) return { ready: false, sessions, serving };
860
862
  if (sessions.length > 0) return { ready: false, sessions };
861
863
  return new Promise((resolve) => waiting.push(resolve));
862
864
  },
863
865
  answer(answer) {
864
866
  for (const resolve of waiting.splice(0)) resolve(answer);
865
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
+ },
866
873
  quiesce() {
867
874
  ready = { ready: true, sessions: [] };
868
875
  this.answer(ready);
@@ -902,7 +909,7 @@ export async function runResident(options, dependencies = {}) {
902
909
  const wake = residentWake();
903
910
  const update = residentUpdate(context, wake);
904
911
  context.update = update;
905
- const served = await serveResident(session, context.directory, options.signal, () => update.request());
912
+ const served = await serveResident(session, context.directory, options.signal, (serving) => update.request(serving));
906
913
  const serve = async (signal) => {
907
914
  context.busy = true;
908
915
  const watcher = new AbortController();