@runuai/host 0.9.146 → 0.9.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,21 @@
4
4
  * mirror remains the authority at flush time so a later Resume wins over an
5
5
  * older queued stop.
6
6
  */
7
+ /**
8
+ * Why the host stopped the task, when the host knows.
9
+ *
10
+ * `"manual"` is ADR-028's local-UI Stop — a person at the machine pressed it.
11
+ * Everything else the host reports through this frame is automatic (writable
12
+ * -container quarantine during a runtime activation or a shared-volume
13
+ * repair), and carries no reason. The cloud needs the difference because
14
+ * ADR-144 lets a collaborator resume a task that stopped on its own but not
15
+ * one a human deliberately paused, and until now both arrived as the same
16
+ * three fields.
17
+ */
18
+ export type StoppedTaskReason = "manual";
19
+
7
20
  export class StoppedTaskStatusQueue {
8
- private readonly pending = new Set<string>();
21
+ private readonly pending = new Map<string, StoppedTaskReason | undefined>();
9
22
 
10
23
  constructor(
11
24
  private readonly statusForTask: (
@@ -13,8 +26,13 @@ export class StoppedTaskStatusQueue {
13
26
  ) => string | null | undefined,
14
27
  ) {}
15
28
 
16
- enqueue(taskId: string): void {
17
- this.pending.add(taskId);
29
+ enqueue(taskId: string, reason?: StoppedTaskReason): void {
30
+ // A reason, once known, STICKS across a re-enqueue. The same task can be
31
+ // queued twice before a flush — an operator Stop, then a quarantine sweep
32
+ // that finds its container already down — and the automatic pass arriving
33
+ // second does not make the human's decision retroactively automatic.
34
+ // Dropping it would silently hand the task back to every collaborator.
35
+ this.pending.set(taskId, reason ?? this.pending.get(taskId));
18
36
  }
19
37
 
20
38
  /** A failed taskUp may have stopped a legacy runtime before recreation lost
@@ -35,9 +53,9 @@ export class StoppedTaskStatusQueue {
35
53
  return true;
36
54
  }
37
55
 
38
- flush(publish: (taskId: string) => void): void {
39
- for (const taskId of [...this.pending]) {
40
- if (this.statusForTask(taskId) === "stopped") publish(taskId);
56
+ flush(publish: (taskId: string, reason?: StoppedTaskReason) => void): void {
57
+ for (const [taskId, reason] of [...this.pending]) {
58
+ if (this.statusForTask(taskId) === "stopped") publish(taskId, reason);
41
59
  this.pending.delete(taskId);
42
60
  }
43
61
  }
package/lib/ulid.ts CHANGED
@@ -4,4 +4,13 @@ export function newId(): string {
4
4
  return ulid().toLowerCase();
5
5
  }
6
6
 
7
- export { ulid as monotonicUlid };
7
+ /**
8
+ * A ULID with its canonical UPPERCASE base32, unlike `newId()` above.
9
+ *
10
+ * Renamed from `monotonicUlid`, which it never was: the plain generator
11
+ * randomises the suffix, so two ids minted in the same millisecond do not
12
+ * sort in call order. Nothing in this package imports it — it is kept only to
13
+ * mirror `lib/ulid.ts`, and it is renamed here too so the false promise is
14
+ * not sitting in the copy someone reads next.
15
+ */
16
+ export { ulid as rawUlid };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.146",
3
+ "version": "0.9.148",
4
4
  "description": "Uai host — runs ephemeral AI tasks in containers on a machine you control.",
5
5
  "license": "MIT",
6
6
  "author": "Uai Tech <team@runuai.com>",
package/src/main.ts CHANGED
@@ -28,7 +28,10 @@ import {
28
28
  getHostTask,
29
29
  hasInFlightHostTaskLifecycle,
30
30
  } from "../lib/runtime-state";
31
- import { StoppedTaskStatusQueue } from "../lib/stopped-task-status-queue";
31
+ import {
32
+ StoppedTaskStatusQueue,
33
+ type StoppedTaskReason,
34
+ } from "../lib/stopped-task-status-queue";
32
35
  import {
33
36
  getOrchestrator,
34
37
  quarantineWritableRuntimeContainers,
@@ -209,6 +212,7 @@ import {
209
212
  HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE,
210
213
  HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
211
214
  HOST_ENGINE_LOGIN_PROTOCOL_FEATURE,
215
+ LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
212
216
  isEngineAccountScope,
213
217
  HOST_LOGS_PROTOCOL_FEATURE,
214
218
  HOST_TASK_INVENTORY_PROTOCOL_FEATURE,
@@ -866,7 +870,11 @@ async function startLocalUi(): Promise<void> {
866
870
  invalidateMachineTunnelRoute(taskId);
867
871
  try {
868
872
  const result = await getOrchestrator().stopTask(taskId);
869
- if (result.ok) queueStoppedTaskStatus(taskId);
873
+ // A PERSON pressed Stop on this machine's own UI — ADR-028. Labelled
874
+ // as such because the cloud otherwise cannot tell it apart from the
875
+ // quarantine sweeps below, which reach the bridge through this same
876
+ // frame, and ADR-144 turns that difference into who may undo it.
877
+ if (result.ok) queueStoppedTaskStatus(taskId, "manual");
870
878
  return result;
871
879
  } finally {
872
880
  invalidateMachineTunnelRoute(taskId);
@@ -909,6 +917,7 @@ function buildCapabilities(): HostCapabilities {
909
917
  HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE,
910
918
  HOST_ENGINE_ACCOUNT_MANAGE_PROTOCOL_FEATURE,
911
919
  HOST_ENGINE_DISCONNECT_PROTOCOL_FEATURE,
920
+ LIVE_ROSTER_EDIT_PROTOCOL_FEATURE,
912
921
  // The echo adapter cannot execute the in-task CLI. Advertising typed
913
922
  // dispatch in mock mode would let the composer create a Secretary that
914
923
  // has no way to wake crew.
@@ -1042,15 +1051,18 @@ function sendCapabilities(): void {
1042
1051
 
1043
1052
  /** Keep a rolling-isolation/local-UI stop from remaining cloud-ghost-running
1044
1053
  * merely because the bridge was between connections at the transition. */
1045
- function queueStoppedTaskStatus(taskId: string): void {
1046
- pendingRuntimeIsolationStatuses.enqueue(taskId);
1054
+ function queueStoppedTaskStatus(
1055
+ taskId: string,
1056
+ reason?: StoppedTaskReason,
1057
+ ): void {
1058
+ pendingRuntimeIsolationStatuses.enqueue(taskId, reason);
1047
1059
  flushStoppedTaskStatuses();
1048
1060
  }
1049
1061
 
1050
1062
  function flushStoppedTaskStatuses(socket: WebSocket | null = ws): void {
1051
1063
  if (!socket || socket.readyState !== WebSocket.OPEN) return;
1052
- pendingRuntimeIsolationStatuses.flush((taskId) => {
1053
- send(socket, { kind: "task.status", taskId, status: "stopped" });
1064
+ pendingRuntimeIsolationStatuses.flush((taskId, reason) => {
1065
+ send(socket, { kind: "task.status", taskId, status: "stopped", reason });
1054
1066
  });
1055
1067
  }
1056
1068
 
package/src/protocol.ts CHANGED
@@ -120,6 +120,14 @@ export const HOST_ENGINE_ACCOUNT_MANAGE_PROTOCOL_FEATURE =
120
120
  "host-engine-account-manage-v1";
121
121
  export const HOST_ENGINE_ACCOUNT_SCOPES_PROTOCOL_FEATURE =
122
122
  "host-engine-account-scopes-v1";
123
+ /** ADR-049 addendum: live roster edits. On channel.ensure the host diffs the
124
+ * live roster against the spec — an agent whose kind, model or effort changed
125
+ * has its session recycled so the reconciler respawns it on the new settings,
126
+ * and an agent missing from the spec has its session stopped (its session dir
127
+ * stays). A host without this keeps a changed agent's old process until some
128
+ * unrelated respawn and never stops a removed one, so the cloud refuses to
129
+ * edit or remove a running task's agents on it. */
130
+ export const LIVE_ROSTER_EDIT_PROTOCOL_FEATURE = "live-roster-edit-v1";
123
131
  /** The owner-level account scope; every pre-scope account lives here. */
124
132
  export const HOST_ACCOUNT_SCOPE = "host";
125
133
  const ORG_ACCOUNT_SCOPE_RE = /^org:[A-Za-z0-9_-]{1,128}$/;
@@ -1812,9 +1820,21 @@ export type HostToCloud =
1812
1820
  // ADR-103: `seq` is the outbox sequence number (monotonic within a bootId).
1813
1821
  // Absent from pre-103 hosts; the cloud dedups replay overlap by it.
1814
1822
  | { kind: "event"; event: HostEvent; seq?: number }
1815
- // Host-pushed task lifecycle (ADR-028 local-UI Stop): the host operator paused
1816
- // a task's containers, so the cloud mirrors the status (currently "stopped").
1817
- | { kind: "task.status"; taskId: string; status: string }
1823
+ // Host-pushed task lifecycle: the cloud mirrors the status (currently
1824
+ // "stopped"). TWO different events arrive through this one frame — ADR-028's
1825
+ // local-UI Stop, where the host operator paused the task's containers, and
1826
+ // the automatic writable-container quarantine a runtime activation or
1827
+ // shared-volume repair performs. `reason: "manual"` is the first; its
1828
+ // absence is the second.
1829
+ //
1830
+ // The cloud cannot infer it. ADR-144 lets a collaborator resume a task that
1831
+ // stopped on its own and not one a human deliberately paused, and with both
1832
+ // stops indistinguishable on the wire the deliberate one was reversible by
1833
+ // anyone on the task. OPTIONAL so an older host stays compatible: it sends
1834
+ // no reason, the cloud records none, and that host's operator Stop keeps the
1835
+ // pre-ADR-144 behaviour rather than mislabelling its quarantines as manual
1836
+ // and blocking the recovery path they exist to enable.
1837
+ | { kind: "task.status"; taskId: string; status: string; reason?: string }
1818
1838
  | { kind: "ping"; ts: number }
1819
1839
  // ADR-133: a sidecar asks for (or refreshes) its task-scoped AWS lease.
1820
1840
  | { kind: "lease.request"; opId: string; taskId: string }
@@ -2025,6 +2045,18 @@ export type HostEvent =
2025
2045
  agentId: string;
2026
2046
  reason: string;
2027
2047
  }
2048
+ // A roster change moved an agent's session (LIVE_ROSTER_EDIT_PROTOCOL_FEATURE).
2049
+ // `restarting`: its kind/model/effort changed and the old session was stopped
2050
+ // (at once when idle, otherwise when its turn ended) — the reconciler is
2051
+ // respawning it. `ready`: that replacement session bound. `removed`: the
2052
+ // agent left the roster and its session was stopped. An older cloud ignores
2053
+ // the unknown kind.
2054
+ | {
2055
+ kind: "agent.session";
2056
+ taskId: string;
2057
+ agentId: string;
2058
+ state: "restarting" | "ready" | "removed";
2059
+ }
2028
2060
  // A host-originated system note for the task channel (ADR-027) — e.g. gh
2029
2061
  // auth expired. The cloud renders it as an author:"system" message.
2030
2062
  | {