@intentic/sandbox-contract 1.249.0 → 1.251.0

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.
Files changed (56) hide show
  1. package/README.md +7 -0
  2. package/dist/contracts/ci.contract.d.ts +4 -0
  3. package/dist/contracts/ci.contract.d.ts.map +1 -1
  4. package/dist/contracts/skills.contract.d.ts +6 -0
  5. package/dist/contracts/skills.contract.d.ts.map +1 -1
  6. package/dist/contracts/skills.contract.js +13 -4
  7. package/dist/contracts/skills.contract.js.map +1 -1
  8. package/dist/ids/conversation-ids.d.ts +15 -0
  9. package/dist/ids/conversation-ids.d.ts.map +1 -1
  10. package/dist/ids/conversation-ids.js +30 -0
  11. package/dist/ids/conversation-ids.js.map +1 -1
  12. package/dist/index.d.ts +12 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +2 -0
  15. package/dist/index.js.map +1 -1
  16. package/dist/models/model-order.d.ts.map +1 -1
  17. package/dist/models/model-order.js +2 -0
  18. package/dist/models/model-order.js.map +1 -1
  19. package/dist/schemas/ci.d.ts +4 -0
  20. package/dist/schemas/ci.d.ts.map +1 -1
  21. package/dist/schemas/ci.js +4 -0
  22. package/dist/schemas/ci.js.map +1 -1
  23. package/dist/schemas/devices.d.ts +2 -0
  24. package/dist/schemas/devices.d.ts.map +1 -1
  25. package/dist/schemas/devices.js +2 -0
  26. package/dist/schemas/devices.js.map +1 -1
  27. package/dist/schemas/settings.d.ts +5 -0
  28. package/dist/schemas/settings.d.ts.map +1 -1
  29. package/dist/schemas/settings.js +6 -2
  30. package/dist/schemas/settings.js.map +1 -1
  31. package/dist/state/fix-attempt-plan.d.ts +24 -0
  32. package/dist/state/fix-attempt-plan.d.ts.map +1 -0
  33. package/dist/state/fix-attempt-plan.js +22 -0
  34. package/dist/state/fix-attempt-plan.js.map +1 -0
  35. package/dist/state/fix-stance.d.ts +11 -0
  36. package/dist/state/fix-stance.d.ts.map +1 -0
  37. package/dist/state/fix-stance.js +79 -0
  38. package/dist/state/fix-stance.js.map +1 -0
  39. package/dist/state/runtime-state.d.ts.map +1 -1
  40. package/dist/state/runtime-state.js.map +1 -1
  41. package/package.json +4 -4
  42. package/src/contracts/skills.contract.ts +21 -8
  43. package/src/ids/conversation-ids.test.ts +61 -1
  44. package/src/ids/conversation-ids.ts +58 -1
  45. package/src/index.ts +2 -0
  46. package/src/models/model-order.test.ts +35 -0
  47. package/src/models/model-order.ts +10 -2
  48. package/src/schemas/ci.ts +8 -0
  49. package/src/schemas/devices.ts +5 -0
  50. package/src/schemas/settings.ts +9 -4
  51. package/src/state/arrival.ts +1 -1
  52. package/src/state/fix-attempt-plan.test.ts +64 -0
  53. package/src/state/fix-attempt-plan.ts +54 -0
  54. package/src/state/fix-stance.test.ts +99 -0
  55. package/src/state/fix-stance.ts +140 -0
  56. package/src/state/runtime-state.ts +2 -1
@@ -30,7 +30,7 @@ export const ArrivalGroupSchema = z.enum([
30
30
  "capability",
31
31
  // The agent settings that differ from their defaults.
32
32
  "settings",
33
- // A merge into CLAUDE.md / AGENTS.md.
33
+ // A merge into the workspace's memory file.
34
34
  "memory",
35
35
  "skill",
36
36
  "automation",
@@ -0,0 +1,64 @@
1
+ import { expect, test } from "vitest";
2
+ import { ciFixConversationId, fixAttemptId } from "../ids/conversation-ids.js";
3
+ import type { AgentSummary } from "../schemas/agents.js";
4
+ import { planFixAttempt } from "./fix-attempt-plan.js";
5
+
6
+ const NO_ATTENTION = { plan: false, question: false, permission: false, capability: false, credential: false, conflict: false };
7
+ const BASE = ciFixConversationId(`web`, 41);
8
+
9
+ const agent = (id: string, over: Partial<AgentSummary> = {}): AgentSummary => ({
10
+ id,
11
+ status: `running`,
12
+ provider: `claude`,
13
+ harness: `native`,
14
+ attention: { ...NO_ATTENTION },
15
+ updatedAt: 1_000,
16
+ ...over,
17
+ });
18
+
19
+ test("with nobody on the failure, a press opens attempt 1 under the failure's own id", () => {
20
+ expect(planFixAttempt(BASE, [], [])).toEqual({ kind: `new`, conversationId: BASE, attempt: 1 });
21
+ });
22
+
23
+ // The archive counts: attempt 1 was set aside earlier, and re-minting its id would un-archive it.
24
+ test("an archived attempt is skipped over, not resurrected", () => {
25
+ expect(planFixAttempt(BASE, [], [BASE])).toEqual({ kind: `new`, conversationId: fixAttemptId(BASE, 2), attempt: 2 });
26
+ });
27
+
28
+ test("a landed attempt is closed: the same gates red again get a fresh attempt", () => {
29
+ const plan = planFixAttempt(BASE, [agent(BASE, { status: `landed` })], []);
30
+ expect(plan).toEqual({ kind: `new`, conversationId: fixAttemptId(BASE, 2), attempt: 2 });
31
+ });
32
+
33
+ test("an attempt that ended is continued by a plain press, and by an explicit Continue", () => {
34
+ const roster = [agent(BASE, { status: `error`, failure: `no capacity` })];
35
+ expect(planFixAttempt(BASE, roster, [])).toEqual({ kind: `continue`, conversationId: BASE, attempt: 1 });
36
+ expect(planFixAttempt(BASE, roster, [], `continue`)).toEqual({ kind: `continue`, conversationId: BASE, attempt: 1 });
37
+ });
38
+
39
+ test("start over retires the latest attempt and numbers the next past it", () => {
40
+ const second = fixAttemptId(BASE, 2);
41
+ const plan = planFixAttempt(BASE, [agent(BASE, { status: `stopped` }), agent(second, { status: `error` })], [], `start-over`);
42
+ expect(plan).toEqual({ kind: `start-over`, retire: second, stopFirst: false, conversationId: fixAttemptId(BASE, 3), attempt: 3 });
43
+ });
44
+
45
+ // Only a running turn needs stopping before the archive will take it; a parked one is already still.
46
+ test("starting over on a working attempt stops it first", () => {
47
+ const plan = planFixAttempt(BASE, [agent(BASE, { status: `running` })], [], `start-over`);
48
+ expect(plan).toMatchObject({ kind: `start-over`, retire: BASE, stopFirst: true });
49
+ const parked = planFixAttempt(BASE, [agent(BASE, { status: `awaiting`, attention: { ...NO_ATTENTION, question: true } })], [], `start-over`);
50
+ expect(parked).toMatchObject({ kind: `start-over`, stopFirst: false });
51
+ });
52
+
53
+ // A second agent beside a live one is the race the derived id exists to prevent; the press opens the live one instead.
54
+ test.each([`running`, `awaiting`, `ready`] as const)("an attempt still in play (%s) is busy, not a fresh press", (status) => {
55
+ const plan = planFixAttempt(BASE, [agent(BASE, { status })], []);
56
+ expect(plan).toMatchObject({ kind: `busy`, conversationId: BASE });
57
+ // Continue is not a way around it either.
58
+ expect(planFixAttempt(BASE, [agent(BASE, { status })], [], `continue`).kind).toBe(`busy`);
59
+ });
60
+
61
+ test("a spent allowance is a wait, not an ending: the press does not spend it again", () => {
62
+ const plan = planFixAttempt(BASE, [agent(BASE, { status: `error`, failureCode: `rate_limit` })], []);
63
+ expect(plan.kind).toBe(`busy`);
64
+ });
@@ -0,0 +1,54 @@
1
+ import { fixAttemptOf, latestFixAttempt, nextFixAttemptId } from "../ids/conversation-ids.js";
2
+ import type { AgentSummary } from "../schemas/agents.js";
3
+ import { type FixStance, fixStance } from "./fix-stance.js";
4
+
5
+ /* WHAT A PRESS ON A FAILURE'S FIX CONTROL DOES, decided once for every surface that has one — the push question in
6
+ * the shell, the pipelines board's rows, the daemon's own /ci/fix route — so the browser and the daemon cannot
7
+ * disagree about whether a press continues an attempt, opens the next one, or has to wait. Pure: the roster in, a
8
+ * plan out. Carrying the plan out (stopping, archiving, starting) is the caller's, since each side does that
9
+ * through its own doors.
10
+ *
11
+ * THE RULE, in the order it is read: nothing live (or the last answer landed) opens the NEXT attempt; "start over"
12
+ * sets the latest aside and opens the next; an attempt that ENDED is continued; anything else is still in play and
13
+ * the reader is sent to it rather than allowed to race a second agent beside it. */
14
+
15
+ // How the reader ended the picker, when they did; absent is the plain press on the button.
16
+ export type FixResume = "continue" | "start-over";
17
+
18
+ export type FixAttemptPlan =
19
+ // Nothing live answers this failure, or the last answer landed: open the next attempt with the full prompt.
20
+ | { readonly kind: "new"; readonly conversationId: string; readonly attempt: number }
21
+ // The latest attempt ended without a fix: carry on in it, with a nudge rather than the whole opening prompt.
22
+ | { readonly kind: "continue"; readonly conversationId: string; readonly attempt: number }
23
+ // Set the latest attempt aside (stopped first if still running) and open the next one on a clean worktree.
24
+ | {
25
+ readonly kind: "start-over";
26
+ readonly retire: string;
27
+ readonly stopFirst: boolean;
28
+ readonly conversationId: string;
29
+ readonly attempt: number;
30
+ }
31
+ // The latest attempt is still in play: working, parked on the reader, or holding a fix to review. Open it instead.
32
+ | { readonly kind: "busy"; readonly conversationId: string; readonly stance: FixStance };
33
+
34
+ // `roster` is the live fleet; `knownIds` is every conversation id that exists, archived ones included, since the next
35
+ // attempt's number must clear the archive too (conversation-ids.ts, nextFixAttemptId).
36
+ export const planFixAttempt = (base: string, roster: readonly AgentSummary[], knownIds: Iterable<string>, resume?: FixResume): FixAttemptPlan => {
37
+ const latest = latestFixAttempt(base, roster);
38
+ const stance = latest === undefined ? undefined : fixStance(latest.agent);
39
+ const known = [...knownIds, ...(latest === undefined ? [] : [latest.agent.id])];
40
+ const next = (): { conversationId: string; attempt: number } => {
41
+ const conversationId = nextFixAttemptId(base, known);
42
+ return { conversationId, attempt: fixAttemptOf(base, conversationId) ?? 1 };
43
+ };
44
+ if (latest === undefined || stance === undefined || stance.kind === "landed") {
45
+ return { kind: "new", ...next() };
46
+ }
47
+ if (resume === "start-over") {
48
+ return { kind: "start-over", retire: latest.agent.id, stopFirst: stance.kind === "working", ...next() };
49
+ }
50
+ if (stance.retry) {
51
+ return { kind: "continue", conversationId: latest.agent.id, attempt: latest.attempt };
52
+ }
53
+ return { kind: "busy", conversationId: latest.agent.id, stance };
54
+ };
@@ -0,0 +1,99 @@
1
+ import { expect, test } from "vitest";
2
+ import { fixStance } from "./fix-stance.js";
3
+ import type { AgentSummary } from "../schemas/agents.js";
4
+
5
+ const NO_ATTENTION = { plan: false, question: false, permission: false, capability: false, credential: false, conflict: false };
6
+
7
+ const agent = (over: Partial<AgentSummary> = {}): AgentSummary => ({
8
+ id: `ci-fix-web-42`,
9
+ status: `running`,
10
+ provider: `claude`,
11
+ harness: `native`,
12
+ attention: { ...NO_ATTENTION },
13
+ updatedAt: 1_000,
14
+ ...over,
15
+ });
16
+
17
+ test("a live turn reads as work in progress, and asks nothing of the reader", () => {
18
+ const stance = fixStance(agent({ status: `running` }));
19
+ expect(stance.kind).toBe(`working`);
20
+ expect(stance.ongoing).toBe(true);
21
+ expect(stance.retry).toBe(false);
22
+ });
23
+
24
+ test.each([`resuming`, `stopping`, `dismissing`] as const)("%s still reads as working", (status) => {
25
+ expect(fixStance(agent({ status })).kind).toBe(`working`);
26
+ });
27
+
28
+ test("a parked turn is read off the attention flag, not the status", () => {
29
+ const stance = fixStance(agent({ status: `idle`, attention: { ...NO_ATTENTION, question: true } }));
30
+ expect(stance.kind).toBe(`needs-you`);
31
+ expect(stance.label).toBe(`Question for you`);
32
+ expect(stance.ongoing).toBe(true);
33
+ });
34
+
35
+ test("a setup that is not connected yet outranks a question raised beside it", () => {
36
+ const stance = fixStance(agent({ status: `awaiting`, attention: { ...NO_ATTENTION, question: true, capability: true } }));
37
+ expect(stance.label).toBe(`Setup needed`);
38
+ });
39
+
40
+ test("a turn parked with no flag yet still says it is waiting on you", () => {
41
+ expect(fixStance(agent({ status: `awaiting` })).kind).toBe(`needs-you`);
42
+ });
43
+
44
+ test("held work reads as a fix to review, whether the fleet called it ready or left it idle", () => {
45
+ expect(fixStance(agent({ status: `ready` })).kind).toBe(`ready`);
46
+ expect(fixStance(agent({ status: `idle`, diff: { files: 4, insertions: 120, deletions: 8 } })).kind).toBe(`ready`);
47
+ });
48
+
49
+ // Diff present doesn't change a failed status to ready; it only earns a mention in the hint.
50
+ test("an agent that failed after writing files still reads as failed", () => {
51
+ const stance = fixStance(agent({ status: `error`, diff: { files: 2, insertions: 34, deletions: 6 } }));
52
+ expect(stance.kind).toBe(`ended`);
53
+ expect(stance.retry).toBe(true);
54
+ expect(stance.hint).toContain(`2 changed files on its branch`);
55
+ });
56
+
57
+ test("a finished turn that changed nothing offers to try again", () => {
58
+ const stance = fixStance(agent({ status: `idle` }));
59
+ expect(stance.kind).toBe(`ended`);
60
+ expect(stance.label).toBe(`Nothing changed`);
61
+ expect(stance.retry).toBe(true);
62
+ expect(stance.ongoing).toBe(false);
63
+ });
64
+
65
+ test("a landed fix is over, and stops holding the branch's other rows back", () => {
66
+ const stance = fixStance(agent({ status: `landed`, diff: { files: 2, insertions: 10, deletions: 1 } }));
67
+ expect(stance.kind).toBe(`landed`);
68
+ expect(stance.ongoing).toBe(false);
69
+ expect(stance.retry).toBe(false);
70
+ });
71
+
72
+ test("a failed turn carries the provider's own sentence into the hint", () => {
73
+ const stance = fixStance(agent({ status: `error`, failure: `the model endpoint refused the request` }));
74
+ expect(stance.kind).toBe(`ended`);
75
+ expect(stance.retry).toBe(true);
76
+ expect(stance.hint).toContain(`the model endpoint refused the request`);
77
+ });
78
+
79
+ // Retry is false because retrying would spend the same already-refused allowance.
80
+ test("a spent allowance reads as a wait rather than an error", () => {
81
+ const stance = fixStance(agent({ status: `error`, failureCode: `rate_limit`, failure: `usage limit reached` }));
82
+ expect(stance.kind).toBe(`waiting`);
83
+ expect(stance.retry).toBe(false);
84
+ expect(stance.ongoing).toBe(true);
85
+ });
86
+
87
+ test("a stopped turn says so, and offers the press again", () => {
88
+ const stance = fixStance(agent({ status: `stopped` }));
89
+ expect(stance.label).toBe(`Stopped`);
90
+ expect(stance.retry).toBe(true);
91
+ });
92
+
93
+ // An unknown status can arrive from a newer daemon; the stance must still read rather than crash on a missing ending.
94
+ test("an unknown ending still produces a stance", () => {
95
+ const stance = fixStance(agent({ status: `teleported` as AgentSummary["status"] }));
96
+ expect(stance.kind).toBe(`ended`);
97
+ expect(stance.label).toBe(`Agent stopped`);
98
+ expect(stance.retry).toBe(true);
99
+ });
@@ -0,0 +1,140 @@
1
+ import type { AgentSummary } from "../schemas/agents.js";
2
+
3
+ /* WHAT BECAME OF THE AGENT A SURFACE SENT AFTER A FAILURE — a red pipeline row, a refused push — read off the fleet
4
+ * summary that surface joined by conversation id (ids/conversation-ids.ts). Reads both status and attention, since
5
+ * a parked turn is `idle` with a flag rather than finished. Mirrors the fleet board's own labels except for naming
6
+ * the subject (`Fix ready`), because on these surfaces the failure is what's read. Pure: no host, no clock.
7
+ *
8
+ * IN THE CONTRACT, NOT IN ONE SURFACE, because the words must not differ between the pipelines board (an
9
+ * extension) and the push question (the shell): the same agent, one click apart, described two ways is a bug a
10
+ * reader notices before a test does. Only the reading lives here; how each kind is DRAWN (icon, colour) is the UI
11
+ * kit's `fixStanceLook`, which an extension cannot load in a test and this package must not know about. */
12
+
13
+ export type FixStanceKind =
14
+ // A turn is in flight. Nothing is owed by the reader.
15
+ | "working"
16
+ // Parked on the user: a question, a plan, a permission, a setup, a conflict.
17
+ | "needs-you"
18
+ // The fix is written and held on the agent's branch, waiting to be reviewed and landed.
19
+ | "ready"
20
+ // It is in the workspace. What is left is proving it: re-run the check.
21
+ | "landed"
22
+ // Stopped on a spent allowance, which is a wait rather than a failure and must not be drawn as one.
23
+ | "waiting"
24
+ // Over, with nothing held: it failed, it was interrupted, it was stopped, or it changed nothing.
25
+ | "ended";
26
+
27
+ export interface FixStance {
28
+ readonly kind: FixStanceKind;
29
+ // True while this agent is still the failure's live answer; false once ended or landed.
30
+ readonly ongoing: boolean;
31
+ // True only for an ended fix: the one state where continuing it is a press rather than a chip.
32
+ readonly retry: boolean;
33
+ readonly label: string;
34
+ // Why the chip says what it says, and what pressing it does; no time-relative words.
35
+ readonly hint: string;
36
+ }
37
+
38
+ const needsYou = (label: string, hint: string): FixStance => ({ kind: "needs-you", ongoing: true, retry: false, label, hint });
39
+
40
+ // Ranks parked flags like the fleet does: setup outranks a plain question since it blocks the agent outright.
41
+ const ATTENTION: readonly { readonly flag: keyof AgentSummary["attention"]; readonly label: string; readonly why: string }[] = [
42
+ { flag: "plan", label: "Approval needed", why: "it has proposed a plan and is waiting for a yes" },
43
+ { flag: "capability", label: "Setup needed", why: "it needs something connected that is not connected yet" },
44
+ { flag: "question", label: "Question for you", why: "it has asked you something" },
45
+ { flag: "permission", label: "Permission needed", why: "it wants to use a tool it needs permission for" },
46
+ { flag: "conflict", label: "Land conflict", why: "its work cannot be merged until somebody resolves a clash" },
47
+ ];
48
+
49
+ // Live turn: running, unwinding a Stop, or waiting on a daemon repair; `awaiting` is handled above.
50
+ const IN_FLIGHT: ReadonlySet<AgentSummary["status"]> = new Set(["running", "resuming", "stopping", "dismissing"]);
51
+
52
+ const WORKING: FixStance = {
53
+ kind: "working",
54
+ ongoing: true,
55
+ retry: false,
56
+ label: "Agent working",
57
+ hint: "An agent is already working on this failure. Open the conversation to watch it.",
58
+ };
59
+
60
+ const READY: FixStance = {
61
+ kind: "ready",
62
+ ongoing: true,
63
+ retry: false,
64
+ label: "Fix ready",
65
+ hint: "The fix is written and held on the agent's branch. Open it to review the diff and land it.",
66
+ };
67
+
68
+ const LANDED: FixStance = {
69
+ kind: "landed",
70
+ ongoing: false,
71
+ retry: false,
72
+ label: "Fix landed",
73
+ hint: "The fix is in your workspace. Running the check again is what proves it.",
74
+ };
75
+
76
+ const WAITING: FixStance = {
77
+ kind: "waiting",
78
+ ongoing: true,
79
+ retry: false,
80
+ label: "Waiting",
81
+ hint: "The fix agent's allowance is spent. Nothing is owed: the turn goes again when the provider's window reopens.",
82
+ };
83
+
84
+ // True only for a clean finish (`ready`, or `idle` with a diff); a crashed turn with files also has a diff, but that
85
+ // belongs in the ending's hint, not here.
86
+ const holdingWork = (agent: AgentSummary): boolean =>
87
+ agent.status === "ready" || (agent.status === "idle" && agent.diff !== undefined && agent.diff.files > 0);
88
+
89
+ // Each ending's label states the reader's next move, not just what happened; `idle` counts too, since finishing with no
90
+ // changed files fixed nothing.
91
+ const ENDINGS: Partial<Record<AgentSummary["status"], { readonly label: string; readonly why: string }>> = {
92
+ error: { label: "Agent failed", why: "The fix agent's turn failed" },
93
+ interrupted: { label: "Interrupted", why: "The fix agent's turn was cut off when the sandbox went away" },
94
+ stopped: { label: "Stopped", why: "The fix agent was stopped before it finished" },
95
+ idle: { label: "Nothing changed", why: "The fix agent finished without changing any files" },
96
+ };
97
+
98
+ const ended = (agent: AgentSummary): FixStance => {
99
+ const ending = ENDINGS[agent.status] ?? { label: "Agent stopped", why: "The fix agent's turn ended" };
100
+ // File count left behind by a turn that died mid-edit; not a fix, but worth surfacing.
101
+ const files = agent.diff?.files ?? 0;
102
+ const partial = files === 0 ? "" : ` It left ${files} changed file${files === 1 ? "" : "s"} on its branch.`;
103
+ return {
104
+ kind: "ended",
105
+ ongoing: false,
106
+ retry: true,
107
+ label: ending.label,
108
+ // Includes the provider's own failure sentence when present; often the only record of an unwatched run.
109
+ hint: `${ending.why}${agent.failure === undefined ? "" : `: ${agent.failure}`}.${partial} Continuing carries on in the same conversation; starting over opens a fresh one.`,
110
+ };
111
+ };
112
+
113
+ // List order is priority, so a new status is inserted at its rank rather than appended as a trailing if. A spent
114
+ // allowance is checked first, ahead of `error`, since it is not a failure and must not offer a retry that spends the
115
+ // same allowance again.
116
+ const RULES: readonly ((agent: AgentSummary) => FixStance | undefined)[] = [
117
+ (agent) => (agent.status === "error" && agent.failureCode === "rate_limit" ? WAITING : undefined),
118
+ (agent) => {
119
+ const parked = ATTENTION.find((entry) => agent.attention[entry.flag]);
120
+ return parked === undefined ? undefined : needsYou(parked.label, `The fix agent is waiting on you: ${parked.why}.`);
121
+ },
122
+ // Bare `awaiting`/`conflict` with no flag raised: parked, with nothing more specific to say.
123
+ (agent) =>
124
+ agent.status === "awaiting" || agent.status === "conflict"
125
+ ? needsYou("Needs you", "The fix agent has stopped and is waiting on you.")
126
+ : undefined,
127
+ (agent) => (IN_FLIGHT.has(agent.status) ? WORKING : undefined),
128
+ (agent) => (holdingWork(agent) ? READY : undefined),
129
+ (agent) => (agent.status === "landed" ? LANDED : undefined),
130
+ ];
131
+
132
+ export const fixStance = (agent: AgentSummary): FixStance => {
133
+ for (const rule of RULES) {
134
+ const stance = rule(agent);
135
+ if (stance !== undefined) {
136
+ return stance;
137
+ }
138
+ }
139
+ return ended(agent);
140
+ };
@@ -31,7 +31,8 @@ const RUNTIME_DOMAINS = [
31
31
  { domain: "subagents", invalidates: [["subagents"]] },
32
32
 
33
33
  // Machines on the other end of a socket (devices, browsers, runners); "online" is the socket itself, never sampled.
34
- // hosts/webext land on capabilities since a pairing card's state is literally the hub's online answer.
34
+ // hosts/webext land on capabilities since a pairing card's state is literally the hub's online answer. `hosts` also
35
+ // carries a device reading that landed too late for the reader waiting on it (hosts/device-reports.ts).
35
36
  { domain: "hosts", invalidates: [["capabilities"], ["devices"]] },
36
37
  { domain: "webext", invalidates: [["capabilities"]] },
37
38
  { domain: "runners", invalidates: [["runners"]] },