@runuai/host 0.9.146 → 0.9.147

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/lib/agent-cli.ts CHANGED
@@ -305,7 +305,22 @@ async function main() {
305
305
  case "memory search": {
306
306
  const q = pos.join(" ") || flags.q || "";
307
307
  const author = flags.as ? "&author=" + encodeURIComponent(flags.as) : "";
308
- out((await api("GET", "/api/agent/memory?q=" + encodeURIComponent(q) + author)).memories);
308
+ const limit = flags.limit ? "&limit=" + encodeURIComponent(String(flags.limit)) : "";
309
+ const res = await api(
310
+ "GET",
311
+ "/api/agent/memory?q=" + encodeURIComponent(q) + author + limit,
312
+ );
313
+ out(res.memories);
314
+ // Truncation goes to stderr so it cannot corrupt piped JSON, and it is
315
+ // stated rather than implied: recalling across tasks means reading the
316
+ // WHOLE store, and a short page that says nothing reads as all there is.
317
+ // NOTE: this file is a template literal. No backticks, no \${} in here.
318
+ if (res.truncated) {
319
+ console.error(
320
+ "uai: showing " + res.memories.length + " of " + res.total +
321
+ " — re-run with --limit all for every row.",
322
+ );
323
+ }
309
324
  break;
310
325
  }
311
326
  case "memory save": {
@@ -453,7 +468,7 @@ async function main() {
453
468
  " uai task list",
454
469
  " uai task create --name <n> --prompt <p> [--projects id,id] [--team id] [--agents handle,handle]",
455
470
  ' uai task close --status finished|canceled [--reason "final report"]',
456
- " uai memory search <query>",
471
+ " uai memory search <query> [--as handle] [--limit N|all]",
457
472
  " uai memory save <text> [--project id] [--tags a,b]",
458
473
  " uai memory delete <id>",
459
474
  " uai todo list",
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runuai/host",
3
- "version": "0.9.146",
3
+ "version": "0.9.147",
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,
@@ -866,7 +869,11 @@ async function startLocalUi(): Promise<void> {
866
869
  invalidateMachineTunnelRoute(taskId);
867
870
  try {
868
871
  const result = await getOrchestrator().stopTask(taskId);
869
- if (result.ok) queueStoppedTaskStatus(taskId);
872
+ // A PERSON pressed Stop on this machine's own UI — ADR-028. Labelled
873
+ // as such because the cloud otherwise cannot tell it apart from the
874
+ // quarantine sweeps below, which reach the bridge through this same
875
+ // frame, and ADR-144 turns that difference into who may undo it.
876
+ if (result.ok) queueStoppedTaskStatus(taskId, "manual");
870
877
  return result;
871
878
  } finally {
872
879
  invalidateMachineTunnelRoute(taskId);
@@ -1042,15 +1049,18 @@ function sendCapabilities(): void {
1042
1049
 
1043
1050
  /** Keep a rolling-isolation/local-UI stop from remaining cloud-ghost-running
1044
1051
  * merely because the bridge was between connections at the transition. */
1045
- function queueStoppedTaskStatus(taskId: string): void {
1046
- pendingRuntimeIsolationStatuses.enqueue(taskId);
1052
+ function queueStoppedTaskStatus(
1053
+ taskId: string,
1054
+ reason?: StoppedTaskReason,
1055
+ ): void {
1056
+ pendingRuntimeIsolationStatuses.enqueue(taskId, reason);
1047
1057
  flushStoppedTaskStatuses();
1048
1058
  }
1049
1059
 
1050
1060
  function flushStoppedTaskStatuses(socket: WebSocket | null = ws): void {
1051
1061
  if (!socket || socket.readyState !== WebSocket.OPEN) return;
1052
- pendingRuntimeIsolationStatuses.flush((taskId) => {
1053
- send(socket, { kind: "task.status", taskId, status: "stopped" });
1062
+ pendingRuntimeIsolationStatuses.flush((taskId, reason) => {
1063
+ send(socket, { kind: "task.status", taskId, status: "stopped", reason });
1054
1064
  });
1055
1065
  }
1056
1066
 
package/src/protocol.ts CHANGED
@@ -1812,9 +1812,21 @@ export type HostToCloud =
1812
1812
  // ADR-103: `seq` is the outbox sequence number (monotonic within a bootId).
1813
1813
  // Absent from pre-103 hosts; the cloud dedups replay overlap by it.
1814
1814
  | { 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 }
1815
+ // Host-pushed task lifecycle: the cloud mirrors the status (currently
1816
+ // "stopped"). TWO different events arrive through this one frame — ADR-028's
1817
+ // local-UI Stop, where the host operator paused the task's containers, and
1818
+ // the automatic writable-container quarantine a runtime activation or
1819
+ // shared-volume repair performs. `reason: "manual"` is the first; its
1820
+ // absence is the second.
1821
+ //
1822
+ // The cloud cannot infer it. ADR-144 lets a collaborator resume a task that
1823
+ // stopped on its own and not one a human deliberately paused, and with both
1824
+ // stops indistinguishable on the wire the deliberate one was reversible by
1825
+ // anyone on the task. OPTIONAL so an older host stays compatible: it sends
1826
+ // no reason, the cloud records none, and that host's operator Stop keeps the
1827
+ // pre-ADR-144 behaviour rather than mislabelling its quarantines as manual
1828
+ // and blocking the recovery path they exist to enable.
1829
+ | { kind: "task.status"; taskId: string; status: string; reason?: string }
1818
1830
  | { kind: "ping"; ts: number }
1819
1831
  // ADR-133: a sidecar asks for (or refreshes) its task-scoped AWS lease.
1820
1832
  | { kind: "lease.request"; opId: string; taskId: string }